Sessions artistiques virtuelles

Détails de la session artistique

Résumé

Six artistes ont été invités à peindre, dessiner et sculpter en RV. C'est ainsi que nous avons enregistré leurs sessions, converti les données et les avons présentées en temps réel avec les navigateurs Web.

https://g.co/VirtualArtSessions

Quelle occasion de vivre ! 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, ce sentiment de dessin avec des contrôleurs suivis du mouvement associé à la présence d'être "dans une pièce dotée de super-pouvoirs" persiste avec vous. Il n'y a vraiment pas 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 à des personnes 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 illustrateur, un concept designer, un artiste de mode, un artiste d'installation et des artistes de rue pour créer des œuvres dans leur propre style sur ce nouveau support.

Enregistrer des dessins en réalité virtuelle

Intégré à Unity, le logiciel Tilt Brush lui-même est une application de bureau qui utilise la RV à l'échelle de la pièce pour suivre la position de votre tête (écran casque ou HMD) et les contrôleurs dans 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 pris conscience que nous avions besoin de plus que des données d'illustration. Nous avons travaillé en étroite collaboration avec l'équipe Tilt Brush pour modifier Tilt Brush afin d'exporter 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.

Lors du dessin, Tilt Brush utilise la position et l'angle de votre contrôleur pour convertir plusieurs points au fil du temps en un "trait". Pour consulter un exemple, cliquez ici. Nous avons écrit des plug-ins qui extraient ces traits et les affichent en tant que fichiers JSON bruts.

    {
      "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, de type: "STROKE". En plus des actions de trait, nous voulions montrer un artiste faisant des erreurs et changer d'avis au milieu d'un croquis. Il était donc essentiel d'enregistrer les actions "DELETE" qui servent d'effacement ou d'annulation sur un trait entier.

Les informations de base de chaque trait sont enregistrées. Ainsi, le type de pinceau, la taille du pinceau et la couleur RVB sont collectés.

Enfin, chaque sommet du trait est enregistré. Cela inclut la position, l'angle, la durée, ainsi que la force de pression du déclencheur (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 imitant le fonctionnement de Tilt Brush.

Bien que Tilt Brush génère des bandes triangulaires 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 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 indiqué ci-dessus, moveVector dans l'extrait de code ci-dessous). Chaque point contient également une orientation, c'est-à-dire un quaternion représentant l'angle actuel du contrôleur. Pour produire une bande triangulaire, nous itérerons sur chacun de ces points, produisant ainsi 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);
}

La combinaison de la direction et de l'orientation du trait renvoie des résultats mathématiquement ambigus. Il pourrait y avoir plusieurs normales dérivées et donnerait souvent un "twist" dans la géométrie.

Lors de l'itération sur les points d'un trait, nous conservons un vecteur "droit privilégié" et le transmettons à la fonction computeSurfaceFrame(). Cette fonction nous donne une normale à partir de laquelle nous pouvons déduire un quadrillère dans le quad band, 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, elle renvoie également un nouveau vecteur "droit privilégié" pour l'ensemble de calculs suivant.

Traits

Après avoir généré les quadrants en fonction des points de contrôle de chaque trait, nous fusionnons les quadrants 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 fusionnés
Quadrants 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 coup ressemble à un trait différent du pinceau. Pour ce faire, utilisez un 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 un pinceau à huile
Dans Tilt Brush
Dans Tilt Brush
Avec WebGL
Dans WebGL

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

L'intégralité du croquis ci-dessus est réalisée en un seul appel de dessin dans WebGL.
Le dessin ci-dessus est réalisé en 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 quand même diffusé à 60 ips en WebGL.

Étant donné que chacun des sommets d'origine d'un trait contenait également du temps, nous pouvons facilement rejouer les données. Le recalcul des traits par frame serait très lent. Au lieu de cela, nous avons précalculé l'intégralité du croquis lors de la charge et avons simplement dévoilé chaque quadruple quadruple lorsqu'il était temps de le faire.

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

Un domaine à améliorer consiste à manipuler entièrement les sommets sur le GPU à l'aide des nuanceurs. La mise en œuvre actuelle les place en parcourant le tableau de sommets à partir de l'horodatage actuel, en vérifiant les sommets à afficher, puis en mettant à jour la géométrie. Cela sollicite beaucoup 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 esquisses, 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.

Comme le corps de l'artiste s'occultait et nous empêchait de voir ce qui se trouve derrière, nous avons utilisé un système Kinect double, les deux 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 à l'aide d'appareils photo reflex numériques standards. Nous avons utilisé l'excellent logiciel DepthKit pour calibrer et fusionner les séquences vidéo de l'appareil photo de profondeur et des appareils photo couleur. Le 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 des vidéos en haute définition.

Pour enregistrer la vidéo, nous avons construit une pièce spéciale où se trouve le HTC Vive, l'artiste et la caméra. Toutes les surfaces étaient recouvertes de matériaux qui absorbaient la lumière infrarouge pour nous donner un nuage de points plus propre (duvetyne sur les murs, caoutchouc à côtes sur le 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 distrayant 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 créé des outils supplémentaires 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 au-dessus et deux canaux de profondeur en bas)
Les quatre canaux d'une session vidéo enregistrée (deux canaux de couleur au-dessus et deux en bas)

En plus de montrer les artistes, nous voulions effectuer le rendu du matériel et des manettes en 3D. Cela n'était pas seulement important pour montrer clairement le HMD dans la sortie finale (les lentilles réfléchissantes du HTC Vive échappaient les lectures infrarouges de Kintect), mais cela nous donnait des points de contact pour déboguer la sortie des particules et aligner les vidéos avec le croquis.

Écran vis-à-vis, contrôleurs et particules alignés
Écran casque, contrôleurs et particules alignés

Pour ce faire, nous avons écrit un plug-in personnalisé dans Tilt Brush qui a extrait les positions du HMD et des contrôleurs de chaque image. Étant donné que Tilt Brush s'exécute à 90 FPS, des tonnes de données sont diffusées et les données d'entrée d'un croquis étaient non compressées (plus de 20 Mo). 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, 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 capturées, l'un des plus grands défis consistait à 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. Il fallait ensuite aligner les séquences de ces deux supports pour former un seul artiste. Ensuite, nous devions aligner notre artiste 3D sur les données capturées à partir de son dessin. Ouf ! Nous avons écrit des outils basés sur un navigateur pour vous aider avec la plupart de ces tâches. Vous pouvez les essayer vous-même sur cette page.

Artistes vinyles

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 édités et synchronisés. Pour réduire la taille du fichier, nous avons fait trois choses. Tout d'abord, nous avons réduit la justesse de chaque nombre à virgule flottante afin qu'il n'atteigne pas plus de trois décimales. Ensuite, 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 code JSON brut avec des paires clé/valeur, un ordre de valeurs est créé pour la position et la rotation du matériel HMD et des contrôleurs. Cela permet de réduire la taille du fichier à seulement 3 Mo, ce qui était acceptable pour une diffusion sur le fil.

Artistes interprètes

Étant donné que la vidéo 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 cachée 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 la façon dont vous pouvez faire avec des images provenant directement de DepthKit.

iOS impose des restrictions sur la lecture de vidéos intégrées, qui visent à éviter que les utilisateurs ne soient perturbés par des annonces vidéo Web en lecture automatique. Nous avons utilisé une technique semblable à d'autres solutions de contournement sur le Web, qui consiste à copier l'image vidéo dans un canevas et à mettre à jour manuellement le temps de recherche 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 baisse significative de la fréquence d'images sous iOS, car la copie du tampon de pixels de la vidéo vers le canevas nécessite une utilisation intensive du 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

Depuis 2016, le consensus général s'est fixé pour le développement de logiciels de RV : les géométries et les nuanceurs sont simples afin de pouvoir fonctionner à 90 FPS et plus dans un HMD. Il s'est avéré très utile pour les démos WebGL, car les techniques utilisées dans la carte Tilt Brush sont parfaitement adaptées à WebGL.

Bien que les navigateurs Web affichant des maillages 3D complexes ne soient pas passionnants en soi, il s'agissait d'une preuve de concept que la pollinisation croisée des travaux de réalité virtuelle et du Web est tout à fait possible.