Tonite de dança na WebVR

Fiquei empolgado quando a equipe do Google Data Arts abordou Moniker e eu para trabalhar juntos para explorar as possibilidades introduzidas pela WebVR. Vi o trabalho vindo da equipe deles ao longo dos anos, e os projetos sempre foram marcantes para mim. Nossa colaboração resultou na Dance Tonite, uma experiência de dança em RV que muda constantemente com a LCD Soundsystem e os fãs. Foi assim que fizemos.

Conceito

Começamos com o desenvolvimento de uma série de protótipos usando a WebVR, um padrão aberto que possibilita entrar na RV acessando um site pelo navegador. O objetivo é facilitar a entrada de experiências de RV para todos, independente de qual dispositivo você tenha.

Levamos isso a sério. Tudo que apresentamos deve funcionar em todos os tipos de RV, desde headsets de RV que funcionam com smartphones, como Daydream View do Google, Cardboard e o Gear VR da Samsung, até sistemas escalonáveis como o HTC VIVE e Oculus Rift, que refletem seus movimentos físicos no ambiente virtual. Talvez o mais importante: achamos que estaria no espírito da Web criar algo que também funcionasse para todos que não tenham um dispositivo de RV.

1. Faça você mesmo: captura de movimento

Como queríamos envolver os usuários de forma criativa, começamos a analisar as possibilidades de participação e autoexpressão com a RV. Ficamos impressionados com a facilidade com que você podia se mover e olhar ao redor em RV e com a fidelidade dele. Isso nos deu uma ideia. Em vez de fazer os usuários olharem ou criarem algo, que tal registrar os movimentos deles?

Alguém gravando a si mesma no Dance Tonite. A tela atrás deles
  mostra o que eles estão vendo no fone de ouvido

Criamos um protótipo em que gravamos as posições dos nossos óculos e controladores de RV enquanto dançamos. Substituímos as posições gravadas por formas abstratas e ficamos impressionados com os resultados. Os resultados foram tão humanos e contem muita personalidade! Percebemos rapidamente que poderíamos usar a WebVR para fazer capturas de movimento baratas em casa.

Com a WebVR, o desenvolvedor tem acesso à posição e orientação da cabeça do usuário com o objeto VRPose. Esse valor é atualizado a cada frame pelo hardware de RV para que o código possa renderizar novos frames do ponto de vista correto. Com a API GamePad com WebVR, também podemos acessar a posição/orientação dos controladores de usuários pelo objeto GamepadPose. Simplesmente armazenamos todos esses valores de posição e orientação a cada frame, criando uma "gravação" dos movimentos do usuário.

2. Minimalismo e fantasias

Com os equipamentos de RV atuais na escala de sala, podemos monitorar três pontos do corpo do usuário: a cabeça e as duas mãos. No Dance Tonite, queríamos manter o foco na humanidade no movimento desses três pontos no espaço. Para conseguir isso, colocamos a estética o mais mínima possível para nos concentrar no movimento. Gostamos da ideia de colocar o cérebro das pessoas para trabalhar.

Este vídeo demonstrando o trabalho do psicólogo sueco Gunnar Johansson foi um dos exemplos que mencionamos quando pensamos em remover tudo o máximo possível. Ele mostra como pontos brancos flutuantes são instantaneamente reconhecíveis como corpos quando vistos em movimento.

Visualmente, fomos inspirados pelos quartos coloridos e trajes geométricos nesta gravação da releitura de 1970 de Margarete Hastings do balé Triádico de Oskar Schlemmer.

Enquanto Schlemmer escolheu trajes geométricos abstratos era limitar os movimentos dos dançarinos aos de marionetes e marionetes, tínhamos o objetivo oposto para o Dance Tonite.

Acabamos baseando nossa escolha de formas em quanta informação elas transmitiram por rotação. Uma esfera tem a mesma aparência, não importa como é girada, mas um cone realmente aponta na direção para a qual está olhando e tem aparência diferente da frente e da parte de trás.

3. Pedal de loop para movimento

Queríamos mostrar grandes grupos de pessoas gravadas dançando e se movendo entre si. Não seria viável fazer isso ao vivo, já que os dispositivos de RV não estão disponíveis em números grandes o suficiente. Ainda assim, queríamos ter grupos de pessoas reagindo umas às outras com o movimento. Nossa mente nos dedicou à apresentação recursiva de Norman McClaren no vídeo “Canon”, de 1964.

A performance de McClaren apresenta uma série de movimentos altamente coreografados que começam a interagir entre si após cada repetição. Assim como um pedal de loop na música, em que os músicos tocam música ao mesmo tempo em camadas diferentes de música ao vivo, queríamos ver se poderíamos criar um ambiente onde os usuários pudessem improvisar livremente versões mais soltas das apresentações.

4. Quartos interconectados

Quartos interconectados

Como muitas músicas, as faixas do LCD Soundsystem são criadas usando medidas com marcação de tempo. A música deles, Tonite, que faz parte do nosso projeto, apresenta medidas com exatamente oito segundos de duração. Queríamos que os usuários fizessem um desempenho para cada loop de oito segundos na faixa. Embora o ritmo dessas medidas não mude, o conteúdo musical muda. À medida que a música avança, há momentos com diferentes instrumentos e vocais aos quais os artistas podem reagir de maneiras diferentes. Cada uma dessas medidas é expressa como uma sala, em que as pessoas podem fazer uma apresentação que se encaixe.

Otimizações para melhorar o desempenho: não descartar frames

Criar uma experiência de RV multiplataforma executada em uma única base de código com desempenho ideal para cada dispositivo ou plataforma não é uma tarefa simples.

Na RV, uma das coisas mais enjoadas que você pode ter é quando o frame rate não acompanha o movimento. Se você virar a cabeça, mas os visuais que seus olhos enxergarem não corresponderem ao movimento do seu ouvido interno, isso causará uma rotatividade instantânea do estômago. Por esse motivo, precisávamos evitar qualquer atraso grande de frame rate. Aqui estão algumas otimizações que implementamos.

1. Geometria do buffer em instância

Como todo o nosso projeto usa apenas alguns objetos 3D, conseguimos melhorar o desempenho usando a geometria de buffer da instância. Basicamente, ela permite que você faça upload do objeto para a GPU uma vez e desenhe quantas "instâncias" dele quiser em uma única chamada de desenho. No Dance Tonite, temos apenas três objetos diferentes (um cone, um cilindro e um cômodo com um buraco), mas possivelmente centenas de cópias deles. A geometria do buffer da instância faz parte do ThreeJS, mas usamos a bifurcação experimental e em andamento de Dusan Bosnjak que implementa THREE.InstanceMesh, o que facilita muito o trabalho com a geometria de buffer da instância.

2. Como evitar o coletor de lixo

Como acontece com muitas outras linguagens de script, o JavaScript libera memória automaticamente ao descobrir quais objetos alocados não são mais usados. Esse processo é chamado de coleta de lixo.

Os desenvolvedores não têm controle sobre quando isso acontece. O coletor de lixo pode aparecer nas nossas portas a qualquer momento e começar a esvaziar o lixo, resultando em frames descartados à medida que eles avançam no tempo doce.

A solução para isso é produzir o mínimo de lixo possível reciclando nossos objetos. Em vez de criar um novo objeto vetorial para cada cálculo, marcamos os objetos de rascunho para reutilização. Como nos guardamos, movendo nossa referência para fora de nosso escopo, eles não foram marcados para remoção.

Por exemplo, confira nosso código para converter a matriz de localização da cabeça e das mãos do usuário na matriz de valores de posição/rotação que armazenamos para cada frame. Ao reutilizar SERIALIZE_POSITION, SERIALIZE_ROTATION e SERIALIZE_SCALE, evitamos a alocação de memória e a coleta de lixo que ocorreriam se criássemos novos objetos sempre que a função fosse chamada.

const SERIALIZE_POSITION = new THREE.Vector3();
const SERIALIZE_ROTATION = new THREE.Quaternion();
const SERIALIZE_SCALE = new THREE.Vector3();
export const serializeMatrix = (matrix) => {
    matrix.decompose(SERIALIZE_POSITION, SERIALIZE_ROTATION, SERIALIZE_SCALE);
    return SERIALIZE_POSITION.toArray()
    .concat(SERIALIZE_ROTATION.toArray())
    .map(compressNumber);
};

3. Serializando movimento e reprodução progressiva

Para capturar os movimentos dos usuários em RV, foi necessário serializar a posição e rotação dos fones de ouvido e dos controladores e fazer upload desses dados para nossos servidores. Começamos capturando as matrizes de transformação completas para cada frame. Isso funcionou bem, mas com 16 números multiplicados por 3 posições cada a 90 quadros por segundo, o que gerava arquivos muito grandes e, portanto, espera longa no upload e download dos dados. Ao extrair apenas os dados de posição e rotação das matrizes de transformação, conseguimos reduzir esses valores de 16 para 7.

Como os visitantes da Web geralmente clicam em um link sem saber exatamente o que esperar, precisamos mostrar o conteúdo visual rapidamente. Caso contrário, eles vão embora em segundos.

Por isso, queríamos garantir que nosso projeto pudesse começar a ser executado o mais rápido possível. Inicialmente, estávamos usando JSON como formato para carregar nossos dados de movimento. O problema é que temos que carregar o arquivo JSON completo antes de podermos analisá-lo. Não muito progressivo.

Para que um projeto como o Dance Tonite seja exibido no maior frame rate possível, o navegador só precisa de um pequeno tempo para cada frame em cálculos em JavaScript. Se você demorar muito, as animações começarão a falhar. No início, estávamos com travamentos à medida que esses arquivos JSON enormes eram decodificados pelo navegador.

Encontramos um formato de dados de streaming conveniente baseado em NDJSON ou JSON delimitado por nova linha. O truque é criar um arquivo com uma série de strings JSON válidas, cada uma na própria linha. Isso permite que você analise o arquivo durante o carregamento, o que nos permite exibir as performances antes que elas sejam totalmente carregadas.

Veja como seria uma seção de uma das nossas gravações:

{"fps":15,"count":1,"loopIndex":"1","hideHead":false}
[-464,17111,-6568,-235,-315,-44,9992,-3509,7823,-7074, ... ]
[-583,17146,-6574,-215,-361,-38,9991,-3743,7821,-7092, ... ]
[-693,17158,-6580,-117,-341,64,9993,-3977,7874,-7171, ... ]
[-772,17134,-6591,-93,-273,205,9994,-4125,7889,-7319, ... ]
[-814,17135,-6620,-123,-248,408,9988,-4196,7882,-7376, ... ]
[-840,17125,-6644,-173,-227,530,9982,-4174,7815,-7356, ... ]
[-868,17120,-6670,-148,-183,564,9981,-4069,7732,-7366, ... ]
...

O uso do NDJSON nos permite manter a representação de dados dos frames individuais das apresentações como strings. Poderíamos esperar até alcançar o tempo necessário, antes de decodificá-los em dados posicionais, espalhando o processamento necessário ao longo do tempo.

4. Movimento de interpolação

Como esperávamos exibir de 30 a 60 apresentações ao mesmo tempo, precisávamos diminuir ainda mais a taxa de dados. A equipe de Data Arts abordou o mesmo problema no projeto Virtual Art Sessions, em que reproduzia gravações de artistas que pintam em RV usando o inclinação, Para resolver isso, a equipe criou versões intermediárias dos dados do usuário com frame rates mais baixos e interpolando entre os frames durante a reprodução. Ficamos surpresos ao descobrir que quase não conseguimos detectar a diferença entre uma gravação interpolada em execução a 15 QPS e a gravação original a 90 QPS.

Para conferir por conta própria, você pode forçar o Dance Tonite a reproduzir os dados em várias taxas usando a string de consulta ?dataRate=. Você pode usar isso para comparar o movimento gravado em 90 quadros por segundo, 45 frames por segundo ou 15 quadros por segundo.

Para a posição, fazemos uma interpolação linear entre o frame-chave anterior e o próximo, com base na proximidade entre os frames-chave (proporção):

const { x: x1, y: y1, z: z1 } = getPosition(previous, performanceIndex, limbIndex);
const { x: x2, y: y2, z: z2 } = getPosition(next, performanceIndex, limbIndex);
interpolatedPosition = new THREE.Vector3();
interpolatedPosition.set(
    x1 + (x2 - x1) * ratio,
    y1 + (y2 - y1) * ratio,
    z1 + (z2 - z1) * ratio
    );

Para orientação, fazemos uma interpolação linear esférica (slerp, na sigla em inglês) entre frames-chave. A orientação é armazenada como Quaternions.

const quaternion = getQuaternion(previous, performanceIndex, limbIndex);
quaternion.slerp(
    getQuaternion(next, performanceIndex, limbIndex),
    ratio
    );

5. Sincronizando movimentos com a música

Para saber qual frame das animações gravadas deve ser reproduzida, precisamos saber o tempo atual da música, até o milissegundo. Na verdade, embora o elemento de áudio HTML seja perfeito para carregar e reproduzir sons progressivamente, a propriedade de tempo que ele fornece não muda em sincronia com o loop de frames do navegador. Está sempre um pouco diferente. Às vezes, uma fração de ms é muito cedo, às vezes uma fração muito atrasada.

Isso causa travamentos nas nossas belas gravações de dança, e nós queremos evitar a música a todo custo. Para corrigir isso, implementamos nosso próprio timer no JavaScript. Dessa forma, podemos ter certeza de que a quantidade de tempo que muda entre os frames é exatamente o tempo decorrido desde o último frame. Sempre que o timer fica fora de sincronia com a música por mais de 10 ms, ele é sincronizado novamente.

6. Coração e neblina

Toda história precisa de um bom final. Por isso, queríamos fazer algo surpreendente para os usuários que chegaram ao final da experiência. Ao sair da última sala, você entra em um cenário tranquilo de cones e cilindros. "Este é o fim?", você se pergunta. Conforme você avança pelo campo, de repente os tons da música fazem com que diferentes grupos de cones e cilindros se transformem em dançarinos. Você se encontra no meio de uma grande festa! Então, quando a música para abruptamente, tudo cai no chão.

Isso foi ótimo para os espectadores, mas trouxe alguns obstáculos de desempenho para resolver. Os dispositivos de RV em larga escala e os equipamentos de jogo de última geração tiveram um desempenho perfeito com as 40 performances extras necessárias para o novo final. Mas os frame rates em determinados dispositivos móveis foram reduzidos pela metade.

Para neutralizar isso, lançamos a névoa. Depois de uma certa distância, tudo ficará lento e lentamente. Como não precisamos calcular nem desenhar o que não está visível, selecionamos o desempenho em salas que não estão visíveis. Isso permite economizar trabalho para CPU e GPU. Mas como escolher a distância certa?

Alguns dispositivos podem lidar com tudo o que você joga, e outros são mais restritos. Optamos por implementar uma escala deslizante. Ao medir continuamente a quantidade de quadros por segundo, podemos ajustar a distância da neblina com base nisso. Desde que nosso frame rate esteja sendo executado sem problemas, tentamos executar mais trabalho de renderização evitando a neblina. Se o frame rate não estiver estável, aproximaremos a neblina, o que permite pular as performances de renderização no escuro.

// this is called every frame
// the FPS calculation is based on stats.js by @mrdoob
tick: (interval = 3000) => {
    frames++;
    const time = (performance || Date).now();
    if (prevTime == null) prevTime = time;
    if (time > prevTime + interval) {
    fps = Math.round((frames * 1000) / (time - prevTime));
    frames = 0;
    prevTime = time;
    const lastCullDistance = settings.cullDistance;

    // if the fps is lower than 52 reduce the cull distance
    if (fps <= 52) {
        settings.cullDistance = Math.max(
        settings.minCullDistance,
        settings.cullDistance - settings.roomDepth
        );
    }
    // if the FPS is higher than 56, increase the cull distance
    else if (fps > 56) {
        settings.cullDistance = Math.min(
        settings.maxCullDistance,
        settings.cullDistance + settings.roomDepth
        );
    }
    }

    // gradually increase the cull distance to the new setting
    cullDistance = cullDistance * 0.95 + settings.cullDistance * 0.05;

    // mask the edge of the cull distance with fog
    viewer.fog.near = cullDistance - settings.roomDepth;
    viewer.fog.far = cullDistance;
}

Algo para todos: criando RV para a Web

Projetar e desenvolver experiências assimétricas em várias plataformas significa considerar as necessidades de cada usuário, dependendo do dispositivo. E, a cada decisão de design, precisávamos ver como isso afetaria outros usuários. Como é possível garantir que o que você vê na RV seja tão empolgante quanto sem RV, e vice-versa?

1. A esfera amarela

Portanto, nossos usuários de RV em grande escala fariam as apresentações, mas como os usuários de dispositivos móveis de RV (como Cardboard, Daydream View ou Samsung Gear) viviam com o projeto? Para isso, introduzimos um novo elemento ao ambiente: a esfera amarela.

A esfera amarela
A esfera amarela

Ao assistir ao projeto em RV, você está fazendo isso do ponto de vista do esfera amarelo. Conforme você flutua de um cômodo para outro, os dançarinos reagem à sua presença. Eles gesticulam para você, dançam ao seu redor, fazem movimentos engraçados nas costas e se afastam rapidamente do caminho para não se esbarrar em você. A esfera amarela é sempre o centro das atenções.

Isso ocorre porque, durante a gravação de uma apresentação, a esfera amarela se move pelo centro da sala em sincronia com a música e se repete. A posição da esfera dá ao artista uma ideia do momento em que ele está no tempo e quanto tempo ele ainda tem no loop. Isso proporciona um foco natural para que eles desenvolvam uma performance.

2. Outro ponto de vista

Não queríamos deixar os usuários sem a RV, especialmente porque eles provavelmente seriam nosso maior público. Em vez de criar uma experiência de RV falsa, queríamos dar aos dispositivos baseados em tela a própria experiência. Tivemos a ideia de mostrar as apresentações de cima de uma perspectiva isométrica. Essa perspectiva tem uma rica história em jogos de computador. Ele foi usado pela primeira vez no Zaxxon, um jogo de tiro espacial de 1982. Enquanto os usuários de RV estão no foco, a perspectiva isométrica oferece uma visão divina da ação. Optamos por aumentar um pouco os modelos, um toque estético de casa de bonecas.

3. Sombras: finja até você chegar

Descobrimos que alguns dos nossos usuários estavam tendo dificuldade em ver a profundidade do nosso ponto de vista isométrico. Tenho certeza de que é por esse motivo que o Zaxxon também foi um dos primeiros jogos de computador da história a projetar uma sombra dinâmica sob seus objetos voadores.

Sombras

Na verdade, é difícil criar sombras em 3D. Especialmente para dispositivos restritos, como celulares. Inicialmente, tivemos que tomar a difícil decisão de eliminá-los da equação, mas depois de pedir um conselho ao autor do Three.js e do hacker de demonstração Mr doob, ele teve a nova ideia de... fingir-os.

Em vez de calcular como cada um dos nossos objetos flutuantes está ocultando nossas luzes e, portanto, lançando sombras de diferentes formas, desenhamos a mesma imagem circular de textura desfocada abaixo de cada um. Como nossos recursos visuais não estão tentando imitar a realidade, descobrimos que poderíamos sair disso facilmente com apenas alguns ajustes. Quando os objetos se aproximam do solo, as texturas ficam mais escuras e menores. Quando eles se movem para cima, tornamos as texturas mais transparentes e maiores.

Para criá-las, usamos essa textura com um gradiente branco a preto (sem transparência alfa). Definimos o material como transparente e usamos mistura de subtração. Isso os ajuda a se mesclar bem quando eles se sobrepõem:

function createShadow() {
    const texture = new THREE.TextureLoader().load(shadowTextureUrl);
    const material = new THREE.MeshLambertMaterial({
        map: texture,
        transparent: true,
        side: THREE.BackSide,
        depthWrite: false,
        blending: THREE.SubtractiveBlending,
    });
    const geometry = new THREE.PlaneBufferGeometry(0.5, 0.5, 1, 1);
    const plane = new THREE.Mesh(geometry, material);
    return plane;
    }

4. Estar lá

Ao clicar na cabeça de um artista, visitantes que não usam RV podem ver o que está do ponto de vista do dançarino. Dessa perspectiva, muitos detalhes ficam aparentes. Tentando manter as apresentações em passo, os dançarinos olham rapidamente para os outros. Quando a esfera entra na sala, você a vê olhando nervosa em sua direção. Embora você não possa influenciar esses movimentos como espectador, eles transmitem a sensação de imersão de forma surpreendente. Mais uma vez, preferimos fazer isso em vez de apresentar aos usuários uma simples versão de RV falsa controlada pelo mouse.

5. Compartilhando gravações

Sabemos o quão orgulho você pode sentir ao gravar uma gravação minuciosamente coreografada de 20 camadas de artistas reagindo uns aos outros. Sabíamos que nossos usuários provavelmente queriam mostrá-lo aos amigos. Mas uma imagem estática desse feito não comunica o suficiente. Em vez disso, queríamos permitir que nossos usuários compartilhassem vídeos de suas apresentações. Por que não usar um GIF? Nossas animações têm sombras planas, perfeitas para as paletas de cores limitadas do formato.

Compartilhando gravações

Utilizamos o GIF.js, uma biblioteca JavaScript que permite codificar GIFs animados no navegador. Ela descarrega a codificação de frames para os workers da Web, que podem ser executados em segundo plano como processos separados. Assim, é possível aproveitar o uso de vários processadores trabalhando lado a lado.

Infelizmente, com a quantidade de frames que precisávamos para as animações, o processo de codificação ainda era muito lento. O GIF pode criar arquivos pequenos usando uma paleta de cores limitada. Descobrimos que a maior parte do tempo era passar encontrando a cor mais próxima de cada pixel. Conseguimos otimizar esse processo dez vezes ao hackear um pequeno corte curto: se a cor do pixel for a mesma da último, use a mesma cor da paleta de antes.

Agora tínhamos codificações rápidas, mas os arquivos GIF resultantes eram muito grandes. O formato GIF permite indicar como cada frame será exibido sobre o último, definindo o método de descarte. Para conseguir arquivos menores, em vez de atualizar cada pixel a cada frame, atualizamos apenas os pixels que mudaram. Embora desacelerasse o processo de codificação novamente, isso reduziu significativamente o tamanho dos nossos arquivos.

6. Ponto sólido: Google Cloud e Firebase

O back-end de um site com "conteúdo gerado pelo usuário" geralmente pode ser complicado e frágil, mas criamos um sistema simples e robusto graças ao Google Cloud e ao Firebase. Quando um artista envia uma nova dança para o sistema, ele é autenticado anonimamente pelo Firebase Authentication. Eles recebem permissão para fazer upload da gravação em um espaço temporário usando o Cloud Storage para Firebase. Quando o upload é concluído, a máquina cliente chama um acionador HTTP do Cloud Functions para Firebase usando o token do Firebase. Isso aciona um processo do servidor que valida o envio, cria um registro de banco de dados e move a gravação para um diretório público no Google Cloud Storage.

Terreno sólido

Todo o conteúdo público é armazenado em uma série de arquivos simples em um bucket do Cloud Storage. Isso significa que nossos dados ficam rapidamente acessíveis em todo o mundo, e não precisamos nos preocupar com grandes cargas de tráfego que afetam a disponibilidade dos dados.

Usamos endpoints do Firebase Realtime Database e da função do Cloud para criar uma ferramenta simples de moderação/seleção que permite assistir a cada novo envio em RV e publicar novas playlists em qualquer dispositivo.

7. Service Workers

Os service workers são uma inovação bastante recente que ajuda a gerenciar o armazenamento em cache de recursos do site. Em nosso caso, os service workers carregam nosso conteúdo rapidamente para visitantes que retornam e até permitem que o site funcione off-line. Esses são recursos importantes, já que muitos dos nossos visitantes usam conexões móveis de qualidade variada.

Adicionar um service worker ao projeto foi fácil graças a um plug-in webpack que faz a maior parte do trabalho para você. Na configuração abaixo, geramos um service worker que armazenará automaticamente todos os arquivos estáticos em cache. Ele extrairá o arquivo de playlist mais recente da rede, se disponível, já que a playlist será atualizada o tempo todo. Todos os arquivos JSON de gravação precisam ser extraídos do cache, se disponíveis, porque eles nunca mudam.

const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
config.plugins.push(
    new SWPrecacheWebpackPlugin({
    dontCacheBustUrlsMatching: /\.\w{8}\./,
    filename: 'service-worker.js',
    minify: true,
    navigateFallback: 'index.html',
    staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
    runtimeCaching: [{
        urlPattern: /playlist\.json$/,
        handler: 'networkFirst',
    }, {
        urlPattern: /\/recordings\//,
        handler: 'cacheFirst',
        options: {
        cache: {
            maxEntries: 120,
            name: 'recordings',
        },
        },
    }],
    })
);

Atualmente, o plug-in não processa recursos de mídia carregados progressivamente, como nossos arquivos de música. Por isso, resolvemos isso configurando o cabeçalho Cache-Control do Cloud Storage nesses arquivos como public, max-age=31536000 para que o navegador armazene o arquivo em cache por até um ano.

Conclusão

Queremos ver como os artistas vão contribuir para essa experiência e usá-la como uma ferramenta para expressão criativa com movimentos. Lançamos todo o código aberto, que pode ser encontrado em https://github.com/puckey/dance-tonite (link em inglês). Nos primeiros dias da RV, especialmente na WebVR, estamos ansiosos para ver quais novas direções criativas e inesperadas essa nova mídia vai tomar. Dance on-line.