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 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.

Obra de arte virtual

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.

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 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.

Trazos

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 );
}
Cuadrículas fusionados
Cuadrículas fusionados.

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

    });

}
Cuatro texturas en un atlas de texturas para el pincel de aceite
Cuatro texturas en un atlas de texturas para el pincel de óleo
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 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.

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

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.

Obra de arte virtual

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.

Artista de la grabación

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.

Los cuatro canales de una sesión de video grabada (dos canales de color arriba y dos de profundidad)
Los cuatro canales de una sesión de video grabada (dos canales de color en la parte superior y dos en profundidad)

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.

La pantalla montada en el cabezal, los controles y las partículas alineados
La pantalla montada en el cabezal, los controles y las partículas alineadas

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í.

Artistas discográficos
Una vez alineados 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 precisión de cada número de punto flotante para que tengan un máximo de 3 decimales. En segundo lugar, reducimos la cantidad de puntos en un tercio a 30 FPS y, luego, interpolamos las posiciones 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 3 MB, lo cual era aceptable para entregarlo por cable.
Artistas de grabación

Dado que el video en sí se sirve como un elemento de video HTML5 que se lee en una textura de WebGL para convertirse en partículas, el video en sí tiene que reproducirse oculto en segundo plano. Un sombreador convierte los colores de las imágenes de profundidad en posiciones en un espacio 3D. James George compartió un excelente ejemplo de lo que puedes hacer con las grabaciones directamente de DepthKit.

iOS tiene restricciones para la reproducción de videos intercalados, lo que asumimos que es para evitar que los usuarios sean molestos por los 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 marco 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 lamentable 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 CPU muy intensivo. Para evitar esto, simplemente entregamos versiones más pequeñas 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 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 excelente 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 es emocionante en sí mismo, esta fue una prueba de concepto de que la polinización cruzada del trabajo de RV y la Web es completamente posible.