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