Sesiones virtuales de arte

Detalle de la sesión artística

Resumen

Se invitó a seis artistas a pintar, diseñar y esculpir en RV. Este es el proceso de cómo registramos sus sesiones, convertimos los datos y los presentamos en tiempo real con navegadores web.

https://g.co/VirtualArtSessions

¡Qué momento de vivir! Con la introducción de la realidad virtual como un producto para usuarios, 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". Realmente no existe una experiencia como poder dibujar en el espacio vacío a tu alrededor.

Obra de arte virtual

Al equipo de Data Arts de Google se le presentó el desafío de mostrar esta experiencia a quienes no tienen un visor de RV, en la Web donde Tilt Brush aún no opera. Con ese fin, el equipo incorporó a un escultor, un ilustrador, un diseñador de conceptos, un artista de moda, un instalador y artistas callejeros para que crearan obras de arte con su propio estilo dentro de este nuevo medio.

Cómo grabar dibujos en realidad virtual

Integrado en Unity, el software Tilt Brush en sí mismo es una aplicación para computadoras que usa RV a escala de habitación para hacer un seguimiento de la posición de tu cabeza (pantalla montada en la cabeza, o HMD) y los controles 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 algo más que solo los datos del material gráfico. Trabajamos estrechamente con el equipo de Tilt Brush para modificar Tilt Brush para que exportó las acciones de deshacer y eliminar, así como las posiciones de la cabeza y las manos del artista 90 veces por segundo.

Cuando dibujas, Tilt Brush adopta la posición y el ángulo del control y convierte varios puntos a lo largo del 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 el tipo: "STROKE". Además de las acciones de trazo, quisimos mostrar a un artista cometiendo errores y cambiando de opinión en medio del boceto. Por lo tanto, era fundamental guardar las acciones de "DELETE" que sirven como acciones de borrar o deshacer 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 la iluminación 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 activador del controlador (anotada 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 esbozos 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 de forma interna.

Si bien Tilt Brush produce bandas triangulares en tiempo real según el movimiento de la mano del usuario, el boceto ya estará "terminado" para cuando lo mostremos en la Web. Esto nos permite omitir gran parte del cálculo en tiempo real y preparar la geometría en la carga.

Bocetos de WebGL

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

Cuando se combina la dirección y la orientación del trazo por sí solas, se obtienen resultados matemáticamente ambiguos: podría haber varias normales derivadas y, a menudo, se generaría un "giro" en la geometría.

Cuando se itera sobre los puntos de un trazo, se mantiene un vector "preferido a la derecha" y lo pasamos a la función computeSurfaceFrame(). Esta función nos proporciona un normal desde el cual podemos derivar un cuadrante en la franja del cuadrante, 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 más importante es que también muestra un nuevo vector de “preferido a la derecha” para el siguiente conjunto de cálculos.

Accidentes cerebrovasculares

Después de generar cuadrantes basados en los puntos de control de cada trazo, fusionamos los cuadrantes mediante la 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 );
}
Cuatriciclos fusionados
Cuatriciclos fusionados

Cada cuadrante también contiene las islas UV que se generan como un paso siguiente. 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, _donde cada textura de 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 );

    });

}
Cuatro texturas en un atlas de texturas para un pincel de aceite
Cuatro texturas en un atlas de texturas para pincel de aceite
En Tilt Brush
En Tilt Brush
En WebGL
En WebGL

Dado que cada esbozo tiene un número ilimitado de trazos y no será necesario modificar los trazos durante el tiempo de ejecución, calculamos previamente la geometría del trazo con anticipación y los combinamos en una sola malla. Si bien cada nuevo tipo de pincel debe tener material propio, las llamadas de dibujo se reducen a una por pincel.

El boceto anterior se realiza en una llamada de dibujo en WebGL.
El esbozo anterior se realiza en una llamada de dibujo en WebGL

Para poner a prueba el sistema, creamos un esbozo que llevó 20 minutos y llenamos 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, en su lugar, calculamos previamente todo el esbozo sobre la carga y simplemente revelamos cada cuadrante cuando llegó el momento de hacerlo.

Ocultar un cuadrante significaba contraer sus vértices al punto 0,0,0. Cuando el tiempo ha llegado al punto en el que se supone que se debe revelar el cuadrante, volvemos a colocar los vértices en su lugar.

Un área que se debe mejorar es la manipulación de los vértices por completo en la GPU con sombreadores. La implementación actual los coloca mediante un bucle en el array de vértices de la marca de tiempo actual, verificando qué vértices deben revelarse y, luego, actualizando la geometría. Esto supone mucha carga en la CPU, lo que hace que el ventilador gire y, además, desperdicia la duración de la batería.

Obra de arte virtual

Cómo grabar los artistas

Sentimos 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 para registrar 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 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 hacia el 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. 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 hermosos lentes de alta definición y grabar en alta definición.

Para grabar las imágenes, construimos una habitación especial que albergará al HTC Vive, al artista y a la cámara. Todas las superficies estaban cubiertas de material que absorbía la luz infrarroja para brindar una nube de puntos más limpia (acolchado en las paredes, goma viscosa en el piso). En caso de que el material apareciera en el material de imágenes de la nube de puntos, elegimos material negro para que no distrajera tanto como el material blanco.

Artista musical

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

Los cuatro canales de una sesión de video grabada (dos canales de color arriba y dos de profundidad debajo)
Los cuatro canales de una sesión de video grabada (dos canales de color arriba y dos de profundidad)

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

Cabeza de pantalla, controladores y partículas alineados
Pantalla, controles y partículas alineados en el cabezal alineados

Para ello, se escribió un complemento personalizado en Tilt Brush que extrajo las posiciones de las HMD y los controladores de cada fotograma. Dado que 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.

Cuando se procesaron los 4 TB de datos que capturamos, uno de los mayores desafíos fue alinear las diferentes fuentes visuales y 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, las imágenes de estos dos soportes de cámara debían alinearse entre sí para formar un solo artista. Luego, necesitábamos alinear a nuestro artista 3D con los datos obtenidos de su dibujo. ¡Vaya! Redactamos herramientas basadas en el navegador para ayudar con la mayoría de estas tareas. Puedes probarlas por tu cuenta aquí.

Artistas de grabación

Una vez que se alinearon los datos, usamos algunas secuencias de comandos escritas en Node.js para procesar todo y generar un archivo de video y una serie de archivos JSON, todos cortados y sincronizados. Para reducir el tamaño del archivo, hicimos tres cosas. Primero, redujimos la exactitud de cada número de punto flotante para que tengan el valor máximo de precisión de 3 decimales. En segundo lugar, reducimos la cantidad de puntos en un tercio a 30 FPS y, luego, interpolamos las posiciones del lado del cliente. Por último, serializamos los datos de modo que, en lugar de usar JSON sin formato con pares clave-valor, se crea un orden de valores para la posición y la rotación de las HMD y los controladores. Esto redujo el tamaño del archivo a solo 3 MB, lo cual era aceptable para entregar por cable.

Artistas de grabación

Dado que el video en sí se entrega como un elemento de video HTML5 que lee una textura de 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 excelente ejemplo de cómo puedes producir videos directamente desde DepthKit.

iOS tiene restricciones para la reproducción de videos intercalados, que asumimos que es para evitar que los anuncios de video web que se reproducen automáticamente se molesten a los usuarios. Usamos una técnica similar a otras soluciones alternativas en la Web, que consiste en copiar el fotograma 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 requiere una gran cantidad de CPU. Para solucionarlo, simplemente entregamos versiones más pequeñas de los mismos videos que permiten al menos 30 FPS en un iPhone 6.

Conclusión

Desde 2016, el consenso general para el desarrollo de software de RV es que las geometrías y los sombreadores sean simples, de modo que puedas ejecutar a más de 90 FPS en una HMD. Esto resultó ser un objetivo muy bueno para las demostraciones de WebGL, ya que las técnicas utilizadas en Tilt Brush se asignan muy bien a WebGL.

Si bien los navegadores web que muestran mallas 3D complejas no son interesantes en sí mismos, esta prueba de concepto demuestra que la polinización cruzada del trabajo de RV y la Web es totalmente posible.