A Experiência do Hobbit

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

Daniel Isaksson
Daniel Isaksson

Historicamente, trazer experiências interativas, baseadas na Web e com muitos recursos multimídia para dispositivos móveis e tablets tem sido um desafio. As principais restrições foram desempenho, disponibilidade de API, limitações no áudio HTML5 em dispositivos e a falta de reprodução de vídeo inline perfeita.

No início deste ano, iniciamos um projeto com amigos do Google e da Warner Bros. para criar uma experiência da Web que prioriza dispositivos móveis para o novo filme O Hobbit: A Desolação de Smaug. Criar um experimento do Chrome para dispositivos móveis com muitos recursos multimídia foi uma tarefa muito inspiradora e desafiadora.

A experiência é otimizada para o Chrome para Android nos novos dispositivos Nexus, onde agora temos acesso ao WebGL e ao Web Audio. No entanto, uma grande parte da experiência também é acessível em dispositivos e navegadores que não usam o WebGL, graças à composição acelerada por hardware e às animações CSS.

Toda a experiência é baseada em um mapa da Terra-média e nos locais e personagens dos filmes do Hobbit. O uso do WebGL nos permitiu dramatizar e explorar o rico mundo da trilogia O Hobbit e permitir que os usuários controlassem 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. Portanto, como desenvolvedor, você precisa decidir se quer oferecer suporte a mais dispositivos com uma experiência menos complexa ou, como fizemos neste caso, limitar os dispositivos compatíveis a aqueles que podem mostrar um mundo 3D mais realista. Para a "Journey through Middle-earth", nos concentramos em dispositivos Nexus e cinco smartphones Android populares.

No experimento, usamos o three.js, como fizemos em alguns dos nossos projetos anteriores do WebGL. Começamos a implementação criando uma versão inicial do jogo Trollshaw que funcionasse bem no tablet Nexus 10. Depois de alguns testes iniciais no dispositivo, tínhamos uma lista de otimizações que se assemelhavam às que normalmente usamos em um laptop de baixa especificação:

  • Usar modelos de baixa poligonalidade
  • Usar texturas de baixa resolução
  • Reduza o número de drawcalls o máximo possível mesclando a geometria.
  • Simplifique os materiais e a iluminação
  • Remover efeitos pós-produção e desativar o anti-aliasing
  • Otimizar a performance do JavaScript
  • Renderizar a tela WebGL pela metade e aumentar a escala com CSS

Depois de aplicar essas otimizações à nossa primeira versão aproximada do jogo, conseguimos uma taxa de frames estável de 30 QPS. Naquele momento, nosso objetivo era melhorar os recursos visuais sem afetar negativamente a taxa de quadros. Testamos muitos truques: alguns realmente afetaram a performance, outros não tiveram o efeito esperado.

Usar modelos de baixa poligonalidade

Vamos começar pelos modelos. O uso de modelos de baixa poligonalidade certamente ajuda no tempo de download e no tempo necessário para inicializar a cena. Descobrimos que podíamos aumentar muito a complexidade sem afetar muito a performance. Os modelos de troll que usamos neste jogo têm cerca de 5 mil faces, e a cena tem cerca de 40 mil faces, e isso funciona bem.

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

Em outro local (ainda não lançado) da experiência, observamos um impacto maior na performance com a redução de polígonos. Nesse caso, carregamos objetos de polígonos mais baixos para dispositivos móveis do que os objetos que carregamos para computadores. A criação de diferentes conjuntos de modelos 3D exige um pouco mais de trabalho e nem sempre é necessária. Isso depende da complexidade dos seus modelos.

Ao trabalhar em cenas grandes com muitos objetos, tentamos ser estratégicos na forma como dividimos a geometria. Isso nos permitiu ativar e desativar rapidamente malhas menos importantes para encontrar uma configuração que funcionasse para todos os dispositivos móveis. Em seguida, podemos mesclar a geometria em JavaScript no momento da execução para otimização dinâmica ou mesclar na pré-produção para salvar solicitações.

Usar texturas de baixa resolução

Para reduzir o tempo de carregamento em dispositivos móveis, optamos por carregar texturas diferentes com metade do tamanho das texturas em computadores. Todos os dispositivos podem processar tamanhos de textura de até 2048 x 2048 pixels, e a maioria pode processar 4096 x 4096 pixels. A pesquisa de textura nas texturas individuais não parece ser um problema depois que elas são enviadas para a GPU. O tamanho total das texturas precisa caber na memória da GPU para evitar que elas sejam constantemente carregadas e descarregadas, mas isso provavelmente não é um grande problema para a maioria das experiências na Web. No entanto, combinar texturas em poucas spritesheets é importante para reduzir o número de drawcalls, o que tem um grande impacto no desempenho em dispositivos móveis.

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

Simplifique os materiais e a iluminação

A escolha dos materiais também pode afetar muito a performance e precisa ser gerenciada com sabedoria em dispositivos móveis. Usamos MeshLambertMaterial (cálculo de luz por vértice) no three.js em vez de MeshPhongMaterial (cálculo de luz por texel) para otimizar o desempenho. Basicamente, tentamos usar sombreadores simples com o menor número possível de cálculos de iluminação.

Para saber como os materiais que você usa afetam o desempenho de uma cena, substitua os materiais da cena com um MeshBasicMaterial . Isso vai permitir uma boa comparação.

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

Otimizar a performance do JavaScript

Ao criar jogos para dispositivos móveis, a GPU nem sempre é o maior obstáculo. Muito tempo é gasto na CPU, principalmente em física e animações de esqueleto. Um truque que às vezes ajuda, dependendo da simulação, é executar esses cálculos caros apenas em todos os outros frames. Você também pode usar as técnicas de otimização de JavaScript disponíveis para agrupamento de objetos, coleta de lixo e criação de objetos.

Atualizar objetos pré-alocados em loops em vez de criar novos objetos é uma etapa importante para evitar "engasgos" 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:

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

Sempre que possível, os manipuladores de eventos devem atualizar apenas propriedades e deixar que o loop de renderização requestAnimationFrame gerencie a atualização do palco.

Outra dica é otimizar e/ou pré-calcular operações de ray-casting. Por exemplo, se você precisar anexar um objeto a uma malha durante um movimento de caminho estático, poderá "registrar" as posições durante um loop e ler esses dados em vez de fazer um ray-casting na malha. Ou, como fazemos na experiência Rivendell, use o ray-cast para procurar interações do mouse com uma malha invisível de baixa poligonalidade mais simples. A pesquisa de colisões em uma malha de alta poligonalização é muito lenta e precisa ser evitada em um loop de jogo em geral.

Renderizar a tela WebGL pela metade e aumentar a escala 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 usada para desenhar a cena 3D, mais pixels precisam ser renderizados em cada frame. Isso afeta a performance.O Nexus 10, com tela de alta densidade de 2560x1600 pixels, precisa enviar quatro vezes mais pixels do que um tablet de baixa densidade. Para otimizar isso para dispositivos móveis, usamos um truque em que definimos a tela com metade do tamanho (50%) e, em seguida, aumentamos para o tamanho desejado (100%) com transformações 3D do CSS aceleradas por hardware. A desvantagem disso é uma imagem pixelizada em que linhas finas podem se tornar um problema, mas em uma tela de alta resolução o efeito não é tão ruim. Vale a pena o desempenho extra.

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

Objetos como elementos básicos

Para criar o grande labirinto do castelo de Dol Guldur e o vale sem fim de Rivendell, fizemos um conjunto de modelos 3D de blocos de construção que reutilizamos. A reutilização de objetos permite 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 Rivendell, temos várias seções de solo que reposicionamos constantemente na profundidade Z à medida que a jornada do usuário avança. À medida que o usuário passa por seções, elas são reposicionadas na distância.

Para o castelo de 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 uma cena muito grande e em um desempenho ruim. Para resolver esse problema, decidimos ocultar e mostrar os elementos básicos dependendo de estarem ou não na visualização. Desde o início, tínhamos a ideia de usar um script de raycaster 2D, mas, no final, usamos o culling de frustum integrado do three.js. Reutilizamos o script do raycaster para dar zoom no "perigo" que o jogador está enfrentando.

A próxima grande coisa a ser processada é a interação do usuário. Em computadores, você tem entrada de mouse e teclado. Em dispositivos móveis, os usuários interagem com toque, deslizar, beliscar, orientação do dispositivo etc.

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

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

Você pode usar ambos, toque e mouse. O Chromebook Pixel e outros laptops com tela touch têm suporte a mouse e toque. Um erro comum é verificar se o dispositivo está ativado para toque e adicionar apenas listeners de eventos de toque, e nenhum para mouse.

Não atualize a renderização em listeners de eventos. Salve os eventos de toque em variáveis e reaja a eles no loop de renderização de requestAnimationFrame. Isso melhora o desempenho e também combina eventos conflitantes. Reutilize objetos em vez de criar novos objetos nos listeners de eventos.

Lembre-se 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 de deslizes, usamos um atraso antes de verificar se o toque se moveu (deslize) ou se ele está parado (toque). Para fazer isso, medimos a distância entre os dois toques iniciais e como ela muda ao longo do tempo.

Em um mundo 3D, você precisa decidir como a câmera reage às ações do mouse e do toque. Uma maneira comum de adicionar movimento à 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 (mudança de posição). Nem sempre você quer o mesmo comportamento em um dispositivo móvel que em um navegador para computador. Fizemos muitos testes para decidir qual era a melhor opção para cada versão.

Ao lidar com telas menores e telas sensíveis ao toque, você vai perceber que os dedos do usuário e os gráficos de interação da interface muitas vezes atrapalham o que você quer mostrar. Isso é algo que usamos ao projetar apps nativos, mas não tínhamos pensado antes em experiências da Web. Esse é um desafio real para designers e designers de UX.

Resumo

Nossa experiência geral com esse projeto é que o WebGL em dispositivos móveis funciona muito bem, especialmente em dispositivos mais recentes e de alta qualidade. Em relação ao desempenho, parece que a contagem de polígonos e o tamanho da textura afetam principalmente os tempos de download e inicialização, e que materiais, sombreadores e o tamanho da tela WebGL são as partes mais importantes a serem otimizadas para o desempenho em dispositivos móveis. No entanto, é a soma das partes que afeta o desempenho, então tudo o que você pode fazer para otimizar conta.

O direcionamento a dispositivos móveis também significa que você precisa se acostumar a pensar em interações por 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 foi uma jornada fantástica. Esperamos que você goste!

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