A Experiência do Hobbit

Como dar vida à Terra-média com WebGL para dispositivos móveis

Daniel Isaksson
Daniel Isaksson

Historicamente, levar experiências multimídia, interativas e baseadas na Web para celulares e tablets tem sido um desafio. As principais restrições são o desempenho, a disponibilidade da API, as limitações no áudio HTML5 em dispositivos e a falta de reprodução contínua de vídeo inline.

No início deste ano, iniciamos um projeto com amigos do Google e da Warner Bros. para fazer uma experiência na Web para dispositivos móveis do novo filme do Hobbit, O Hobbit: A Desolação de Smaug. Criar um Chrome Experiment para dispositivos móveis com conteúdo multimídia é uma tarefa muito inspiradora e desafiadora.

A experiência é otimizada para o Google Chrome para Android nos novos dispositivos Nexus, onde agora temos acesso a WebGL e Web Audio. No entanto, uma grande parte da experiência também pode ser acessada em dispositivos e navegadores sem WebGL, graças à composição acelerada por hardware e animações CSS.

Toda a experiência é baseada em um mapa da Terra-média e nos locais e personagens dos filmes de Hobbit. Com o WebGL, foi possível dramatizar e explorar o rico mundo da trilogia Hobbit e deixar os usuários controlarem a experiência.

Desafios da WebGL em dispositivos móveis

Primeiro, o termo "dispositivos móveis" é muito amplo. As especificações dos dispositivos variam muito. Como desenvolvedor, você precisa decidir se quer oferecer suporte a mais dispositivos com uma experiência menos complexa ou, como fizemos nesse caso, limitar os dispositivos compatíveis àqueles que possam exibir um mundo em 3D mais realista. Para a “Viagem pela Terra-média”, nosso foco foram os dispositivos Nexus e cinco smartphones Android populares.

No experimento, usamos a three.js (em inglês) como fizemos em alguns dos nossos projetos WebGL anteriores. Começamos a implementação criando uma versão inicial do jogo Trollshaw que funcionaria bem no tablet Nexus 10. Após alguns testes iniciais no dispositivo, tínhamos uma lista de otimizações em mente que se parecia muito com o que normalmente usaríamos para um laptop de baixa especificação:

  • Usar modelos de lowpoly
  • Usar texturas de baixa resolução
  • Reduzir o número de chamadas de desenho o máximo possível mesclando geometrias.
  • Simplificar materiais e iluminação
  • Remover os efeitos de postagens e desativar a suavização
  • Otimize o desempenho do JavaScript
  • Renderize a tela WebGL na metade do tamanho e aumente com CSS

Depois de aplicar essas otimizações à nossa primeira versão aproximada do jogo, tínhamos um frame rate estável de 30 QPS. Naquela época, nossa meta era melhorar os recursos visuais sem afetar negativamente o frame rate. Tentamos muitos truques: alguns realmente tiveram um impacto real no desempenho, outros não tiveram um efeito tão grande quanto esperávamos.

Usar modelos de lowpoly

Vamos começar com os modelos. O uso de modelos lowpoly certamente ajuda no tempo de download e na inicialização da cena. Descobrimos que era possível aumentar bastante a complexidade sem afetar muito o desempenho. Os modelos de troll que usamos neste jogo têm aproximadamente 5 mil rostos. A cena tem cerca de 40 mil rostos, o que funciona bem.

Um dos trolls da floresta de Trollshaw
Um dos trolls da floresta de Trollshaw

Em outro local (ainda não lançado) na experiência, a redução de polígonos teve um impacto maior no desempenho. Nesse caso, carregamos objetos com polígonos inferiores para dispositivos móveis do que os objetos carregados para computadores. Criar diferentes conjuntos de modelos 3D requer trabalho extra e nem sempre é necessário. Isso depende da complexidade dos modelos no início.

Ao trabalhar em cenas grandes com muitos objetos, tentamos dividir a geometria de forma estratégica. Assim, pudemos ativar e desativar as malhas menos importantes rapidamente para encontrar uma configuração que funcionasse para todos os dispositivos móveis. Em seguida, poderíamos optar por mesclar a geometria em JavaScript no tempo de execução para otimização dinâmica ou por mesclá-la na pré-produção para salvar as solicitações.

Usar texturas de baixa resolução

Para reduzir o tempo de carregamento em dispositivos móveis, optamos por carregar diferentes texturas que tinham metade do tamanho das texturas no computador. Todos os dispositivos podem processar texturas de até 2.048 x 2.048 pixels e a maioria deles pode ter 4.096 x 4.096 pixels. A pesquisa de texturas nas texturas individuais não parece ser um problema depois do upload para a GPU. O tamanho total das texturas precisa caber na memória da GPU para que elas não sejam constantemente atualizadas e transferidas por download. No entanto, isso provavelmente não é um grande problema para a maioria das experiências da Web. No entanto, combinar texturas no menor número possível de folhas de sprite é importante para reduzir o número de chamadas de desenho. Isso é algo que tem um grande impacto no desempenho em dispositivos móveis.

Textura de um dos trolls da floresta de Trollshaw
Textura de um dos trolls da floresta de Trollshaw
(tamanho original 512x512px)

Simplificar materiais e iluminação

A escolha dos materiais também pode afetar muito o desempenho e deve ser gerenciada de forma inteligente em dispositivos móveis. Usar MeshLambertMaterial (por cálculo de luz de vértice) em three.js em vez de MeshPhongMaterial (por cálculo de luz de texel) é um dos itens que usamos para otimizar a performance. Basicamente, tentamos usar sombreadores simples com o mínimo possível de cálculos de iluminação.

Para ver como os materiais usados afetam a performance de uma cena, substitua os materiais com uma MeshBasicMaterial . Assim, você terá uma boa comparação.

scene.overrideMaterial = new THREE.MeshBasicMaterial({color:0x333333, wireframe:true});

Otimizar o desempenho do JavaScript

Ao criar jogos para dispositivos móveis, a GPU nem sempre é o maior obstáculo. É gasto muito tempo na CPU, especialmente física e animações esqueléticas. Dependendo da simulação, um truque que ajuda algumas vezes é executar esses cálculos caros apenas a cada dois frames. Você também pode usar as técnicas de otimização JavaScript disponíveis quando se trata de pool de objetos, coleta de lixo e criação de objetos.

Atualizar objetos pré-alocados em loops, em vez de criar novos, é uma etapa importante para evitar "falhas" na coleta de lixo durante o jogo.

Por exemplo, considere um código como este:

var currentPos = new THREE.Vector3();

function gameLoop() {
  currentPos = new THREE.Vector3(0+offsetX,100,0);
}

Uma versão aprimorada desse loop evita a criação de novos objetos que precisam ser coletados da lixeira:

var originPos = new THREE.Vector3(0,100,0);
var currentPos = new THREE.Vector3();
function gameLoop() {
  currentPos.copy(originPos).x += offsetX;
  //or
  currentPos.set(originPos.x+offsetX,originPos.y,originPos.z);
}

Na medida do possível, os manipuladores de eventos precisam atualizar apenas as propriedades e permitir que o loop de renderização requestAnimationFrame atualize o cenário.

Outra dica é otimizar e/ou pré-calcular as operações de lançamento de raios. Por exemplo, se você precisar anexar um objeto a uma malha durante um movimento de caminho estático, "registre" as posições durante um loop e leia esses dados em vez de lançar raios na malha. Ou, como fazemos na experiência Rvendell, use raycast para procurar interações do mouse com uma malha invisível de lowpoly mais simples. A pesquisa de colisões em uma malha de muitos polígonos é muito lenta e precisa ser evitada em um loop de jogo em geral.

Renderize a tela WebGL na metade do tamanho e aumente com CSS

O tamanho da tela do WebGL é provavelmente o parâmetro mais eficaz que você pode ajustar para otimizar o desempenho. Quanto maior a tela que você usar para desenhar a cena 3D, mais pixels terão que ser desenhados em cada frame. Isso, é claro, afeta o desempenho. O Nexus 10, com sua tela de alta densidade de 2.560 x 1.600 pixels, precisa enviar quatro vezes o número de pixels de um tablet de baixa densidade. Para otimizar isso em dispositivos móveis, usamos um truque em que definimos a tela para metade do tamanho (50%) e a dimensionamos para o tamanho pretendido (100%) com transformações CSS 3D aceleradas por hardware. A desvantagem disso é uma imagem pixelada, em que linhas finas podem se tornar um problema, mas em uma tela de alta resolução o efeito não é tão ruim. Com certeza vale a pena o desempenho extra.

A mesma cena, sem dimensionamento de tela no Nexus 10 (16 QPS) e dimensionada para 50% (33 QPS)
A mesma cena, sem dimensionamento de tela, no Nexus 10 (16 QPS) e dimensionada para 50% (33 QPS).

Objetos como elementos básicos

Para conseguir criar o grande labirinto do castelo Dol Guldur e o vale infinito da Valfenda, criamos um conjunto de modelos 3D de blocos de construção que reutilizamos. A reutilização de objetos nos permite garantir que eles sejam instanciados e enviados no início da experiência, e não no meio dela.

Blocos de construção de objetos 3D usados no labirinto de Dol Guldur.
Elementos básicos de objetos 3D usados no labirinto de Dol Guldur.

Em Valfenda, temos várias seções de solo que são constantemente reposicionadas em profundidade Z à medida que a jornada do usuário avança. À medida que o usuário passa por seções, elas são reposicionadas à medida que o usuário passa pelas seções.

Para o castelo Dol Guldur, queríamos que o labirinto fosse regenerado para cada jogo. Para isso, criamos um script que regenera o labirinto.

Mesclar toda a estrutura em uma grande malha desde o início resulta em um cenário muito grande e um desempenho ruim. Para resolver isso, decidimos ocultar e mostrar os elementos básicos, dependendo se eles estão ou não visíveis. Desde o início, tínhamos uma ideia de usar um script raycaster 2D, mas, no final, usamos a opção três.js integrada do frustrum culling. Reutilizamos o script do raycaster para ampliar o "perigo" que o jogador estava enfrentando.

O próximo passo é a interação do usuário. No computador, há entrada para mouse e teclado. Em dispositivos móveis, os usuários interagem com toques, gestos e gestos de pinça, além da orientação do dispositivo.

Uso da interação por toque em experiências da Web para dispositivos móveis

Adicionar o suporte por toque não é difícil. Há ótimos artigos para ler sobre o assunto. Mas algumas pequenas coisas podem complicar tudo.

É possível usar tanto o toque quanto o mouse. O Chromebook Pixel e outros laptops com touchscreen oferecem suporte a mouse e toque. Um erro comum é verificar se o dispositivo está ativado para toque e, em seguida, adicionar apenas listeners de eventos de toque e nenhum para o mouse.

Não atualize a renderização em listeners de eventos. Em vez disso, salve os eventos de toque em variáveis e reaja a eles no loop de renderização requestAnimationFrame. Isso melhora o desempenho e também agrupa eventos conflitantes. Reutilize os objetos em vez de criar novos nas escutas de evento.

Não se esqueça de que é multitoque: event.touches é uma matriz de todos os toques. Em alguns casos, é mais interessante analisar event.targetTouches ou event.changedTouches e reagir apenas aos toques de seu interesse. Para separar toques e deslizar, usamos um atraso antes de verificar se o toque se moveu (deslizar) ou se está imóvel (tocar). Para fazer uma pinça, medimos a distância entre os dois toques iniciais e como isso muda com o tempo.

Em um mundo 3D, você tem que decidir como sua câmera reage às ações do mouse ou de deslizar. Uma maneira comum de adicionar movimentos da câmera é seguir o movimento do mouse. Isso pode ser feito com o controle direto, usando a posição do mouse, ou com um movimento delta (alteração de posição). Nem sempre você quer o mesmo comportamento em um dispositivo móvel e em um navegador para computador. Testamos extensivamente para decidir o que parecia certo para cada versão.

Ao lidar com telas menores e touchscreens, você vai perceber que os dedos do usuário e os gráficos de interação com a interface geralmente atrapalham o que você quer mostrar. Estamos acostumados a fazer isso ao projetar aplicativos nativos, mas ainda não tivemos que pensar nisso antes com as experiências da Web. Esse é um desafio real para designers e designers de UX.

Resumo

Nossa experiência geral deste projeto é que a WebGL em dispositivos móveis funciona muito bem, especialmente em dispositivos mais novos e sofisticados. Quando se trata de desempenho, parece que a contagem de polígonos e o tamanho da textura afetam principalmente os tempos de download e inicialização. Os materiais, sombreadores e o tamanho da tela WebGL são os aspectos mais importantes da otimização para o desempenho em dispositivos móveis. No entanto, é a soma das partes que afetam o desempenho, então tudo o que você pode fazer para otimizar conta.

Segmentar dispositivos móveis também significa que você deve se acostumar a pensar em interações de toque e que não se trata apenas do tamanho do pixel, mas também do tamanho físico da tela. Em alguns casos, tivemos que aproximar a câmera 3D para ver o que estava acontecendo.

O experimento foi lançado e tem sido uma jornada fantástica. Esperamos que você goste!

Quer fazer um teste? Faça sua própria Jornada para a Terra-média.