Como a Toca Boca construiu um backend de renderização escalável e de alto desempenho
Bertrand Guay-Paquet - Toca Boca
Senior Graphics Engineer
Para atender às demandas de desempenho do nosso ambicioso jogo móvel 3D multiplayer, Toca Boca Days – especialmente em dispositivos de menor capacidade – precisávamos inovar. O jogo (agora em pausa) apresentava níveis expansivos e suportava até 32 jogadores com avatares altamente personalizáveis.
No final de 2022, lançamos nosso sistema de backend Batch Renderer, construído sobre a API BatchRendererGroup do Unity. Esse novo sistema proporcionou ganhos significativos de desempenho, que continuamos refinando.
Neste artigo, compartilharei insights sobre a arquitetura do sistema enquanto nos preparamos para usá-lo em nosso próximo jogo.
Visão geral da arquitetura de dados
Em um nível alto, nosso sistema divide os dados de um GameObject e seu componente Renderer em três camadas.

Arquitetura de dados em camadas
Na primeira camada, cada componente Renderer corresponde a uma única instância de culling. Essa camada contém todos os dados necessários para determinar a visibilidade com base nos planos de culling da câmera, máscara de camada e qualquer grupo LOD pai.
A segunda camada representa o(s) material(is) de cada Renderer, onde cada par Renderer-material forma uma instância de desenho. As instâncias de desenho armazenam dados relacionados à renderização, como a malha e o material.
A terceira camada consiste em lotes BatchRendererGroup (daí o nome Batch Renderer) e suas instâncias. Cada lote possui seu próprio GraphicsBuffer contendo propriedades por instância integradas, como a matriz ObjectToWorld, juntamente com um conjunto opcional de propriedades de material por instância.
O conjunto de propriedades de material por instância de um Renderer determina seu tipo de lote.

Vários livros com valores de propriedades de material por instância
Nosso sistema armazena todos os dados do lado da CPU em uma estrutura compacta, orientada a dados, no formato de arrays (SoA). Ele se baseia em tipos blitáveis dentro de contêineres nativos sempre que possível, permitindo o uso de trabalhos de Burst de alto desempenho para tarefas como culling de instâncias e geração de comandos de desenho. Essa arquitetura permite que o sistema lide eficientemente com grandes quantidades de instâncias.
Propriedades de material por instância
Para atribuir propriedades de material por instância a um Renderer, adicionamos um componente que referencia um ativo de "conjunto de propriedades"ScriptableObject. Os conjuntos de propriedades devem ser limitados em número, pois cada um define um tipo de lote separado – instâncias de lotes diferentes não são renderizadas juntas.
Para facilitar a autoria, fornecemos um componente para definir valores de propriedades instanciadas iniciais, juntamente com uma janela de Inspector personalizada. Sem propriedades por instância, esses valores precisariam ser definidos em materiais únicos, reduzindo a eficiência do lote.

Inspector de propriedades de material por instância
Skinning e esqueletos
Em Toca Boca Days, os avatares apresentavam extensa personalização de personagens. Para suportar roupas e acessórios variados, dividimos as partes esqueléticas da malha de cada avatar em 17 componentes Skinned Mesh Renderer (por exemplo, braço superior esquerdo). A maioria dos itens de vestuário adiciona componentes Renderer esqueléticos adicionais, que podem se acumular rapidamente à medida que o número de jogadores aumenta.

Avatares de Toca Boca Days
Como nossos avatares são complexos, evitamos usá-los para mapas de sombras, que seriam caros. Em vez disso, usamos sombras em forma de blob, e na maioria dos casos, os avatares são renderizados apenas uma vez por quadro. Isso significa que não podemos amortizar o custo de skinning de vértices embutido no Unity em vários comandos de desenho para uma única malha.
Em vez disso, lidamos com o skinning diretamente no shader de vértice. Todas as matrizes de skinning são armazenadas em um único GraphicsBuffer global, e cada Renderer recebe um deslocamento para esse buffer para ajustar os índices de osso por vértice e buscar suas matrizes de skinning correspondentes.
O deslocamento do esqueleto é armazenado como dados de instância do BatchRendererGroup, permitindo que agrupemos chamadas de desenho entre personagens que compartilham a mesma malha (por exemplo, todos os braços superiores direitos juntos), melhorando o desempenho.
Calcular matrizes de skinning normalmente requer contagem de renderizadores × contagem média de ossos por renderizador em cálculos. Para reduzir isso, combinamos duas estratégias:
Certifique-se de que todos os componentes Renderer com pele em um avatar usem índices de ossos consistentes – por exemplo, o índice do osso 2 sempre se refere ao peito, mesmo para um Renderer do pé direito que não o referencia.
Pré-processar malhas para identificar conjuntos únicos de índices de ossos e matrizes de pose de ligação, idealmente resultando em um único conjunto por avatar.

Componente pré-processador de rig de esqueleto
Com essa abordagem, podemos consolidar o skinning de todos os componentes Skinned Mesh Renderer de um avatar em um único par de "esqueleto de animação" e "esqueleto de renderer".

Esqueletos de animação e renderer (mostrando dois esqueletos de renderer)
Em cada quadro, nosso gerenciador de esqueletos realiza os seguintes passos:
Extração de animação: Os trabalhos de animação puxam as transformações atuais dos ossos do esqueleto de animação via a API Playables (Mecanim).
Cálculo de pose: Os trabalhos de Burst geram matrizes de pose animadas a partir da hierarquia de ossos e das transformações extraídas.
Montagem da matriz de skinning: Os trabalhos de Burst combinam matrizes de pose animadas e matrizes de pose de ligação em matrizes de skinning.
Upload para GPU: As novas matrizes de skinning são enviadas para o GraphicsBuffer global usando um shader de computação.
Esse processo é complexo, mas altamente eficiente, reduzindo as matrizes de skinning por um fator de dois para um avatar nu e por um fator de quatro ou mais quando a roupa é aplicada.
Olhando para o futuro
Desenvolver um backend de renderização personalizado para Toca Boca Days e agora modernizá-lo para Unity 6.3 provou ser valioso. A atualização do Unity 2022.3 foi tranquila, e nosso sistema de Batch Renderer continua a oferecer um desempenho forte para jogos com extensa personalização de personagens e propriedades de material por instância.
Ter controle total sobre a geração de comandos de desenho via a API BatchRendererGroup permite estratégias de culling personalizadas – como portais de oclusão e culling de oclusão personalizado – e otimizações de animação, como limitar as atualizações da matriz de skinning com base na proximidade e importância da câmera.
A extensibilidade do Unity, combinada com soluções eficientes prontas para uso, como o Universal Render Pipeline (URP) e o GPU Resident Drawer (com acesso ao código-fonte), nos permitiu alcançar nossas metas de desempenho enquanto mantínhamos um pipeline de renderização personalizado altamente otimizado.

O Batch Renderer voltará
Toca Boca está desenvolvendo um jogo ainda não anunciado; siga o estúdio no Instagram, TikTok e YouTube para atualizações. Explore mais histórias de desenvolvedores de Unity mobile em nossa página de soluções mobile e em nosso hub de Recursos.