Construindo um mundo aberto no navegador, parte 17: Animações que não precisaram de retargeting e uma busca de assets em tempo real
Por Oleg Sidorkin, CTO e cofundador da Cinevva
Chegou agora? Consulte o guia da série. Ele explica o que é um spike e reúne links para todas as partes.
A Parte 16 nos deu um mundo no qual é possível posicionar objetos. Esta parte oferece ao jogador algo melhor para fazer nele: um conjunto de animações com qualidade para combate e uma forma de adicionar ao mundo qualquer um dos milhares de modelos CC0 simplesmente digitando uma busca.
262 clipes, um esqueleto, três correções de retargeting
Abrir o Spike 35 em uma nova aba ↗ · Ver código-fonte
O rig do jogador é um esqueleto 3MIKE no estilo CC4, com 213 ossos, incluindo um rig facial com mais de 80 ossos e articulações completas dos dedos. As fontes das animações não correspondem a ele. A primeira versão fez o retargeting de clipes de combate do Mixamo selecionados manualmente, mas o conjunto de origem era limitado, e a locomoção estava dividida de forma pouco prática entre o Mixamo e algumas animações ociosas BVH do Kimodo. O verdadeiro avanço veio quando migramos a biblioteca de origem para os dois pacotes Universal Animation Library da Quaternius: 262 clipes no total, todos em um único manequim consistente no estilo UE5, com 65 ossos. Há combos de espada, tiro com arco, escaladas, corridas em paredes, esquivas, reações a golpes, emotes e um conjunto de locomoção muito mais limpo.
Isso significa levar uma origem de 65 ossos para um destino de 213 ossos, sem nomes de ossos, orientações de pose T ou proporções de membros em comum. Cada clipe é remapeado em três etapas: um mapa de nomes de ossos, um alinhamento de bind pose para que os dois rigs compartilhem uma orientação de referência e o escalonamento das trilhas de posição para que o rig de origem, mais baixo, não deixe o personagem enterrado até os joelhos no chão. Fazer isso corretamente exigiu investigar uma sequência de bugs, cada um trazendo um aprendizado específico.
O personagem ficava excessivamente torcido, com todos os ombros e cotovelos girados cerca de 30° além do necessário. A causa era uma incompatibilidade de poses: a origem UAL vem em uma pose T verdadeira, o bind do CC4 está em pose A, e o retargeter pressupunha que os dois rigs estavam em poses de referência correspondentes. Assim, a diferença entre A e T era somada a cada delta por frame. A correção força a cadeia dos braços do CC4 a assumir uma pose T verdadeira ao capturar o bind e, em seguida, faz o retargeting, mantendo os deltas pequenos.
Depois, o corpo parou de se deslocar. Os knockbacks faziam a parte superior do corpo recuar, mas deixavam os pés plantados. A UAL coloca o deslocamento no osso root, não na pelve, portanto ler a posição da pelve resultava em movimento quase nulo. A correção soma root.position e pelvis.position, escala a parte horizontal e grava uma única trilha de posição do quadril. Durante esse trabalho, descobrimos que os deslocamentos verticais estavam errados em cerca de 12%, porque havíamos usado a proporção global dos membros para o eixo Y, quando a altura da pelve exige a proporção entre o quadril e o chão. Duas proporções, dois eixos: RL_BoneRoot.position foi rastreada até o mapeamento do root da UAL, que fica em
A menor correção foi a mais satisfatória de observar. O personagem segurava a espada com os dedos frouxos da bind pose porque o mapa de ossos não continha entradas para os dedos, e o retargeter só grava trilhas para ossos mapeados. Adicionar 30 ossos dos dedos — cinco dedos, três segmentos, duas mãos, descartando o quarto osso auxiliar da ponta usado pela UAL, que não deforma a malha — fez a mão se fechar no cabo e se abrir ao soltá-lo.
Validando 262 clipes sem assistir a 262 clipes
Não é possível verificar visualmente uma biblioteca com 262 clipes, então criamos um teste offline de paridade de trajetórias: um script Node headless que carrega cada pacote, amostra o esqueleto de origem a 60 Hz, executa o pipeline de retargeting e compara as posições e rotações globais de cada osso com a origem após o escalonamento. O desvio máximo no Y da pelve ficou em 0,003 m. As mãos apresentaram um deslocamento constante de 2,5°, que inicialmente pareceu indicar que “os dedos não estão acompanhando”, mas um deslocamento constante é o delta do bind entre a mão plana da UAL e o bind ligeiramente curvado do CC4, e permanece invariável ao longo do clipe. Erros reais de animação aparecem como desvios que variam de um frame para outro. Depois que isso ficou claro, o teste se tornou uma verificação de regressão executada de uma só vez: se o desvio de um clipe deixar de manter essa linha de base constante, alguma alteração recente quebrou o retargeting.
Um manequim de referência exibido lado a lado tornou decisiva a parte visual da depuração. Pressionar a barra invertida mostra, ao lado do jogador, o rig de origem responsável pelo clipe atual. Assim, a pergunta “o ombro está torcido?” passa a ser “a torção já existe na origem ou foi adicionada pelo retargeting?”. Os testes numéricos detectam regressões, a referência visual identifica erros de bind pose que os números não revelam e, juntos, eles eliminaram as tentativas no escuro.
Com o pipeline sólido, trocar a base de WASD, salto e natação do Mixamo para a UAL exigiu apenas um pequeno mapa de aliases: a máquina de estados finitos continua usando nomes genéricos de estados, como idle e walk, que são resolvidos para nomes de clipes da UAL durante a reprodução. A natação exigiu offsets de rig específicos para cada clipe, porque as poses de nado livre e de sustentação vertical posicionam a pelve em alturas anatômicas diferentes. Por isso, submergimos o rig em meio metro durante a natação ativa e ainda mais durante a sustentação, fazendo a transição entre as duas a uma frequência suave de 5 Hz.
Digite uma palavra, obtenha um modelo
Abrir o Spike 36 em uma nova aba ↗ · Ver código-fonte
O Spike 34 consumiu um dia inteiro na seleção manual de um pacote CC0. A solução de longo prazo é uma caixa de busca. O Polyhaven publica cerca de 1.100 modelos CC0 por meio de uma API JSON permissiva e de uma CDN determinística, e este spike conecta todo o fluxo — consulta, miniaturas, carregamento e renderização — a partir de uma página estática sem etapa de build, usando aproximadamente 300 linhas de JavaScript puro mais three.js.
Na inicialização, ele busca o catálogo completo uma única vez, cerca de 600 KB. A busca usa pontuação inteiramente no cliente — nome tem prioridade sobre ID, que tem prioridade sobre categoria, que tem prioridade sobre tag — com debounce de 120 ms, renderizando os 60 primeiros cards. As miniaturas são carregadas de forma lazy por meio de um IntersectionObserver, para que digitar não dispare 60 solicitações de uma só vez. A parte interessante é o carregamento. O endpoint de arquivos do Polyhaven expõe glTF com vários arquivos, não GLB, com texturas compartilhadas entre resoluções e divididas em arquivos separados, além de retornar um mapa include de caminhos relativos para URLs absolutas da CDN. Em vez de baixar e modificar o JSON por conta própria, passamos esse mapa pelo LoadingManager.setURLModifier, que é acionado para cada dependência necessária ao loader — o .bin e cada textura — e a resolve por meio da CDN. Um clique, um único arquivo aparente. Tanto a API quanto a CDN definem CORS permissivo, verificado com curl antes de escrever qualquer código cliente, portanto não é necessário um proxy. Os materiais PBR são renderizados corretamente com RoomEnvironment e os padrões de tone mapping ACES, sem ajustes específicos para cada asset, enquanto as texturas 1k mantêm um modelo típico entre 2 e 5 MB, em vez dos 20 a 40 MB das versões 4k.
Uma etapa de meshoptimizer em WASM completa o conjunto com um redutor não destrutivo: cada malha armazena um clone de sua geometria original, e alterar a proporção reconstrói um buffer de índices a partir desse clone, em vez de simplificar cumulativamente. Geometrias com vários materiais são simplificadas por grupo e têm seus geometry.groups reconstruídos, para que os slots de materiais não sejam mesclados. Uma poltrona passa de 5.626 triângulos na qualidade máxima para 2.812 na metade.
Tecnologias mencionadas neste capítulo
Retargeting de esqueletos com alinhamento de bind pose. Mapear uma animação de um esqueleto para outro com nomes de ossos, proporções e poses de referência diferentes exige três correções: um mapa de nomes de ossos, um alinhamento de bind pose para que os dois rigs compartilhem uma orientação de referência — forçando a cadeia dos braços da pose A do destino a assumir a pose T da origem — e o escalonamento das trilhas de posição. Uma incompatibilidade de poses adiciona a rotação de A para T a cada delta por frame, duplicando a rotação das articulações. Ossos auxiliares não mapeados precisam ser descartados, pois um osso raiz em
Duas proporções de escala para um único rig. O deslocamento horizontal usa a proporção global dos membros — o tamanho geral do esqueleto —, mas o deslocamento vertical da pelve usa a proporção entre quadril e chão root, não na pelve, portanto os dois precisam ser somados em uma única trilha de posição do quadril para preservar o movimento em clipes de knockback, escalada e locomoção.
Teste offline de paridade de trajetórias. Um script headless amostra o esqueleto de origem a 60 Hz, executa o pipeline de retargeting e compara as transformações globais de cada osso com a origem. Um deslocamento constante a cada frame é o delta inofensivo do bind, enquanto um desvio que varia de frame para frame é um erro real. Assim, o teste se torna uma verificação de regressão acionada quando uma alteração remove ou distorce uma trilha. O isolamento dos GLBs por pacote — carregando um novo e liberando-o antes do próximo — evita contenção de cache durante toda a varredura.
LoadingManager.setURLModifier para grafos glTF em CDN. Quando uma CDN distribui glTF como um grafo de URIs relativas acompanhado por um mapa de inclusão — caminho relativo para URL absoluta —, setURLModifier resolve pela CDN cada dependência solicitada pelo loader sem reescrever o JSON. Isso transforma uma distribuição com vários arquivos e várias resoluções em um carregamento de um único clique. Descartar a geometria, os materiais e os mapas de textura de cada modelo anterior antes de carregar o próximo evita o acúmulo de centenas de MB de memória da GPU durante uma sessão de navegação.
Simplificação não destrutiva de malhas. Armazenar um clone da geometria original de cada malha e reconstruir apenas o buffer de índices para cada proporção de simplificação mantém as alterações rápidas e evita os danos cumulativos causados por simplificações repetidas. Executar o processo por fatia de geometry.groups e reconstruir os grupos preserva as atribuições de múltiplos materiais. Consulte LOD e meshoptimizer para entender como isso alimenta o LOD baseado em distância.
Parte 17 de 29. Anterior: Parte 16 — Estrutura para um mundo que continua crescendo Próxima: Parte 18 — Um pincel de dispersão que parece usar posicionamento por IA Guia da série: /pt-BR/blog/2026-02-25-open-world-browser-series-guide