Sessions artistiques virtuelles

Détails de la session artistique

Résumé

Six artistes ont été invités à peindre, concevoir et sculpter en RV. Il s'agit du processus permettant d'enregistrer leurs sessions, de convertir les données et de les présenter en temps réel avec des navigateurs Web.

https://g.co/VirtualArtSessions

Quelle époque pour la vie ! Avec l'introduction de la réalité virtuelle en tant que produit grand public, des possibilités nouvelles et inexplorées se font découvrir. Tilt Brush, un produit Google disponible sur le HTC Vive, vous permet de dessiner dans un espace en trois dimensions. Lorsque nous avons essayé Tilt Brush pour la première fois, cette sensation de dessiner avec des contrôleurs suivis de mouvements associée à la présence "dans une pièce dotée de superpouvoirs" persiste. Il n'y a pas vraiment d'expérience comme de dessiner dans l'espace vide qui vous entoure.

Œuvre d'art virtuelle

L'équipe Data Arts de Google a dû montrer cette expérience à ceux qui n'ont pas de casque de RV, sur le Web où Tilt Brush ne fonctionne pas encore. À cette fin, l'équipe a fait appel à un sculpteur, un artiste, un concepteur conceptuel, un artiste de mode, un artiste spécialisé dans l'installation et des artistes de rue pour créer des œuvres dans leur propre style sur ce nouveau support.

Enregistrement de dessins en réalité virtuelle

Intégré à Unity, le logiciel Tilt Brush est lui-même une application de bureau qui utilise la réalité virtuelle à l'échelle d'une salle pour suivre la position de votre tête (écran fixe sur la tête) et les manettes de chacune de vos mains. Par défaut, les illustrations créées dans Tilt Brush sont exportées au format .tilt. Pour proposer cette expérience sur le Web, nous avons réalisé que nous avions besoin de bien 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 et de suppression ainsi que les positions de la tête et des mains de l'artiste 90 fois par seconde.

Lorsque vous dessinez, Tilt Brush utilise la position et l'angle de votre contrôleur et convertit plusieurs points au fil du temps en un "trait". Pour consulter un exemple, cliquez ici. Nous avons écrit des plug-ins permettant d'extraire ces traits et de les générer 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 ci-dessus décrit le format du format JSON de résumé.

Ici, chaque trait est enregistré en tant qu'action, avec le type "STROKE". En plus des actions de trait, nous voulions montrer un artiste faisant des erreurs et changer d'avis en cours de croquis. Il était donc essentiel d'enregistrer des actions "DELETE" (suppression) qui servent d'actions d'effacement ou d'annulation pour un trait entier.

Les informations de base pour chaque trait sont enregistrées, de sorte que le type de pinceau, la taille du pinceau et la couleur de rendu soient collectés.

Enfin, chaque sommet du trait est enregistré. Cela inclut la position, l'angle, l'heure, ainsi que la force de pression du déclencheur du contrôleur (notée p à l'intérieur de chaque point).

Notez que la rotation est un quaternion à quatre composants. C'est important plus tard lorsque nous afficherons les traits pour éviter le blocage du cardan.

Lire des croquis avec WebGL

Pour afficher les croquis dans un navigateur Web, nous avons utilisé THREE.js et écrit un code de génération de géométrie semblable à celui de Tilt Brush.

Si Tilt Brush génère des bandes triangulaires en temps réel en fonction du mouvement de la main de l'utilisateur, le croquis est déjà "terminé" au moment où nous l'affichons sur le Web. Cela nous permet de contourner une grande partie du calcul en temps réel et d'intégrer la géométrie lors de la charge.

Esquisses 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 obtenir une bande triangulaire, nous effectuons une itération sur chacun de ces points, ce qui génère des normales perpendiculaires à la direction et à l'orientation du contrôleur.

Le processus de calcul de la bande triangulaire 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);
}

La combinaison de la direction et de l'orientation du trait en elle-même renvoie des résultats mathématiquement ambigus. Il peut y avoir plusieurs normales dérivées et donnerait souvent un "twist" à la géométrie.

Lors de l'itération sur les points d'un trait, nous conservons un vecteur "droit de préférence" et le transmettons à la fonction computeSurfaceFrame(). Cette fonction nous donne une normale à partir de laquelle nous pouvons déduire un quad dans la bande de quadrillages, en fonction de la direction du trait (du dernier point jusqu'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é les quads en fonction des points de contrôle de chaque trait, nous les fusionnons en interpolant leurs angles, 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
Quads fusionnés.

Chaque quad contient également des UV, qui sont générés en tant qu'étape suivante. Certains pinceaux contiennent une variété de motifs de trait pour donner l'impression que chaque trait s'apparente à un trait différent du pinceau. Pour ce faire, utilisez l'atlas de texture, où chaque texture de pinceau contient toutes les variantes possibles. La bonne texture 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 à huile
Quatre textures dans un atlas de texture pour le pinceau à huile
Dans Tilt Brush
In Tilt Brush
Avec WebGL
En WebGL

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

Le croquis ci-dessus est réalisé en un seul appel de dessin dans WebGL.
L'intégralité du croquis ci-dessus est réalisée dans un seul appel de dessin dans WebGL.

Pour effectuer un test de contrainte sur le système, nous avons créé un croquis qui a pris 20 minutes pour remplir l'espace avec autant de sommets que possible. Le sketch obtenu est toujours diffusé à 60 images par seconde en WebGL.

Étant donné que chacun des sommets d'origine d'un trait contenait également du temps, nous pouvons facilement lire les données. Recalculer le nombre de traits par image serait très lent. Au lieu de cela, nous avons précalculé l'intégralité du croquis au moment de la charge et dévoilé simplement chaque quadrillage au moment opportun.

Masquer un quad signifiait simplement réduire ses sommets au point 0,0,0. Lorsque l'heure a atteint le point auquel le quad doit être révélé, nous repositionnons les sommets en place.

Un domaine à améliorer consiste à manipuler l'intégralité des sommets sur le GPU à l'aide des nuanceurs. L'implémentation actuelle les place en parcourant le tableau de sommets en boucle à partir de l'horodatage actuel, en vérifiant les sommets à afficher, puis en mettant à jour la géométrie. Cela entraîne une charge importante sur le processeur, ce qui fait tourner le ventilateur et gaspille l'autonomie de la batterie.

Œuvre d'art virtuelle

Enregistrement des artistes

Nous avons estimé que les croquis ne suffiraient pas. Nous voulions montrer les artistes à l'intérieur de leurs croquis, en peignant chaque coup de pinceau.

Pour capturer les artistes, nous avons utilisé des appareils photo Microsoft Kinect afin d'enregistrer les données de profondeur de leur corps dans l'espace. Cela nous donne la possibilité de montrer leurs figures en trois dimensions dans le même espace que les dessins.

Étant donné que le corps de l'artiste s'occultait et nous empêchait de voir ce qui se trouve derrière, nous avons utilisé un double système Kinect, les deux côtés opposés de la pièce pointant au 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 séquences de l'appareil photo de profondeur et des appareils photo couleur. Kinect est capable d'enregistrer des couleurs, mais nous avons choisi d'utiliser des 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 l'enregistrement, nous avons construit une pièce 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 nous donner un nuage de points plus propre (duvetyne sur les murs, caoutchouc à côtes au sol). Si le matériau apparaît dans les images de nuages de points, nous avons choisi un matériau noir pour qu'il ne soit pas aussi gênant qu'un élément blanc.

Artiste interprète

Les enregistrements vidéo obtenus nous ont fourni suffisamment d'informations pour projeter un système de particules. Nous avons écrit d'autres outils dans openFrameworks pour nettoyer davantage la vidéo, en particulier pour supprimer 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 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 rendre le HMD et les contrôleurs en 3D. Cela était non seulement important pour montrer clairement le HMD dans la sortie finale (les lentilles réfléchissantes du HTC Vive généraient des dérangements infrarouges de Kiinect), mais cela nous donnait des points de contact pour déboguer la sortie de particules et aligner les vidéos avec le croquis.

L'écran visé, les contrôleurs et les particules alignées
L'écran visé, les contrôleurs et les particules sont alignés

Pour ce faire, nous avons écrit dans Tilt Brush un plug-in personnalisé qui a extrait les positions du HMD et des contrôleurs pour chaque frame. Étant donné que Tilt Brush fonctionne à 90 FPS, des tonnes de données sont diffusées et les données d'entrée d'un croquis étaient de plus de 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 de sauvegarde Tilt Brush classique, comme 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 capturées, l'un des plus grands défis a été d'aligner les différentes sources de données et visuelles. Chaque vidéo d'un appareil photo reflex numérique doit être alignée sur le Kinect correspondant, de sorte que les pixels soient alignés dans l'espace et dans le temps. Ensuite, les images de ces deux supports de caméra devaient être alignées l'une avec l'autre pour former un seul artiste. Ensuite, nous devions aligner notre artiste 3D avec les données capturées à partir de son dessin. Ouf ! Nous avons conçu des outils basés sur un navigateur pour vous aider à effectuer la plupart de ces tâches. Vous pouvez les essayer vous-même sur cette page.

Artistes disquaires
Une fois les données alignées, nous avons utilisé des scripts écrits en NodeJS pour tout 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 du fichier, nous avons effectué trois opérations. Tout d'abord, nous avons réduit la justesse de chaque nombre à virgule flottante pour qu'il n'atteigne pas plus 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 de sorte qu'au lieu d'utiliser un fichier JSON brut avec des paires clé/valeur, un ordre des valeurs est créé pour la position et la rotation du HMD et des contrôleurs. Cela permet de réduire la taille du fichier à 3 Mo, ce qui était acceptable sur le réseau.
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 se transformer en particules, la vidéo elle-même devait être lue en arrière-plan. Un nuanceur convertit les couleurs des représentations de profondeur en positions dans l'espace 3D. James George a donné un excellent exemple de la façon dont il est possible d'utiliser des séquences provenant directement de DepthKit.

iOS impose des restrictions sur la lecture de vidéos intégrées, qui sont censées éviter aux utilisateurs d'être agacés par des annonces vidéo Web en lecture automatique. Nous avons utilisé une technique semblable à d'autres solutions sur le Web, qui consiste à copier l'image de la vidéo dans un canevas et à mettre à jour manuellement le temps de recherche de la vidéo toutes les 30 secondes.

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 l'effet secondaire d'une réduction significative de la fréquence d'images iOS, car la copie du tampon de pixels de la vidéo vers le canevas nécessite beaucoup de ressources processeur. Pour contourner ce problème, nous avons simplement diffusé des versions plus petites des mêmes vidéos qui autorisent au moins 30 FPS sur un iPhone 6.

Conclusion

À partir de 2016, le consensus général s'est fixé pour le développement de logiciels de réalité virtuelle à partir de 2016. Cela s'est avéré être une excellente cible pour les démonstrations WebGL, car les techniques utilisées dans la carte Tilt Brush pour WebGL sont parfaitement adaptées.

Bien que l'affichage de maillages 3D complexes dans les navigateurs Web ne soit pas passionnant en soi, il a été démontré que la pollinisation croisée du travail de RV et du Web est tout à fait possible.