Sessioni di arte virtuale

Dettagli della sessione artistica

Riepilogo

Sei artisti sono stati invitati a dipingere, progettare e scolpire in VR. Questa è la procedura con cui abbiamo registrato le sessioni, convertito i dati e li abbiamo presentati in tempo reale con i browser web.

https://g.co/VirtualArtSessions

Che periodo! Con l'introduzione della realtà virtuale come prodotto consumer, vengono scoperte nuove possibilità inesplorate. Tilt Brush, un prodotto Google disponibile su HTC Vive, ti consente di disegnare in uno spazio tridimensionale. Quando abbiamo provato Tilt Brush per la prima volta, la sensazione di disegnare con i controller con rilevamento dei movimenti, unita alla sensazione di essere "in una stanza con superpoteri", è rimasta indelebile. Non c'è davvero un'esperienza come quella di poter disegnare nello spazio vuoto che ti circonda.

Opera d'arte virtuale

Il team di Data Arts di Google si è trovato di fronte alla sfida di mostrare questa esperienza a chi non ha un visore VR, sul web, dove Tilt Brush non è ancora disponibile. A tal fine, il team ha coinvolto uno scultore, un illustratore, un designer di concept, un artista di moda, un artista di installazioni e artisti di strada per creare artwork nel loro stile all'interno di questo nuovo mezzo.

Registrazione di disegni in realtà virtuale

Realizzato in Unity, il software Tilt Brush è un'applicazione per computer che utilizza la realtà virtuale a livello di stanza per monitorare la posizione della testa (display montato sulla testa o HMD) e i controller in ciascuna mano. L'artwork creata in Tilt Brush viene esportata per impostazione predefinita come file .tilt. Per portare questa esperienza sul web, abbiamo capito che non erano sufficienti solo i dati sull'artwork. Abbiamo lavorato a stretto contatto con il team di Tilt Brush per modificare l'app in modo che esporti le azioni di annullamento/eliminazione, nonché le posizioni della testa e della mano dell'artista, a 90 volte al secondo.

Quando disegni, Tilt Brush rileva la posizione e l'angolazione del controller e trasforma più punti nel tempo in un "tratto". Puoi vedere un esempio qui. Abbiamo scritto plug-in che estraggono questi tratti e li esportano come JSON non elaborato.

    {
      "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
      ]
    }

Lo snippet riportato sopra illustra il formato del JSON dello schizzo.

Qui ogni tratto viene salvato come azione, con un tipo: "STROKE". Oltre alle azioni di tratto, volevamo mostrare un artista che commette errori e cambia idea durante lo schizzo, quindi era fondamentale salvare le azioni "DELETE" che fungono da azioni di cancellazione o annullamento per un'intera traccia.

Vengono salvate le informazioni di base per ogni tratto, quindi vengono raccolti il tipo di pennello, le dimensioni del pennello e il colore RGB.

Infine, ogni vertice del tratto viene salvato e include la posizione, l'angolo, il tempo e l'intensità della pressione del trigger del controller (indicata come p in ogni punto).

Tieni presente che la rotazione è un quaternione a 4 componenti. Questo è importante in seguito quando eseguiamo il rendering dei tratti per evitare il blocco del gimbal.

Riproduzione di schizzi con WebGL

Per mostrare gli schizzi in un browser web, abbiamo utilizzato THREE.js e scritto codice di generazione della geometria che imitasse il funzionamento di Tilt Brush.

Sebbene Tilt Brush produca strisce di triangoli in tempo reale in base al movimento della mano dell'utente, l'intero schizzo è già "finito" quando lo mostriamo sul web. In questo modo possiamo bypassare gran parte del calcolo in tempo reale e incorporare la geometria al caricamento.

Sketch di WebGL

Ogni coppia di vertici in un tratto produce un vettore di direzione (le linee blu che collegano ogni punto come mostrato sopra, moveVector nello snippet di codice di seguito). Ogni punto contiene anche un orientamento, un quaternione che rappresenta l'angolo corrente del controller. Per produrre una striscia di triangoli, eseguiamo un'iterazione su ciascuno di questi punti producendo le normali perpendicolari alla direzione e all'orientamento del controller.

La procedura per calcolare la striscia di triangoli per ogni tratto è quasi identica al codice utilizzato in 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 combinazione della direzione e dell'orientamento del tratto da sola restituisce risultati matematicamente ambigui; potrebbero essere derivate più normali e spesso si ottiene una "torsione" nella geometria.

Quando esegui l'iterazione sui punti di un tratto, manteniamo un vettore "a destra preferito" e lo passiamo alla funzione computeSurfaceFrame(). Questa funzione ci fornisce una normale da cui possiamo ricavare un quad nella striscia di quad, in base alla direzione del tratto (dall'ultimo punto al punto corrente) e all'orientamento del controller (un quaternione). Ancora più importante, restituisce anche un nuovo vettore "diritto preferito" per l'insieme successivo di calcoli.

Tratti

Dopo aver generato i quadri in base ai punti di controllo di ogni tratto, li fondiamo interpolando i relativi angoli, da un quad all'altro.

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 );
}
Quad fusi
Quadri fusi.

Ogni quad contiene anche UV che vengono generati come passaggio successivo. Alcuni pennelli contengono una serie di motivi di tratto per dare l'impressione che ogni tratto sia stato realizzato con un pennello diverso. Questo viene ottenuto utilizzando la _texture atlasing_, in cui ogni texture del pennello contiene tutte le possibili varianti. La trama corretta viene selezionata modificando i valori UV del tratto.

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 );

    });

}
Quattro texture in un atlante di texture per il pennello a olio
Quattro texture in un atlante di texture per il pennello a olio
In Tilt Brush
In Tilt Brush
In WebGL
In WebGL

Poiché ogni schizzo ha un numero illimitato di tratti e questi non devono essere modificati in fase di esecuzione, precompiliamo la geometria dei tratti in anticipo e li uniamo in un'unica mesh. Anche se ogni nuovo tipo di pennello deve essere un materiale distinto, le chiamate di disegno vengono comunque ridotte a una per pennello.

L'intero sketch riportato sopra viene eseguito in una chiamata draw in WebGL
L'intero sketch riportato sopra viene eseguito in una chiamata draw in WebGL

Per sottoporre il sistema a stress test, abbiamo creato uno schizzo che ha richiesto 20 minuti per riempire lo spazio con il maggior numero possibile di vertici. Lo sketch risultante è stato riprodotto ancora a 60 FPS in WebGL.

Poiché ciascuno dei vertici originali di un tratto conteneva anche il tempo, possiamo riprodurre facilmente i dati. Il calcolo ripetuto dei tratti per frame sarebbe molto lento, quindi abbiamo precompilato l'intero schizzo al caricamento e abbiamo semplicemente rivelato ogni quad quando era il momento.

Nascondere un quad significava semplicemente comprimere i suoi vertici nel punto 0,0,0. Quando il tempo ha raggiunto il punto in cui il quad dovrebbe essere rivelato, riposizioni i vertici.

Un'area di miglioramento è la manipolazione dei vertici interamente sulla GPU con gli shader. L'implementazione attuale li inserisce eseguendo un ciclo nell'array di vertici dal timestamp corrente, controllando quali vertici devono essere mostrati e poi aggiornando la geometria. Questo comporta un carico elevato sulla CPU, che fa girare la ventola e spreca la durata della batteria.

Opera d'arte virtuale

Registrazione degli artisti

Riteniamo che gli schizzi stessi non siano sufficienti. Volevamo mostrare gli artisti all'interno dei loro schizzi, mentre dipingevano ogni pennellata.

Per acquisire gli artisti, abbiamo utilizzato le videocamere Microsoft Kinect per registrare i dati di profondità del corpo degli artisti nello spazio. In questo modo possiamo mostrare le loro figure tridimensionali nello stesso spazio in cui appaiono i disegni.

Poiché il corpo dell'artista si sarebbe ostruito impedendoci di vedere cosa c'è dietro, abbiamo utilizzato un doppio sistema Kinect, entrambi ai lati opposti della stanza rivolti verso il centro.

Oltre alle informazioni sulla profondità, abbiamo acquisito anche le informazioni sui colori della scena con fotocamere DSLR standard. Abbiamo utilizzato l'eccellente software DepthKit per calibrare e unire i filmati della fotocamera per la profondità e delle fotocamere a colori. Kinect è in grado di registrare a colori, ma abbiamo scelto di utilizzare le reflex digitali perché potevamo controllare le impostazioni di esposizione, usare obiettivi di alta qualità e registrare in alta definizione.

Per registrare i filmati, abbiamo costruito una stanza speciale per ospitare HTC Vive, l'artista e la videocamera. Tutte le superfici sono state coperte con materiale che assorbe la luce infrarossa per ottenere un cloud di punti più pulito (duvetyne sulle pareti, tappeti di gomma a coste sul pavimento). Nel caso in cui il materiale fosse apparso nel filmato del cloud di punti, abbiamo scelto un materiale nero in modo che non distraesse quanto un materiale bianco.

Artista

Le registrazioni video risultanti ci hanno fornito informazioni sufficienti per proiettare un sistema di particelle. Abbiamo scritto alcuni strumenti aggiuntivi in openFrameworks per ripulire ulteriormente i filmati, in particolar modo rimuovendo pavimenti, pareti e soffitto.

Tutti e quattro i canali di una sessione video registrata (due canali a colori sopra e due di profondità sotto)
Tutti e quattro i canali di una sessione video registrata (due canali di colore sopra e due di profondità sotto)

Oltre a mostrare gli artisti, volevamo anche eseguire il rendering dell'HMD e dei controller in 3D. Questo non era importante solo per mostrare chiaramente l'HMD nell'output finale (le lenti riflettenti di HTC Vive stavano alterando le letture IR di Kinect), ma ci ha fornito punti di contatto per il debug dell'output delle particelle e l'allineamento dei video con lo sketch.

Il display montato sulla testa, i controller e le particelle allineati
L'HMD (Head-Mounted Display), i controller e le particelle allineati

Per farlo, abbiamo scritto un plug-in personalizzato in Tilt Brush che estraeva le posizioni dell'HMD e dei controller in ogni fotogramma. Poiché Tilt Brush funziona a 90 fps, vengono trasmessi moltissimi dati e i dati di input di uno schizzo superano i 20 MB non compressi. Abbiamo utilizzato questa tecnica anche per acquisire eventi che non vengono registrati nel tipico file di salvataggio di Tilt Brush, ad esempio quando l'artista seleziona un'opzione nel riquadro degli strumenti e la posizione del widget specchio.

Durante l'elaborazione dei 4 TB di dati acquisiti, una delle maggiori sfide è stata allineare tutte le diverse origini dati/visuali. Ogni video di una fotocamera DSLR deve essere allineato al Kinect corrispondente, in modo che i pixel siano allineati sia nello spazio che nel tempo. Poi, i filmati di queste due attrezzature dovevano essere allineati tra loro per formare un unico artista. Poi abbiamo dovuto allineare il nostro artista 3D ai dati acquisiti dal suo disegno. Finalmente. Abbiamo scritto strumenti basati su browser per aiutarti a svolgere la maggior parte di queste attività. Puoi provarli personalmente qui

Artisti che registrano

Una volta allineati i dati, abbiamo utilizzato alcuni script scritti in NodeJS per elaborarli tutti e generare un file video e una serie di file JSON, tutti tagliati e sincronizzati. Per ridurre le dimensioni del file, abbiamo fatto tre cose. Innanzitutto, abbiamo ridotto la precisione di ogni numero in virgola mobile in modo che abbiano una precisione massima di 3 decimali. In secondo luogo, abbiamo ridotto il numero di punti di un terzo a 30 fps e abbiamo interpolato le posizioni lato client. Infine, abbiamo serializzato i dati in modo che, anziché utilizzare JSON normale con coppie chiave/valore, venga creato un ordine di valori per la posizione e la rotazione dell'HMD e dei controller. In questo modo, le dimensioni del file sono state ridotte a poco meno di 3 MB, un valore accettabile per il trasferimento tramite rete.

Artisti che registrano

Poiché il video stesso viene pubblicato come elemento video HTML5 letto da una texture WebGL per diventare particelle, doveva essere riprodotto nascosto in background. Uno shader converte i colori nelle immagini di profondità in posizioni nello spazio 3D. James George ha condiviso un ottimo esempio di cosa puoi fare con i filmati di DepthKit.

iOS ha limitazioni alla riproduzione di video in linea, il che presumibilmente serve a evitare che gli utenti vengano infastiditi dagli annunci video web con riproduzione automatica. Abbiamo utilizzato una tecnica simile ad altri workaround disponibili sul web, ovvero abbiamo copiato il frame del video in un canvas e aggiornato manualmente il tempo di ricerca del video ogni 1/30 di secondo.

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 );

Il nostro approccio ha avuto lo sfortunato effetto collaterale di ridurre notevolmente il frame rate di iOS, poiché la copia del buffer dei pixel dal video alla tela richiede molto utilizzo della CPU. Per ovviare a questo problema, abbiamo semplicemente pubblicato versioni di dimensioni ridotte degli stessi video che consentano almeno 30 fps su un iPhone 6.

Conclusione

Il consenso generale per lo sviluppo di software VR a partire dal 2016 è mantenere semplici geometrie e shader in modo da poter eseguire a più di 90 fps in un HMD. Si è rivelato un obiettivo davvero eccezionale per le demo WebGL, poiché le tecniche utilizzate in Tilt Brush si adattano molto bene a WebGL.

Anche se i browser web che mostrano mesh 3D complessi non sono particolarmente interessanti di per sé, si è trattato di una prova del concetto che la contaminazione reciproca della realtà virtuale e del web è del tutto possibile.