Sessioni di arte virtuale

Dettagli sessione artistica

Riepilogo

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

https://g.co/VirtualArtSessions

Che epoca di vita! Con l'introduzione della realtà virtuale come prodotto di consumo, stanno scoprendo nuove e inesplorate possibilità. 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, permane la sensazione di disegnare con controller basati sul movimento, unita alla presenza di essere "in una stanza dotata di superpoteri", e non c'è esperienza come quella di riuscire a disegnare lo spazio vuoto intorno a voi.

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 VR, sul web dove Tilt Brush non funziona ancora. A tal fine, il team ha collaborato uno scultore, un illustratore, un concept designer, un artista di moda, un installazioni e artisti di strada per creare opere d'arte a proprio stile con questo nuovo mezzo.

Registrazione di disegni in realtà virtuale

Integrato in Unity, il software Tilt Brush è un'applicazione desktop che utilizza la VR su scala locale per monitorare la posizione della testa (head mount display o HMD) e i controller in ognuna delle tue mani. L'artwork creata in Tilt Brush viene esportata per impostazione predefinita come file .tilt. Per portare questa esperienza sul web, ci siamo resi conto che dovevamo andare oltre i 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 nel tempo converte più punti in un "tratto". Puoi vedere un esempio qui. Abbiamo scritto plug-in che hanno estratto questi tratti e li hanno restituiti 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 descrive il formato del formato JSON dello schizzo.

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

Vengono salvate le informazioni di base di ogni tratto, in modo da raccogliere tutti i dati relativi a tipo di pennello, dimensione del pennello e predisposizione colore.

Infine, ogni vertice del tratto viene salvato e include la posizione, l'angolo, il tempo, nonché la forza di pressione del trigger del controller (indicata come p all'interno di ciascun punto).

Tieni presente che la rotazione è un quaternione a quattro componenti. Questo aspetto è 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 un codice di generazione della geometria che imitava ciò che fa Tilt Brush in background.

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

Disegni 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 triangolare, eseguiamo l'iterazione su ciascuno di questi punti producendo normali perpendicolari alla direzione e all'orientamento del controller.

Il processo per calcolare la striscia di triangolo per ogni tratto è quasi identico a quello 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 derivare più normali e spesso produrrebbe una "torsione" nella geometria.

Durante l'iterazione sui punti di un tratto, manteniamo il vettore "destra preferita" e lo passiamo alla funzione computeSurfaceFrame(). Questa funzione ci fornisce una normale da cui possiamo ricavare un quad nella striscia quadrata, 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.

Tiri

Dopo aver generato i quadricipiti in base ai punti di controllo di ogni tratto, fondiamo i quadri 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 fusibili
Quad fuso.

Ogni riquadro contiene anche raggi UV, che vengono generati come passaggio successivo. Alcuni pennelli contengono una varietà di modelli di tratto per dare l'impressione che ogni tratto sembrasse un tratto diverso. A questo scopo, utilizza l'atlante dell'_texture, in cui ogni texture del pennello contiene tutte le possibili variazioni. Modificando i valori UV del tratto, viene selezionata la texture corretta.

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 pennello
Quattro texture in un atlante di texture per pennello a olio
Con 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 fase di esecuzione, precalcoliamo in anticipo la geometria del tratto e li uniamo in un'unica mesh. Anche se ogni nuovo tipo di pennello deve essere 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 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 schizzo risultante è stato comunque riprodotto a 60 f/s in WebGL.

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

Nascondere un quad significava semplicemente far collassare i vertici fino al punto 0,0,0. Una volta raggiunto il punto in cui dovrebbe essere mostrato il riquadro, riposiziona i vertici.

Un'area di miglioramento è la manipolazione dei vertici interamente della GPU con gli ombrelli. L'implementazione attuale li inserisce eseguendo il loop dell'array di vertici a partire dal timestamp attuale, controllando quali vertici devono essere visualizzati e poi aggiornando la geometria. Questo carica sulla CPU un carico elevato, che fa girare la ventola e sprecando la batteria.

Opera d'arte virtuale

Registrare gli artisti

Abbiamo pensato che gli schizzi 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. Questo ci permette di 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 utilizzato un doppio sistema Kinect, entrambi sui lati opposti della stanza e puntati verso il centro.

Oltre alle informazioni sulla profondità, abbiamo acquisito anche le informazioni sul colore della scena con 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 a colori, ma abbiamo scelto di usare le DSLR perché potevamo controllare le impostazioni di esposizione, utilizzare bellissimi obiettivi di fascia alta e registrare in alta definizione.

Per registrare il filmato, abbiamo costruito una stanza speciale per l'HTC Vive, l'artista e la fotocamera. Tutte le superfici erano ricoperte di materiale che assorbeva la luce a infrarossi per ottenere una nuvola di punti più pulita (duvetyne alle pareti, tappetino di gomma a coste sul pavimento). Nel caso in cui il materiale venisse visualizzato tra i filmati della nuvola di punti, abbiamo scelto il materiale nero in modo che non risulti distraente quanto bianco.

Artista musicale

Le registrazioni video ottenute ci hanno fornito informazioni sufficienti per proiettare un sistema di particelle. 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 colore sopra e due
in profondità sotto)
Tutti e quattro i canali di una sessione video registrata (due canali a colori sopra e due in profondità sotto)

Oltre a mostrare gli artisti, volevamo 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 dell'HMD non venivano misurate con le letture IR di Kinect), ma ci ha fornito punti di contatto per il debug dell'output delle particelle e per allineare i video allo schizzo.

Il display montato sulla testa, i controller e le particelle in fila
Display montato sulla testa, controller e particelle allineati

Per farlo, scrivi un plug-in personalizzato in Tilt Brush che ha estratto le posizioni dell'HMD e controlla ogni frame. Poiché Tilt Brush funziona a 90 fps, tonnellate di dati sono stati trasmessi in uscita e i dati di input di uno schizzo superavano i 20 MB non 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.

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

Artisti di Recordin
Una volta allineati i dati, abbiamo utilizzato alcuni script scritti in NodeJS per elaborarli tutti e produrre 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 la precisione di ogni numero in virgola mobile in modo che corrisponda al massimo a tre decimali di precisione. In secondo luogo, abbiamo ridotto di un terzo il numero di punti a 30 fps e abbiamo interpolato le posizioni lato client. Infine, abbiamo serializzato i dati in modo che, anziché utilizzare un JSON semplice 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 ridotte a 3 MB, che era accettabile per la pubblicazione via cavo.
Artisti

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 ombreggiatore converte i colori nelle immagini di profondità in posizioni nello spazio 3D. James George ha condiviso un ottimo esempio di come puoi ottenere filmati direttamente da depthKit.

iOS ha restrizioni alla riproduzione dei video in linea, che riteniamo sia quella di evitare che gli utenti siano disturbati dagli annunci video sul web con riproduzione automatica. Abbiamo utilizzato una tecnica simile ad altre soluzioni alternative sul Web, che consiste nel copiare il fotogramma del video su 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 in modo significativo la frequenza fotogrammi iOS, dato che la copia del pixel buffer dal video al canvas richiede molto utilizzo 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

Secondo l'opinione generale dello sviluppo di software di realtà virtuale a partire dal 2016, è consigliabile mantenere semplici geometrie e smoothr, in modo da poter eseguire a più di 90 fps 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é esaltanti, questo è stato un proof of concept del fatto che l'impollinazione incrociata del lavoro di realtà virtuale e del web è del tutto possibile.