Sessões virtuais de arte

Detalhe da sessão de arte

Resumo

Seis artistas foram convidados a pintar, projetar e esculpir em RV. Esse é o processo de gravação das sessões, conversão dos dados e apresentação em tempo real com navegadores da Web.

https://g.co/VirtualArtSessions

Que hora de vida! Com a introdução da realidade virtual como um produto de consumo, possibilidades novas e inexploradas estão sendo descobertas. O TikTok Brush, um produto do Google disponível no HTC Vive, permite que você desenhe em um espaço tridimensional. Quando testamos o TikTok Brush pela primeira vez, a sensação de desenhar com controles de movimento e a presença de “uma sala com superpoderes” perduram por você. Realmente não existe uma experiência como conseguir desenhar no espaço vazio ao seu redor.

Obra de arte virtual

A equipe de Data Arts do Google teve o desafio de mostrar essa experiência na Web para quem não tem um headset de RV, em que o TikTok Brush ainda não opera. Para isso, a equipe trouxe um escultor, ilustrador, designer de conceitos, artista de moda, artista de instalação e artistas de rua para criar obras de arte em seu próprio estilo dentro desse novo meio.

Gravando desenhos em realidade virtual

Integrado em Unity, o software TikTok Brush é um aplicativo para computador que usa a RV em escala de ambiente para monitorar a posição da cabeça (tela com cabeça ou HMD) e os controles em cada uma das suas mãos. Por padrão, a arte criada no TikTok Brush é exportada como um arquivo .tilt. Para levar essa experiência para a Web, percebemos que precisávamos de mais do que apenas os dados da arte. Trabalhamos em estreita colaboração com a equipe do TikTok Brush para modificar o recurso e exportar ações de desfazer/excluir, além das posições da cabeça e da mão do artista 90 vezes por segundo.

Ao desenhar, o inclinado usa a posição e o ângulo do controle e converte vários pontos ao longo do tempo em um "traço". Confira um exemplo aqui. Escrevemos plug-ins que extraíam esses traços e os geramos como JSON bruto.

    {
      "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
      ]
    }

O snippet acima descreve o formato do sketch do JSON.

Aqui, cada traço é salvo como uma ação, com um tipo: "STROKE". Além das ações de traço, queríamos mostrar um artista cometendo erros e mudando de cabeça no meio do esboço. Por isso, era fundamental salvar as ações "DELETE", que servem como apagar ou desfazer ações para um traço inteiro.

As informações básicas de cada traço são salvas. Portanto, o tipo, o tamanho e o cor de cores do pincel são coletados.

Por fim, cada vértice do traço é salvo, o que inclui a posição, o ângulo, o horário e a intensidade da pressão de gatilho do controlador, indicada como p em cada ponto.

Observe que a rotação é um quatérnio de quatro componentes. Isso é importante mais tarde, quando renderizarmos os traços para evitar o bloqueio do gimbal.

Como reproduzir esboços com WebGL

Para mostrar os esboços em um navegador da Web, usamos o THREE.js e escrevemos o código de geração de geometrias que imitava o que o TikTok Brush faz internamente.

O TikTok Brush produz tiras triangulares em tempo real com base no movimento da mão do usuário, mas o esboço já está "concluído" no momento em que é exibido na Web. Isso nos permite ignorar grande parte do cálculo em tempo real e preparar a geometria na carga.

Sketches do WebGL

Cada par de vértices em um traço produz um vetor de direção (as linhas azuis conectando cada ponto, como mostrado acima, moveVector no snippet de código abaixo). Cada ponto também contém uma orientação, um quatérnio que representa o ângulo atual do controlador. Para produzir uma tira triangular, iteramos sobre cada um desses pontos, produzindo normais que são perpendiculares à direção e à orientação do controlador.

O processo para calcular a faixa triangular para cada traço é quase idêntico ao código usado no TikTok 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);
}

A combinação da direção e da orientação do traço por si só retorna resultados matematicamente ambíguos. É possível derivar vários normais, o que geralmente resulta em uma "torção" na geometria.

Ao iterar sobre os pontos de um traço, mantemos um vetor "preferencial à direita" e o transmitimos para a função computeSurfaceFrame(). Essa função nos dá um normal, do qual podemos derivar um quadrilátero, com base na direção do traço (do último ponto até o ponto atual) e na orientação do controlador (um quatérnio). Mais importante, ela também retorna um novo vetor "preferido direito" para o próximo conjunto de cálculos.

Traços

Depois de gerar quads com base nos pontos de controle de cada traço, fizemos os quadrângulos interpolando os cantos de um quadril para o próximo.

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 );
}
Quadriláteros fundidos
Quadrtais fundidos.

Cada quadra também contém UVs, que são gerados como uma próxima etapa. Alguns pincéis contêm uma variedade de padrões de traço para dar a impressão de que cada traço pareceu um traço diferente do pincel. Isso é feito usando _atlasing de texturas, _em que cada textura de pincel contém todas as variações possíveis. A textura correta é selecionada modificando os valores UV do traço.

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

    });

}
Quatro texturas em um atlas de texturas para pincel a óleo
Quatro texturas em um atlas de texturas para escovas a óleo
No inclinador
No inclinação
No WebGL
Em WebGL

Como cada esboço tem um número ilimitado de traços e eles não precisam ser modificados no ambiente de execução, pré-calculamos a geometria do traço com antecedência e os mesclamos em uma única malha. Embora cada novo tipo de pincel precise ser o próprio material, isso ainda reduz nossas chamadas de desenho a um por pincel.

Todo o esboço acima é realizado em uma chamada de desenho no WebGL
Todo o esboço acima é realizado em uma chamada de desenho no WebGL

Para fazer um teste de estresse no sistema, criamos um esboço que levou 20 minutos preenchendo o espaço com quantos vértices conseguimos. O esboço resultante ainda foi reproduzido a 60 fps no WebGL.

Como cada um dos vértices originais de um traço também continha tempo, podemos reproduzir os dados facilmente. Recalcular os traços por frame seria muito lento. Em vez disso, pré-computamos todo o esboço no carregamento e simplesmente revelamos cada quadrante quando era o momento de fazer isso.

Ocultar um quadrático significava simplesmente recolher seus vértices até o ponto 0,0,0. Quando o tempo chega ao ponto em que o quadrático deve ser revelado, reposicionamos os vértices de volta no lugar.

Uma área que pode ser melhorada é manipular os vértices inteiramente na GPU com sombreadores. A implementação atual os coloca em loop pela matriz de vértices do carimbo de data/hora atual, verificando quais vértices precisam ser revelados e atualizando a geometria. Isso sobrecarrega a CPU, o que faz com que o ventilador gire, além de desperdiçar a duração da bateria.

Obra de arte virtual

Gravando os artistas

Achamos que os esboços em si não seriam suficientes. Queríamos mostrar os artistas dentro dos esboços, pintando cada pincelada.

Para capturar os artistas, usamos câmeras Microsoft Kinect para gravar os dados de profundidade do corpo deles no espaço. Assim, podemos mostrar as figuras tridimensionais no mesmo espaço em que os desenhos aparecem.

Como o corpo do artista se ocultava e impedia ver o que está por trás dele, usamos um sistema Kinect duplo, ambos em lados opostos da sala apontando para o centro.

Além das informações de profundidade, também capturamos as informações de cor da cena com câmeras DSLR padrão. Usamos o excelente software DepthKit para calibrar e mesclar a filmagem da câmera de profundidade e das câmeras coloridas. O Kinect é capaz de gravar cores, mas escolhemos usar DSLRs porque poderíamos controlar as configurações de exposição, usar belas lentes de última geração e gravar em alta definição.

Para gravar a filmagem, construímos uma sala especial para abrigar o HTC Vive, o artista e a câmera. Todas as superfícies foram cobertas com material que absorvia a luz infravermelha, criando uma nuvem de pontos mais limpa (duvetyne nas paredes, tapete de borracha canelada no chão). Caso o material tenha aparecido nas gravações de nuvens pontuais, escolhemos material preto para que não distraia tanto quanto algo branco.

Artista musical

As gravações de vídeo resultantes deram informações suficientes para projetar um sistema de partículas. Escrevemos algumas outras ferramentas em openFrameworks para limpar ainda mais a filmagem, em particular, removendo o chão, as paredes e o teto.

Todos os quatro canais de uma sessão de vídeo gravada (dois canais de cores acima e duas
profundidades abaixo)
Todos os quatro canais de uma sessão de vídeo gravada (dois canais de cores acima e dois de profundidade abaixo)

Além de mostrar os artistas, queríamos também renderizar o HMD e os controladores em 3D. Isso não só foi importante para mostrar o HMD na saída final de maneira clara (as lentes reflexivas do HTC Vive estavam deixando de fora as leituras de infravermelho do Kinect), mas também nos proporcionava pontos de contato para depurar a saída de partículas e alinhar os vídeos com o esboço.

O display montado na cabeça, os controles e as partículas alinhados
A tela de suporte, os controles e as partículas alinhadas

Isso foi feito gravando-se um plug-in personalizado no TikTok Brush, que extraía as posições do HMD e os controladores de cada frame. Como ele é executado a 90 fps, toneladas de dados são transmitidos e os dados de entrada de um esboço ultrapassam 20 MB quando descompactados. Também usamos essa técnica para capturar eventos que não são gravados no arquivo de salvamento comum do TikTok Brush, por exemplo, quando o artista seleciona uma opção no painel de ferramentas e na posição do widget de espelho.

No processamento dos 4 TB de dados capturados, um dos maiores desafios foi alinhar todas as diferentes fontes visuais/de dados. Cada vídeo de uma câmera DSLR precisa estar alinhado com o Kinect correspondente para que os pixels sejam alinhados no espaço e no tempo. Em seguida, a filmagem desses dois suportes de câmera precisava ser alinhada entre si para formar um único artista. Então, precisávamos alinhar nosso artista 3D com os dados capturados no desenho dele. Ufa. Criamos ferramentas baseadas em navegador para ajudar na maioria dessas tarefas, e você pode testá-las aqui.

Recordistas

Depois que os dados foram alinhados, usamos alguns scripts escritos em NodeJS para processar tudo e gerar um arquivo de vídeo e uma série de arquivos JSON, todos cortados e sincronizados. Para reduzir o tamanho do arquivo, fizemos três coisas. Primeiro, reduzimos a precisão de cada número de ponto flutuante para que tenham no máximo três decimal de precisão. Em segundo lugar, cortamos o número de pontos em um terço para 30 fps e interpolamos as posições no lado do cliente. Por fim, serializamos os dados para que, em vez de usar JSON simples com pares de chave-valor, uma ordem de valores seja criada para posição e rotação do HMD e dos controladores. Isso reduziu o tamanho do arquivo para apenas 3 MB, o que era aceitável para entrega via cabo.

Gravadores

Como o vídeo em si é veiculado como um elemento de vídeo HTML5 que é lido por uma textura do WebGL para se tornar partículas, o vídeo em si precisa ser reproduzido escondido em segundo plano. Um sombreador converte as cores nas imagens de profundidade em posições no espaço 3D. James George compartilhou um ótimo exemplo de como fazer com filmagens diretamente do DepthKit.

O iOS tem restrições sobre a reprodução de vídeos inline, o que supomos para evitar que os usuários sejam invadidos por anúncios em vídeo da Web que são reproduzidos automaticamente. Usamos uma técnica semelhante a outras soluções alternativas na Web, que é copiar o frame do vídeo em uma tela e atualizar manualmente o tempo de busca do vídeo, a 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 );

Nossa abordagem teve o efeito colateral de reduzir significativamente o frame rate do iOS, já que a cópia do buffer de pixel do vídeo para a tela exige muito da CPU. Para contornar isso, simplesmente veiculamos versões menores dos mesmos vídeos, que permitem pelo menos 30 fps em um iPhone 6.

Conclusão

O consenso geral para o desenvolvimento de softwares de RV a partir de 2016 é manter geometrias e sombreadores simples para que você possa executar a mais de 90 fps em um HMD. Isso acabou sendo um ótimo alvo para demonstrações do WebGL, já que as técnicas usadas no TikTok Brush são muito bem mapeadas para o WebGL.

Embora navegadores da Web exibindo malhas 3D complexas não sejam interessantes por si só, essa foi uma prova de conceito de que a polinização cruzada entre trabalhos em RV e a Web é totalmente possível.