Construindo um jogo WebGL multiplataforma com Babylon.js
Publicados: 2022-03-10Aqui está um desafio para você: que tal construir um jogo 3D no fim de semana? Babylon.js é um framework JavaScript para construção de jogos 3D com HTML5, WebGL e Web Audio , construído por você e pela equipe Babylon.js. Para comemorar a nova versão 2.3 da biblioteca, decidimos construir uma nova demo chamada “Sponza” para destacar o que pode ser feito com o motor WebGL e HTML5 quando se trata de construir grandes jogos hoje em dia.
A ideia era criar uma experiência consistente, semelhante, senão idêntica, em todas as plataformas suportadas pelo WebGL e tentar alcançar os recursos dos aplicativos nativos. Neste artigo, explicarei como tudo funciona em conjunto, juntamente com os vários desafios que enfrentamos e as lições que aprendemos ao construí-lo.
Leitura adicional no SmashingMag:
- Construindo Shaders com Babylon.js
- Usando a API do Gamepad em jogos da web
- Introdução à modelagem poligonal e Three.js
- Como criar uma máquina de bateria de 8 bits responsiva
Para atingir esse objetivo, o Sponza usa vários recursos HTML5, como WebGL, Web Audio, bem como Pointer Events (amplamente suportado agora graças ao polyfill JQuery PEP), Gamepad API, IndexedDB, HTML5 AppCache, transição/animação CSS3, flexbox e Fullscreen API. Você pode testar a demonstração do Sponza em seu desktop, celular ou Xbox One.

Descobrindo a demonstração
Primeiro, você começará em uma sequência de animação automática, dando os créditos a quem construiu a cena. A maioria dos membros da equipe vem da cena de demonstração. Você descobrirá que esta é uma parte importante da cultura dos desenvolvedores 3D. Do meu lado, eu estava no Atari enquanto David Catuhe estava no Amiga, que ainda é uma fonte regular de conflitos entre nós, acredite ou não. Eu estava codificando um pouco, mas principalmente compondo a música no meu grupo de demonstração. Eu era um grande fã do Future Crew e mais especificamente do Purple Motion, meu compositor de cena demo favorito de todos os tempos. Mas não vamos nos desviar do assunto.
Para Sponza, aqui estão os colaboradores:
- Michel Rousseau , também conhecido como “Mitch”, fez animações visuais notáveis e otimizações de renderização atuando como o artista 3D. Ele pegou o modelo Sponza fornecido gratuitamente pela Crytek em seu site e usou o exportador 3DS Max para gerar o que você vê.
- David Catuhe, também conhecido como “deltakosh” e eu fizemos a parte principal do mecanismo Babylon.js e também todo o código para a demonstração (carregador personalizado, efeitos especiais para o modo de demonstração usando pós-processos de fade to black etc.), bem como um novo tipo de câmera chamada “ UniversalCamera ” que trata todo o tipo de entradas de forma genérica.
- Eu compus a música usando Renoise e o banco de som EastWest Symphonic Orchestra. Se você estiver interessado, eu já compartilhei meu fluxo de trabalho e processo no artigo sobre Compondo a música para o jogo World Monger Windows 8 usando o rastreador Renoise e plug-ins East West VST
- Julien Moreau-Mathis nos ajudou construindo uma nova ferramenta para ajudar os artistas 3D a finalizar o trabalho entre as ferramentas de modelagem (3DS Max, Blender) e o resultado final. Por exemplo, Michel o usou para testar e ajustar várias câmeras animadas e injetar partículas na cena.
Se você esperar até o final da sequência automática até o “final épico”, você passará automaticamente para o modo interativo. Se você quiser ignorar o modo de demonstração, basta clicar no ícone da câmera ou pressionar A
no seu gamepad.
No modo interativo, se você estiver em um Mac ou PC, poderá se mover dentro da cena usando o teclado/mouse como um jogo FPS. Se você estiver em um smartphone, poderá se mover usando um único toque (e 2
para girar a câmera). Finalmente, em um Xbox One, você pode usar o gamepad (ou desktop se estiver conectando um gamepad nele). Curiosidade: em um Windows touch PC, você pode usar 3 tipos de entrada ao mesmo tempo.
A atmosfera é diferente no modo interativo. Você tem três fontes de áudio de tempestade posicionadas aleatoriamente no ambiente 3D, sopros de vento e rachaduras de fogo em cada canto. Em navegadores compatíveis (Chrome, Firefox, Opera e Safari), você pode até alternar entre o modo de alto-falante normal e o modo de fone de ouvido clicando no ícone dedicado. Em seguida, ele usará a renderização de áudio binaural do Web Audio para uma simulação de áudio mais realista - se você estiver ouvindo com fones de ouvido.
Para ter uma experiência completa semelhante a um aplicativo, geramos ícones e blocos para todas as plataformas. Isso significa, por exemplo, que no Windows 8 ⁄ 10 você pode fixar o aplicativo da web no menu “Iniciar”. Temos até vários tamanhos disponíveis:


Off-line primeiro!
Depois que a demonstração estiver completamente carregada, você pode alternar seu telefone para o modo avião para cortar a conectividade e clicar no ícone do Sponza. O aplicativo da web ainda fornecerá a experiência completa com renderização WebGL, áudio da web 3D e suporte ao toque. Mude para tela cheia e você literalmente não poderá sentir a diferença entre a demonstração e uma experiência de aplicativo nativo.
Estamos usando a camada IndexedDB disponível nativamente dentro do Babylon.js para isso. A cena (formato JSON) e os recursos (texturas JPG/PNG, bem como MP3 para a música e sons) são armazenados no IDB. A camada do BID juntamente com o cache do aplicativo HTML5 está fornecendo a experiência offline. Para saber mais sobre esta parte e como configurar seu jogo para obter resultados semelhantes, você pode ler o artigo Usando IndexedDB para lidar com seus ativos 3D WebGL: compartilhando feedbacks e dicas do Babylon.JS e Recursos de cache no IndexedDB no Babylon.js
Xbox One gosta do show
Por último, mas não menos importante, a mesma demonstração funciona perfeitamente no MS Edge no seu Xbox One:

Pressione A
para alternar para o modo interativo . O Xbox One irá notificá-lo de que agora você pode se mover usando seu gamepad dentro da cena 3D:

Então, vamos recapitular brevemente.
A mesma base de código funciona em Mac, Linux, Windows no MS Edge, Chrome, Firefox, Opera e Safari, no iPhone/iPad, em dispositivos Android com Chrome ou Firefox, Firefox OS e no Xbox One! Isso não é legal? Ser capaz de direcionar tantos dispositivos com uma experiência nativa completa diretamente do seu servidor web?
Já compartilhei minha empolgação com o potencial da tecnologia em um artigo anterior na web: a próxima fronteira do jogo?
Hackeie a cena com a camada de depuração
Se você quiser entender como Michel está dominando a magia da modelagem 3D, você pode hackear a cena usando a ferramenta Babylon.js Debug Layer . Para habilitá-lo em uma máquina com teclado, pressione CMD/CTRL + SHIFT + D
e se estiver usando um gamepad no PC ou Xbox, pressione Y
. Observe que exibir a camada de depuração custa um pouco de desempenho devido ao trabalho de composição que o mecanismo de renderização precisa fazer. Portanto, o FPS exibido é um pouco menos importante do que o FPS real que você tem sem a camada de depuração exibida.
Vamos testá-lo em um PC, por exemplo.
Aproxime-se da cabeça do leão e corte o canal de colisão do pipeline do nosso shader:

Você deve ver que a cabeça agora é menos realista. Jogue com o outro canal para verificar o que está acontecendo.
Você também pode cortar o mecanismo de relâmpago dinâmico ou desativar o mecanismo de colisões para voar ou se mover pelas paredes. Por exemplo, desative a caixa de seleção “ colisões ” e voe para o primeiro andar. Coloque a câmera na frente das bandeiras vermelhas. Você pode vê-los se movendo ligeiramente. Michel usou o suporte de ossos/esqueletos do Babylon.js para movê-los. Agora, desative a opção “ esqueletos ” e eles devem parar de se mover:

Por fim, você pode exibir a árvore de malhas no canto superior direito. Você pode ativá-los ou desativá-los para interromper completamente o trabalho feito por Michel:

Remover as geometrias, os canais do sombreador ou algumas opções do mecanismo pode ajudá-lo a solucionar problemas de desempenho em um dispositivo específico para ver o que está custando muito no momento. Você também pode verificar se é limitado por CPU ou GPU, embora na maioria das vezes você seja limitado por CPU no WebGL devido à natureza mono-threading do JavaScript. Por fim, a ferramenta também é muito útil para ajudá-lo a aprender como uma cena foi construída pelo artista 3D.
A propósito, também funciona muito bem no Xbox One:

Desafios
Ao longo do caminho, enfrentamos vários problemas e desafios para construir a demo. Vejamos alguns deles em detalhes.
Desempenho WebGL e compatibilidade entre plataformas
O lado da programação foi provavelmente o mais fácil de resolver, já que é completamente tratado pelo próprio mecanismo Babylon.js. Estamos usando uma arquitetura de shader personalizada que se adapta à plataforma tentando encontrar o melhor shader disponível para o navegador/GPU atual usando vários fallbacks. A ideia é diminuir a qualidade e complexidade do motor de renderização até conseguirmos exibir algo significativo na tela.
O Babylon.js é baseado principalmente no WebGL 1.0 para garantir que as experiências 3D construídas sobre ele sejam executadas em praticamente todos os lugares. Ele foi construído com a filosofia da web em mente, por isso estamos aprimorando progressivamente o processo de compilação do shader. Isso é completamente transparente para o artista 3D que não quer lidar com essas complexidades na maioria das vezes.
Ainda assim, o artista 3D tem um papel muito importante na otimização do desempenho. Ela precisa conhecer a plataforma que está segmentando, recursos e limitações compatíveis. Você não pode pegar ativos vindos de jogos AAA feitos para GPUs de ponta e DirectX 12 e simplesmente integrá-los em um jogo rodando em um mecanismo WebGL. Eu diria que direcionar o WebGL hoje é bastante semelhante ao trabalho que você terá que fazer para otimizar as experiências em dispositivos móveis, com uma pitada de JavaScript extra que precisa ser altamente mono-thread.
Mitch é extremamente bom nisso: otimizar as texturas, pré-calcular o relâmpago para colocá-lo nas texturas, reduzir o número de chamadas de desenho o máximo possível, etc. Ele tem anos de experiência por trás dele e viu as várias gerações de Hardware e motores 3D (desde PowerVR/3DFX até as GPUs atuais) que realmente ajudaram a fazer a demonstração acontecer.
Ele já compartilhou um pouco desses fundamentos em seus artigos sobre Real Time 3D: fazendo uma demonstração para WebGL Purposes–Basics e já provou várias vezes que você pode criar uma experiência visual bastante fascinante na web com alto desempenho em pequenas GPUs integradas, por exemplo, em Cenas de demonstração Mansion, Hill Valley ou Espilit. Se você estiver interessado, aproveite para assistir sua palestra no NGF2014 – Criar ativos 3D para o mundo móvel e a web, o ponto de vista de um designer 3D onde ele compartilhou sua experiência e como ele conseguiu otimizar a cena de Hill Valley a partir de menos de 1 fps a 60 fps.

O objetivo inicial de Sponza era construir duas cenas. Um para desktop e outro para celular com menos complexidade, texturas menores e malhas e geometrias mais simples. Mas durante nossos testes, finalmente descobrimos que a versão para desktop também estava funcionando muito bem no celular, pois pode rodar até 60fps em um iPhone 6s ou um Android OnePlus 2. Decidimos então não continuar trabalhando na versão móvel mais simples.
Mas, novamente, provavelmente teria sido melhor ter uma abordagem mobile-first limpa no Sponza para atingir 30fps+ em muitos dispositivos móveis e, em seguida, aprimorar a cena para dispositivos móveis e desktops de última geração. Ainda assim, a maioria dos comentários que recebemos até agora no Twitter parece indicar que o resultado final funciona muito bem na maioria dos dispositivos. É certo que o Sponza foi otimizado em uma GPU HD4000 (Intel Core i5 integrado) que é mais ou menos equivalente às GPUs de celulares de última geração.
Ficamos muito felizes com o desempenho que conseguimos alcançar. Sponza está usando nosso shader com ambiente , difuso , colisão , especular e reflexo ativado. Temos algumas partículas para simular pequenos incêndios em cada canto, ossos animados para as bandeiras vermelhas, sons posicionados em 3D e colisões quando você está se movendo usando o modo interativo.
Tecnicamente falando, temos 98 malhas usadas na cena , gerando até 377781 vértices, 16 ossos ativos, mais de 60 partículas que poderiam gerar até 36 chamadas de desenho. O que aprendemos? Uma coisa é certa: ter menos chamadas de sorteio é a chave para o desempenho ideal, ainda mais na web.
O carregador
Para o Sponza, queríamos criar um novo carregador, diferente do padrão que estamos usando no site do BabylonJS, para ter um aplicativo web limpo e polido. Pedi então a Michel que sugerisse algo novo.
Ele primeiro enviou a seguinte tela para mim:

De fato, a tela parece muito bonita quando você a vê pela primeira vez. Mas então você pode começar a se perguntar como fará isso funcionar em todos os dispositivos, de uma maneira verdadeiramente responsiva? Vamos descobrir.
Vamos falar sobre o fundo primeiro. O efeito borrado criado por Michel era bom, mas não estava funcionando bem em todos os tamanhos de janela e resoluções gerando algum moiré. Eu então a substituí por uma captura de tela “clássica” da cena. No entanto, eu queria que o plano de fundo preenchesse completamente a tela sem barras pretas e sem esticar a imagem para quebrar a proporção.
A solução vem principalmente do CSS background-size: cover
+ centralização da imagem nos eixos X e Y. Como resultado, temos a experiência que eu estava procurando, qualquer que seja a proporção da tela usada:

As outras partes estão usando o bom e velho posicionamento CSS baseado em porcentagem. Ok, com isso classificado, como lidamos com a tipografia — o tamanho da fonte deve ser baseado no tamanho da janela de visualização. Obviamente, podemos usar unidades de viewport para isso. vw
e vh
(onde 1vw é 1% da largura da janela de visualização e 1vh é 1% da altura da janela de visualização) são bastante bem suportados em navegadores, em particular em todos os navegadores compatíveis com WebGL. (Há também um artigo sobre Tipografia de tamanho de viewport na Smashing Magazine que eu recomendo que você leia.)
Por fim, estamos brincando com a propriedade opacity
da imagem de fundo para movê-la de 0
para 1
com base no processo de download atual passando de 0 para 100%.
Ah, a propósito, as animações de texto são feitas simplesmente usando transições CSS ou animações combinadas com um layout flexbox para ter uma maneira simples, mas eficiente de exibir no centro ou em cada canto.
Manipulando todas as entradas de forma transparente
Nosso mecanismo WebGL está fazendo todo o trabalho no lado da renderização para exibir os visuais corretamente em todas as plataformas. Mas como podemos garantir que o usuário será capaz de se mover dentro da cena qualquer que seja o tipo de entrada usado?
Na versão anterior do Babylon.js, estávamos suportando todos os tipos de entrada e interações do usuário: teclado/mouse, toque, joysticks de toque virtual, gamepad, orientação do dispositivo VR (para Card Board) e WebVR, cada um através de uma câmera dedicada. Você pode ler nossa documentação para saber um pouco mais sobre eles.
O Touch está sendo gerenciado universalmente com a especificação Pointer Events propagada para todas as plataformas por meio do polyfill PEP do jQuery (gerando Touch Events para o aplicativo quando necessário). Para saber mais sobre Pointer Events, você pode ler em Unifying touch and mouse: como Pointer Events facilitará o suporte ao toque entre navegadores
Volte para a demonstração então. A ideia do Sponza era ter uma câmera única, lidando com todos os cenários de usuários de uma só vez: desktop, mobile e console.
Acabamos criando a UniversalCamera . Para ser honesto, era tão óbvio e simples de criar que ainda não sei por que não o fizemos antes. A UniversalCamera é mais ou menos uma câmera gamepad que estende a TouchCamera que estende a FreeCamera .
A FreeCamera está fornecendo a lógica de teclado/mouse; a TouchCamera está fornecendo a lógica de toque e a extensão final fornece a lógica do gamepad.
A UniversalCamera agora é usada no Babylon.js por padrão. Se você estiver navegando pelas demos, poderá se mover dentro das cenas usando o mouse, o toque e o gamepad em todas elas. Novamente, você pode estudar o código para ver exatamente como ele é feito.
Sincronizando as transições com a música
Agora, esta parte é onde nós nos perguntamos mais. Você deve ter notado que a seqüência de introdução é sincronizada com áreas específicas da faixa de reprodução de música . As primeiras linhas são exibidas quando alguns dos tambores começam a tocar, e a sequência final final está mudando rapidamente de uma câmera para outra em cada nota do instrumento de trompa que estamos usando.
Sincronizar áudio com o loop de renderização WebGL não é fácil. Novamente, essa é a natureza mono-threaded do JavaScript que gera essa complexidade. Os artigos sobre Introdução aos HTML5 Web Workers: a abordagem multithreading do JavaScript compartilham alguns insights por trás disso. É muito importante entender o problema para entender o problema global que estamos enfrentando, mas entrar em detalhes aqui está fora do escopo deste artigo.
Normalmente, em cenas de demonstração (e videogames), se você quiser sincronizar o visual com os sons/música, você será guiado pela pilha de áudio. Duas abordagens são frequentemente usadas:
- Gerar metadados que seriam injetados nos arquivos de áudio, e que então poderiam “chamar” alguns eventos quando você estiver alcançando uma parte específica dele,
- Análise em tempo real do fluxo de áudio via FFT ou tecnologias semelhantes para detectar picos interessantes ou alterações de BPM que gerariam novamente eventos para o mecanismo visual.
Essas abordagens funcionam particularmente bem em ambientes multithread como C++. Mas em JavaScript, com Web Audio, temos dois problemas:
- JavaScript, que é monothread e, infelizmente, na maioria das vezes, os web workers não nos ajudam,
- O Web Audio não tem nenhum evento que possa ser enviado de volta ao thread da interface do usuário, mesmo que o Web Audio esteja sendo tratado em um thread separado pelo navegador.
O Web Audio tem um temporizador muito mais preciso que o JavaScript. Teria sido fantástico poder usar esse cronômetro separado em um thread separado para direcionar os eventos de volta ao thread da interface do usuário. Mas hoje, você não pode fazer isso (ainda?).
Por outro lado, estamos renderizando a cena usando WebGL e o método requestAnimationFrame
. Isso significa que, nos “melhores casos”, temos uma janela de tempo de 16 ms. Se estiver faltando um, você terá que esperar até 16ms para poder agir no próximo quadro para refletir a sincronização do som (por exemplo, para lançar um efeito “fade-to-black”).
Eu estava pensando em injetar a lógica de sincronização no loop requestAnimationFrame
. Estudei o tempo gasto desde o início da sequência e analisei a opção de ajustar o visual para reagir a um evento de áudio. A boa notícia é que o áudio da Web renderizará o som independentemente do que estiver acontecendo no thread principal da interface do usuário. Por exemplo, você pode ter certeza de que o timestamp de 12s da música chegará exatamente 12s depois que a música começar a tocar, mesmo que a GPU esteja tendo dificuldades para renderizar a cena.
No final, finalmente escolhemos provavelmente a solução mais simples de todas: usar chamadas setTimeout()
! Sim eu sei. Se você olhar para a maioria dos artigos por aí, incluindo o que eu vinculei acima, você descobrirá que não é confiável. Mas no nosso caso, uma vez que a cena está pronta para ser renderizada, sabemos que baixamos todos os nossos recursos (texturas e sons) e compilamos nossos shaders. Não devemos ficar muito incomodados com eventos inesperados que saturam o thread da interface do usuário. O GC pode ser um problema , mas também passamos muito tempo lutando contra ele no mecanismo: reduzindo a pressão no coletor de lixo usando a barra de desenvolvedor F12 do Internet Explorer 11.
Ainda assim, sabemos que esta solução está longe de ser a ideal. Alternar para outra guia ou bloquear seu telefone e desbloqueá-lo alguns segundos depois pode gerar alguns problemas na parte de sincronização da demonstração. Podemos corrigir esses problemas usando a API Page Visibility, por exemplo, pausando o loop de renderização, vários sons e re-computando os próximos prazos para as chamadas setTimeout()
.
Mas talvez tenhamos perdido alguma coisa; talvez, e até provavelmente, houvesse uma abordagem melhor para lidar com esse problema. Adoraríamos ouvir seus pensamentos e sugestões na seção de comentários, se você acha que há uma maneira melhor de resolver o mesmo problema.
Manipulando o áudio da Web no iOS
O último desafio que gostaria de compartilhar com vocês é a forma como o Web Audio está sendo tratado pelo iOS no iPhone e iPad. Se você está procurando artigos sobre “áudio da web + iOS”, você encontrará muitas pessoas com dificuldades para reproduzir sons no iOS. Agora, o que está acontecendo lá?
O iOS tem um suporte notável de Web Audio - até mesmo o modo de áudio binaural. Mas a Apple decidiu que uma página da web não pode reproduzir nenhum som por padrão sem a interação de um usuário específico. Essa decisão provavelmente foi tomada para evitar publicidade ou qualquer outra coisa que perturbe o usuário ao reproduzir sons não solicitados.
O que isso significa para os desenvolvedores da web? Bem, primeiro você precisa desbloquear o contexto de áudio da web do iOS após o toque de um usuário - antes de tentar reproduzir qualquer som. Caso contrário, seu aplicativo da Web permanecerá desesperadamente mudo.
Infelizmente, a única maneira que encontrei para fazer essa verificação é fazendo uma abordagem de detecção de plataforma do usuário, pois não encontrei uma maneira de detecção de recursos para fazê-lo. Isso é nada menos que uma técnica horrível e não à prova de balas, mas não consegui encontrar nenhuma outra solução que resolvesse o problema. O código? Aqui está!
Se você não estiver no iPad/iPhone/iPod, o contexto de áudio estará imediatamente disponível para uso. Caso contrário, estamos desbloqueando o contexto de áudio do iOS reproduzindo um som vazio gerado por código no evento touchend . Você pode se registrar no evento onAudioUnlocked se quiser esperar antes de iniciar o jogo. Então, se você estiver iniciando o Sponza em um iPhone/iPad, você terá esta tela final no final da sequência de carregamento:

Tocar em qualquer lugar na tela desbloqueará a pilha de áudio do iOS e iniciará o “show”.
Aqui vamos nos! Espero que você tenha gostado de alguns insights por trás do desenvolvimento da demonstração. Para saber mais, leia o código-fonte completo desta demonstração em nosso GitHub. Obviamente, tudo é open source, e você pode encontrar os arquivos principais no GitHub: index.js e babylon.demo.ts.
Finalmente, eu realmente espero que você esteja ainda mais convencido de que a web é definitivamente uma ótima plataforma para jogos! Fique atento, pois estamos trabalhando em novas demos neste exato momento, e esperamos que elas também sejam bastante impressionantes.
Este artigo faz parte da série de desenvolvimento da Web dos evangelistas e engenheiros de tecnologia da Microsoft sobre aprendizado prático de JavaScript, projetos de código aberto e práticas recomendadas de interoperabilidade, incluindo o navegador Microsoft Edge.Incentivamos você a testar em navegadores e dispositivos, incluindo o Microsoft Edge – o navegador padrão para Windows 10 – com ferramentas gratuitas em dev.microsoftedge.com, incluindo ferramentas de desenvolvedor F12 – sete ferramentas distintas e totalmente documentadas para ajudá-lo a depurar, testar e acelerar suas páginas da web. Além disso, visite o blog Edge para se manter atualizado e informado dos desenvolvedores e especialistas da Microsoft.