@pose_flow/babylon 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,12 @@ O banco compara **pose visível + velocidade + trajetória desejada** e selecion
10
10
 
11
11
  O [roadmap revisado de Motion Matching + Pose Search](docs/ROADMAP.md) prioriza schemas configuráveis, preparação genérica de rigs/animações e conjuntos por contexto para chegar à versão estável. A Fase 1 está implementada: [schemas configuráveis e migração](docs/SCHEMAS.md). A Fase 2 está concluída no escopo documentado e inclui preparação GLB seletiva, recorte temporal, composição de loops medidos, hashes de assets, conferência de bind e CLI offline; retargeting automático continua futuro.
12
12
 
13
+ Animação de combate também faz parte do escopo obrigatório planejado da 1.0.0 (etapa 4.1): seleção de ataques/combos, esquivas, defesa e reações, com entradas controladas, sequência temporal protegida, eventos de apresentação e camadas. Ainda não implementado; regras de combate permanecem no jogo/servidor. Motion warping é uma expansão posterior.
14
+
15
+ Fase 3 em andamento: filtros por contexto, intervalos elegíveis, política de variantes de idle e roteamento de bancos. Exemplo local `?profile=/profiles/kaykit-context.json`, com dois subconjuntos e variantes no mesmo bake. Cobertura de olhar lateral/camadas permanece pendente; contrato em [PROFILES.md](docs/PROFILES.md) e [AI_USAGE.md](docs/AI_USAGE.md).
16
+
17
+ Fase 4 em andamento: painel de candidatos/custos por canal/rejeições, trajetórias, gravação/replay de queries, máscaras disjuntas e inertialization opcional. Exemplo `?profile=/profiles/kaykit-transitions.json&debug=1`. Confirmar os limites e o aceite pendente em [task-14](task/task-14/task.md); gesto de tronco é somente prévia de apresentação, sem contratos de combate.
18
+
13
19
  O MMORPG será usado como consumidor de teste durante o desenvolvimento, em paralelo ao laboratório. Ajustes e problemas com prints, código, logs e erros são acompanhados em [task/summary.md](task/summary.md); veja o [modelo e instruções](task/README.md). A integração prática não precisa aguardar o término do roadmap.
14
20
 
15
21
  ## Guias distribuídos com o pacote
@@ -49,7 +55,7 @@ O build produz `dist/` (biblioteca ESM + declarações) e `demo-dist/` (laborat
49
55
  - Núcleo sem engine: schemas configuráveis de características (preset original de 27 dimensões e exemplo de 33), normalização compartilhada, busca exata ponderada em `Float32Array`, filtros de gait e banco versionado JSON + float32 little endian.
50
56
  - Continuidade: avanço natural, loops, fim de clipe, intervalo de busca, tempo mínimo entre trocas e margem de melhora. Defaults de tuning, não parâmetros oficiais do Unreal.
51
57
  - Predição com aceleração, frenagem e velocidade angular; horizontes configuráveis (preset: 0,2 / 0,4 / 0,6 s).
52
- - Adaptador Babylon.js 7.54.3: leitura dos pés visíveis e reprodução com crossfade, incluindo saltos entre dois tempos do mesmo clipe.
58
+ - Adaptador Babylon.js 9.32.1: leitura dos pés visíveis e reprodução com crossfade, incluindo saltos entre dois tempos do mesmo clipe.
53
59
  - Bake offline via o mesmo carregador/interpolador Babylon usado na demonstração.
54
60
  - Quatro bancos separados: **Knight com mãos, 174 poses / 6 clipes / 33 dimensões**; **Knight, 174 poses / 6 clipes**; **Paladin, 610 poses / 9 clipes**; **Barbarian, 138 poses / 6 clipes**. Todos têm idle, walk, run, backward, left, right; o Paladin inclui stop, turn-left e turn-right sem loop.
55
61
  - Trajetória medida dos hips para os três novos clipes: posição, yaw contínuo, velocidade, interpolação e término sem repetir o início. Os dados extraídos alimentam apenas o bake; deslocamento e heading em runtime continuam definidos pelo controlador.
@@ -110,7 +116,7 @@ Instalar localmente no jogo, quando integrar: `npm install D:/motion_match`. Cop
110
116
 
111
117
  ## Limites atuais
112
118
 
113
- Ainda não há IK, foot locking, contato dos pés, inertialization, motion warping, root-motion-driven movement, worker, KD-tree ou PCA. Starts e giros em movimento ainda não foram preparados. Os três clipes medidos do perfil Paladin legado são sem loop; há composição de ciclos de trajetórias medidas no núcleo/bake, com caminhada medida do Paladin preparada e conferida no laboratório. A busca linear é apropriada para medir esta base pequena; não há garantia de orçamento em bancos grandes ou com muitos jogadores.
119
+ Ainda não há IK, foot locking, contato dos pés, motion warping, root-motion-driven movement, worker, KD-tree ou PCA. Inertialization é opt-in e não garante melhor qualidade ou pés travados. Starts e giros em movimento ainda não foram preparados. Os três clipes medidos do perfil Paladin legado são sem loop; há composição de ciclos de trajetórias medidas no núcleo/bake, com caminhada medida do Paladin preparada e conferida no laboratório. A busca linear é apropriada para medir esta base pequena; não há garantia de orçamento em bancos grandes ou com muitos jogadores.
114
120
 
115
121
  Schemas JSON configuráveis não incluem editor visual nem histórico temporal de ossos. Não há retargeting automático entre esqueletos diferentes. KayKit e Mixamo possuem bancos e mapas de ossos independentes. Crossfade de clips com trilhas ausentes exige preparação de cobertura dos ossos.
116
122
 
@@ -119,5 +125,5 @@ O laboratório tem um bundle de demonstração grande pelo carregamento amplo de
119
125
  ## Referências e proveniência
120
126
 
121
127
  - [Epic: Motion Matching](https://dev.epicgames.com/documentation/en-us/unreal-engine/motion-matching-in-unreal-engine): banco, pose, trajetória e busca.
122
- - [Babylon: Advanced Animations](https://doc.babylonjs.com/features/featuresDeepDive/animation/advanced_animations): grupos de animação e pesos. A implementação foi validada contra o código instalado da versão 7.54.3.
128
+ - [Babylon: Advanced Animations](https://doc.babylonjs.com/features/featuresDeepDive/animation/advanced_animations): grupos de animação e pesos. A implementação foi validada contra o código instalado da versão 9.32.1.
123
129
  - [Assets locais](docs/ASSETS.md), [evidências e pendências](docs/VALIDATION.md), [referência fornecida pelo usuário](MOTION_MATCHING_POSE_SEARCH_WEB_BASE_REFERENCIAS.md).
package/dist/AI_USAGE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Tutorial de integração para IA — @pose_flow/babylon
2
2
 
3
- Contrato local em desenvolvimento, revisado em 28/10/2026: Fases 1 e 2, com perfis v2, bancos v2 e schemas v1. A versão publicada 0.1.0 não contém estas APIs; conferir a versão instalada e a migração antes de integrar. **Ler este documento antes de integrar ou modificar o uso da biblioteca.** Conferir também os arquivos .d.ts da versão instalada. Não presumir recursos futuros a partir do nome “motion matching”.
3
+ Contrato local em desenvolvimento, revisado em 28/10/2026: Fases 1/2 e implementações parciais das Fases 3/4, com perfis v2, bancos v2 e schemas v1; package.json local 0.2.0. Não presumir que alterações deste workspace já foram publicadas: conferir a versão instalada, seus .d.ts e a migração antes de integrar. **Ler este documento antes de integrar ou modificar o uso da biblioteca.** Não presumir recursos futuros a partir do nome “motion matching”.
4
4
 
5
5
  Fonte mantida no repositório: docs/AI_USAGE.md. Build: dist/AI_USAGE.md. Após instalar pelo npm: node_modules/@pose_flow/babylon/dist/AI_USAGE.md. Também pode ser localizado com `import.meta.resolve('@pose_flow/babylon/AI_USAGE.md')` em Node ESM, sem importar o Markdown como JavaScript.
6
6
 
@@ -9,10 +9,10 @@ Fonte mantida no repositório: docs/AI_USAGE.md. Build: dist/AI_USAGE.md. Após
9
9
  Biblioteca TypeScript/ESM de busca de poses e reprodução de locomoção, com canais configuráveis de ossos e trajetória; os exemplos validados são bípedes. Não depende de MMORPG, servidor específico ou personagem específico. O núcleo não importa Babylon; o adaptador é uma entrada separada.
10
10
 
11
11
  ```powershell
12
- npm install @pose_flow/babylon @babylonjs/core@7.54.3 @babylonjs/loaders@7.54.3
12
+ npm install @pose_flow/babylon @babylonjs/core@9.32.1 @babylonjs/loaders@9.32.1
13
13
  ```
14
14
 
15
- Se Babylon já estiver instalado, respeitar as versões do projeto; a faixa peer atual é ^7.54.3, e a validação foi feita em 7.54.3. Não declarar compatibilidade com Babylon 8/9 sem teste.
15
+ A base local migrou para Babylon 9.32.1: core de desenvolvimento e loaders estão fixos nessa versão; a faixa peer de core é ^9.32.1. Manter core/loaders alinhados no consumidor e conferir `npm ls @babylonjs/core @babylonjs/loaders` para evitar versões incompatíveis ou cópias duplicadas. Esta alteração deixa de declarar suporte a Babylon 7/8; não contornar a faixa peer com force/legacy-peer-deps. Outras versões 9.x admitidas pela faixa ainda exigem validação no projeto consumidor. Conferir a versão publicada da biblioteca: o exemplo acima requer uma distribuição que contenha esta migração.
16
16
 
17
17
  O pacote local distribui runtime JS, tipos, licença, README, estes guias e CLI offline `pose-flow` para inspeção/preparação/validação/bake. Não inclui Blender, assets, perfis ou bancos prontos. A versão publicada ainda deve ser conferida. Os comandos `npm run bake` e `npm run validate:assets` abaixo são do repositório de desenvolvimento, não scripts automaticamente instalados no consumidor.
18
18
 
@@ -24,7 +24,7 @@ Preparar uma vez, offline, um perfil JSON v2, um schema JSON v1, um conjunto de
24
24
  - Não mudar a convenção de toda uma cena existente para adaptar esta lib; adaptar os dados/referenciais do personagem ou gerar banco correspondente.
25
25
  - Controller raiz deve ter escala 1; aplicar profile.scale nos rootNodes do modelo, não no controller. A escala das fontes no bake deve coincidir com a do modelo.
26
26
  - `rig` mapeia papéis semânticos do schema aos nomes de nodes do modelo; `rig.root` continua obrigatório para validar o rig. boneMap associa nomes das fontes aos do modelo; não faz retargeting de bind pose, proporções ou orientação local.
27
- - Animações são in-place. stripRoot é opcional e remove as trilhas de posição (inclusive componentes) e a variação de rotação do node indicado. Uma rotação constante de autoria integra a pose e é preservada; canais de rotação com tangentes não são considerados constantes. Usar apenas quando essa política reproduzir o que foi indexado no bake. Sem ele, passar string vazia ao player e fornecer clipes já preparados.
27
+ - Animações são in-place. stripRoot é opcional e remove as trilhas de posição (inclusive componentes) e a variação de rotação do node indicado. Uma rotação constante de autoria integra a pose e é preservada; canais de rotação com tangentes não são considerados constantes. Usar apenas quando essa política reproduzir o que foi indexado no bake. Para clipes já preparados, **omitir stripRoot no perfil JSON**; o parser recusa string vazia no perfil. Somente no argumento rootName do BabylonMotionPlayer passar `profile.stripRoot ?? ''` para desativar essa remoção.
28
28
  - Cada clipe usa nominalVelocity OU motionFile. Velocidade nominal é autoria em m/s finais, não uma medida. motionFile é entrada offline com trajetória medida de hips (proxy de pelvis) ou root dedicado; aceita looping com composição de ciclos.
29
29
  - Features são definidas pelo schema. O preset `LOCOMOTION_SCHEMA` preserva 27 dimensões: pés XYZ (0–5), velocidades dos pés no mundo expressas localmente (6–11), velocidade do controller local (12–14), posições futuras XZ (15–20) e direções futuras XZ (21–26), em +0,2/+0,4/+0,6 s. Outros schemas usam dimensões/roles/horizontes próprios; usar `SchemaQueryBuilder`, sem offsets fixos.
30
30
  - poses.bin guarda float32 normalizados little endian; manifest guarda mean/std usados também pela query. Usar decodeDatabase; não normalizar a query duas vezes.
@@ -178,21 +178,23 @@ O exemplo não adiciona um render loop: integrar ao loop existente. Não usar se
178
178
 
179
179
  ## 4. API e comportamento que a IA deve preservar
180
180
 
181
- - `PoseSearch.search({features,schemaSignature,gaits?,excludedClips?})` retorna melhor Match ou null. Features são brutas. Pesos e normalização vêm dos canais do schema; no preset original, pose/velocidade 1 e trajetória/direção 3 por dimensão. Para custo customizado no matcher, seus pesos estão em matcher.search.weights.
181
+ - `PoseSearch.search({features,schemaSignature,gaits?,excludedClips?,context?})` retorna melhor Match ou null. Features são brutas. Pesos e normalização vêm dos canais do schema; no preset original, pose/velocidade 1 e trajetória/direção 3 por dimensão. Para custo customizado no matcher, seus pesos estão em matcher.search.weights. Filtros de contexto/entrada/exclusão precedem também o fallback físico de velocidade.
182
182
  - `MotionMatcher.update(dt, query)` retorna clip/time/switched/match. clip é índice no manifest; clip < 0 indica ausência de playback. match é o último candidato da busca, que pode diferir do playback por continuidade/cooldown; não apresentá-lo sempre como o clipe renderizado.
183
183
  - Defaults: searchInterval=0.1 s, minimumHold=0.12 s, improvement=0.1, jumpPenalty=0.5, timeSeparation=0.2 s, reselectDelay=0.6 s. Critérios de tuning locais; não parâmetros oficiais do Unreal.
184
184
  - Sem loop: clamp no fim, exclusão temporária do clipe concluído. Loop: tempo circula na duração. Não somar dt novamente ao tempo retornado antes de chamar o player.
185
185
  - `filterLocomotionGaits`: acima de 0.2 m/s retira idle se há marcha; abaixo de 0.05 retira marchas se há idle. Preserva fallback em conjuntos incompletos e não decide prioridades de combate.
186
- - `BabylonMotionPlayer` cria grupos próprios e mistura tempos diferentes do mesmo clipe. Usar update antes de renderizar. Não iniciar as mesmas fontes sobre o personagem em paralelo. Grupos pausados + goToFrame com pesos são intencionais para Babylon 7.
186
+ - `BabylonMotionPlayer` cria grupos próprios e mistura tempos diferentes do mesmo clipe. Usar update antes de renderizar. Não iniciar as mesmas fontes sobre o personagem em paralelo. Grupos pausados + goToFrame com pesos são intencionais e cobertos por testes em Babylon 9.32.1.
187
187
  - `BabylonSchemaPoseReader` lê ossos semânticos visíveis após avaliação/blending; o wrapper BabylonPoseReader continua para dois pés; não substituir por pose pura do banco quando houver crossfade.
188
188
  - `parseProfile` valida perfil v2; `parseSchema`/`parseAnimationSet` validam os JSON independentes e `resolveProfile` confere roles requeridos; `suggestRig` só propõe nomes; `profileSignature` identifica configuração. Validar assets offline é uma etapa separada.
189
189
  - `rootMotionFeatures` e `sampleRootMotion` operam em trajetórias medidas com yaw contínuo, clamp por padrão e composição de ciclos quando looping=true. Não aplicar seu deslocamento sobre a posição já simulada do controller.
190
190
 
191
191
  ## 5. Combate, multiplayer e recursos
192
192
 
193
- Gameplay/servidor continuam responsáveis por colisões, posição, dano, hitbox, cooldowns e janelas de ação. Motion matching é apresentação de locomoção. Não transmitir clipId como autoridade de ataque/acerto.
193
+ Gameplay/servidor continuam responsáveis por colisões, posição, dano, hitbox, cooldowns e janelas de ação. O runtime implementado hoje é de apresentação de locomoção. Não transmitir clipId como autoridade de ataque/acerto.
194
194
 
195
- O player deste pacote é de corpo inteiro, sem máscara de ossos. Não sobrepor seus grupos aos grupos de combate nos mesmos nodes. Para camadas, adaptar a seleção do núcleo ao player mascarado do projeto ou preparar um adaptador próprio. Suspender/dispor a reprodução de locomoção ao transferir controle de corpo inteiro a uma ação e reiniciar a seleção a partir da pose visível ao retornar.
195
+ **Planejado, ainda sem API implementada:** a etapa 4.1 de docs/ROADMAP.md inclui animação de combate no escopo obrigatório da 1.0.0: ataques/combos, ataques direcionais/em movimento, esquivas/rolamentos, bloqueios/contra-ataques, reações a impactos e transições com locomoção. Pose Search escolherá variante e entrada entre candidatos autorizados pelo consumidor, com intervalos de entrada/saída, progressão temporal protegida, eventos de apresentação e máscaras/camadas. Não cadastrar novos gaits de combate nem presumir que as opções atuais de continuidade garantem execução segura de golpes. Regras de combo, interrupção e relógio de ataque continuarão externos; eventos visuais não aplicarão dano nem controlarão janelas autoritativas. Motion warping segue como expansão posterior, fora do aceite in-place da 1.0.0.
196
+
197
+ O player usa corpo inteiro por padrão e aceita máscara opcional de nomes exatos dos nodes do modelo no sétimo argumento de opções (ver seção da Fase 4). Players da biblioteca com máscaras sobrepostas são recusados; grupos externos do consumidor não são detectados automaticamente. Não sobrepor grupos nos mesmos nodes. Para transferir controle de corpo inteiro, dispor o player anterior antes de criar o próximo; para camadas, definir propriedade disjunta. Reiniciar a seleção a partir da pose visível ao retornar. Isso não implementa contratos de execução de golpes/combos da etapa 4.1.
196
198
 
197
199
  Banco e fontes podem ser carregados uma vez por cena; player, matcher e histórico do leitor precisam ser individuais. Liberar grupos do player na remoção do avatar. Não dispor fontes compartilhadas antes de todos os consumidores. O exemplo possui seus próprios assets e não dispõe o controller fornecido pelo jogo.
198
200
 
@@ -212,9 +214,9 @@ Banco e fontes podem ser carregados uma vez por cena; player, matcher e históri
212
214
 
213
215
  No consumidor, testar idle, aceleração, walk/run, lados/costas quando disponíveis, frenagem, giro, transições, teleporte, múltiplos avatares e remoção. Medir no navegador alvo; resultados de benchmark Node não são orçamento de renderização. Um sample correto não prova qualidade de todo o ciclo.
214
216
 
215
- Não implementados: retargeting automático, IK/foot locking, contatos de pés, inertialization, motion warping, movimento autoritativo por root motion, worker, PCA/KD-tree, quadrúpedes ou schema visual configurável. Não prometer eliminação de sliding ou paridade com Unreal.
217
+ Não implementados: retargeting automático, IK/foot locking, contatos de pés, motion warping, movimento autoritativo por root motion, worker, PCA/KD-tree, quadrúpedes ou schema visual configurável. Inertialization opcional está descrita na seção da Fase 4, com limites próprios. Não prometer eliminação de sliding ou paridade com Unreal.
216
218
 
217
- Fase 1 implementa schemas configuráveis e arquivos de conjunto separados. O laboratório oferece biped-locomotion (27 dimensões) e biped-hands (33 dimensões, mãos/horizontes diferentes). Ainda não há tags arbitrárias, alternância contextual de conjuntos, variação automática de idles, intenção de olhar ou busca conjunta em múltiplos bancos. Preparação genérica GLB está implementada na Fase 2; retargeting automático continua futuro; os schemas não adaptam animações de rigs incompatíveis.
219
+ Fase 1 implementa schemas configuráveis e arquivos de conjunto separados. O laboratório oferece biped-locomotion (27 dimensões) e biped-hands (33 dimensões, mãos/horizontes diferentes). A implementação parcial da Fase 3 acrescenta contexto extensível, intervalos, roteamento de bancos e política de idle, conforme a seção abaixo. Não há síntese de olhar, camadas ou comparação conjunta de custos entre bancos independentes. Preparação genérica GLB está implementada na Fase 2; retargeting automático continua futuro; os schemas não adaptam animações de rigs incompatíveis.
218
220
 
219
221
  ## 7. Manutenção obrigatória
220
222
 
@@ -230,7 +232,7 @@ Registro de manutenção (28/10/2026): tutorial inicial da série 0.1.x, entrada
230
232
 
231
233
  Revisão do roadmap (28/10/2026): documentadas as diferenças entre perfis reutilizáveis atuais e schemas/conjuntos configuráveis planejados. Nenhuma API ou capacidade de animação foi alterada nesta revisão documental.
232
234
 
233
- Complemento do roadmap (28/10/2026): explicitados o cenário de múltiplos idles com política de variação e a distinção entre deslocamento, orientação corporal e intenção de olhar. Alternância contextual de idles e seleção orientada pelo olhar continuam planejadas; cadastrar vários clipes não implementa essas políticas na API atual.
235
+ Complemento do roadmap (28/10/2026, histórico anterior à task-13): explicitados múltiplos idles e a distinção entre deslocamento, corpo e olhar. A task-13 implementa filtros/política descritos abaixo; cobertura visual de olhar lateral permanece pendente.
234
236
 
235
237
  Marco de estabilidade (28/10/2026): o roadmap passa a exigir uma release estável 1.0.0, com API/dados versionados, migração, pipeline distribuível, múltiplos rigs, cenários visuais, desempenho e instalação em consumidor independente validados. O processo prevê versões candidatas antes da release. A base atual permanece 0.1.0; este marco não declara recursos futuros implementados nem autoriza publicação imediata. Consultar ROADMAP.md para o escopo e PUBLISHING.md para os critérios de release.
236
238
 
@@ -252,7 +254,7 @@ Clipes aceitam `range: [inicio,fim]` em segundos. O bake guarda `sourceStart` e
252
254
 
253
255
  O manifesto v2 ganha `assetHashes` SHA-256 por URL de GLB/trajetória. A fingerprint continua somente configuração. O laboratório exige hashes e recusa assets alterados; bancos antigos exigem novo bake. `decodeDatabase` valida o formato, sem ler arquivos/rede. O consumidor externo deve conferir hashes ou controlar identidade dos assets na distribuição. O laboratório faz uma leitura extra dos bytes, além do carregamento Babylon.
254
256
 
255
- CLI Node incluída no pacote local: `pose-flow inspect modelo.glb`, `pose-flow validate --profile public/profiles/rig.json`, `pose-flow bake --profile public/profiles/rig.json`. `--public-dir caminho` muda a raiz dos URLs públicos em validação/bake; `--profile` é caminho de arquivo. Caminhos relativos ao diretório atual do consumidor. Node 24.21.0 e Babylon 7.54.3 são o ambiente deste teste; o loader GLTF 7.54.3 é dependência do pacote. Não fazer bake no início do jogo. Assets, perfis e bancos são fornecidos pelo consumidor. Não houve publicação.
257
+ CLI Node incluída no pacote local: `pose-flow inspect modelo.glb`, `pose-flow validate --profile public/profiles/rig.json`, `pose-flow bake --profile public/profiles/rig.json`. `--public-dir caminho` muda a raiz dos URLs públicos em validação/bake; `--profile` é caminho de arquivo. Caminhos relativos ao diretório atual do consumidor. A base atual usa Babylon 9.32.1; o loader GLTF 9.32.1 é dependência do pacote. A preparação offline original foi testada com Node 24.21.0 e Babylon 7.54.3; os resultados da migração estão na task-15. Não fazer bake no início do jogo. Assets, perfis e bancos são fornecidos pelo consumidor. Não houve publicação.
256
258
 
257
259
  Consulte PROFILES.md para remoção de root e roteiro offline. Esse primeiro incremento foi parcial; as pendências de preparação foram atendidas na continuação abaixo. Retargeting automático, IK e inertialization não foram implementados.
258
260
 
@@ -289,3 +291,96 @@ No perfil paladin-measured, a parada a 2× substitui a parada lenta no conjunto;
289
291
  O laboratório define false quando a intenção está estável (erro de velocidade desejada/atual menor que 0,05 m/s e erro angular menor que 0,08 rad). Em aceleração, frenagem ou giro volta a permitir replanejamento. Esses limiares são política do consumidor de demonstração, não limites universais da biblioteca. Outros consumidores devem considerar também troca de contexto/ação, teleporte e reconciliação; não manter a fase protegida quando o estado da personagem exige uma nova decisão. Resetar matcher/reader nos casos já descritos no tutorial.
290
292
 
291
293
  A investigação encontrou retrocessos repetidos de aproximadamente 0,7 para 0,5 s na caminhada, com feedback entre crossfade/velocidade da pose visível e nova busca. No cenário reproduzido, proteger a fase removeu 12 retrocessos amostrados em dois ciclos de caminhada. Os testes mantêm busca para outro clipe, retorno ao replanejamento, wrap natural e o cenário de frenagem corrigido. Dados/bancos não mudam por essa opção. Não há inertialization ou foot locking novos.
294
+
295
+ ## Manutenção — planejamento de animação de combate (task-12, 28/10/2026)
296
+
297
+ Roadmap ampliado por pedido do mantenedor para incluir combate visual como entrega obrigatória da 1.0.0, com contratos e critérios próprios na etapa 4.1. Esta alteração é documental: não adiciona APIs, eventos, máscaras, bancos de combate ou garantias de sincronização ao runtime atual. O procedimento de transferência de controle de corpo inteiro acima continua sendo o contrato disponível. Implementação e validação de combate permanecem pendentes; a task-10 continua acompanhando máscaras. Sem mudança no processo de build/publicação.
298
+
299
+ ## Manutenção — seleção contextual, Fase 3 parcial (task-13, 28/10/2026)
300
+
301
+ Clipes do conjunto/manifesto aceitam `context?: Record<string,string>` e `entryWindows?`, `exitWindows?`, `excludedWindows?`. As chaves são semântica explícita do consumidor: por exemplo set/equipment/posture/look/action/combo. Não inferir contexto de nomes de arquivos/clipes. Campos opcionais preservam os formatos perfil v2/banco v2/conjunto v1; bibliotecas anteriores não aplicam esses filtros e não devem reproduzir bancos que dependam deles. Alterar metadados exige novo bake/fingerprint.
302
+
303
+ ```ts
304
+ const matcher = new MotionMatcher(db, {
305
+ idleVariation: { interval: 4, historySize: 2 },
306
+ });
307
+ // A cada frame, após construir features/trajectory no referencial atual:
308
+ const state = matcher.update(dt, {
309
+ ...builder.build(pose, points),
310
+ gaits, // Set(['idle']) ativa a política enquanto parado
311
+ context: { set: 'relaxed', posture: 'standing', look: 'forward' },
312
+ allowTemporalReselection: false, // usar apenas quando a intenção estiver estável
313
+ });
314
+ player.update(state, dt);
315
+ ```
316
+
317
+ Todas as chaves solicitadas precisam existir com valor igual no candidato; metadado ausente não é curinga. Contexto/gait/excludedClips e intervalos de entrada/exclusão são aplicados antes do custo e do fallback de velocidade. Pool vazio retorna null; matcher sem clipe válido devolve clip=-1 e o player limpa a reprodução. Se a reprodução corrente ainda for compatível mas não houver novas entradas, ela continua naturalmente. O consumidor decide a apresentação alternativa; nunca interpretar ausência como permissão de ação.
318
+
319
+ Intervalos fechados `[início,fim]` usam segundos relativos ao clipe recortado/retimado, dentro de `[0,duration]`. Ausente libera; entryWindows=[] impede entradas, exitWindows=[] impede saídas discricionárias, excludedWindows=[] não exclui nada. As entradas fora das janelas e dentro das exclusões não concorrem, mas a reprodução atravessa esses trechos. Não há remoção física dos samples. exitWindows limita trocas discricionárias no tempo corrente; término, contexto/gait inválido e excludedClips prevalecem. dt que pula a janela aguarda próxima oportunidade ou término. Não é contrato seguro de golpes/combos nem trava autoritativa de saída.
320
+
321
+ idleVariation é opt-in e só atua quando gaits contém exclusivamente idle. Antes de interval mantém o clipe, respeitando a política de replanejamento temporal. Depois busca outro idle elegível por custo, sem sortear/forçar clipId ou tempo; historySize controla variantes recentes a evitar. Se necessário relaxa histórico antigo, nunca os filtros do consumidor. Se só existir uma variante, mantém o ciclo. A troca respeita minimumHold e exitWindows; no máximo uma variação por update, sem recuperar múltiplas mudanças após dt longo. Sair de idle reinicia o timer; reset limpa timer/histórico.
322
+
323
+ O perfil kaykit-context demonstra dois subconjuntos no mesmo banco/normalização com Idle_A/B e Walking_A/B/C. Corpo (yaw), deslocamento (velocidade/trajetória) e olhar (context.look) têm entradas separadas. O olhar apenas filtra clipes preparados; não gera pose de cabeça. Esse dataset cobre somente forward. Caminhada olhando lateralmente, máscaras e cobertura visual integral permanecem pendentes. Não declarar a Fase 3 concluída por esses filtros.
324
+
325
+ Para bancos independentes, `selectContextBank(banks,context)` recebe `{id,context,database}[]`, retorna o único banco compatível ou null e rejeita ambiguidade/IDs repetidos. Roteia antes da busca, sem comparar custos entre normalizações. O consumidor usa o schema daquele banco, valida rig/preparação e mantém seu matcher/player. Não há transferência automática de pose/tempo entre bancos ou validação de retargeting pelo roteador. Para competição conjunta de variantes, usar um bake compatível com estatísticas comuns.
326
+
327
+ Task-11: corrigido cabeçalho para identificar a base local sem afirmar a versão publicada atual e distinguido stripRoot do perfil (omitir) e argumento do player (string vazia). Sem alteração do parser, build ou publicação; confirmação no cenário original continua com o responsável pelo consumidor.
328
+
329
+ ## Manutenção — diagnóstico, máscaras e transições, Fase 4 (task-14, 28/10/2026)
330
+
331
+ ### Inspecionar a busca e a continuidade
332
+
333
+ `PoseSearch.inspect(query,limit=5)` faz uma passagem diagnóstica opt-in e retorna best, candidates (índice/clipe/tempo/custo, custos por canal e erro físico de velocidade quando há esse canal), contagens de rejected, exemplos limitados, eligible/total e velocityLimit. Limit é inteiro de 1 a 50. Usa a mesma busca exata, estatísticas e pesos de search; os custos por canal somam o custo total. Rejeições têm precedência única: excluded-clip, gait, context, entry-window, excluded-window, velocity. Contagens não representam todas as causas simultâneas. O limite físico reflete o fallback do pool elegível; null significa não aplicado ou pool vazio.
334
+
335
+ `new MotionMatcher(db,{diagnostics:true})` guarda snapshot da query da última busca efetiva, incluindo exclusões internas de término/histórico de idle. `matcher.inspect(limit)` inspeciona esse snapshot; retorna null sem diagnóstico ou antes da primeira busca. Não representa necessariamente a query do frame atual. `matcher.decision` informa se houve busca, motivo da decisão, clipes anterior/atual, custos comparados e parâmetros de continuidade. Entre buscas reason=search-interval. Histórico do laboratório armazena até 32 trocas; o núcleo não acumula um log ilimitado. Inspeção tem custo adicional e deve ficar desligada/menos frequente fora de diagnóstico.
336
+
337
+ `readCandidateTrajectory(db,index)` reconstrói metros/direções por horizonte com mean/std do banco. Position/facing são opcionais conforme os canais presentes. Não confundir com posição absoluta do controller. O laboratório desenha trajetória desejada em verde e a do candidato em laranja no referencial atual; candidato pode diferir do playback por continuidade.
338
+
339
+ ### Máscaras e propriedade
340
+
341
+ ```ts
342
+ const lowerMask = profile.masks?.['lower-body'];
343
+ const upperMask = profile.masks?.['upper-body'];
344
+ if (!lowerMask || !upperMask) throw new Error('Perfil sem máscaras de pernas/tronco');
345
+ const legs = new BabylonMotionPlayer(scene, clips, sources, targets,
346
+ 0.30, profile.stripRoot ?? '', { mask: lowerMask });
347
+ const torso = new BabylonMotionPlayer(scene, clips, sources, targets,
348
+ 0.30, profile.stripRoot ?? '', { mask: upperMask });
349
+ // Atualizar ambos antes de scene.render(). Não somar dt novamente a state.time.
350
+ legs.update(locomotionState, dt);
351
+ torso.update(presentationState, dt);
352
+ ```
353
+
354
+ Os nomes da máscara são **do modelo**, após boneMap. Não há expansão automática de descendentes. Lista vazia, duplicada, node não mapeado/ambíguo e ausência de trilhas elegíveis falham. A propriedade é por node, incluindo todas as suas propriedades, reservada no construtor e liberada em dispose (não em clip=-1/reset). Dois players da lib sobre o mesmo node são recusados. Não há detecção de AnimationGroups criados fora da lib: o consumidor coordena esses grupos. Partilhar fontes é permitido; grupos de reprodução são próprios e fontes permanecem intactas.
355
+
356
+ Máscaras respeitam range/sourceStart, loops, rootOrientation/stripRoot e crossfade de tempos do mesmo clipe. O root só recebe a orientação constante se pertencer à máscara; clipes das outras camadas herdam a transformação do pai controlado pela base. Não duplicar root em duas máscaras. Comparar trilhas locais e poses dos ossos selecionados sob a mesma cadeia de pais; combinação de camadas não equivale à pose de corpo inteiro de um único clipe. Uma ação de tronco ainda herda deslocamento/orientação de hips/root da base. Preparar rigs/clipes de acordo.
357
+
358
+ Perfis v2 aceitam `masks: {nome:[nodes...]}` e `layerPreview:{baseMask,overlayMask,context}` opcional apenas para o laboratório. layerPreview exige duas máscaras disjuntas e contexto explícito; não autoriza ação de gameplay. O novo perfil kaykit-transitions permite pernas em locomoção e Interact no tronco, com entrada escolhida por Pose Search e avanço visual sequencial simples. É uma prévia de apresentação; não entrega ataques, combos, eventos/janelas autoritativas ou todas as garantias da etapa 4.1. Task-10 permanece parcial até confirmação no cenário original pelo responsável do jogo.
359
+
360
+ ### Inertialization opcional
361
+
362
+ No mesmo argumento de opções: `{transition:'inertialization',inertialHalfLife:0.1}`. Padrão transition=crossfade preserva a reprodução anterior. Inertialization mantém um único grupo ativo e captura pose local visível/velocidade de apresentação ao trocar clipe ou tempo. Calcula residual para a pose/velocidade de entrada, amortecendo-o criticamente sobre posição/escala e log de quaternion. Não continua avaliando o clipe de saída; interrupções partem da pose já corrigida. A meia-vida é em segundos e corresponde a metade do offset com velocidade residual inicial zero. `decayInertialOffset` é a função escalar do núcleo, sem engine.
363
+
364
+ Suporte atual: trilhas locais position/scaling/rotationQuaternion, inclusive componentes compatíveis. Euler não removido e outras propriedades são recusados no modo inercial. A correção de root constante continua compartilhada com o crossfade/bake. A derivada de entrada é amostrada da animação em ±1/120 s (ou passo menor para clipe curto), com loops/recortes corretos; a de saída vem de poses visíveis consecutivas. Rotação usa aproximação em espaço tangente, sem garantia de continuidade angular exata para grandes giros; velocidades/seams ruins e diferenças grandes de escala podem causar overshoot. Validar conteúdo e meia-vida por rig. Não é IK, foot locking, correção de contato nem garantia de melhor aparência que crossfade.
365
+
366
+ Manutenção numérica: o residual evita a operação Infinity × 0 após pausas muito longas; elapsed=0 preserva exatamente o offset mesmo com meia-vida positiva muito pequena.
367
+
368
+ Chamar update antes de scene.render e ler a pose depois. A correção é aplicada em onAfterAnimationsObservable, antes das matrizes mundiais/rendering; respeita a máscara. dt=0 não estima nova velocidade; há no máximo um avanço do residual por update, mesmo se renderizar várias vezes. O primeiro frame após uma troca mantém o offset inicial (sem consumir dt da troca). `player.reset()` limpa grupos/histórico de transição e retém propriedade; chamar também matcher.reset e reader.reset em teleporte/reconciliação brusca. dispose remove observer/grupos e libera propriedade, sem dispor fontes, modelo ou controller. Nenhuma posição autoritativa é alterada.
369
+
370
+ ### Gravar/reproduzir comparações
371
+
372
+ No laboratório, escolher teste e transição, abrir “Gravar e comparar cenário”, gravar, parar e reproduzir com cada modo. Exportar/importar JSON é local. Query/resultado do matcher, dt, posição/yaw do controller e tempo apresentado (incluindo saltos de fase do teste) são registrados; não são comandos do jogo. A reprodução usa **queries gravadas**, não feedback de uma nova pose visual. Isso isola o efeito do player para comparar modos com as mesmas decisões; não comprova estabilidade de uma simulação fechada diferente. Gestos manuais de tronco não são gravados; usar corpo inteiro para comparar apresentação do cenário base.
373
+
374
+ Reiniciar a apresentação ou trocar o modo de transição/camada encerra a gravação antes do reset e preserva o trecho válido; não introduz silenciosamente um reset do matcher no meio de um arquivo reproduzível. Ao recriar players, o laboratório restaura a referência local original do modelo antes do bootstrap, para não capturar uma pose já animada como bind/baseline de tracks ausentes.
375
+
376
+ `captureScenarioFrame`, `scenarioDatabaseIdentity`, `parseMotionScenario` e `restoreScenarioQuery` estão no núcleo. Formato v1 exige identidade do manifesto/features, opções do matcher, filtros e valores finitos. Rejeita dados incompatíveis, gait inválido, tempo/clipe fora dos limites, mais de 6.000 frames ou dois milhões de valores de features; importação do laboratório limita arquivo a 16 MB. Identidade é fingerprint de conteúdo não criptográfica, sem garantia de segurança. JSON é dado; não executa código. Não editar bancos/saídas geradas para satisfazer identidade.
377
+
378
+ Fase 4 continua acompanhada como PARCIAL até registrar o aceite aplicável e confirmar as limitações de camadas no cenário original; Fase 3 continua parcial por cobertura de olhar/dataset. Schemas atuais usam pose atual e não exigiram histórico temporal de ossos novo nesta entrega. Sem mudança no processo de build/publicação; nenhuma publicação automática.
379
+
380
+ ## Manutenção — migração Babylon 9.32.1 (28/10/2026)
381
+
382
+ Core/loaders e lockfile atualizados juntos. A API pública e os formatos de perfil v2/banco v2/schema v1 permanecem iguais. Os 95 testes passaram em 9.32.1, incluindo reprodução real em NullEngine, máscaras, crossfade e inertialization. Os nove perfis existentes foram rebaked offline; os 18 arquivos manifest.json/poses.bin ficaram idênticos por SHA-256 à referência 7.54.3, sem necessidade de substituir esses bancos por causa desta migração. Isso não garante equivalência para assets/perfis externos: validar preparo/bake/player com o conteúdo do consumidor e manter exatamente a mesma remoção de root.
383
+
384
+ Babylon 9 introduz diagnóstico de limite de uniforms de esqueletos; o NullEngine usado offline reporta limites artificiais baixos e emite aviso para os rigs com 65 ossos. Esse aviso não é medição de GPU do jogo e não invalida o bake. Em dispositivos reais, conferir capacidades/renderização; não aplicar configurações de GPU do laboratório indiscriminadamente ao consumidor. A atualização não resolve a conversão LH/RH da task-09 nem confirma as camadas no MMORPG (task-10). Não há mudança em deslocamento autoritativo, combate ou contratos multiplayer.
385
+
386
+ Laboratório conferido em Edge/WebGL2 com Knight (incluindo camadas), Paladin e Mannequin Large, nos dois modos de transição; sem erros/avisos no roteiro final. Instalação isolada confirmou imports públicos, core único e CLI validate. Evidências e limites da migração: task/task-15/.
@@ -4,6 +4,8 @@ Fonte deste documento: `docs/PUBLISHING.md`. O build gera `dist/PUBLISHING.md`,
4
4
 
5
5
  ## Preparar uma atualização
6
6
 
7
+ Migração Babylon (task-15, 28/10/2026): core peer ^9.32.1, core de desenvolvimento e loaders de produção fixos em 9.32.1. A base anterior era 7.54.3; esta mudança remove o suporte declarado a 7/8 e exige instrução de migração antes de distribuir. A versão local do pacote permanece 0.2.0; escolher nova versão 0.x adequada à quebra antes de publicar, sem sobrescrever uma versão existente. Conferir `npm ls @babylonjs/core @babylonjs/loaders` na biblioteca e em instalação isolada, com core/loaders alinhados. Não copiar o lockfile da biblioteca para o jogo. O processo build/prepack continua igual.
8
+
7
9
  Executar na raiz do repositório da biblioteca. Os comandos abaixo não pertencem ao projeto consumidor.
8
10
 
9
11
  ```powershell
@@ -0,0 +1,29 @@
1
+ import { Quaternion, Vector3, type Scene, type TransformNode } from '@babylonjs/core';
2
+ export interface LocalPose {
3
+ position: Vector3;
4
+ rotation: Quaternion;
5
+ scaling: Vector3;
6
+ }
7
+ export type LocalPoses = Map<TransformNode, LocalPose>;
8
+ export declare function captureLocal(node: TransformNode): LocalPose;
9
+ export declare function rotationVector(q: Quaternion): Vector3;
10
+ export declare function poseVelocity(before: LocalPose, after: LocalPose, dt: number): LocalPose;
11
+ /** Owns local presentation residuals only. Evaluation happens before world matrices/rendering. */
12
+ export declare class BabylonInertializer {
13
+ private scene;
14
+ readonly halfLife: number;
15
+ private observer;
16
+ private last;
17
+ private velocity;
18
+ private residuals;
19
+ private target;
20
+ private dt;
21
+ private elapsed;
22
+ private revision;
23
+ private renderedRevision;
24
+ constructor(scene: Scene, halfLife: number);
25
+ update(target: LocalPoses, targetVelocity: LocalPoses, dt: number, switched: boolean): void;
26
+ private render;
27
+ reset(): void;
28
+ dispose(): void;
29
+ }
@@ -0,0 +1,88 @@
1
+ import { Quaternion, Vector3 } from '@babylonjs/core';
2
+ import { decayInertialOffset } from './core/inertialization.js';
3
+ export function captureLocal(node) {
4
+ return { position: node.position.clone(), rotation: node.rotationQuaternion?.clone() ?? Quaternion.FromEulerVector(node.rotation), scaling: node.scaling.clone() };
5
+ }
6
+ export function rotationVector(q) {
7
+ q = q.clone().normalize();
8
+ if (q.w < 0)
9
+ q.scaleInPlace(-1);
10
+ const length = Math.hypot(q.x, q.y, q.z);
11
+ return length < 1e-8 ? new Vector3(q.x * 2, q.y * 2, q.z * 2) : new Vector3(q.x, q.y, q.z).scale(2 * Math.atan2(length, q.w) / length);
12
+ }
13
+ function rotationFromVector(v) {
14
+ const angle = v.length();
15
+ return angle < 1e-8 ? new Quaternion(v.x / 2, v.y / 2, v.z / 2, 1).normalize() : Quaternion.RotationAxis(v.scale(1 / angle), angle);
16
+ }
17
+ export function poseVelocity(before, after, dt) {
18
+ return { position: after.position.subtract(before.position).scale(1 / dt), scaling: after.scaling.subtract(before.scaling).scale(1 / dt),
19
+ // rotation stores a tangent vector in XYZ; W is unused for velocities.
20
+ rotation: Quaternion.FromArray([...rotationVector(after.rotation.multiply(Quaternion.Inverse(before.rotation))).scale(1 / dt).asArray(), 0]) };
21
+ }
22
+ const zeroVelocity = () => ({ position: Vector3.Zero(), scaling: Vector3.Zero(), rotation: new Quaternion(0, 0, 0, 0) });
23
+ /** Owns local presentation residuals only. Evaluation happens before world matrices/rendering. */
24
+ export class BabylonInertializer {
25
+ scene;
26
+ halfLife;
27
+ observer;
28
+ last = new Map();
29
+ velocity = new Map();
30
+ residuals = new Map();
31
+ target = new Map();
32
+ dt = 0;
33
+ elapsed = 0;
34
+ revision = 0;
35
+ renderedRevision = -1;
36
+ constructor(scene, halfLife) {
37
+ this.scene = scene;
38
+ this.halfLife = halfLife;
39
+ this.observer = scene.onAfterAnimationsObservable.add(() => this.render());
40
+ }
41
+ update(target, targetVelocity, dt, switched) {
42
+ if (switched) {
43
+ this.residuals.clear();
44
+ this.elapsed = 0;
45
+ for (const [node, pose] of target) {
46
+ const before = this.last.get(node);
47
+ if (!before)
48
+ continue;
49
+ const v = this.velocity.get(node) ?? zeroVelocity(), incoming = targetVelocity.get(node) ?? zeroVelocity();
50
+ const angular = rotationVector(before.rotation.multiply(Quaternion.Inverse(pose.rotation)));
51
+ this.residuals.set(node, { offset: { position: before.position.subtract(pose.position), scaling: before.scaling.subtract(pose.scaling), rotation: new Quaternion(angular.x, angular.y, angular.z, 0) },
52
+ velocity: { position: v.position.subtract(incoming.position), scaling: v.scaling.subtract(incoming.scaling), rotation: v.rotation.subtract(incoming.rotation) } });
53
+ }
54
+ }
55
+ else
56
+ this.elapsed += dt;
57
+ this.target = target;
58
+ this.dt = dt;
59
+ this.revision++;
60
+ }
61
+ render() {
62
+ for (const [node, target] of this.target) {
63
+ const residual = this.residuals.get(node);
64
+ const decay = (offset, velocity) => new Vector3(...offset.asArray().map((v, i) => decayInertialOffset(v, velocity.asArray()[i], this.elapsed, this.halfLife)));
65
+ node.position.copyFrom(target.position);
66
+ node.scaling.copyFrom(target.scaling);
67
+ node.rotationQuaternion ??= Quaternion.Identity();
68
+ node.rotationQuaternion.copyFrom(target.rotation);
69
+ if (residual) {
70
+ node.position.addInPlace(decay(residual.offset.position, residual.velocity.position));
71
+ node.scaling.addInPlace(decay(residual.offset.scaling, residual.velocity.scaling));
72
+ const r = residual.offset.rotation, v = residual.velocity.rotation;
73
+ node.rotationQuaternion.copyFrom(rotationFromVector(decay(new Vector3(r.x, r.y, r.z), new Vector3(v.x, v.y, v.z))).multiply(target.rotation).normalize());
74
+ }
75
+ if (this.renderedRevision !== this.revision) {
76
+ const pose = captureLocal(node), previous = this.last.get(node);
77
+ if (previous && this.dt > 0)
78
+ this.velocity.set(node, poseVelocity(previous, pose, this.dt));
79
+ this.last.set(node, pose);
80
+ }
81
+ }
82
+ this.renderedRevision = this.revision;
83
+ if (this.elapsed > this.halfLife * 40)
84
+ this.residuals.clear();
85
+ }
86
+ reset() { this.last.clear(); this.velocity.clear(); this.residuals.clear(); this.target.clear(); this.elapsed = 0; this.renderedRevision = -1; }
87
+ dispose() { this.reset(); this.scene.onAfterAnimationsObservable.remove(this.observer); }
88
+ }
package/dist/babylon.d.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { AnimationGroup, TransformNode, type Scene } from '@babylonjs/core';
2
2
  import type { Clip, Playback, Pose } from './index.js';
3
3
  import { type PoseSchema, type SchemaPose } from './core/schema.js';
4
+ export interface BabylonPlayerOptions {
5
+ /** Exact MODEL node names. No descendant expansion or name mapping is implied. */
6
+ mask?: readonly string[];
7
+ transition?: 'crossfade' | 'inertialization';
8
+ /** Positive seconds. Applies only to inertialization; default 0.1. */
9
+ inertialHalfLife?: number;
10
+ }
4
11
  /** Clone onto a target rig. Two playback slots allow crossfade between times of the same clip. */
5
12
  export declare class BabylonMotionPlayer {
6
13
  private scene;
@@ -14,9 +21,15 @@ export declare class BabylonMotionPlayer {
14
21
  private disposed;
15
22
  private strippedRoot?;
16
23
  private rootRestRotation?;
17
- constructor(scene: Scene, clips: readonly Clip[], sources: ReadonlyMap<string, AnimationGroup>, nodes: ReadonlyMap<string, TransformNode>, fadeSeconds?: number, rootName?: string);
24
+ private owned;
25
+ private rest;
26
+ private inertia?;
27
+ constructor(scene: Scene, clips: readonly Clip[], sources: ReadonlyMap<string, AnimationGroup>, nodes: ReadonlyMap<string, TransformNode>, fadeSeconds?: number, rootName?: string, options?: BabylonPlayerOptions);
18
28
  private create;
19
29
  update(state: Playback, dt: number): void;
30
+ private sampleLocal;
31
+ /** Clear presentation history after teleport/reconciliation; ownership stays until dispose. */
32
+ reset(): void;
20
33
  private clear;
21
34
  dispose(): void;
22
35
  }
package/dist/babylon.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Animation, AnimationGroup, Matrix, Quaternion, Vector3 } from '@babylonjs/core';
2
2
  import { compileSchema, LOCOMOTION_SCHEMA } from './core/schema.js';
3
3
  import { parseRootOrientation } from './core/profile.js';
4
+ import { BabylonInertializer, captureLocal, poseVelocity } from './babylon-inertialization.js';
5
+ const nodeOwners = new WeakMap();
4
6
  /** Constant authoring orientation belongs to the pose; varying root heading belongs to gameplay. */
5
7
  function constantRootRotation(animation) {
6
8
  const keys = animation.getKeys();
@@ -32,7 +34,10 @@ export class BabylonMotionPlayer {
32
34
  disposed = false;
33
35
  strippedRoot;
34
36
  rootRestRotation;
35
- constructor(scene, clips, sources, nodes, fadeSeconds = 0.15, rootName = 'root') {
37
+ owned = new Set();
38
+ rest = new Map();
39
+ inertia;
40
+ constructor(scene, clips, sources, nodes, fadeSeconds = 0.15, rootName = 'root', options = {}) {
36
41
  this.scene = scene;
37
42
  this.clips = clips;
38
43
  this.sources = sources;
@@ -41,6 +46,10 @@ export class BabylonMotionPlayer {
41
46
  this.rootName = rootName;
42
47
  if (!Number.isFinite(fadeSeconds) || fadeSeconds < 0)
43
48
  throw new Error('Invalid crossfade duration');
49
+ if (options.transition !== undefined && !['crossfade', 'inertialization'].includes(options.transition))
50
+ throw new Error('Invalid transition mode');
51
+ if (options.inertialHalfLife !== undefined && (!Number.isFinite(options.inertialHalfLife) || options.inertialHalfLife <= 0))
52
+ throw new Error('Invalid inertial half life');
44
53
  for (const clip of clips) {
45
54
  const source = sources.get(clip.id);
46
55
  if (!source)
@@ -50,8 +59,23 @@ export class BabylonMotionPlayer {
50
59
  || start + clip.duration > (source.to - source.from) / fps + 1e-5)
51
60
  throw new Error(`Invalid source range: ${clip.id}`);
52
61
  }
53
- this.strippedRoot = [...nodes.values()].find(n => n.name === rootName);
54
- if (this.strippedRoot) {
62
+ const mapped = new Set(nodes.values());
63
+ if (options.mask !== undefined && (!Array.isArray(options.mask) || !options.mask.length || new Set(options.mask).size !== options.mask.length
64
+ || options.mask.some(name => typeof name !== 'string' || [...mapped].filter(n => n.name === name).length !== 1)))
65
+ throw new Error('Invalid player mask: use unique mapped model node names');
66
+ const mask = options.mask === undefined ? mapped : new Set([...mapped].filter(n => options.mask.includes(n.name)));
67
+ for (const clip of clips)
68
+ for (const targeted of sources.get(clip.id).targetedAnimations) {
69
+ const node = nodes.get(targeted.target.name);
70
+ if (node && mask.has(node))
71
+ this.owned.add(node);
72
+ }
73
+ if (!this.owned.size)
74
+ throw new Error('No matching rig animation tracks in player mask');
75
+ if ([...this.owned].some(node => nodeOwners.has(node)))
76
+ throw new Error('Animation node already owned by another motion player');
77
+ this.strippedRoot = [...mapped].find(n => n.name === rootName);
78
+ if (this.strippedRoot && this.owned.has(this.strippedRoot)) {
55
79
  this.strippedRoot.rotationQuaternion ??= Quaternion.FromEulerVector(this.strippedRoot.rotation);
56
80
  this.rootRestRotation = this.strippedRoot.rotationQuaternion.clone();
57
81
  }
@@ -61,6 +85,18 @@ export class BabylonMotionPlayer {
61
85
  if (!this.strippedRoot)
62
86
  throw new Error('rootOrientation requires a mapped stripped root');
63
87
  }
88
+ if (options.transition === 'inertialization') {
89
+ for (const clip of clips)
90
+ for (const targeted of sources.get(clip.id).targetedAnimations) {
91
+ const node = nodes.get(targeted.target.name), property = targeted.animation.targetProperty;
92
+ if (node && this.owned.has(node) && node !== this.strippedRoot && !/^(?:(?:position|scaling)(\.[xyz])?|rotationQuaternion(\.[xyzw])?)$/.test(property))
93
+ throw new Error('Inertialization supports local position/scaling/quaternion tracks only');
94
+ }
95
+ this.rest = new Map([...this.owned].map(node => [node, captureLocal(node)]));
96
+ this.inertia = new BabylonInertializer(scene, options.inertialHalfLife ?? .1);
97
+ }
98
+ for (const node of this.owned)
99
+ nodeOwners.set(node, this);
64
100
  }
65
101
  create(clip, time) {
66
102
  const source = this.sources.get(this.clips[clip].id);
@@ -68,7 +104,7 @@ export class BabylonMotionPlayer {
68
104
  let rootRotation = this.rootRestRotation?.clone();
69
105
  for (const targeted of source.targetedAnimations) {
70
106
  const node = this.nodes.get(targeted.target.name);
71
- if (!node)
107
+ if (!node || !this.owned.has(node))
72
108
  continue;
73
109
  const property = targeted.animation.targetProperty;
74
110
  if (node === this.strippedRoot) {
@@ -88,7 +124,7 @@ export class BabylonMotionPlayer {
88
124
  const correction = this.clips[clip].rootOrientation;
89
125
  if (correction)
90
126
  rootRotation = Quaternion.FromArray([...correction]).normalize();
91
- if (this.strippedRoot && rootRotation) {
127
+ if (this.strippedRoot && this.owned.has(this.strippedRoot) && rootRotation) {
92
128
  // An explicit constant binding also restores the baseline after a clip with
93
129
  // another orientation, and participates in the same weighted crossfade.
94
130
  const rotation = new Animation('mm-root-pose', 'rotationQuaternion', source.targetedAnimations[0].animation.framePerSecond, Animation.ANIMATIONTYPE_QUATERNION);
@@ -115,6 +151,13 @@ export class BabylonMotionPlayer {
115
151
  }
116
152
  if (!this.clips[state.clip])
117
153
  throw new Error('Unknown playback clip');
154
+ const switched = !this.active || state.switched || this.active.clip !== state.clip;
155
+ if (switched && this.inertia) {
156
+ for (const track of this.tracks)
157
+ track.group.dispose();
158
+ this.tracks = [];
159
+ this.active = undefined;
160
+ }
118
161
  if (!this.active || state.switched || this.active.clip !== state.clip) {
119
162
  // Bound simultaneous slots during rapid replanning; residual poses retain relative weights.
120
163
  if (this.tracks.length >= 4) {
@@ -124,7 +167,18 @@ export class BabylonMotionPlayer {
124
167
  this.active = this.create(state.clip, state.time);
125
168
  }
126
169
  this.active.time = state.time;
127
- const alpha = this.fadeSeconds === 0 ? 1 : 1 - Math.exp(-dt * 4 / this.fadeSeconds);
170
+ if (this.inertia) {
171
+ const clip = this.clips[state.clip], h = Math.min(1 / 120, clip.duration / 4);
172
+ const time = clip.looping ? ((state.time % clip.duration) + clip.duration) % clip.duration : Math.max(0, Math.min(state.time, clip.duration));
173
+ const low = clip.looping ? time - h : Math.max(0, time - h), high = clip.looping ? time + h : Math.min(clip.duration, time + h);
174
+ let incomingVelocity = new Map();
175
+ if (switched) {
176
+ const before = this.sampleLocal(this.active, low), after = this.sampleLocal(this.active, high);
177
+ incomingVelocity = new Map([...before].map(([node, pose]) => [node, poseVelocity(pose, after.get(node), high - low)]));
178
+ }
179
+ this.inertia.update(this.sampleLocal(this.active, time), incomingVelocity, dt, switched);
180
+ }
181
+ const alpha = this.inertia || this.fadeSeconds === 0 ? 1 : 1 - Math.exp(-dt * 4 / this.fadeSeconds);
128
182
  for (const track of this.tracks) {
129
183
  if (track !== this.active)
130
184
  track.time += dt;
@@ -147,9 +201,35 @@ export class BabylonMotionPlayer {
147
201
  return true;
148
202
  });
149
203
  }
204
+ sampleLocal(track, time) {
205
+ const result = new Map([...this.rest].map(([node, pose]) => [node, { position: pose.position.clone(), scaling: pose.scaling.clone(), rotation: pose.rotation.clone() }]));
206
+ const clip = this.clips[track.clip], source = this.sources.get(clip.id);
207
+ time = clip.looping ? ((time % clip.duration) + clip.duration) % clip.duration : Math.max(0, Math.min(time, clip.duration));
208
+ for (const targeted of track.group.targetedAnimations) {
209
+ const node = targeted.target, pose = result.get(node);
210
+ const [property, component] = targeted.animation.targetProperty.split('.');
211
+ const value = targeted.animation.evaluate(source.from + ((clip.sourceStart ?? 0) + time) * targeted.animation.framePerSecond);
212
+ const target = property === 'rotationQuaternion' ? pose.rotation : property === 'scaling' ? pose.scaling : pose.position;
213
+ if (component)
214
+ target[component] = value;
215
+ else
216
+ target.copyFrom(value);
217
+ }
218
+ return result;
219
+ }
220
+ /** Clear presentation history after teleport/reconciliation; ownership stays until dispose. */
221
+ reset() { this.clear(); }
150
222
  clear() { for (const t of this.tracks)
151
- t.group.dispose(); this.tracks = []; this.active = undefined; }
152
- dispose() { this.clear(); this.disposed = true; }
223
+ t.group.dispose(); this.tracks = []; this.active = undefined; this.inertia?.reset(); }
224
+ dispose() {
225
+ this.clear();
226
+ this.disposed = true;
227
+ this.inertia?.dispose();
228
+ for (const node of this.owned)
229
+ if (nodeOwners.get(node) === this)
230
+ nodeOwners.delete(node);
231
+ this.owned.clear();
232
+ }
153
233
  }
154
234
  /** Sample AFTER scene animation evaluation. Semantic roles are defined by the schema. */
155
235
  export class BabylonSchemaPoseReader {