
Resumen
Se invitó a seis artistas a pintar, diseñar y esculpir en RV. Este es el proceso de cómo grabamos sus sesiones, convertimos los datos y los presentamos en tiempo real con navegadores web.
https://g.co/VirtualArtSessions
¡Qué época para vivir! Con la introducción de la realidad virtual como producto para consumidores, se están descubriendo posibilidades nuevas y sin explorar. Tilt Brush, un producto de Google disponible en HTC Vive, te permite dibujar en un espacio tridimensional. Cuando probamos Tilt Brush por primera vez, la sensación de dibujar con controles de seguimiento de movimiento junto con la presencia de estar “en una habitación con superpoderes” permanece en tu mente. No hay una experiencia como la de poder dibujar en el espacio vacío a tu alrededor.

El equipo de Artes de Datos de Google se enfrentó al desafío de mostrar esta experiencia a quienes no tienen visores de RV, en la Web, donde Tilt Brush aún no opera. Para ello, el equipo contrató a un escultor, un ilustrador, un diseñador de conceptos, un artista de moda, un artista de instalaciones y artistas callejeros para crear obras de arte en su propio estilo dentro de este nuevo medio.
Cómo grabar dibujos en realidad virtual
Compilado en Unity, el software Tilt Brush es una aplicación para computadoras de escritorio que usa la RV de sala para hacer un seguimiento de la posición de la cabeza (gafas de realidad virtual o HMD) y los controladores en cada una de tus manos. El material gráfico creado en Tilt Brush se exporta de forma predeterminada como un archivo .tilt
. Para llevar esta experiencia a la Web, nos dimos cuenta de que necesitábamos más que solo los datos del material gráfico. Trabajamos en estrecha colaboración con el equipo de Tilt Brush para modificarlo de modo que exportara las acciones de deshacer o borrar, así como las posiciones de la cabeza y la mano del artista 90 veces por segundo.
Cuando dibujas, Tilt Brush toma la posición y el ángulo del controlador y convierte varios puntos a lo largo del tiempo en un “trazo”. Puedes ver un ejemplo aquí. Escribimos complementos que extraen estos trazos y los muestran como JSON sin procesar.
{
"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
]
}
En el fragmento anterior, se describe el formato del JSON del boceto.
Aquí, cada trazo se guarda como una acción, con el tipo "STROKE". Además de las acciones de trazo, queríamos mostrar a un artista cometiendo errores y cambiando de opinión en medio del boceto, por lo que era fundamental guardar las acciones "DELETE", que sirven como acciones de borrar o deshacer para un trazo completo.
Se guarda la información básica de cada trazo, por lo que se recopilan el tipo de pincel, el tamaño del pincel y el color RGB.
Por último, se guarda cada vértice del trazo, lo que incluye la posición, el ángulo, el tiempo y la fuerza de presión del activador del controlador (que se indica como p
dentro de cada punto).
Ten en cuenta que la rotación es un cuaternion de 4 componentes. Esto es importante más adelante cuando renderizamos los trazos para evitar el bloqueo del gimbal.
Cómo reproducir bocetos con WebGL
Para mostrar los bocetos en un navegador web, usamos THREE.js y escribimos código de generación de geometría que imitaba lo que hace Tilt Brush en su funcionamiento interno.
Si bien Tilt Brush produce tiras triangulares en tiempo real según el movimiento de la mano del usuario, todo el boceto ya está “terminado” cuando lo mostramos en la Web. Esto nos permite omitir gran parte del cálculo en tiempo real y compilar la geometría durante la carga.

Cada par de vértices de un trazo produce un vector de dirección (las líneas azules que conectan cada punto como se muestra arriba, moveVector
en el siguiente fragmento de código).
Cada punto también contiene una orientación, un cuaternion que representa el ángulo actual del controlador. Para producir una tira de triángulos, iteramos sobre cada uno de estos puntos y generamos normales perpendiculares a la dirección y la orientación del controlador.
El proceso para calcular la tira de triángulos para cada trazo es casi idéntico al código que se usa en 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);
}
Combinar la dirección y la orientación del trazo por sí solos muestra resultados matemáticamente ambiguos; podría haber varias normales derivadas y, a menudo, se produce un "giro" en la geometría.
Cuando iteramos sobre los puntos de un trazo, mantenemos un vector "derecho preferido" y lo pasamos a la función computeSurfaceFrame()
. Esta función nos brinda un vector normal a partir del cual podemos derivar un cuádruple en la tira cuádruple, según la dirección del trazo (del último punto al punto actual) y la orientación del controlador (un cuaternion). Lo más importante es que también muestra un nuevo vector "derecho preferido" para el siguiente conjunto de cálculos.

Después de generar cuadrángulos en función de los puntos de control de cada trazo, combinamos los cuadrángulos interpolando sus esquinas, de un cuadrángulo al siguiente.
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 );
}

Cada cuádruple también contiene UV que se generan como un paso adicional. Algunos pinceles contienen una variedad de patrones de trazo para dar la impresión de que cada trazo se siente como un trazo diferente del pincel. Esto se logra con el _atlas de texturas_, en el que cada textura del pincel contiene todas las variaciones posibles. Para seleccionar la textura correcta, se modifican los valores UV del trazo.
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 );
});
}



Dado que cada boceto tiene una cantidad ilimitada de trazos y no es necesario modificarlos durante el tiempo de ejecución, precalculamos la geometría del trazo con anticipación y los combinamos en una sola malla. Aunque cada tipo de pincel nuevo debe ser su propio material, eso aún reduce nuestras llamadas de dibujo a una por pincel.

Para realizar una prueba de esfuerzo del sistema, creamos un boceto que tardó 20 minutos en llenar el espacio con tantos vértices como pudimos. El boceto resultante se seguía reproduciendo a 60 fps en WebGL.
Como cada uno de los vértices originales de un trazo también contenía tiempo, podemos reproducir los datos con facilidad. Volver a calcular los trazos por fotograma sería muy lento, por lo que, en su lugar, calculamos previamente todo el boceto durante la carga y simplemente revelamos cada cuadrángulo cuando llegó el momento de hacerlo.
Ocultar un cuádruple simplemente significaba contraer sus vértices al punto 0,0,0. Cuando el tiempo llega al punto en el que se supone que se debe revelar el cuádruple, volvemos a colocar los vértices en su lugar.
Un área de mejora es manipular los vértices por completo en la GPU con sombreadores. La implementación actual los coloca haciendo un bucle a través del array de vértices desde la marca de tiempo actual, verificando qué vértices deben revelarse y, luego, actualizando la geometría. Esto genera mucha carga en la CPU, lo que hace que el ventilador gire y desperdicie la duración de la batería.

Grabación de los artistas
Sentimos que los bocetos por sí solos no serían suficientes. Queríamos mostrar a los artistas dentro de sus bocetos, pintando cada pincelada.
Para capturar a los artistas, usamos cámaras Microsoft Kinect para grabar los datos de profundidad del cuerpo de los artistas en el espacio. Esto nos permite mostrar sus figuras tridimensionales en el mismo espacio en el que aparecen los dibujos.
Como el cuerpo del artista se ocluiría y nos impediría ver lo que hay detrás, usamos un sistema Kinect doble, ambos en lados opuestos de la habitación, apuntando al centro.
Además de la información de profundidad, también capturamos la información de color de la escena con cámaras DSLR estándar. Usamos el excelente software DepthKit para calibrar y combinar el material de la cámara de profundidad y las cámaras de color. Kinect puede grabar en color, pero elegimos usar cámaras réflex digitales porque podíamos controlar la configuración de exposición, usar lentes de alta gama y grabar en alta definición.
Para grabar el video, armamos una sala especial para alojar el HTC Vive, el artista y la cámara. Todas las superficies se cubrieron con material que absorbía la luz infrarroja para brindarnos una nube de puntos más limpia (duvetyne en las paredes, alfombra de goma con nervaduras en el piso). En caso de que el material apareciera en el material de la nube de puntos, elegimos el material negro para que no fuera tan distraído como algo blanco.

Las grabaciones de video resultantes nos brindaron suficiente información para proyectar un sistema de partículas. Escribimos algunas herramientas adicionales en openFrameworks para limpiar aún más el video, en particular, quitar los pisos, las paredes y el techo.

Además de mostrar a los artistas, queríamos renderizar el HMD y los controles en 3D. Esto no solo fue importante para mostrar el HMD en el resultado final con claridad (los lentes reflectantes del HTC Vive arrojaban lecturas de IR de Kinect), sino que nos proporcionó puntos de contacto para depurar el resultado de las partículas y alinear los videos con el boceto.

Para ello, se escribió un complemento personalizado en Tilt Brush que extraía las posiciones del HMD y los controladores en cada fotograma. Dado que Tilt Brush se ejecuta a 90 fps, se transmitieron toneladas de datos, y los datos de entrada de un boceto superaron los 20 MB sin comprimir. También usamos esta técnica para capturar eventos que no se registran en el archivo de guardado típico de Tilt Brush, como cuando el artista selecciona una opción en el panel de herramientas y la posición del widget de espejo.
Al procesar los 4 TB de datos que capturamos, uno de los mayores desafíos fue alinear todas las diferentes fuentes visuales o de datos. Cada video de una cámara DSLR debe alinearse con el Kinect correspondiente para que los píxeles se alineen en el espacio y en el tiempo. Luego, el material de estas dos plataformas de cámara debía alinearse para formar un solo artista. Luego, tuvimos que alinear a nuestro artista en 3D con los datos capturados de su dibujo. ¡Vaya! Escribimos herramientasbasadas en el navegador para ayudar con la mayoría de estas tareas, y puedes probarlas aquí

Una vez que se alinearon los datos, usamos algunas secuencias de comandos escritas en NodeJS para procesarlos todos y generar un archivo de video y una serie de archivos JSON, todos recortados y sincronizados. Para reducir el tamaño del archivo, hicimos tres cosas. Primero, reducimos la precisión de cada número de punto flotante para que tengan un máximo de 3 decimales de precisión. En segundo lugar, reducimos la cantidad de puntos a un tercio para obtener 30 fps y, luego, interpolamos las posiciones del cliente. Por último, serializamos los datos para que, en lugar de usar JSON sin formato con pares clave-valor, se cree un orden de valores para la posición y la rotación de los HMD y los controladores. Esto redujo el tamaño del archivo a poco menos de 3 MB, que era aceptable para enviar por cable.

Dado que el video se entrega como un elemento de video HTML5 que lee una textura WebGL para convertirse en partículas, el video debía reproducirse oculto en segundo plano. Un sombreador convierte los colores de las imágenes de profundidad en posiciones en el espacio 3D. James George compartió un gran ejemplo de lo que puedes hacer con el material de DepthKit.
iOS tiene restricciones en la reproducción de videos intercalados, lo que suponemos que es para evitar que los usuarios se vean acosados por anuncios de video web que se reproducen automáticamente. Usamos una técnica similar a otras soluciones alternativas en la Web, que consiste en copiar el fotograma del video en un lienzo y actualizar manualmente el tiempo de búsqueda del video cada 1/30 de segundo.
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 );
Nuestro enfoque tuvo el desafortunado efecto secundario de reducir significativamente la velocidad de fotogramas de iOS, ya que la copia del búfer de píxeles del video al lienzo es muy intensiva en la CPU. Para solucionar este problema, simplemente publicamos versiones de menor tamaño de los mismos videos que permiten al menos 30 fps en un iPhone 6.
Conclusión
El consenso general para el desarrollo de software de RV a partir de 2016 es mantener las geometrías y los sombreadores simples para que puedas ejecutar más de 90 fps en un HMD. Esto resultó ser un objetivo excelente para las demostraciones de WebGL, ya que las técnicas que se usan en Tilt Brush se ajustan muy bien a WebGL.
Si bien los navegadores web que muestran mallas 3D complejas no son emocionantes en sí mismos, esta fue una prueba de concepto que demuestra que la polinización cruzada del trabajo de la realidad virtual y la Web es completamente posible.