Sessions artistiques virtuelles

Détails de la session artistique

Résumé

Six artistes ont été invités à peindre, concevoir et sculpter en réalité virtuelle. Voici comment nous avons enregistré leurs sessions, converti les données et les avons présentées en temps réel avec des navigateurs Web.

https://g.co/VirtualArtSessions

Quelle époque ! L'arrivée de la réalité virtuelle en tant que produit grand public ouvre de nouvelles possibilités inexplorées. Tilt Brush, un produit Google disponible sur le HTC Vive, vous permet de dessiner dans un espace tridimensionnel. Lorsque nous avons essayé Tilt Brush pour la première fois, la sensation de dessiner avec des contrôleurs à suivi de mouvement, associée à la sensation d'être "dans une pièce avec des super-pouvoirs", nous a marqués. Il n'existe vraiment rien de comparable à pouvoir dessiner dans l'espace vide qui vous entoure.

Œuvre d'art virtuelle

L'équipe Data Arts de Google a été chargée de présenter cette expérience aux utilisateurs qui ne disposent pas de casque de RV, sur le Web, où Tilt Brush ne fonctionne pas encore. Pour ce faire, l'équipe a fait appel à un sculpteur, un illustrateur, un concepteur, un artiste de mode, un artiste d'installation et des artistes de rue pour créer des œuvres dans leur propre style dans ce nouveau média.

Enregistrer des dessins en réalité virtuelle

Intégré à Unity, le logiciel Tilt Brush est une application de bureau qui utilise la VR à l'échelle de la pièce pour suivre la position de votre tête (écran monté sur la tête ou HMD) et les contrôleurs dans chacune de vos mains. Les illustrations créées dans Tilt Brush sont exportées par défaut au format .tilt. Pour proposer cette expérience sur le Web, nous avons réalisé que nous avions besoin de plus que des données sur les illustrations. Nous avons travaillé en étroite collaboration avec l'équipe Tilt Brush pour modifier Tilt Brush afin qu'il exporte les actions d'annulation/de suppression, ainsi que les positions de la tête et de la main de l'artiste 90 fois par seconde.

Lorsque vous dessinez, Tilt Brush prend la position et l'angle de votre manette et convertit plusieurs points au fil du temps en "trait". Pour en voir un exemple, cliquez ici. Nous avons écrit des plug-ins qui extraient ces traits et les génèrent au format JSON brut.

    {
      "metadata": {
        "BrushIndex": [
          "d229d335-c334-495a-a801-660ac8a87360"
        ]
      },
      "actions": [
        {
          "type": "STROKE",
          "time": 12854,
          "data": {
            "id": 0,
            "brush": 0,
            "b_size": 0.081906750798225,
            "color": [
              0.69848710298538,
              0.39136275649071,
              0.211316883564
            ],
            "points": [
              [
                {
                  "t": 12854,
                  "p": 0.25791856646538,
                  "pos": [
                    [
                      1.9832634925842,
                      17.915264129639,
                      8.6014995574951
                    ],
                    [
                      -0.32014992833138,
                      0.82291424274445,
                      -0.41208130121231,
                      -0.22473378479481
                    ]
                  ]
                }, ...many more points
              ]
            ]
          }
        }, ... many more actions
      ]
    }

L'extrait de code ci-dessus décrit le format JSON du croquis.

Ici, chaque trait est enregistré en tant qu'action, avec un type: "TRAIT". En plus des actions de trait, nous voulions montrer un artiste commettre des erreurs et changer d'avis en plein croquis. Il était donc essentiel d'enregistrer des actions de "SUPPRESSION" qui servent à effacer ou à annuler des actions pour un trait entier.

Les informations de base de chaque trait sont enregistrées. Le type de pinceau, sa taille et sa couleur RVB sont donc tous collectés.

Enfin, chaque sommet du trait est enregistré, y compris la position, l'angle, le temps, ainsi que la force de pression du déclencheur du contrôleur (indiquée par p dans chaque point).

Notez que la rotation est un quaternion à quatre composantes. Cela est important plus tard lorsque nous lisons les traits pour éviter le verrouillage de la gimbal.

Lire des croquis avec WebGL

Pour afficher les croquis dans un navigateur Web, nous avons utilisé THREE.js et écrit du code de génération de géométrie qui imitait ce que Tilt Brush fait en arrière-plan.

Bien que Tilt Brush produise des bandes de triangles en temps réel en fonction du mouvement de la main de l'utilisateur, l'intégralité du croquis est déjà "terminée" au moment où nous l'affichons sur le Web. Cela nous permet de contourner une grande partie du calcul en temps réel et de cuire la géométrie au chargement.

Croquis WebGL

Chaque paire de sommets d'un trait produit un vecteur de direction (les lignes bleues reliant chaque point, comme illustré ci-dessus, moveVector dans l'extrait de code ci-dessous). Chaque point contient également une orientation, un quaternion qui représente l'angle actuel du contrôleur. Pour produire une bande de triangles, nous itérons sur chacun de ces points en produisant des normales perpendiculaires à la direction et à l'orientation du contrôleur.

Le processus de calcul de la bande de triangles pour chaque trait est presque identique au code utilisé dans Tilt Brush:

const V_UP = new THREE.Vector3( 0, 1, 0 );
const V_FORWARD = new THREE.Vector3( 0, 0, 1 );

function computeSurfaceFrame( previousRight, moveVector, orientation ){
    const pointerF = V_FORWARD.clone().applyQuaternion( orientation );

    const pointerU = V_UP.clone().applyQuaternion( orientation );

    const crossF = pointerF.clone().cross( moveVector );
    const crossU = pointerU.clone().cross( moveVector );

    const right1 = inDirectionOf( previousRight, crossF );
    const right2 = inDirectionOf( previousRight, crossU );

    right2.multiplyScalar( Math.abs( pointerF.dot( moveVector ) ) );

    const newRight = ( right1.clone().add( right2 ) ).normalize();
    const normal = moveVector.clone().cross( newRight );
    return { newRight, normal };
}

function inDirectionOf( desired, v ){
    return v.dot( desired ) >= 0 ? v.clone() : v.clone().multiplyScalar(-1);
}

Combiner la direction et l'orientation du trait ne renvoie que des résultats mathématiquement ambigus. Plusieurs normales peuvent être dérivées et donner souvent une "torsion" dans la géométrie.

Lors de l'itération des points d'un trait, nous maintenons un vecteur "droit privilégié" et le transmettons à la fonction computeSurfaceFrame(). Cette fonction nous donne une normale à partir de laquelle nous pouvons dériver un quadrilatère dans la bande de quadrilatères, en fonction de la direction du trait (du dernier point au point actuel) et de l'orientation du contrôleur (un quaternion). Plus important encore, il renvoie également un nouveau vecteur "droit privilégié" pour le prochain ensemble de calculs.

Traits

Après avoir généré des quads en fonction des points de contrôle de chaque trait, nous fusionnons les quads en interpolant leurs coins, d'un quad à l'autre.

function fuseQuads( lastVerts, nextVerts) {
    const vTopPos = lastVerts[1].clone().add( nextVerts[0] ).multiplyScalar( 0.5
);
    const vBottomPos = lastVerts[5].clone().add( nextVerts[2] ).multiplyScalar(
0.5 );

    lastVerts[1].copy( vTopPos );
    lastVerts[4].copy( vTopPos );
    lastVerts[5].copy( vBottomPos );
    nextVerts[0].copy( vTopPos );
    nextVerts[2].copy( vBottomPos );
    nextVerts[3].copy( vBottomPos );
}
Quads fusionnés
Quads fusionnés.

Chaque quad contient également des UV qui sont générés à l'étape suivante. Certains pinceaux contiennent différents types de traits pour donner l'impression que chaque trait est différent. Pour ce faire, utilisez l'_atlasage de texture_, où chaque texture de pinceau contient toutes les variations possibles. La texture appropriée est sélectionnée en modifiant les valeurs UV du trait.

function updateUVsForSegment( quadVerts, quadUVs, quadLengths, useAtlas,
atlasIndex ) {
    let fYStart = 0.0;
    let fYEnd = 1.0;

    if( useAtlas ){
    const fYWidth = 1.0 / TEXTURES_IN_ATLAS;
    fYStart = fYWidth * atlasIndex;
    fYEnd = fYWidth * (atlasIndex + 1.0);
    }

    //get length of current segment
    const totalLength = quadLengths.reduce( function( total, length ){
    return total + length;
    }, 0 );

    //then, run back through the last segment and update our UVs
    let currentLength = 0.0;
    quadUVs.forEach( function( uvs, index ){
    const segmentLength = quadLengths[ index ];
    const fXStart = currentLength / totalLength;
    const fXEnd = ( currentLength + segmentLength ) / totalLength;
    currentLength += segmentLength;

    uvs[ 0 ].set( fXStart, fYStart );
    uvs[ 1 ].set( fXEnd, fYStart );
    uvs[ 2 ].set( fXStart, fYEnd );
    uvs[ 3 ].set( fXStart, fYEnd );
    uvs[ 4 ].set( fXEnd, fYStart );
    uvs[ 5 ].set( fXEnd, fYEnd );

    });

}
Quatre textures dans un atlas de texture pour un pinceau à l'huile
Quatre textures dans un atlas de textures pour le pinceau à l'huile
Dans Tilt Brush
Dans Tilt Brush
Dans WebGL
Dans WebGL

Étant donné que chaque croquis comporte un nombre illimité de traits et que les traits n'ont pas besoin d'être modifiés au moment de l'exécution, nous précalculons la géométrie des traits à l'avance et les fusionnons en un seul maillage. Même si chaque nouveau type de pinceau doit être son propre matériau, cela réduit toujours nos appels de dessin à un par pinceau.

L'ensemble du croquis ci-dessus est effectué en un seul appel de dessin dans WebGL.
L'ensemble du croquis ci-dessus est effectué en un seul appel de dessin dans WebGL.

Pour effectuer un test de stress du système, nous avons créé un croquis qui a pris 20 minutes et qui a rempli l'espace avec autant de sommets que possible. Le croquis obtenu était toujours lu à 60 FPS en WebGL.

Étant donné que chacun des sommets d'origine d'un trait contient également le temps, nous pouvons facilement lire les données. Recalculer les traits par frame serait très lent. Nous avons donc précalculé l'ensemble du croquis au chargement et simplement révélé chaque quad au moment voulu.

Masquer un quadrilatère signifiait simplement réduire ses sommets au point 0,0,0. Lorsque le moment est venu de révéler le quad, nous repositionnons les sommets.

Un domaine à améliorer est la manipulation des sommets entièrement sur le GPU avec des nuanceurs. L'implémentation actuelle les place en parcourant la matrice de sommets à partir du code temporel actuel, en vérifiant les sommets à révéler, puis en mettant à jour la géométrie. Cela met une forte charge sur le processeur, ce qui fait tourner le ventilateur et réduit l'autonomie de la batterie.

Œuvre d'art virtuelle

Enregistrer les artistes

Nous avons estimé que les croquis eux-mêmes ne suffiraient pas. Nous voulions montrer les artistes dans leurs croquis, peignant chaque coup de pinceau.

Pour capturer les artistes, nous avons utilisé des caméras Microsoft Kinect afin d'enregistrer les données de profondeur du corps des artistes dans l'espace. Cela nous permet de présenter leurs figures tridimensionnelles dans le même espace que les dessins.

Étant donné que le corps de l'artiste s'obscurcissait et nous empêchait de voir ce qui se trouvait derrière lui, nous avons utilisé un double système Kinect, les deux à des côtés opposés de la pièce, pointant vers le centre.

En plus des informations de profondeur, nous avons également capturé les informations de couleur de la scène avec des appareils photo reflex numériques standards. Nous avons utilisé l'excellent logiciel DepthKit pour calibrer et fusionner les images de la caméra de profondeur et des caméras couleur. Le Kinect est capable d'enregistrer des images en couleur, mais nous avons choisi d'utiliser des appareils photo reflex numériques, car nous pouvions contrôler les paramètres d'exposition, utiliser de superbes objectifs haut de gamme et enregistrer en haute définition.

Pour enregistrer les images, nous avons construit une salle spéciale pour accueillir le HTC Vive, l'artiste et la caméra. Toutes les surfaces étaient recouvertes d'un matériau qui absorbait la lumière infrarouge pour obtenir un nuage de points plus net (duvetyne sur les murs, tapis en caoutchou nervuré sur le sol). Au cas où le matériau apparaisse dans le séquence de nuage de points, nous avons choisi un matériau noir pour qu'il ne soit pas aussi gênant qu'un matériau blanc.

Artiste

Les enregistrements vidéo obtenus nous ont fourni suffisamment d'informations pour projeter un système de particules. Nous avons écrit des outils supplémentaires dans openFrameworks pour nettoyer davantage les images, en particulier en supprimant les sols, les murs et le plafond.

Les quatre canaux d'une session vidéo enregistrée (deux canaux de couleur en haut et deux canaux de profondeur en bas)
Les quatre canaux d'une session vidéo enregistrée (deux canaux de couleur en haut et deux canaux de profondeur en bas)

En plus de montrer les artistes, nous voulions également afficher le casque et les contrôleurs en 3D. Cela était non seulement important pour afficher clairement le HMD dans la sortie finale (les objectifs réfléchissants du HTC Vive perturbaient les lectures infrarouges du Kinect), mais cela nous a également fourni des points de contact pour déboguer la sortie de particules et aligner les vidéos avec le croquis.

Visiocasque, contrôleurs et particules alignés
Visiocasque, contrôleurs et particules alignés

Pour ce faire, nous avons écrit un plug-in personnalisé dans Tilt Brush qui extrait les positions du HMD et des contrôleurs à chaque frame. Étant donné que Tilt Brush s'exécute à 90 FPS, des tonnes de données ont été diffusées et les données d'entrée d'un croquis dépassaient 20 Mo non compressées. Nous avons également utilisé cette technique pour capturer des événements qui ne sont pas enregistrés dans le fichier d'enregistrement Tilt Brush standard, par exemple lorsque l'artiste sélectionne une option dans le panneau d'outils et la position du widget miroir.

Lors du traitement des 4 To de données que nous avons collectées, l'un des plus grands défis a été d'aligner toutes les différentes sources visuelles/de données. Chaque vidéo d'une caméra DSLR doit être alignée avec le Kinect correspondant, de sorte que les pixels soient alignés dans l'espace et dans le temps. Les images de ces deux systèmes de caméras devaient ensuite être alignées pour former un seul artiste. Nous avons ensuite dû aligner notre artiste 3D sur les données capturées à partir de son dessin. Ouf ! Nous avons écrit des outils basés sur le navigateur pour vous aider à effectuer la plupart de ces tâches. Vous pouvez les essayer vous-même sur cette page.

Artistes

Une fois les données alignées, nous avons utilisé des scripts écrits en NodeJS pour les traiter et générer un fichier vidéo et une série de fichiers JSON, tous coupés et synchronisés. Pour réduire la taille des fichiers, nous avons effectué trois actions. Tout d'abord, nous avons réduit la précision de chaque nombre à virgule flottante afin qu'elle soit au maximum de trois décimales. Deuxièmement, nous avons réduit le nombre de points d'un tiers à 30 FPS et interpolé les positions côté client. Enfin, nous avons sérialisé les données. Ainsi, au lieu d'utiliser du JSON simple avec des paires clé/valeur, un ordre de valeurs est créé pour la position et la rotation du HMD et des contrôleurs. La taille du fichier a ainsi été réduite à un peu moins de 3 Mo, ce qui était acceptable pour la transmission par ligne.

Artistes

Étant donné que la vidéo elle-même est diffusée en tant qu'élément vidéo HTML5 lu par une texture WebGL pour devenir des particules, la vidéo elle-même devait être lue en arrière-plan. Un nuanceur convertit les couleurs des images de profondeur en positions dans l'espace 3D. James George a partagé un excellent exemple de ce que vous pouvez faire avec les images directement issues de DepthKit.

iOS impose des restrictions sur la lecture vidéo intégrée, ce qui, selon nous, vise à éviter que les utilisateurs ne soient importunés par des annonces vidéo Web en lecture automatique. Nous avons utilisé une technique semblable à d'autres solutions de contournement sur le Web, à savoir copier le frame vidéo dans un canevas et mettre à jour manuellement le temps de recherche de la vidéo, toutes les 1/30 de seconde.

videoElement.addEventListener( 'timeupdate', function(){
    videoCanvas.paintFrame( videoElement );
});

function loopCanvas(){

    if( videoElement.readyState === videoElement.HAVE\_ENOUGH\_DATA ){

    const time = Date.now();
    const elapsed = ( time - lastTime ) / 1000;

    if( videoState.playing && elapsed >= ( 1 / 30 ) ){
        videoElement.currentTime = videoElement.currentTime + elapsed;
        lastTime = time;
    }

    }

}

frameLoop.add( loopCanvas );

Notre approche a eu le désavantage malheureux de réduire considérablement la fréquence d'images iOS, car la copie du tampon de pixels de la vidéo vers le canevas est très gourmande en processeur. Pour contourner ce problème, nous avons simplement diffusé des versions de taille réduite des mêmes vidéos, qui permettent d'atteindre au moins 30 images par seconde sur un iPhone 6.

Conclusion

Depuis 2016, le consensus général pour le développement de logiciels de RV est de garder les géométries et les nuanceurs simples afin de pouvoir exécuter à plus de 90 FPS dans un HMD. Cela s'est avéré être une excellente cible pour les démonstrations WebGL, car les techniques utilisées dans Tilt Brush se prêtent très bien à WebGL.

Bien que les navigateurs Web affichant des maillages 3D complexes ne soient pas en soi passionnants, il s'agissait d'une preuve de concept démontrant que la pollinisation croisée entre le travail en VR et le Web est tout à fait possible.