Sessioni di arte virtuale

Dettagli della sessione artistica

Riepilogo

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

https://g.co/VirtualArtSessions

Che ora di vivere! Con l'introduzione della realtà virtuale come prodotto di consumo, si scoprono nuove e inesplorate possibilità. Tilt Brush, un prodotto Google disponibile su HTC Vive, ti consente di disegnare nello spazio tridimensionale. Quando abbiamo provato Tilt Brush per la prima volta, ti accompagni la sensazione di disegnare con controller basati sul movimento insieme alla presenza di essere "in una stanza con superpoteri" e non c'è niente di meglio di poter disegnare lo spazio vuoto intorno a te.

Opera d'arte virtuale

Il team Data Arts di Google ha dovuto affrontare la sfida di mostrare questa esperienza a chi non disponeva di un visore di realtà virtuale, sul web dove Tilt Brush non funziona ancora. A tal fine, il team ha chiesto a uno scultore, un illustratore, un concept designer, un artista di moda, un installazioni artist e artisti di strada per creare opere d'arte nel proprio stile all'interno di questo nuovo mezzo.

Disegni registrati in realtà virtuale

Il software Tilt Brush, integrato in Unity, è un'applicazione desktop che utilizza una realtà virtuale con scalabilità in base alle stanze per monitorare la posizione della testa (head mount display o HMD) e i controller in ognuna delle tue mani. L'artwork creato in Tilt Brush viene esportato per impostazione predefinita come file .tilt. Per portare questa esperienza sul web, ci siamo resi conto di aver bisogno di qualcosa di più dei semplici dati dell'artwork. Abbiamo lavorato a stretto contatto con il team di Tilt Brush per modificare Tilt Brush, in modo da esportare le azioni di annullamento/eliminazione e le posizioni della testa e della mano dell'artista a 90 volte al secondo.

Quando disegni, Tilt Brush prende la posizione e l'angolazione del controller e converte più punti nel tempo in un "tratto". Puoi vedere un esempio qui. Abbiamo scritto plug-in che hanno estratto questi tratti e li hanno restituiti come JSON grezzo.

    {
      "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 descrive il formato del formato JSON dello schizzo.

Qui ogni tratto viene salvato come azione, con il tipo "CORSA". Oltre alle azioni dei tratti, volevamo mostrare un artista che commetteva errori e cambiava idea a metà dello schizzo, quindi era fondamentale salvare le azioni "ELIMINA" che consentono di cancellare o annullare le azioni per un intero tratto.

Vengono salvate le informazioni di base di ogni tratto, quindi vengono raccolti tutti i tipi di pennello, le dimensioni del pennello e la combinazione di colori.

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

Tieni presente che la rotazione è un quaternione a quattro componenti. Questo è importante in seguito, quando eseguiamo il rendering dei tratti per evitare il blocco dello stelo.

Riproduzione di schizzi con WebGL

Per mostrare gli schizzi in un browser web, abbiamo usato THREE.js e abbiamo scritto un codice di generazione della geometria che riproducesse ciò che fa Tilt Brush in background.

Mentre Tilt Brush produce strisce triangolari in tempo reale in base al movimento della mano dell'utente, quando lo mostriamo sul web, lo schizzo è già "finito". Questo ci consente di bypassare gran parte del calcolo in tempo reale e di eseguire l'applicazione della geometria al carico.

Schizzi 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 riportato di seguito). Ogni punto contiene anche un orientamento, un quaternione che rappresenta l'angolo attuale del controller. Per produrre una striscia di triangolo, eseguiamo l'iterazione su ciascuno di questi punti generando valori normali perpendicolari alla direzione e all'orientamento del controller.

Il processo per calcolare la striscia di triangolo per ogni tratto è quasi identico 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 restituisce risultati matematici ambigui; potrebbero derivare più normali normali che spesso produrrebbero uno "storsione" nella geometria.

Durante l'iterazione sui punti di un tratto, manteniamo il vettore "destra preferita" e lo passiamo alla funzione computeSurfaceFrame(). Questa funzione ci dà un valore normale da cui possiamo ricavare un quad nella quad strip, 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 "destra preferita" per i successivi calcoli.

Tratti

Dopo aver generato quadricipiti in base ai punti di controllo di ogni tratto, fondiamo i quadricipiti interpolando gli angoli, da un riquadro 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
Quad fuso.

Ogni quad contiene anche raggi ultravioletti, che vengono generati nel passaggio successivo. Alcuni pennelli contengono vari motivi per dare l'impressione che ogni tratto sembrava diverso. Per ottenere questo risultato, utilizza l'atlazione _texture, in cui ogni texture del pennello contiene tutte le possibili variazioni. La texture 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 trame in un atlante di texture per pennello da olio
Quattro texture in un atlante di texture per pennello a olio
In Tilt Brush
In Tilt Brush
In WebGL
In WebGL

Poiché ogni schizzo ha un numero illimitato di tratti e i tratti non dovranno essere modificati in tempo di esecuzione, precalcolo in anticipo la geometria del tratto e le uniamo in un'unica mesh. Anche se ogni nuovo tipo di pennello deve avere un proprio materiale, in questo modo le chiamate di disegno vengono ridotte a una per pennello.

L'intero schizzo sopra viene eseguito in un'unica chiamata di disegno in WebGL
L'intero schizzo sopra viene eseguito in un'unica chiamata di disegno in WebGL

Per fare uno stress test del sistema, abbiamo creato uno schizzo che ha impiegato 20 minuti a riempire lo spazio con il maggior numero di vertici possibile. Lo schizzo risultante è ancora riprodotto a 60 f/s con WebGL.

Poiché anche ciascuno dei vertici originali di un tratto conteneva tempo, è facile riprodurre i dati. Il ricalcolo dei tratti per frame sarebbe stato molto lento, quindi abbiamo precalcolato l'intero schizzo al caricamento e abbiamo rivelato ogni quad al momento opportuno.

Nascondere un quad significa semplicemente collassare i suoi vertici fino al punto 0,0,0. Quando raggiungi il momento in cui il quadrato dovrebbe essere rivelato, riposiziona i vertici.

Un'area di miglioramento è la manipolazione dei vertici interamente sulla GPU con gli Shader. L'implementazione attuale li inserisce eseguendo il loop dell'array di vertici a partire dal timestamp attuale, controllando quali vertici devono essere rivelati e quindi aggiornando la geometria. Questo carica molto sulla CPU, quindi la ventola gira e spreca la batteria.

Opera d'arte virtuale

Registrazione degli artisti

Abbiamo pensato che gli schizzi da soli non sarebbero stati sufficienti. Volevamo mostrare gli artisti all'interno dei loro schizzi, dipingendo ogni pennellata.

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

Poiché il corpo dell'artista si nascondeva impedendoci di vedere cosa c'è dietro, abbiamo usato un doppio sistema Kinect, entrambi ai lati opposti della stanza puntando al centro.

Oltre alle informazioni sulla profondità, acquisiamo anche le informazioni sul colore della scena con le fotocamere DSLR standard. Abbiamo utilizzato l'eccellente software DepthKit per calibrare e unire i filmati della fotocamera di profondità e delle fotocamere a colori. La Kinect è in grado di registrare i colori, ma abbiamo scelto di usare una fotocamera DSLR perché potevamo controllare le impostazioni di esposizione, usare bellissimi obiettivi di fascia alta e registrare in alta definizione.

Per registrare il filmato, abbiamo costruito una stanza speciale per ospitare l'HTC Vive, l'artista e la videocamera. Tutte le superfici erano ricoperte con materiale che assorbiva la luce a infrarossi per creare una nuvola di punti più pulita (piumino sulle pareti, tappetino di gomma a coste sul pavimento). Nel caso in cui il materiale venisse visualizzato nel filmato della nuvola di punti, abbiamo scelto il materiale nero in modo che non risulti di disturbo quanto un elemento bianco.

Artista musicale

Le registrazioni video ottenute ci hanno fornito informazioni sufficienti per proiettare un sistema particellare. Abbiamo scritto alcuni strumenti aggiuntivi in openFrameworks per ripulire ulteriormente il filmato, in particolare rimuovendo pavimenti, pareti e soffitto.

Tutti e quattro i canali di una sessione video registrata (due canali di colore sopra e due
in profondità sotto)
Tutti e quattro i canali di una sessione video registrata (due canali di colore sopra e due 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 esaltando le letture IR di Kinect), ma ci ha fornito anche punti di contatto per eseguire il debug dell'output delle particelle e allineare i video allo schizzo.

Display montato sulla testa, controller e particelle allineati
Display montato sulla testa, controller e particelle allineati

Per farlo, scrivi un plug-in personalizzato in Tilt Brush che estrae le posizioni dell'HMD e controlla ogni frame. Poiché Tilt Brush funziona a 90 f/s, tonnellate di dati sono state trasmesse in uscita e i dati di input di uno schizzo non erano compressi. Abbiamo utilizzato questa tecnica anche per acquisire eventi che non sono 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 di mirroring.

Durante l'elaborazione dei 4 TB di dati acquisiti, una delle principali sfide è stata l'allineamento di tutte le diverse origini visive/dati. Ogni video di una fotocamera DSLR deve essere allineato con il Kinect corrispondente, in modo che i pixel siano allineati nello spazio e nel tempo. Poi, le riprese di queste due sonde dovevano essere allineate tra loro per formare un unico artista. Poi dovevamo allineare l'artista 3D ai dati acquisiti dal suo disegno. Finalmente. Abbiamo scritto strumenti basati su browser per svolgere la maggior parte di queste attività. Puoi provarli anche tu qui

Artisti Recordin

Una volta allineati i dati, abbiamo utilizzato alcuni script scritti in NodeJS per elaborarli tutti e abbiamo prodotto un file video e una serie di file JSON, tutti tagliati e sincronizzati. Per ridurre le dimensioni del file, abbiamo fatto tre passaggi. Innanzitutto, abbiamo ridotto l'accuratezza di ogni numero in virgola mobile in modo che corrisponda al massimo a tre decimali. In secondo luogo, abbiamo ridotto di un terzo il numero di punti a 30 fps e interpolato le posizioni lato client. Infine, abbiamo serializzato i dati in modo che, invece di utilizzare JSON semplice con coppie chiave/valore, venga creato un ordine dei valori per la posizione e la rotazione dell'HMD e dei controller. In questo modo le dimensioni del file sono state ridotte a 3 MB, che erano accettabili per la distribuzione via cavo.

Artisti registrazioni

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

iOS prevede restrizioni alla riproduzione dei video in linea per evitare che gli utenti siano disturbati dagli annunci video web con riproduzione automatica. Abbiamo utilizzato una tecnica simile ad altre soluzioni alternative sul web, che consiste nel copiare il fotogramma del video in una tela e aggiornare 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 la frequenza fotogrammi di iOS, dato che la copia del pixel buffer dal video al canvas richiede un'elevata intensità di CPU. Per ovviare a questo problema, abbiamo semplicemente pubblicato versioni più piccole degli stessi video che consentono almeno 30 FPS su un iPhone 6.

Conclusione

A partire dal 2016, lo sviluppo di software per la realtà virtuale è d'accordo sul mantenere semplici geometrie e Shadr, in modo da poter correre a 90+ f/s in un HMD. Questo si è rivelato un ottimo obiettivo per le demo WebGL, dato che le tecniche utilizzate in Tilt Brush mappano molto bene con WebGL.

Sebbene i browser web che mostrino mesh 3D complesse non siano di per sé entusiasmanti, questa era una dimostrazione del concetto secondo cui l'impollinazione incrociata del lavoro in VR e del web è del tutto possibile.