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é momento de vida! Con la introducción de la realidad virtual como un producto para consumidores, se descubren 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, perdura la sensación de dibujar con controles con seguimiento de movimiento junto con la presencia de estar "en una habitación con superpoderes". En realidad, no hay una experiencia similar a poder dibujar en el espacio vacío a tu alrededor.
Al equipo de Data Arts de Google se le presentó el desafío de mostrarles esta experiencia a quienes no tenían un visor de RV, en la Web donde aún no funciona Tilt Brush. Con ese fin, el equipo incorporó a un escultor, un ilustrador, un diseñador de conceptos, un artista de la moda, un instalador y artistas callejeros para crear obras de arte con su propio estilo dentro de este nuevo medio.
Cómo grabar dibujos en realidad virtual
Integrado en Unity, el software de Tilt Brush en sí es una aplicación para computadoras que usa RV a escala de habitación para hacer un seguimiento de la posición de la cabeza (pantalla montada en la cabeza o HMD) y los controles de cada una de las 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 algo más que los datos del material gráfico. Trabajamos en estrecha colaboración con el equipo de Tilt Brush a fin de modificar este objeto para exportar acciones de deshacer y borrar, así como las posiciones de la cabeza y las manos del artista 90 veces por segundo.
Cuando dibujas, Tilt Brush toma la posición y el ángulo del control y convierte varios puntos en el tiempo en un "trazo". Puedes ver un ejemplo aquí. Escribimos complementos que extraían estos trazos y los mostraban 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 formato JSON de esbozo.
Aquí, cada trazo se guarda como una acción, con un tipo: "STROKE". Además de las acciones de trazo, quisimos mostrar a un artista cometiendo errores y cambiando su mente en medio del boceto, por lo que fue fundamental guardar las acciones "DELETE", que sirven como borrar o deshacer acciones para un trazo completo.
Se guarda la información básica de cada trazo para que se recopilen el tipo de pincel, el tamaño del pincel y el filtro de color.
Por último, se guarda cada vértice del trazo, lo que incluye la posición, el ángulo, el tiempo y la intensidad de la presión del gatillo del control (se indica como p
dentro de cada punto).
Ten en cuenta que la rotación es un cuaternión de 4 componentes. Esto es importante más adelante, cuando renderices los trazos para evitar el bloqueo de estabilizador.
Cómo reproducir bocetos con WebGL
Para mostrar los bocetos en un navegador web, usamos THREE.js y escribimos un código de generación de geometría que imitaba lo que hace Tilt Brush en niveles más profundos.
Si bien Tilt Brush produce franjas 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 preparar la geometría en 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 cuaternión que representa el ángulo actual del control. Para producir una franja triangular, iteramos sobre cada uno de estos puntos para producir normales que son perpendiculares a la dirección y a la orientación del control.
El proceso para calcular la franja triangular 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);
}
Si combinas la dirección y la orientación del trazo por sí mismas, obtendrás resultados matemáticamente ambiguos: podría haber varias normales derivadas y, a menudo, generaría un "giro" en la geometría.
Cuando se itera sobre los puntos de un trazo, mantenemos un vector "preferido a la derecha" y lo pasamos a la función computeSurfaceFrame()
. Esta función nos proporciona una función normal desde la que podemos derivar un cuadrante en la franja de cuatro según la dirección del trazo (desde el último punto hasta el punto actual) y la orientación del controlador (un cuaternión). Lo que es más importante, también muestra un nuevo vector "preferido a la derecha" para el siguiente conjunto de cálculos.
Después de generar cuádruples basados en los puntos de control de cada trazo, fusionamos los cuádruples con una interpolación de sus esquinas, de un cuadrante a otro.
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 cuadrante también contiene las UV, que se generan como un paso siguiente. Algunos pinceles contienen una variedad de patrones para dar la impresión de que cada trazo se sintió como un trazo diferente del pincel. Esto se logra con el atlas _texture, _donde la textura de cada pincel contiene todas las variaciones posibles. Se selecciona la textura correcta modificando los valores de 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 esbozo tiene un número ilimitado de trazos y no será necesario modificar estos durante el tiempo de ejecución, calculamos previamente la geometría de trazo con anticipación y la combinamos en una sola malla. Si bien cada nuevo tipo de pincel debe ser su propio material, eso reduce nuestras llamadas de dibujo a una por pincel.
Para someter el sistema a una prueba de esfuerzo, creamos un esbozo que tardó 20 minutos en llenar el espacio con tantos vértices como pudimos. El boceto resultante se reprodujo a 60 fps en WebGL.
Dado que 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 calculamos previamente todo el esbozo cuando se carga y simplemente revelamos cada cuadrante cuando llegó el momento de hacerlo.
Ocultar un cuadrante significaba simplemente contraer sus vértices al punto 0,0,0. Cuando el tiempo ha llegado al punto en el que se supone que se revela el cuadrante, volvemos a colocar los vértices en su lugar.
Un área de mejora es la manipulación de los vértices por completo en la GPU con sombreadores. La implementación actual los ubica mediante 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 se desperdicia la duración de la batería.
Grabación de los artistas
Sentíamos que los bocetos en sí 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 a fin de registrar los datos de profundidad de su cuerpo en el espacio. Esto nos permite mostrar sus figuras tridimensionales en el mismo espacio en el que aparecen los dibujos.
Dado que el cuerpo del artista se ocluía y nos impedía ver lo que hay detrás, utilizamos 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 las imágenes de la cámara de profundidad y las de color. La cámara Kinect es capaz de grabar color, pero elegimos usar cámaras réflex digitales porque podíamos controlar la configuración de exposición, usar hermosas lentes de alta gama y grabar en alta definición.
Para grabar las imágenes, construimos una habitación especial en la que se aloja el HTC Vive, el artista y la cámara. Todas las superficies estaban cubiertas con material que absorbía la luz infrarroja para crear una nube de puntos más limpia (duvetyne en las paredes, revestimiento de goma acanalada en el suelo). En caso de que el material apareciera en el material de imágenes de la nube de puntos, elegimos el material negro para que no distrajera tanto como algo que era blanco.
Las grabaciones de video resultantes proporcionaron suficiente información para proyectar un sistema de partículas. Redactamos algunas herramientas adicionales en openFrameworks para limpiar aún más el video, en particular, quitar pisos, paredes y techos.
Además de mostrar a los artistas, queríamos renderizar la HMD y los controladores en 3D. Esto no solo era importante para mostrar claramente la HMD en la salida final (las lentes reflectantes de HTC Vive se quitaban las lecturas de IR de Kinect), sino que nos dio puntos de contacto para depurar la salida de partículas y alinear los videos con el esbozo.
Para ello, se escribió un complemento personalizado en Tilt Brush que extrajo las posiciones de la HMD y los controladores de cada fotograma. Como Tilt Brush se ejecuta a 90 FPS, se transmitieron toneladas de datos y los datos de entrada de un esbozo superaron los 20 MB sin comprimir. También usamos esta técnica para capturar eventos que no se graban 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 duplicación.
Durante el procesamiento de los 4 TB de datos que capturamos, uno de los mayores desafíos era alinear las diferentes fuentes visuales y de datos. Cada video de una cámara réflex digital debe alinearse con el Kinect correspondiente para que los píxeles estén alineados en el espacio y en el tiempo. Luego, las imágenes de estos dos soportes de cámara debían alinearse entre sí para formar un solo artista. Luego, tuvimos que alinear nuestro artista 3D con los datos recogidos en su dibujo. ¡Vaya! Escribimos herramientas basadas en el navegador para ayudarte con la mayoría de estas tareas. Puedes probarlas tú mismo aquí.