@pose_flow/babylon 0.1.0 → 0.2.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 +123 -105
- package/dist/AI_USAGE.md +291 -0
- package/dist/PUBLISHING.md +108 -0
- package/dist/babylon.d.ts +13 -4
- package/dist/babylon.js +96 -21
- package/dist/core/controller.js +6 -1
- package/dist/core/database.d.ts +8 -2
- package/dist/core/database.js +60 -24
- package/dist/core/features.d.ts +1 -1
- package/dist/core/features.js +9 -11
- package/dist/core/profile.d.ts +23 -6
- package/dist/core/profile.js +35 -10
- package/dist/core/root-motion.d.ts +5 -3
- package/dist/core/root-motion.js +43 -7
- package/dist/core/schema.d.ts +56 -0
- package/dist/core/schema.js +123 -0
- package/dist/core/search.d.ts +8 -2
- package/dist/core/search.js +59 -12
- package/dist/core/types.d.ts +21 -3
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/offline/src/babylon.js +201 -0
- package/dist/offline/src/core/controller.js +97 -0
- package/dist/offline/src/core/database.js +95 -0
- package/dist/offline/src/core/features.js +41 -0
- package/dist/offline/src/core/profile.js +89 -0
- package/dist/offline/src/core/root-motion.js +78 -0
- package/dist/offline/src/core/schema.js +123 -0
- package/dist/offline/src/core/search.js +97 -0
- package/dist/offline/src/core/types.js +3 -0
- package/dist/offline/src/index.js +8 -0
- package/dist/offline/tools/bake.js +86 -0
- package/dist/offline/tools/cli.js +30 -0
- package/dist/offline/tools/glb.js +54 -0
- package/dist/offline/tools/prepare.js +165 -0
- package/dist/offline/tools/profile-assets.js +90 -0
- package/dist/offline/tools/rig.js +47 -0
- package/dist/offline/tools/validate-assets.js +43 -0
- package/package.json +48 -39
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Publicar @pose_flow/babylon no npm
|
|
2
|
+
|
|
3
|
+
Fonte deste documento: `docs/PUBLISHING.md`. O build gera `dist/PUBLISHING.md`, incluído no pacote npm. Atualizar o documento de origem, nunca a cópia.
|
|
4
|
+
|
|
5
|
+
## Preparar uma atualização
|
|
6
|
+
|
|
7
|
+
Executar na raiz do repositório da biblioteca. Os comandos abaixo não pertencem ao projeto consumidor.
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
Set-Location D:\motion_match
|
|
11
|
+
npm ci
|
|
12
|
+
npm run typecheck
|
|
13
|
+
npm test
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Antes da versão, atualizar `docs/AI_USAGE.md` para toda implementação/correção e este documento se o processo mudar. Não prosseguir se algum comando falhar.
|
|
17
|
+
|
|
18
|
+
## Aumentar a versão
|
|
19
|
+
|
|
20
|
+
### Marco planejado de estabilidade 1.0.0
|
|
21
|
+
|
|
22
|
+
O roadmap do repositório exige uma release estável 1.0.0 como entrega final. A série atual 0.1.x não recebe essa classificação por esta atualização documental. Não aumentar a versão agora por causa do planejamento.
|
|
23
|
+
|
|
24
|
+
Antes da 1.0.0, concluir os critérios de docs/ROADMAP.md, preparar candidatos 1.0.0-rc.N e verificar instalação, ferramentas offline e reprodução em consumidor independente. Registrar evidências, matriz de suporte, metas/resultados de desempenho, notas da versão e migração dos contratos 0.x. Defeitos críticos ou de alta severidade no escopo suportado bloqueiam a release. Executar as verificações abaixo para o pacote candidato e novamente para o pacote final; conferir a versão exata instalada, sem presumir que latest aponta para um candidato.
|
|
25
|
+
|
|
26
|
+
Após aprovação dos critérios, a entrega final deve ser 1.0.0, em vez de aplicar automaticamente o incremento patch descrito para manutenção de 0.x. Publicar somente mediante pedido explícito do mantenedor e conferir instalação da versão publicada. Após 1.0.0, preservar contratos em patch/minor; quebras exigem major e guia de migração. A topologia dos pacotes/CLI será definida na etapa de distribuição; os arquivos e comandos atuais abaixo descrevem somente o pacote existente.
|
|
27
|
+
|
|
28
|
+
### Mudança de formatos na Fase 1 (ainda sem publicação)
|
|
29
|
+
|
|
30
|
+
O código local usa banco/perfil v2 e schema v1. Antes da próxima release, escolher uma versão 0.x apropriada à quebra de contrato e publicar as instruções de migração incluídas em AI_USAGE.md; não tratar esta entrega como simples correção patch. A versão em package.json não foi aumentada nesta implementação. Esse era o conteúdo na Fase 1. A Fase 2 abaixo inclui CLI offline; JSON externos e bancos continuam fora do pacote.
|
|
31
|
+
|
|
32
|
+
### Manutenção da série atual
|
|
33
|
+
|
|
34
|
+
A publicação de 0.1.0 foi informada pelo mantenedor. Versões já publicadas não podem ser sobrescritas. A inclusão destes documentos exige uma nova publicação para chegar aos consumidores.
|
|
35
|
+
|
|
36
|
+
Para uma correção ou atualização de documentação, executar **uma única vez**:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
npm version patch --no-git-tag-version
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Se a versão local ainda for 0.1.0, isso gera 0.1.1 e atualiza package.json/package-lock.json, sem criar commit ou tag. Não repetir o comando se já aumentou a versão para essa entrega. Para funcionalidade compatível, avaliar `npm version minor --no-git-tag-version`; para quebra de contrato, planejar a versão e a migração explicitamente, especialmente enquanto a lib estiver em 0.x.
|
|
43
|
+
|
|
44
|
+
## Build e inspeção
|
|
45
|
+
|
|
46
|
+
```powershell
|
|
47
|
+
npm run build
|
|
48
|
+
npm run check:docs
|
|
49
|
+
npm pack --dry-run --ignore-scripts
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Conferir a versão e os arquivos listados: biblioteca JS/tipos em dist/, dist/AI_USAGE.md, dist/PUBLISHING.md, README.md, LICENSE e package.json. Personagens, animações, bancos, perfis de exemplo, testes e fontes TypeScript não entram no pacote. Ferramentas compiladas e seu runtime interno em dist/offline/ acompanham a CLI.
|
|
53
|
+
|
|
54
|
+
O `--ignore-scripts` acima é usado **somente após o build explícito**, para inspecionar sem compilá-lo novamente. No fluxo normal de `npm pack` e `npm publish`, o `prepack` executa o build e copia a documentação atual. Não usar `--ignore-scripts` na publicação normal.
|
|
55
|
+
|
|
56
|
+
Para gerar um .tgz local e testar a instalação antes de publicar:
|
|
57
|
+
|
|
58
|
+
```powershell
|
|
59
|
+
npm pack
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
No projeto de teste, instalar o caminho do .tgz exibido pelo comando anterior. Não assumir o nome/versão do arquivo. Testar imports do núcleo/adaptador e localizar o tutorial em node_modules/@pose_flow/babylon/dist/AI_USAGE.md.
|
|
63
|
+
|
|
64
|
+
## Autenticar e publicar
|
|
65
|
+
|
|
66
|
+
```powershell
|
|
67
|
+
npm login
|
|
68
|
+
npm whoami
|
|
69
|
+
npm publish --access public
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Verificar a conta e a permissão no escopo `pose_flow`. Concluir a autenticação de dois fatores solicitada pelo npm. Não armazenar senha, token ou códigos de recuperação no repositório/documentos. Uma IA só deve executar a publicação se o mantenedor pedir essa ação explicitamente; a autenticação interativa é feita pelo mantenedor.
|
|
73
|
+
|
|
74
|
+
Depois, conferir o registro:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
npm view @pose_flow/babylon version
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
No projeto consumidor:
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
npm install @pose_flow/babylon@latest
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Conferir a versão efetivamente instalada no lockfile e ler o tutorial daquela versão. Não copiar dist/ manualmente para node_modules.
|
|
87
|
+
|
|
88
|
+
## Erros comuns
|
|
89
|
+
|
|
90
|
+
- E403 com mensagem de 2FA: conferir 2FA da conta e autenticar novamente. Não é erro do conteúdo do tarball.
|
|
91
|
+
- Sem permissão no escopo: conferir acesso à organização pose_flow e nome do pacote.
|
|
92
|
+
- Versão já existente: consultar a versão publicada e preparar uma versão nova; não tentar sobrescrever.
|
|
93
|
+
- Tutorial ausente/desatualizado: editar docs/AI_USAGE.md, executar build e check:docs, inspecionar o pacote.
|
|
94
|
+
|
|
95
|
+
## Referências oficiais
|
|
96
|
+
|
|
97
|
+
- [npm publish](https://docs.npmjs.com/cli/v11/commands/npm-publish/): publicação e versões imutáveis.
|
|
98
|
+
- [npm version](https://docs.npmjs.com/cli/v11/commands/npm-version/): incremento de versão e opção sem tag Git.
|
|
99
|
+
- [Lifecycle scripts](https://docs.npmjs.com/cli/v11/using-npm/scripts/): prepack.
|
|
100
|
+
- [Configurar 2FA](https://docs.npmjs.com/configuring-two-factor-authentication/).
|
|
101
|
+
|
|
102
|
+
## CLI offline — Fase 2, sem publicação
|
|
103
|
+
|
|
104
|
+
O build agora executa também `tsc -p tsconfig.offline.json`. `bin.pose-flow` aponta para `dist/offline/tools/cli.js`; `dist/offline/src/` contém o runtime interno das ferramentas. Os exports públicos permanecem dist/index.js e dist/babylon.js. Não importar caminhos internos da CLI como API.
|
|
105
|
+
|
|
106
|
+
`@babylonjs/loaders@7.54.3` passou de devDependency para dependency para permitir inspeção/bake após instalar. Babylon core continua peerDependency. Lockfile atualizado; versão não alterada. Conferir CLI/runtime interno e ambos os guias no dry-run. Testar instalação em diretório independente e executar `npx --no-install pose-flow validate --profile public/profiles/rig.json` e `npx --no-install pose-flow bake --profile public/profiles/rig.json`. O teste deve ficar fora do MMORPG protegido.
|
|
107
|
+
|
|
108
|
+
A CLI agora inclui `prepare --config preparation.json`, com os módulos compilados prepare/glb/rig. Conferir esses módulos no pacote. Os scripts Python de conversão e o experimento Blender não são distribuídos; Blender não é dependência do runtime nem da CLI GLB. Testar também prepare → validate → bake no pacote instalado com GLB de entrada do consumidor. `--check-bind` é opcional e exige dados de skin/bind suportados. O pacote continua sem assets, perfis, bancos ou publicação automática.
|
package/dist/babylon.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AnimationGroup, TransformNode, type Scene } from '@babylonjs/core';
|
|
2
2
|
import type { Clip, Playback, Pose } from './index.js';
|
|
3
|
+
import { type PoseSchema, type SchemaPose } from './core/schema.js';
|
|
3
4
|
/** Clone onto a target rig. Two playback slots allow crossfade between times of the same clip. */
|
|
4
5
|
export declare class BabylonMotionPlayer {
|
|
5
6
|
private scene;
|
|
@@ -11,18 +12,26 @@ export declare class BabylonMotionPlayer {
|
|
|
11
12
|
private tracks;
|
|
12
13
|
private active?;
|
|
13
14
|
private disposed;
|
|
15
|
+
private strippedRoot?;
|
|
16
|
+
private rootRestRotation?;
|
|
14
17
|
constructor(scene: Scene, clips: readonly Clip[], sources: ReadonlyMap<string, AnimationGroup>, nodes: ReadonlyMap<string, TransformNode>, fadeSeconds?: number, rootName?: string);
|
|
15
18
|
private create;
|
|
16
19
|
update(state: Playback, dt: number): void;
|
|
17
20
|
private clear;
|
|
18
21
|
dispose(): void;
|
|
19
22
|
}
|
|
20
|
-
/** Sample AFTER scene animation evaluation.
|
|
21
|
-
export declare class
|
|
23
|
+
/** Sample AFTER scene animation evaluation. Semantic roles are defined by the schema. */
|
|
24
|
+
export declare class BabylonSchemaPoseReader {
|
|
22
25
|
private root;
|
|
23
|
-
private left;
|
|
24
|
-
private right;
|
|
25
26
|
private previous?;
|
|
27
|
+
private nodes;
|
|
28
|
+
constructor(root: TransformNode, bones: ReadonlyMap<string, TransformNode>, schema: PoseSchema);
|
|
29
|
+
reset(): void;
|
|
30
|
+
read(dt: number): SchemaPose;
|
|
31
|
+
}
|
|
32
|
+
/** Compatibility reader for the original two-foot preset. */
|
|
33
|
+
export declare class BabylonPoseReader {
|
|
34
|
+
private reader;
|
|
26
35
|
constructor(root: TransformNode, left: TransformNode, right: TransformNode);
|
|
27
36
|
reset(): void;
|
|
28
37
|
read(dt: number): Pose;
|
package/dist/babylon.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
import { AnimationGroup, Matrix, Vector3 } from '@babylonjs/core';
|
|
1
|
+
import { Animation, AnimationGroup, Matrix, Quaternion, Vector3 } from '@babylonjs/core';
|
|
2
|
+
import { compileSchema, LOCOMOTION_SCHEMA } from './core/schema.js';
|
|
3
|
+
import { parseRootOrientation } from './core/profile.js';
|
|
4
|
+
/** Constant authoring orientation belongs to the pose; varying root heading belongs to gameplay. */
|
|
5
|
+
function constantRootRotation(animation) {
|
|
6
|
+
const keys = animation.getKeys();
|
|
7
|
+
if (!keys.length || keys.some(k => k.inTangent !== undefined || k.outTangent !== undefined))
|
|
8
|
+
return undefined;
|
|
9
|
+
const rotation = (value) => animation.targetProperty === 'rotationQuaternion' && value instanceof Quaternion ? value.clone()
|
|
10
|
+
: animation.targetProperty === 'rotation' && value instanceof Vector3 ? Quaternion.FromEulerVector(value) : undefined;
|
|
11
|
+
const first = rotation(keys[0].value);
|
|
12
|
+
if (!first || !first.asArray().every(Number.isFinite) || first.length() < 1e-8)
|
|
13
|
+
return undefined;
|
|
14
|
+
first.normalize();
|
|
15
|
+
for (const key of keys) {
|
|
16
|
+
const q = rotation(key.value);
|
|
17
|
+
if (!q || !q.asArray().every(Number.isFinite) || q.length() < 1e-8 || Math.abs(Quaternion.Dot(first, q.normalize())) < 1 - 1e-8)
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return first;
|
|
21
|
+
}
|
|
2
22
|
/** Clone onto a target rig. Two playback slots allow crossfade between times of the same clip. */
|
|
3
23
|
export class BabylonMotionPlayer {
|
|
4
24
|
scene;
|
|
@@ -10,6 +30,8 @@ export class BabylonMotionPlayer {
|
|
|
10
30
|
tracks = [];
|
|
11
31
|
active;
|
|
12
32
|
disposed = false;
|
|
33
|
+
strippedRoot;
|
|
34
|
+
rootRestRotation;
|
|
13
35
|
constructor(scene, clips, sources, nodes, fadeSeconds = 0.15, rootName = 'root') {
|
|
14
36
|
this.scene = scene;
|
|
15
37
|
this.clips = clips;
|
|
@@ -19,27 +41,60 @@ export class BabylonMotionPlayer {
|
|
|
19
41
|
this.rootName = rootName;
|
|
20
42
|
if (!Number.isFinite(fadeSeconds) || fadeSeconds < 0)
|
|
21
43
|
throw new Error('Invalid crossfade duration');
|
|
22
|
-
for (const clip of clips)
|
|
23
|
-
|
|
44
|
+
for (const clip of clips) {
|
|
45
|
+
const source = sources.get(clip.id);
|
|
46
|
+
if (!source)
|
|
24
47
|
throw new Error(`Missing animation source: ${clip.id}`);
|
|
48
|
+
const fps = source.targetedAnimations[0]?.animation.framePerSecond, start = clip.sourceStart ?? 0;
|
|
49
|
+
if (!Number.isFinite(start) || start < 0 || !Number.isFinite(clip.duration) || clip.duration <= 0 || !fps
|
|
50
|
+
|| start + clip.duration > (source.to - source.from) / fps + 1e-5)
|
|
51
|
+
throw new Error(`Invalid source range: ${clip.id}`);
|
|
52
|
+
}
|
|
53
|
+
this.strippedRoot = [...nodes.values()].find(n => n.name === rootName);
|
|
54
|
+
if (this.strippedRoot) {
|
|
55
|
+
this.strippedRoot.rotationQuaternion ??= Quaternion.FromEulerVector(this.strippedRoot.rotation);
|
|
56
|
+
this.rootRestRotation = this.strippedRoot.rotationQuaternion.clone();
|
|
57
|
+
}
|
|
58
|
+
for (const clip of clips)
|
|
59
|
+
if (clip.rootOrientation !== undefined) {
|
|
60
|
+
parseRootOrientation(clip.rootOrientation);
|
|
61
|
+
if (!this.strippedRoot)
|
|
62
|
+
throw new Error('rootOrientation requires a mapped stripped root');
|
|
63
|
+
}
|
|
25
64
|
}
|
|
26
65
|
create(clip, time) {
|
|
27
66
|
const source = this.sources.get(this.clips[clip].id);
|
|
28
67
|
const group = new AnimationGroup(`mm-${this.clips[clip].id}-${this.tracks.length}`, this.scene);
|
|
68
|
+
let rootRotation = this.rootRestRotation?.clone();
|
|
29
69
|
for (const targeted of source.targetedAnimations) {
|
|
30
70
|
const node = this.nodes.get(targeted.target.name);
|
|
31
71
|
if (!node)
|
|
32
72
|
continue;
|
|
33
73
|
const property = targeted.animation.targetProperty;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
74
|
+
if (node === this.strippedRoot) {
|
|
75
|
+
if (property === 'position' || property.startsWith('position.'))
|
|
76
|
+
continue;
|
|
77
|
+
if (property === 'rotationQuaternion' || property === 'rotation' || property.startsWith('rotation.') || property.startsWith('rotationQuaternion.')) {
|
|
78
|
+
rootRotation = constantRootRotation(targeted.animation) ?? rootRotation;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
37
82
|
group.addTargetedAnimation(targeted.animation, node);
|
|
38
83
|
}
|
|
39
84
|
if (!group.targetedAnimations.length) {
|
|
40
85
|
group.dispose();
|
|
41
86
|
throw new Error('No matching rig animation tracks');
|
|
42
87
|
}
|
|
88
|
+
const correction = this.clips[clip].rootOrientation;
|
|
89
|
+
if (correction)
|
|
90
|
+
rootRotation = Quaternion.FromArray([...correction]).normalize();
|
|
91
|
+
if (this.strippedRoot && rootRotation) {
|
|
92
|
+
// An explicit constant binding also restores the baseline after a clip with
|
|
93
|
+
// another orientation, and participates in the same weighted crossfade.
|
|
94
|
+
const rotation = new Animation('mm-root-pose', 'rotationQuaternion', source.targetedAnimations[0].animation.framePerSecond, Animation.ANIMATIONTYPE_QUATERNION);
|
|
95
|
+
rotation.setKeys([{ frame: source.from, value: rootRotation }, { frame: source.to, value: rootRotation.clone() }]);
|
|
96
|
+
group.addTargetedAnimation(rotation, this.strippedRoot);
|
|
97
|
+
}
|
|
43
98
|
// Paused groups still participate in Babylon's late weighted bindings when
|
|
44
99
|
// goToFrame(frame, true) is called. Speed zero alone resets on scene.render().
|
|
45
100
|
group.start(true, 0);
|
|
@@ -79,9 +134,10 @@ export class BabylonMotionPlayer {
|
|
|
79
134
|
const total = this.tracks.reduce((sum, t) => sum + t.weight, 0);
|
|
80
135
|
for (const track of this.tracks) {
|
|
81
136
|
const descriptor = this.clips[track.clip], group = track.group;
|
|
82
|
-
const time = descriptor.looping ? track.time % descriptor.duration : Math.min(track.time, descriptor.duration);
|
|
137
|
+
const time = descriptor.looping ? ((track.time % descriptor.duration) + descriptor.duration) % descriptor.duration : Math.max(0, Math.min(track.time, descriptor.duration));
|
|
83
138
|
group.setWeightForAllAnimatables(total > 0 ? track.weight / total : (track === this.active ? 1 : 0));
|
|
84
|
-
|
|
139
|
+
const source = this.sources.get(descriptor.id), fps = source.targetedAnimations[0].animation.framePerSecond;
|
|
140
|
+
group.goToFrame(source.from + ((descriptor.sourceStart ?? 0) + time) * fps, true);
|
|
85
141
|
}
|
|
86
142
|
this.tracks = this.tracks.filter(track => {
|
|
87
143
|
if (track !== this.active && track.weight < 0.001) {
|
|
@@ -95,32 +151,51 @@ export class BabylonMotionPlayer {
|
|
|
95
151
|
t.group.dispose(); this.tracks = []; this.active = undefined; }
|
|
96
152
|
dispose() { this.clear(); this.disposed = true; }
|
|
97
153
|
}
|
|
98
|
-
/** Sample AFTER scene animation evaluation.
|
|
99
|
-
export class
|
|
154
|
+
/** Sample AFTER scene animation evaluation. Semantic roles are defined by the schema. */
|
|
155
|
+
export class BabylonSchemaPoseReader {
|
|
100
156
|
root;
|
|
101
|
-
left;
|
|
102
|
-
right;
|
|
103
157
|
previous;
|
|
104
|
-
|
|
158
|
+
nodes;
|
|
159
|
+
constructor(root, bones, schema) {
|
|
105
160
|
this.root = root;
|
|
106
|
-
|
|
107
|
-
|
|
161
|
+
const roles = compileSchema(schema).bones;
|
|
162
|
+
for (const role of roles)
|
|
163
|
+
if (!bones.has(role))
|
|
164
|
+
throw new Error('Missing schema bone node: ' + role);
|
|
165
|
+
this.nodes = new Map(roles.map(role => [role, bones.get(role)]));
|
|
108
166
|
}
|
|
109
167
|
reset() { this.previous = undefined; }
|
|
110
168
|
read(dt) {
|
|
111
169
|
if (!Number.isFinite(dt) || dt < 0)
|
|
112
170
|
throw new Error('Invalid pose delta');
|
|
113
171
|
this.root.computeWorldMatrix(true);
|
|
114
|
-
this.
|
|
115
|
-
this.
|
|
116
|
-
|
|
172
|
+
const root = this.root.getAbsolutePosition().clone(), positions = new Map();
|
|
173
|
+
for (const [role, node] of this.nodes) {
|
|
174
|
+
node.computeWorldMatrix(true);
|
|
175
|
+
positions.set(role, node.getAbsolutePosition().clone());
|
|
176
|
+
}
|
|
117
177
|
const inverse = Matrix.Invert(this.root.getWorldMatrix());
|
|
118
178
|
// Vector transform through inverse includes inverse model scale: consumers should keep root scale 1.
|
|
119
179
|
const tuple = (v) => [v.x, v.y, v.z];
|
|
120
180
|
const velocity = (value, previous) => previous && dt > 0 ? Vector3.TransformNormal(value.subtract(previous).scale(1 / dt), inverse) : Vector3.Zero();
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
181
|
+
const bones = Object.fromEntries([...positions].map(([role, position]) => [role, {
|
|
182
|
+
position: tuple(Vector3.TransformCoordinates(position, inverse)), velocity: tuple(velocity(position, this.previous?.bones.get(role))),
|
|
183
|
+
}]));
|
|
184
|
+
const pose = { bones, rootVelocity: tuple(velocity(root, this.previous?.root)) };
|
|
185
|
+
this.previous = { bones: positions, root };
|
|
124
186
|
return pose;
|
|
125
187
|
}
|
|
126
188
|
}
|
|
189
|
+
/** Compatibility reader for the original two-foot preset. */
|
|
190
|
+
export class BabylonPoseReader {
|
|
191
|
+
reader;
|
|
192
|
+
constructor(root, left, right) {
|
|
193
|
+
this.reader = new BabylonSchemaPoseReader(root, new Map([['leftFoot', left], ['rightFoot', right]]), LOCOMOTION_SCHEMA);
|
|
194
|
+
}
|
|
195
|
+
reset() { this.reader.reset(); }
|
|
196
|
+
read(dt) {
|
|
197
|
+
const pose = this.reader.read(dt);
|
|
198
|
+
return { leftPosition: pose.bones.leftFoot.position, rightPosition: pose.bones.rightFoot.position,
|
|
199
|
+
leftVelocity: pose.bones.leftFoot.velocity, rightVelocity: pose.bones.rightFoot.velocity, rootVelocity: pose.rootVelocity };
|
|
200
|
+
}
|
|
201
|
+
}
|
package/dist/core/controller.js
CHANGED
|
@@ -25,6 +25,8 @@ export class MotionMatcher {
|
|
|
25
25
|
update(dt, query) {
|
|
26
26
|
if (!Number.isFinite(dt) || dt < 0)
|
|
27
27
|
throw new Error('Invalid delta time');
|
|
28
|
+
// Validate identity even while continuity skips a search this frame.
|
|
29
|
+
this.search.validateQuery(query);
|
|
28
30
|
let switched = false, ended = false;
|
|
29
31
|
for (const [clip, remaining] of this.completed) {
|
|
30
32
|
if (remaining <= dt)
|
|
@@ -58,7 +60,10 @@ export class MotionMatcher {
|
|
|
58
60
|
gap = Math.min(gap, duration - gap);
|
|
59
61
|
const distant = best.clip !== this.clip || gap > this.options.timeSeparation;
|
|
60
62
|
const better = current < 0 || best.cost + this.options.jumpPenalty < this.search.cost(current) * (1 - this.options.improvement);
|
|
61
|
-
|
|
63
|
+
const incompatibleVelocity = current >= 0 && distant && this.sinceSwitch >= this.options.minimumHold && query.velocityTolerance !== undefined
|
|
64
|
+
&& this.search.rootVelocityError(current, query) > Math.max(query.velocityTolerance, this.search.rootVelocityError(best.index, query) + 1e-6);
|
|
65
|
+
const mayReselectTime = query.allowTemporalReselection !== false || best.clip !== this.clip || !this.database.manifest.clips[best.clip].looping;
|
|
66
|
+
if (current < 0 || !allowed || ended || (mayReselectTime && (incompatibleVelocity || (distant && better && this.sinceSwitch >= this.options.minimumHold)))) {
|
|
62
67
|
this.clip = best.clip;
|
|
63
68
|
this.time = best.time;
|
|
64
69
|
this.sinceSwitch = 0;
|
package/dist/core/database.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type Database, type DatabaseManifest } from './types.js';
|
|
2
|
+
import { type PoseSchema } from './schema.js';
|
|
2
3
|
export declare function validateDatabase(db: Database): void;
|
|
3
|
-
|
|
4
|
+
type BuildManifest = Omit<DatabaseManifest, 'version' | 'dimensions' | 'schema' | 'schemaSignature' | 'coordinates' | 'horizons' | 'mean' | 'std'>;
|
|
5
|
+
/** Omitting schema selects the legacy biped preset. New data is always v2. */
|
|
6
|
+
export declare function buildDatabase(manifest: BuildManifest, raw: Float32Array, schema?: PoseSchema): Database;
|
|
4
7
|
/** Binary format is explicitly little endian float32; version/layout live in manifest. */
|
|
5
8
|
export declare function encodeFeatures(db: Database): ArrayBuffer;
|
|
6
|
-
export declare function decodeDatabase(manifest:
|
|
9
|
+
export declare function decodeDatabase(manifest: unknown, bytes: ArrayBuffer, expectedSchema?: PoseSchema): Database;
|
|
10
|
+
/** Explicit, lossless conversion ONLY for the original v1 27-dimensional preset. */
|
|
11
|
+
export declare function migrateLegacyDatabase(input: unknown, bytes: ArrayBuffer): Database;
|
|
12
|
+
export {};
|
package/dist/core/database.js
CHANGED
|
@@ -1,59 +1,95 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { compileSchema, LOCOMOTION_SCHEMA } from './schema.js';
|
|
2
|
+
import { parseRootOrientation } from './profile.js';
|
|
3
|
+
const record = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
2
4
|
export function validateDatabase(db) {
|
|
3
|
-
const m = db
|
|
4
|
-
if (m
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (
|
|
8
|
-
||
|
|
5
|
+
const m = db?.manifest;
|
|
6
|
+
if (!record(m) || m.version !== 2)
|
|
7
|
+
throw new Error('Unsupported pose database version; v1 requires migrateLegacyDatabase or a new bake');
|
|
8
|
+
const layout = compileSchema(m.schema), dimensions = layout.dimensions;
|
|
9
|
+
if (m.dimensions !== dimensions || m.schemaSignature !== layout.signature || m.coordinates !== 'meters-y-up-z-forward'
|
|
10
|
+
|| !Array.isArray(m.horizons) || m.horizons.length !== layout.horizons.length || m.horizons.some((v, i) => v !== layout.horizons[i]))
|
|
11
|
+
throw new Error('Incompatible pose database schema');
|
|
12
|
+
if (!Number.isFinite(m.sampleRate) || m.sampleRate <= 0 || !Array.isArray(m.frames) || !m.frames.length || !Array.isArray(m.clips) || !m.clips.length
|
|
13
|
+
|| !(db.features instanceof Float32Array) || db.features.length !== m.frames.length * dimensions)
|
|
9
14
|
throw new Error('Invalid database size/sample rate');
|
|
10
|
-
if (m.mean.length !==
|
|
11
|
-
|| !m.std.every(v => Number.isFinite(v) && v >= 1e-4) || !db.features.every(Number.isFinite))
|
|
15
|
+
if (!Array.isArray(m.mean) || !Array.isArray(m.std) || m.mean.length !== dimensions || m.std.length !== dimensions || ![...m.mean].every(Number.isFinite)
|
|
16
|
+
|| ![...m.std].every(v => Number.isFinite(v) && v >= 1e-4) || !db.features.every(Number.isFinite))
|
|
12
17
|
throw new Error('Invalid normalization/features');
|
|
18
|
+
for (const { channel, offset, dimensions: width } of layout.channels)
|
|
19
|
+
if (channel.normalization === 'none') {
|
|
20
|
+
for (let j = offset; j < offset + width; j++)
|
|
21
|
+
if (m.mean[j] !== 0 || m.std[j] !== 1)
|
|
22
|
+
throw new Error('Invalid unnormalized channel statistics');
|
|
23
|
+
}
|
|
24
|
+
if (m.assetHashes !== undefined && (!record(m.assetHashes) || !Object.entries(m.assetHashes).length || !Object.entries(m.assetHashes).every(([url, hash]) => url.startsWith('/') && typeof hash === 'string' && /^[a-f0-9]{64}$/.test(hash))))
|
|
25
|
+
throw new Error('Invalid asset hashes');
|
|
13
26
|
const ids = new Set();
|
|
14
27
|
for (const clip of m.clips) {
|
|
15
|
-
if (
|
|
28
|
+
if (!record(clip) || typeof clip.id !== 'string' || ids.has(clip.id) || !clip.id || typeof clip.file !== 'string' || !clip.file
|
|
29
|
+
|| typeof clip.animation !== 'string' || !clip.animation || !Number.isFinite(clip.duration) || clip.duration <= 0 || typeof clip.looping !== 'boolean'
|
|
16
30
|
|| !['idle', 'walk', 'run', 'strafe', 'turn', 'stop'].includes(clip.gait))
|
|
17
31
|
throw new Error('Invalid clip');
|
|
18
32
|
ids.add(clip.id);
|
|
33
|
+
if (clip.sourceStart !== undefined && (!Number.isFinite(clip.sourceStart) || clip.sourceStart < 0))
|
|
34
|
+
throw new Error('Invalid source start');
|
|
35
|
+
if (clip.averageVelocity !== undefined && (!Array.isArray(clip.averageVelocity) || clip.averageVelocity.length !== 3 || !clip.averageVelocity.every(Number.isFinite)))
|
|
36
|
+
throw new Error('Invalid measured average velocity');
|
|
37
|
+
if (clip.rootOrientation !== undefined)
|
|
38
|
+
parseRootOrientation(clip.rootOrientation);
|
|
19
39
|
}
|
|
20
40
|
for (const frame of m.frames) {
|
|
21
|
-
if (!Number.isInteger(frame.clip) || !m.clips[frame.clip] || !Number.isFinite(frame.time)
|
|
41
|
+
if (!record(frame) || !Number.isInteger(frame.clip) || !m.clips[frame.clip] || !Number.isFinite(frame.time)
|
|
22
42
|
|| frame.time < 0 || frame.time > m.clips[frame.clip].duration + 1e-6)
|
|
23
43
|
throw new Error('Invalid pose metadata');
|
|
24
44
|
}
|
|
25
45
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
/** Omitting schema selects the legacy biped preset. New data is always v2. */
|
|
47
|
+
export function buildDatabase(manifest, raw, schema = LOCOMOTION_SCHEMA) {
|
|
48
|
+
const layout = compileSchema(schema), dimensions = layout.dimensions, count = manifest.frames.length;
|
|
49
|
+
if (!count || raw.length !== count * dimensions || !raw.every(Number.isFinite))
|
|
29
50
|
throw new Error('Invalid raw samples');
|
|
30
|
-
const mean = Array(
|
|
51
|
+
const mean = Array(dimensions).fill(0), std = Array(dimensions).fill(0);
|
|
31
52
|
for (let i = 0; i < raw.length; i++)
|
|
32
|
-
mean[i %
|
|
53
|
+
mean[i % dimensions] += raw[i] / count;
|
|
33
54
|
for (let i = 0; i < raw.length; i++)
|
|
34
|
-
std[i %
|
|
35
|
-
|
|
36
|
-
// unsupported trajectories (e.g. turns absent from the dataset) by 10,000×.
|
|
37
|
-
for (let j = 0; j < DIMENSIONS; j++)
|
|
55
|
+
std[i % dimensions] += (raw[i] - mean[i % dimensions]) ** 2 / count;
|
|
56
|
+
for (let j = 0; j < dimensions; j++)
|
|
38
57
|
std[j] = Math.sqrt(std[j]) < 1e-4 ? 1 : Math.sqrt(std[j]);
|
|
39
|
-
const
|
|
40
|
-
|
|
58
|
+
for (const { channel, offset, dimensions: width } of layout.channels)
|
|
59
|
+
if (channel.normalization === 'none') {
|
|
60
|
+
mean.fill(0, offset, offset + width);
|
|
61
|
+
std.fill(1, offset, offset + width);
|
|
62
|
+
}
|
|
63
|
+
const features = Float32Array.from(raw, (v, i) => (v - mean[i % dimensions]) / std[i % dimensions]);
|
|
64
|
+
const db = { manifest: { ...manifest, version: 2, dimensions, schema: layout.schema, schemaSignature: layout.signature,
|
|
65
|
+
coordinates: 'meters-y-up-z-forward', horizons: layout.horizons, mean, std }, features };
|
|
41
66
|
validateDatabase(db);
|
|
42
67
|
return db;
|
|
43
68
|
}
|
|
44
69
|
/** Binary format is explicitly little endian float32; version/layout live in manifest. */
|
|
45
70
|
export function encodeFeatures(db) {
|
|
71
|
+
validateDatabase(db);
|
|
46
72
|
const bytes = new ArrayBuffer(db.features.length * 4), view = new DataView(bytes);
|
|
47
73
|
db.features.forEach((v, i) => view.setFloat32(i * 4, v, true));
|
|
48
74
|
return bytes;
|
|
49
75
|
}
|
|
50
|
-
export function decodeDatabase(manifest, bytes) {
|
|
76
|
+
export function decodeDatabase(manifest, bytes, expectedSchema) {
|
|
51
77
|
if (bytes.byteLength % 4)
|
|
52
78
|
throw new Error('Truncated feature binary');
|
|
53
79
|
const view = new DataView(bytes), features = new Float32Array(bytes.byteLength / 4);
|
|
54
80
|
for (let i = 0; i < features.length; i++)
|
|
55
81
|
features[i] = view.getFloat32(i * 4, true);
|
|
56
|
-
const db = { manifest, features };
|
|
82
|
+
const db = { manifest: manifest, features };
|
|
57
83
|
validateDatabase(db);
|
|
84
|
+
if (expectedSchema && db.manifest.schemaSignature !== compileSchema(expectedSchema).signature)
|
|
85
|
+
throw new Error('Incompatible expected schema; regenerate database');
|
|
58
86
|
return db;
|
|
59
87
|
}
|
|
88
|
+
/** Explicit, lossless conversion ONLY for the original v1 27-dimensional preset. */
|
|
89
|
+
export function migrateLegacyDatabase(input, bytes) {
|
|
90
|
+
const layout = compileSchema(LOCOMOTION_SCHEMA);
|
|
91
|
+
if (!record(input) || input.version !== 1 || input.dimensions !== layout.dimensions || !Array.isArray(input.horizons)
|
|
92
|
+
|| input.horizons.length !== layout.horizons.length || input.horizons.some((h, i) => h !== layout.horizons[i]))
|
|
93
|
+
throw new Error('Unsupported legacy database schema');
|
|
94
|
+
return decodeDatabase({ ...input, version: 2, schema: layout.schema, schemaSignature: layout.signature }, bytes);
|
|
95
|
+
}
|
package/dist/core/features.d.ts
CHANGED
|
@@ -12,4 +12,4 @@ export interface TrajectoryInput {
|
|
|
12
12
|
turnSpeed: number;
|
|
13
13
|
}
|
|
14
14
|
/** Prediction only: never writes to the gameplay controller. Uses fixed substeps. */
|
|
15
|
-
export declare function predictTrajectory(input: TrajectoryInput): TrajectoryPoint[];
|
|
15
|
+
export declare function predictTrajectory(input: TrajectoryInput, horizons?: readonly number[]): TrajectoryPoint[];
|
package/dist/core/features.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { DIMENSIONS, HORIZONS } from './types.js';
|
|
2
|
+
import { SchemaQueryBuilder, validateHorizons } from './schema.js';
|
|
3
|
+
const legacyBuilder = new SchemaQueryBuilder();
|
|
2
4
|
export function featureVector(pose, trajectory, out = new Float32Array(DIMENSIONS)) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
out.set(pose.leftVelocity, 6);
|
|
8
|
-
out.set(pose.rightVelocity, 9);
|
|
9
|
-
out.set(pose.rootVelocity, 12);
|
|
10
|
-
trajectory.forEach((point, i) => { out.set(point.position, 15 + i * 2); out.set(point.facing, 21 + i * 2); });
|
|
11
|
-
return out;
|
|
5
|
+
return legacyBuilder.features({ rootVelocity: pose.rootVelocity, bones: {
|
|
6
|
+
leftFoot: { position: pose.leftPosition, velocity: pose.leftVelocity },
|
|
7
|
+
rightFoot: { position: pose.rightPosition, velocity: pose.rightVelocity },
|
|
8
|
+
} }, trajectory, out);
|
|
12
9
|
}
|
|
13
10
|
/** World delta to current character-local XZ (yaw zero looks along +Z). */
|
|
14
11
|
export function toLocal(x, z, yaw) {
|
|
@@ -16,13 +13,14 @@ export function toLocal(x, z, yaw) {
|
|
|
16
13
|
return [c * x - s * z, s * x + c * z];
|
|
17
14
|
}
|
|
18
15
|
/** Prediction only: never writes to the gameplay controller. Uses fixed substeps. */
|
|
19
|
-
export function predictTrajectory(input) {
|
|
16
|
+
export function predictTrajectory(input, horizons = HORIZONS) {
|
|
17
|
+
validateHorizons(horizons);
|
|
20
18
|
if (![...input.velocity, ...input.desiredVelocity, input.yaw, input.desiredYaw, input.acceleration, input.braking, input.turnSpeed].every(Number.isFinite)
|
|
21
19
|
|| Math.min(input.acceleration, input.braking, input.turnSpeed) < 0)
|
|
22
20
|
throw new Error('Invalid trajectory input');
|
|
23
21
|
let [vx, vz] = input.velocity, x = 0, z = 0, yaw = input.yaw, elapsed = 0;
|
|
24
22
|
const points = [];
|
|
25
|
-
for (const horizon of
|
|
23
|
+
for (const horizon of horizons) {
|
|
26
24
|
while (elapsed < horizon - 1e-9) {
|
|
27
25
|
const dt = Math.min(1 / 60, horizon - elapsed);
|
|
28
26
|
const dx = input.desiredVelocity[0] - vx, dz = input.desiredVelocity[1] - vz, length = Math.hypot(dx, dz);
|
package/dist/core/profile.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { Gait, Vec3 } from './types.js';
|
|
1
|
+
import type { Gait, Quat, Vec3 } from './types.js';
|
|
2
|
+
import { type PoseSchema } from './schema.js';
|
|
2
3
|
export interface CharacterProfile {
|
|
3
|
-
version:
|
|
4
|
+
version: 2;
|
|
4
5
|
id: string;
|
|
5
6
|
label: string;
|
|
6
7
|
assetBase: string;
|
|
@@ -10,13 +11,17 @@ export interface CharacterProfile {
|
|
|
10
11
|
sampleRate: number;
|
|
11
12
|
rig: {
|
|
12
13
|
root: string;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
} & Record<string, string>;
|
|
15
|
+
schema: string;
|
|
16
|
+
animationSet: string;
|
|
16
17
|
/** Source animation node -> model node; name mapping does not perform retargeting. */
|
|
17
18
|
boneMap?: Record<string, string>;
|
|
18
19
|
/** Model node whose position/heading tracks are excluded by the player. */
|
|
19
20
|
stripRoot?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface AnimationSet {
|
|
23
|
+
version: 1;
|
|
24
|
+
id: string;
|
|
20
25
|
clips: {
|
|
21
26
|
id: string;
|
|
22
27
|
file: string;
|
|
@@ -25,12 +30,24 @@ export interface CharacterProfile {
|
|
|
25
30
|
looping: boolean;
|
|
26
31
|
nominalVelocity?: Vec3;
|
|
27
32
|
motionFile?: string;
|
|
33
|
+
rootOrientation?: Quat;
|
|
34
|
+
/** Source interval in seconds. */
|
|
35
|
+
range?: readonly [number, number];
|
|
28
36
|
}[];
|
|
29
37
|
}
|
|
38
|
+
export interface ResolvedProfile extends CharacterProfile {
|
|
39
|
+
schemaDefinition: PoseSchema;
|
|
40
|
+
clips: AnimationSet['clips'];
|
|
41
|
+
setId: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function parseRootOrientation(value: unknown): Quat;
|
|
30
44
|
/** Read untrusted JSON as unknown; report a configuration error before loading assets. */
|
|
31
45
|
export declare function parseProfile(input: unknown): CharacterProfile;
|
|
46
|
+
export declare function parseAnimationSet(input: unknown): AnimationSet;
|
|
47
|
+
/** Resolve external configuration explicitly; core performs no I/O. */
|
|
48
|
+
export declare function resolveProfile(profile: CharacterProfile, schemaInput: unknown, setInput: unknown): ResolvedProfile;
|
|
32
49
|
/** Deterministic configuration fingerprint; detects stale banks, not a security signature. */
|
|
33
|
-
export declare function profileSignature(profile:
|
|
50
|
+
export declare function profileSignature(profile: ResolvedProfile): string;
|
|
34
51
|
/** Suggestions only: ambiguity is returned to the caller, never silently resolved. */
|
|
35
52
|
export declare function suggestRig(names: readonly string[]): Record<'root' | 'leftFoot' | 'rightFoot', string[]>;
|
|
36
53
|
/** Semantic availability, independent of character/clip names. Preserve optional actions. */
|