Resumen
Se invitó a seis artistas a pintar, diseñar y esculpir en RV. Este es el proceso sobre la forma en que registramos sus sesiones, convertimos los datos y presentamos en tiempo real con navegadores web.
https://g.co/VirtualArtSessions
Qué tiempo de vida. Con la introducción de la realidad virtual como consumidor producto, se descubren posibilidades nuevas y sin explorar. Tilt Brush, a producto de Google disponible en el HTC Vive, te permite dibujar en tres espacio dimensional. Cuando probamos Tilt Brush por primera vez, esa sensación de con controles de seguimiento de movimiento y la presencia de "en un con superpoderes" permanezca contigo; en realidad no hay una experiencia que como poder dibujar en el espacio vacío que te rodea.
Al equipo de Data Arts de Google se le presentó el desafío de mostrar esto a los que no tienen un visor de RV, en la Web, donde Tilt Brush no funciona aún operar. Con ese fin, el equipo incorporó a un escultor, un ilustrador, diseñador conceptual, artista de moda, artista de instalaciones y artistas callejeros para crear obras de arte con su propio estilo en este nuevo medio.
Cómo grabar dibujos en realidad virtual
El software Tilt Brush está integrado en Unity y 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 sobre la cabeza o HMD)
y los controles en cada una de tus manos. El material gráfico creado en Tilt Brush es de
de forma predeterminada se exporta como un archivo .tilt
. Para llevar esta experiencia a la Web,
nos dimos cuenta de que necesitábamos más que solo datos de material gráfico. Trabajamos en estrecha colaboración con
El equipo de Tilt Brush modifica Tilt Brush para exportar también acciones para deshacer y eliminar
como la cabeza y la mano
del artista 90 veces por segundo.
Cuando dibujas, Tilt Brush toma la posición y el ángulo del control, y realiza una conversión varios puntos a lo largo del tiempo en un "trazo". Puedes ver un ejemplo aquí. Escribimos complementos que extraían estos trazos y los generaban en formato 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 boceto.
Aquí, cada trazo se guarda como una acción, con el tipo: "STROKE". Además de en las acciones de trazo, queríamos mostrar a un artista cometiendo errores y cambiando mente en medio del boceto, por lo que era fundamental guardar "DELETE" acciones que sirven borrar o deshacer acciones de un trazo completo.
Se guarda la información básica de cada trazo, por lo que el tipo, el tamaño y el color del pincel RGB.
Finalmente, cada vértice del trazo se guarda, lo que incluye la posición,
la hora, así como la intensidad de presión del gatillo del control (notada 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 renderizamos los trazos para evitar el bloqueo cardinal.
Reproducción de bocetos con WebGL
Para mostrar los bocetos en un navegador web, usamos THREE.js y escribiste código de generación de geometría que imitaba el lo que hace Tilt Brush debajo del capó.
Mientras que Tilt Brush produce tiras triangulares en tiempo real en función de la mano del usuario movimiento, la totalidad del boceto ya está "terminado" para cuando lo mostremos en la Web. Esto nos permite evitar gran parte del cálculo en tiempo real y preparar la geometría cuando se carga.
Cada par de vértices de un trazo produce un vector de dirección (las líneas azules
conecta cada punto como se muestra arriba, moveVector
en el fragmento de código a continuación).
Cada punto también contiene una orientación, un cuaternión que representa la
el ángulo actual del controlador. Para producir una tira triangular, iteramos sobre cada uno de
estos puntos producen normales que son perpendiculares a la dirección y
la orientación del control.
El proceso para calcular la tira 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);
}
Combinar la dirección y la orientación del trazo por sí mismo devuelve resultados matemáticamente ambiguos; puede haber múltiples normales derivadas y generaría un "giro" en la geometría.
Al iterar sobre los puntos de un trazo, mantenemos un "derecho preferido"
vector y pasa esto a la función computeSurfaceFrame()
. Esta función
nos da una normal de la que podemos derivar un cuadrante en la franja de cuadrantes, según
la dirección del trazo (desde el último punto hasta el punto actual) y el
del controlador (un cuaternión). Lo más importante es que también devuelve
un nuevo "preferido a la derecha" para el siguiente conjunto de cálculos.
Después de generar cuádruplos basados en los puntos de control de cada trazo, fusionamos los cuadráticos a través de la interpolación de sus esquinas, de un cuadrante 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 cuadrante también contiene 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 sintió como un trazo diferente del pincel. Esto se logra usando _atlas de texturas, _donde cada textura de pincel contiene todos los elementos variaciones. Se selecciona la textura correcta modificando los valores de UV del .
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 un número ilimitado de trazos, y los trazos no necesitan se modifica en el tiempo de ejecución, procesamos previamente la geometría del trazo y la combinamos en una sola malla. Aunque cada tipo de pincel nuevo debe ser propio y reduce las llamadas de dibujo a una por pincel.
Para poner a prueba el sistema, creamos un boceto que tomó 20 minutos para llenar la con tantos vértices como pudiéramos. El boceto resultante seguía reproduciéndose en 60 FPS en WebGL
Como cada uno de los vértices originales de un trazo también contenía tiempo, podemos reproducir fácilmente los datos. Volver a calcular los trazos por fotograma sería realmente lenta, entonces, en cambio, calculamos previamente todo el esbozo en la carga y simplemente revelamos cada cuadrante cuando llegó el momento de hacerlo.
Ocultar un cuadrilátero significaba contraer sus vértices hasta el punto 0,0,0. Cuando hasta el punto en el que se supone que se revela el cuadrante, y reposicionar los vértices nuevamente 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 realizando un bucle en el vértice. array a partir de la marca de tiempo actual, que verifica qué vértices deben revelarse y, luego, actualizando la geometría. Esto ejerce mucha carga en la CPU, lo que provoca para que gire el ventilador y se desperdicie la duración de la batería.
Grabación de los artistas
Nos dimos cuenta de que los bocetos en sí no serían suficientes. Queríamos mostrarles artistas dentro de sus bocetos, pintando cada pincelada.
Para capturar a los artistas, usamos cámaras Microsoft Kinect para registrar la profundidad. y los datos de las sesiones de reproducción de los artistas en el espacio. Esto nos da la capacidad de mostrar sus figuras tridimensionales en el mismo espacio en el que aparecen los dibujos.
Como el cuerpo del artista se ocultaría detrás, usábamos 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 con cámaras DSLR estándar. Usamos la excelente Software DepthKit para calibrarlo y combinarlo el video de la cámara de profundidad y de color. Kinect puede de grabación de color, pero elegimos usar cámaras DSLR porque podíamos controlar la configuración de exposición, usa hermosos lentes de alta gama y graba en alta definición.
Para grabar las imágenes, construimos una habitación especial para guardar el HTC Vive, el artista y la cámara. Todas las superficies estaban cubiertas de material que absorbía los infrarrojos para darnos una nube de puntos más limpia (duvetyne en las paredes, goma acanalada en el suelo). En caso de que el material haya aparecido en la nube de puntos video, elegimos material negro para que no distrajera tanto como algo que era blanco.
Las grabaciones de video resultantes nos brindaron suficiente información para proyectar una partícula. en un sistema de archivos. Escribimos algunas herramientas adicionales en openFrameworks para mejorar la calidad del video en particular, retirar pisos, paredes y cielorraso.
Además de mostrar a los artistas, queríamos renderizar la HMD y el controladores en 3D. Esto no solo era importante para mostrar la HMD en claramente el resultado final (los lentes reflectantes del HTC Vive estaban desprendiendo lecturas IR de Kinect), nos proporcionó puntos de contacto para depurar la partícula del resultado y alinear los videos con el boceto.
Para ello, se escribió un complemento personalizado en Tilt Brush que extrajo el de la HMD y los controladores en cada trama. Como Tilt Brush se ejecuta a 90 FPS, toneladas de datos transmitidos 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 están registrados. en el típico archivo de guardado 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 y alinear todas las fuentes visuales o de datos. Cada video de una cámara DSLR deben alinearse con el Kinect correspondiente, de modo que los píxeles alineados espacio, así como tiempo. Luego, las imágenes de estos dos equipos de cámaras debían se alinean entre sí para formar un solo artista. Luego, debíamos alinear los modelos 3D artista con los datos obtenidos de su dibujo. ¡Vaya! Escribimos los navegadores herramientas para ayudarte con la mayoría de estas tareas, y puedes probarlas tú mismo aquí
Una vez alineados los datos, usamos algunas secuencias de comandos escritas en NodeJS para procesarlos. y genera un archivo de video y una serie de archivos JSON, todos cortados y sincronizada. Para reducir el tamaño del archivo, hicimos tres cosas. Primero, redujimos la precisión de cada número de punto flotante para que esté en un máximo de 3 de precisión de un decimal. Segundo, reducimos el número de puntos por un tercio 30 fps e interpolaron las posiciones del lado del cliente. Por último, serializamos de modo que en lugar de usar JSON sin formato con pares clave-valor, se crea un orden de valores crear para la posición y rotación de la HMD y los controladores. Esto corta el archivo con un tamaño mínimo de 3 MB, que es aceptable entregar por cable.
Como el video en sí se publica como un elemento de video HTML5 que lee un para que la textura de WebGL se convirtiera en partículas, el video en sí necesitaba reproducirse oculto en el en segundo plano. Un sombreador convierte los colores de las imágenes de profundidad en posiciones de Espacio 3D. James George compartió un excelente ejemplo de cómo puedes hacer grabaciones usando DepthKit.
iOS tiene restricciones para la reproducción de videos en línea, lo que suponemos que evita que los anuncios de video web que se reproducen automáticamente se molesten con los usuarios. Utilizamos una técnica similar a otras soluciones alternativas en el web, que consiste en copiar el fotograma en un lienzo y actualizar manualmente el tiempo de búsqueda del video, cada 1/30 de un 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 disminuir velocidad de fotogramas, ya que la copia del búfer de píxeles del video al lienzo es y hacen un uso intensivo de la CPU. Para solucionar esto, simplemente publicamos 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 mantener geometrías y sombreadores simples para que puedas ejecutar a más de 90 FPS en una HMD. Esta resultó ser un gran objetivo para las demostraciones de WebGL, ya que las técnicas en Tilt Brush se mapean muy bien a WebGL.
Si bien los navegadores web que muestran mallas 3D complejas no son muy interesantes en y de era una prueba de concepto de que la polinización cruzada del trabajo web es completamente posible.