Sessões virtuais de arte

Detalhes da sessão de arte

Resumo

Seis artistas foram convidados a pintar, desenhar e esculpir em RV. Esta é a de como gravamos as sessões, convertemos os dados e apresentamos em tempo real com navegadores da Web.

https://g.co/VirtualArtSessions

Que hora para estar vivo! Com a introdução da realidade virtual como consumidor produto, novas e inexploradas possibilidades estão sendo descobertas. inclinação Brush, um O produto do Google, disponível no HTC Vive, permite que você desenhe três espaço dimensional. Quando testamos o inclinador Brush pela primeira vez, desenhando com controles de movimento aliado à presença de estar "em um sala com superpoderes" fica com você; não há uma experiência tão como poder desenhar no espaço vazio ao seu redor.

Obra de arte virtual

A equipe de Data Arts do Google recebeu o desafio de mostrar esse para pessoas que não têm um headset de RV, na Web em que o inclinador não mas funciona. Para isso, a equipe trouxe um escultor, um ilustrador, designer conceitual, artista de moda, instalação e artistas de rua para criar obras de arte com estilo próprio nessa nova mídia.

Como gravar desenhos em realidade virtual

Criado em Unity, o software inclinador Brush é um aplicativo para computador que usa RV com escala de ambiente para monitorar a posição da sua cabeça (com suporte para cabeça montado ou HMD) e os controladores em cada uma de suas mãos. Obra de arte criada no inclinamento Brush por exportado por padrão 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 de perto com a A equipe do flood Brush modificará a ferramenta para exportar também ações de desfazer/excluir ações. como a cabeça e a mão do artista 90 vezes por segundo.

Ao desenhar, o inclinador usa a posição e o ângulo do controle e converte vários pontos ao longo do tempo em um "traço". Podemos conferir um exemplo aqui. Escrevemos plug-ins que extraíam esses traços e os geravam 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 esboço do formato JSON.

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

As informações básicas de cada traço são salvas, para que o tipo, o tamanho e a cor do pincel RGB são todas coletadas.

Por fim, cada vértice do traço é salvo, e isso inclui a posição, ângulo, horário, bem como a intensidade da pressão de gatilho do controle (anotada como p em cada ponto).

A rotação é um quatérnio de quatro componentes. Isso é importante mais tarde, quando renderizamos os traços para evitar o bloqueio de gimbal.

Como reproduzir esboços com WebGL

Para mostrar os esboços em um navegador da Web, usamos THREE.js e escreveu um código de geração de geometria que imitava o que o flood Brush faz em segundo plano.

Enquanto o inclinador produz tiras de triângulo em tempo real com base na mão do usuário movimento, todo o esboço já está "concluído" quando os mostramos na Web. Isso nos permite ignorar grande parte do cálculo em tempo real e preparar a geometria na carga.

Esboços WebGL

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

O processo para calcular a faixa de triângulo de cada traço é quase idêntico com o código usado no inclinador:

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 a direção do traço e a orientação por si só retorna resultados matematicamente ambíguos; pode haver vários normais derivados e geralmente produziam uma "torção" na geometria.

Ao iterar sobre os pontos de um traço, mantemos um "preferido direito" vetor e transmita-o para a função computeSurfaceFrame(). Essa função nos dá uma normal, a partir da qual podemos derivar um quadrângulo, com base em a direção do traço (do último ponto até o ponto atual) e a orientação do controlador (um quatérnio). Mais importante, ele também retorna um novo "direito preferencial" um vetor para o próximo conjunto de cálculos.

Traços

Depois de gerar os quadriciclos com base nos pontos de controle de cada traço, fazemos a fusão dos quadrados ao interpolar os cantos, de um quadrante 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 );
}
Quadriciclos fundidos
Quadrinhos combinados.

Cada quadrante 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 sentia um traço diferente do pincel. Isso é conseguido com o uso _ataque de texturas, _em que cada textura do pincel contém todas as variações. 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 de óleo
Quatro texturas em um atlas de texturas para pincel de óleo
No inclinação Brush
No inclinação Brush
No WebGL
No WebGL

Como cada esboço tem um número ilimitado de traços, e eles não precisarão modificados no ambiente de execução, pré-computamos a geometria do traço com antecedência e mesclamos em uma única malha. Mesmo que cada novo tipo de pincel tenha que ser próprio que ainda reduz as chamadas de desenho a uma 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 espaço com o máximo de vértices possível. O esboço resultante ainda era reproduzido 60 fps no WebGL.

Como cada um dos vértices originais de um traço também continha tempo, reproduzir os dados facilmente. Recalcular os traços por frame seria lento, então nós pré-computamos todo o esboço no carregamento e apenas revelamos em cada quadra quando era hora de fazer isso.

Ocultar um quad significa simplesmente recolher os vértices até o ponto 0,0,0. Quando o chegou ao ponto em que o quad deve ser revelado, reposicionar os vértices de volta no lugar.

Uma área de melhoria é a manipulação completa dos vértices na GPU com sombreadores. A implementação atual os coloca em loop pelo vértice matriz a partir do carimbo de data/hora atual, verificando quais vértices precisam ser revelados e atualizar a geometria. Isso sobrecarrega a CPU, o que faz o ventilador girar, além de desperdiçar bateria.

Obra de arte virtual

Gravação dos artistas

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

Para capturar os artistas, usamos câmeras Microsoft Kinect para gravar a profundidade os dados dos artistas o corpo no espaço. Isso nos dá a capacidade de mostrar figuras tridimensionais no mesmo espaço em que os desenhos aparecem.

Como o corpo da artista ficaria oculto, impedindo que pudéssemos ver o que está atrá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 na cena com câmeras DSLR padrão. Usamos a excelente Software DepthKit para calibrar e mesclar as imagens da câmera de profundidade e das câmeras coloridas. O Kinect é capaz de gravação de cores, mas optamos por usar DSLRs porque podíamos controlar exposição, use lindas lentes de alta qualidade e grave em alta definição.

Para gravar as imagens, 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 absorveu infravermelho luz para gerar uma nuvem mais limpa (duvetyne nas paredes, tapetes no chão). Caso o material tenha aparecido na nuvem de pontos vídeo, escolhemos material preto para que não pudesse causar distração que era branco.

Artista

As gravações de vídeo resultantes nos deram informações suficientes para projetar uma partícula sistema. Escrevemos algumas ferramentas adicionais em openFrameworks para limpar ainda mais a filmagem, especialmente a remoção de pisos, paredes e teto.

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

Além de mostrar os artistas, queríamos renderizar o HMD e os controles em 3D também. Isso não foi importante apenas para mostrar o HMD a saída final claramente (as lentes refletivas do HTC Vive eram descartadas leituras de IR do Kinect), ele nos deu pontos de contato para depurar a partícula e alinhar os vídeos com o esboço.

O monitor montado na cabeça, os controles e as partículas alinhados
As partículas, os controles e os controles montados na cabeça estão alinhados

Isso foi feito escrevendo-se um plug-in personalizado no inclinador Brush que extraía a posições do HMD e dos controladores a cada frame. Como o inclinador é executado a 90 fps, toneladas de dados transmitidos e os dados de entrada de um esboço ultrapassaram 20 MB não compactado. Também usamos essa técnica para capturar eventos que não são registrados no arquivo de salvamento típico do inclinador, como quando o artista seleciona uma opção no painel de ferramentas e a posição do widget de espelho.

No processamento dos 4 TB de dados capturados, um dos maiores desafios foi alinhando todas as diferentes fontes de dados/visuais. Cada vídeo de uma câmera DSLR precisam ser alinhados com o Kinect correspondente, de modo que os pixels estejam alinhados espaço e tempo. Então, a filmagem desses dois suportes de câmera precisava alinhadas umas com as outras para formar um só artista. Depois, precisamos alinhar nosso modelo 3D com os dados capturados nos desenhos. Ufa. Escrevemos com base em navegadores ferramentas para ajudar na maioria dessas tarefas, e você pode experimentá-las por conta própria aqui

Artistas do álbum

Depois que os dados foram alinhados, usamos alguns scripts escritos em NodeJS para processá-los e gerar um arquivo de vídeo e uma série de arquivos JSON, todos cortados 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 estejam, no máximo, 3 o valor de precisão de decimal. Em segundo lugar, cortamos o número de pontos em um terço para 30 fps e interpolamos as posições do lado do cliente. Por fim, serializamos o dados, portanto, em vez de usar JSON simples com pares de chave-valor, uma ordem de valores é criado para posição e rotação do HMD e dos controladores. Isso recortou o arquivo com apenas 3 MB, o que é aceitável para envio via rede.

Músicos

Como o próprio vídeo é veiculado como um elemento de vídeo HTML5 que é lido por um A textura do WebGL para se tornar partículas, o vídeo em si precisava ser reproduzido escondido na plano de fundo. Um sombreador converte as cores nas imagens de profundidade em posições espaço 3D. James George deu um ótimo exemplo de como fazer filmagens do DepthKit.

O iOS tem restrições na reprodução de vídeos inline, que consideramos para evitar que os usuários sejam incomodados por anúncios em vídeo da Web que são reproduzidos automaticamente. Usamos uma técnica semelhante a outras soluções alternativas da Web, que é copiar frame do vídeo em uma tela e atualizar manualmente o tempo de busca do vídeo, a cada 1/30 por 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 desempenho do iOS uma taxa de quadros, pois a cópia do buffer de pixel do vídeo para a tela é muito com uso intensivo da CPU. Para resolver isso, nós simplesmente fornecemos versões menores 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, uma vez que as técnicas usadas no inclinado Brush são muito bem mapeados para WebGL.

Embora os navegadores da Web exibindo malhas complexas em 3D não sejam empolgantes isso foi uma prova de conceito de que a polinização cruzada entre o trabalho em RV e a da Web é totalmente possível.