@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.
Files changed (40) hide show
  1. package/README.md +123 -105
  2. package/dist/AI_USAGE.md +291 -0
  3. package/dist/PUBLISHING.md +108 -0
  4. package/dist/babylon.d.ts +13 -4
  5. package/dist/babylon.js +96 -21
  6. package/dist/core/controller.js +6 -1
  7. package/dist/core/database.d.ts +8 -2
  8. package/dist/core/database.js +60 -24
  9. package/dist/core/features.d.ts +1 -1
  10. package/dist/core/features.js +9 -11
  11. package/dist/core/profile.d.ts +23 -6
  12. package/dist/core/profile.js +35 -10
  13. package/dist/core/root-motion.d.ts +5 -3
  14. package/dist/core/root-motion.js +43 -7
  15. package/dist/core/schema.d.ts +56 -0
  16. package/dist/core/schema.js +123 -0
  17. package/dist/core/search.d.ts +8 -2
  18. package/dist/core/search.js +59 -12
  19. package/dist/core/types.d.ts +21 -3
  20. package/dist/core/types.js +1 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/offline/src/babylon.js +201 -0
  24. package/dist/offline/src/core/controller.js +97 -0
  25. package/dist/offline/src/core/database.js +95 -0
  26. package/dist/offline/src/core/features.js +41 -0
  27. package/dist/offline/src/core/profile.js +89 -0
  28. package/dist/offline/src/core/root-motion.js +78 -0
  29. package/dist/offline/src/core/schema.js +123 -0
  30. package/dist/offline/src/core/search.js +97 -0
  31. package/dist/offline/src/core/types.js +3 -0
  32. package/dist/offline/src/index.js +8 -0
  33. package/dist/offline/tools/bake.js +86 -0
  34. package/dist/offline/tools/cli.js +30 -0
  35. package/dist/offline/tools/glb.js +54 -0
  36. package/dist/offline/tools/prepare.js +165 -0
  37. package/dist/offline/tools/profile-assets.js +90 -0
  38. package/dist/offline/tools/rig.js +47 -0
  39. package/dist/offline/tools/validate-assets.js +43 -0
  40. package/package.json +48 -39
@@ -0,0 +1,90 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createHash } from 'node:crypto';
3
+ import { resolve } from 'node:path';
4
+ import { NullEngine, Scene, SceneLoader, TransformNode } from '@babylonjs/core';
5
+ import { GLTFFileLoader } from '@babylonjs/loaders/glTF/index.js';
6
+ import { parseProfile, resolveProfile, validateRootMotion, compileSchema } from '../src/index.js';
7
+ export function profileArgument() {
8
+ const at = process.argv.indexOf('--profile');
9
+ if (at >= 0 && !process.argv[at + 1])
10
+ throw new Error('Missing --profile JSON path');
11
+ return at >= 0 ? process.argv[at + 1] : `public/profiles/${process.argv.includes('--paladin') ? 'paladin' : 'kaykit'}.json`;
12
+ }
13
+ export const publicPath = (url) => resolve(process.argv.includes('--public-dir') ? process.argv[process.argv.indexOf('--public-dir') + 1] : 'public', '.' + url);
14
+ export async function hashProfileAssets(profile) {
15
+ const urls = new Set([profile.model, ...profile.clips.flatMap(c => [c.file, ...(c.motionFile ? [c.motionFile] : [])])].map(f => profile.assetBase + f));
16
+ return Object.fromEntries(await Promise.all([...urls].sort().map(async (url) => [url, createHash('sha256').update(await readFile(publicPath(url))).digest('hex')])));
17
+ }
18
+ export async function readProfile(path) {
19
+ const profile = parseProfile(JSON.parse(await readFile(path, 'utf8')));
20
+ const [schema, set] = await Promise.all([profile.schema, profile.animationSet].map(async (url) => JSON.parse(await readFile(publicPath(url), 'utf8'))));
21
+ return resolveProfile(profile, schema, set);
22
+ }
23
+ export async function loadProfileAssets(profile) {
24
+ const layout = compileSchema(profile.schemaDefinition);
25
+ const engine = new NullEngine(), scene = new Scene(engine);
26
+ scene.useRightHandedSystem = true;
27
+ const hook = SceneLoader.OnPluginActivatedObservable.add(p => { if (p instanceof GLTFFileLoader)
28
+ p.skipMaterials = true; });
29
+ const containers = new Map();
30
+ const dispose = () => { SceneLoader.OnPluginActivatedObservable.remove(hook); scene.dispose(); engine.dispose(); };
31
+ try {
32
+ for (const file of new Set([profile.model, ...profile.clips.map(c => c.file)])) {
33
+ const data = new Uint8Array(await readFile(publicPath(profile.assetBase + file)));
34
+ const container = await SceneLoader.LoadAssetContainerAsync('', data, scene, undefined, '.glb');
35
+ container.animationGroups.forEach(g => g.stop());
36
+ for (const node of container.rootNodes)
37
+ if (node instanceof TransformNode)
38
+ node.scaling.scaleInPlace(profile.scale);
39
+ containers.set(file, container);
40
+ }
41
+ const body = containers.get(profile.model);
42
+ const names = body.transformNodes.map(n => n.name);
43
+ if (new Set(names).size !== names.length)
44
+ throw new Error('Ambiguous duplicate model node names');
45
+ for (const node of [...Object.values(profile.rig), ...(profile.stripRoot ? [profile.stripRoot] : [])])
46
+ if (!names.includes(node))
47
+ throw new Error(`Model bone missing: ${node}`);
48
+ const warnings = [];
49
+ for (const clip of profile.clips) {
50
+ const container = containers.get(clip.file);
51
+ const groups = container.animationGroups.filter(g => g.name === clip.animation);
52
+ if (groups.length !== 1)
53
+ throw new Error(`${clip.id}: expected one animation named ${clip.animation}`);
54
+ const group = groups[0], fps = group.targetedAnimations[0]?.animation.framePerSecond;
55
+ if (!fps || group.to <= group.from || group.targetedAnimations.some(t => t.animation.framePerSecond !== fps))
56
+ throw new Error(`${clip.id}: empty clip or inconsistent FPS`);
57
+ if (clip.range && clip.range[1] > (group.to - group.from) / fps + 1e-6)
58
+ throw new Error(`${clip.id}: range exceeds source duration`);
59
+ const mapped = new Set();
60
+ for (const track of group.targetedAnimations) {
61
+ const target = profile.boneMap?.[track.target.name] ?? track.target.name;
62
+ if (!names.includes(target))
63
+ throw new Error(`${clip.id}: unmapped target ${track.target.name}; provide boneMap or retarget the animation`);
64
+ mapped.add(target);
65
+ }
66
+ for (const role of Object.values(profile.rig)) {
67
+ const matches = container.transformNodes.filter(n => (profile.boneMap?.[n.name] ?? n.name) === role);
68
+ if (matches.length !== 1)
69
+ throw new Error(`${clip.id}: missing/ambiguous source bone for ${role}`);
70
+ }
71
+ if (clip.motionFile) {
72
+ const motion = JSON.parse(await readFile(publicPath(profile.assetBase + clip.motionFile), 'utf8'));
73
+ validateRootMotion(motion);
74
+ if (Math.abs(motion.duration - (group.to - group.from) / fps) > 1e-5)
75
+ throw new Error(`${clip.id}: motion/GLB duration mismatch`);
76
+ }
77
+ if (layout.bones.some(role => !mapped.has(profile.rig[role])))
78
+ warnings.push(`${clip.id}: schema bones depend on inherited transforms; verify visible pose`);
79
+ }
80
+ for (const gait of ['idle', 'walk', 'run', 'strafe', 'turn', 'stop'])
81
+ if (!profile.clips.some(c => c.gait === gait))
82
+ warnings.push(`Coverage absent: ${gait}`);
83
+ warnings.push('Name compatibility does not validate bind pose, proportions or retargeting. Visually verify a new rig.');
84
+ return { scene, engine, containers, warnings, dispose };
85
+ }
86
+ catch (error) {
87
+ dispose();
88
+ throw error;
89
+ }
90
+ }
@@ -0,0 +1,47 @@
1
+ import { Matrix, Quaternion, Vector3 } from '@babylonjs/core';
2
+ import { readGLB } from './glb.js';
3
+ /** Validate rest geometry, not animation names. Missing bind data is an error, never a pass. */
4
+ export function compareBindRigs(target, source, rootName, boneMap = {}, tolerance = .001) {
5
+ const bind = (bytes) => {
6
+ const { json, binary } = readGLB(bytes);
7
+ const skins = json.skins;
8
+ if (skins?.length !== 1)
9
+ throw new Error('Bind validation requires exactly one skin with inverse bind matrices');
10
+ const skin = skins[0], accessor = json.accessors[skin.inverseBindMatrices];
11
+ if (!accessor || accessor.type !== 'MAT4' || accessor.componentType !== 5126 || accessor.count !== skin.joints.length)
12
+ throw new Error('Unsupported inverse bind accessor');
13
+ const view = json.bufferViews[accessor.bufferView];
14
+ const data = new DataView(binary.buffer, binary.byteOffset, binary.byteLength), result = new Map();
15
+ skin.joints.forEach((node, index) => {
16
+ const name = json.nodes[node].name;
17
+ if (!name || result.has(name))
18
+ throw new Error('Ambiguous skin joint');
19
+ const offset = (view.byteOffset ?? 0) + (accessor.byteOffset ?? 0) + index * (view.byteStride ?? 64);
20
+ const matrix = Matrix.FromArray(Array.from({ length: 16 }, (_, j) => data.getFloat32(offset + j * 4, true)));
21
+ if (!matrix.asArray().every(Number.isFinite) || Math.abs(matrix.determinant()) < 1e-12)
22
+ throw new Error('Invalid inverse bind matrix');
23
+ result.set(name, Matrix.Invert(matrix));
24
+ });
25
+ return result;
26
+ };
27
+ const a = bind(target), b = bind(source), sourceRoot = [...b.keys()].filter(name => (boneMap[name] ?? name) === rootName);
28
+ if (!a.has(rootName) || sourceRoot.length !== 1)
29
+ throw new Error('Missing/ambiguous bind root');
30
+ const inverseA = Matrix.Invert(a.get(rootName)), inverseB = Matrix.Invert(b.get(sourceRoot[0])), errors = [];
31
+ for (const [name, matrix] of b) {
32
+ const mapped = boneMap[name] ?? name, targetMatrix = a.get(mapped);
33
+ if (!targetMatrix) {
34
+ errors.push(`Missing mapped bind joint: ${name} -> ${mapped}`);
35
+ continue;
36
+ }
37
+ const qa = new Quaternion(), qb = new Quaternion(), pa = new Vector3(), pb = new Vector3(), sa = new Vector3(), sb = new Vector3();
38
+ targetMatrix.multiply(inverseA).decompose(sa, qa, pa);
39
+ matrix.multiply(inverseB).decompose(sb, qb, pb);
40
+ if (Vector3.Distance(pa, pb) > tolerance || Vector3.Distance(sa, sb) > tolerance || 1 - Math.abs(Quaternion.Dot(qa, qb)) > 1e-5)
41
+ errors.push(`Bind mismatch: ${name} -> ${mapped} (position ${Vector3.Distance(pa, pb).toFixed(5)} m)`);
42
+ }
43
+ for (const name of a.keys())
44
+ if (![...b.keys()].some(sourceName => (boneMap[sourceName] ?? sourceName) === name))
45
+ errors.push('Missing source bind joint: ' + name);
46
+ return errors;
47
+ }
@@ -0,0 +1,43 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { NullEngine, Scene, SceneLoader } from '@babylonjs/core';
3
+ import { GLTFFileLoader } from '@babylonjs/loaders/glTF/index.js';
4
+ import { suggestRig } from '../src/index.js';
5
+ import { loadProfileAssets, profileArgument, publicPath, readProfile } from './profile-assets.js';
6
+ import { compareBindRigs } from './rig.js';
7
+ const inspectAt = process.argv.indexOf('--inspect');
8
+ if (inspectAt >= 0) {
9
+ const path = process.argv[inspectAt + 1];
10
+ if (!path)
11
+ throw new Error('Provide a local GLB path after --inspect');
12
+ const engine = new NullEngine(), scene = new Scene(engine);
13
+ scene.useRightHandedSystem = true;
14
+ const hook = SceneLoader.OnPluginActivatedObservable.add(p => { if (p instanceof GLTFFileLoader)
15
+ p.skipMaterials = true; });
16
+ try {
17
+ const container = await SceneLoader.LoadAssetContainerAsync('', new Uint8Array(await readFile(path)), scene, undefined, '.glb');
18
+ const nodes = container.transformNodes.map(n => n.name);
19
+ console.log(JSON.stringify({ nodes, animations: container.animationGroups.map(g => g.name), suggestions: suggestRig(nodes), note: 'Suggestions require explicit confirmation in a profile; ambiguous roots are not selected automatically.' }, null, 2));
20
+ }
21
+ finally {
22
+ SceneLoader.OnPluginActivatedObservable.remove(hook);
23
+ scene.dispose();
24
+ engine.dispose();
25
+ }
26
+ }
27
+ else {
28
+ const profile = await readProfile(profileArgument()), assets = await loadProfileAssets(profile);
29
+ try {
30
+ if (process.argv.includes('--check-bind')) {
31
+ const target = new Uint8Array(await readFile(publicPath(profile.assetBase + profile.model)));
32
+ for (const file of new Set(profile.clips.map(c => c.file))) {
33
+ const errors = compareBindRigs(target, new Uint8Array(await readFile(publicPath(profile.assetBase + file))), profile.rig.root, profile.boneMap);
34
+ if (errors.length)
35
+ throw new Error(`${file}: incompatible bind geometry; retarget offline first\n${errors.join('\n')}`);
36
+ }
37
+ }
38
+ console.log(JSON.stringify({ profile: profile.id, valid: true, bindChecked: process.argv.includes('--check-bind'), clips: profile.clips.length, warnings: assets.warnings }, null, 2));
39
+ }
40
+ finally {
41
+ assets.dispose();
42
+ }
43
+ }
package/package.json CHANGED
@@ -1,39 +1,48 @@
1
- {
2
- "name": "@pose_flow/babylon",
3
- "version": "0.1.0",
4
- "license": "MIT",
5
- "type": "module",
6
- "files": [
7
- "dist"
8
- ],
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
13
- },
14
- "./babylon": {
15
- "types": "./dist/babylon.d.ts",
16
- "import": "./dist/babylon.js"
17
- }
18
- },
19
- "scripts": {
20
- "dev": "vite --host 127.0.0.1",
21
- "build": "tsc -p tsconfig.build.json && vite build",
22
- "typecheck": "tsc --noEmit",
23
- "test": "tsx --test tests/*.test.ts",
24
- "bake": "tsx tools/bake.ts",
25
- "validate:assets": "tsx tools/validate-assets.ts",
26
- "benchmark": "tsx tools/benchmark.ts"
27
- },
28
- "peerDependencies": {
29
- "@babylonjs/core": "^7.54.3"
30
- },
31
- "devDependencies": {
32
- "@babylonjs/core": "7.54.3",
33
- "@babylonjs/loaders": "7.54.3",
34
- "@types/node": "^22.0.0",
35
- "tsx": "^4.19.0",
36
- "typescript": "^5.7.0",
37
- "vite": "^6.0.0"
38
- }
39
- }
1
+ {
2
+ "name": "@pose_flow/babylon",
3
+ "version": "0.2.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./babylon": {
15
+ "types": "./dist/babylon.d.ts",
16
+ "import": "./dist/babylon.js"
17
+ },
18
+ "./AI_USAGE.md": "./dist/AI_USAGE.md",
19
+ "./PUBLISHING.md": "./dist/PUBLISHING.md"
20
+ },
21
+ "scripts": {
22
+ "dev": "vite --host 127.0.0.1",
23
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.offline.json && vite build && node tools/copy-docs.mjs",
24
+ "prepack": "npm run build",
25
+ "check:docs": "node tools/copy-docs.mjs --check",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "tsx --test tests/*.test.ts",
28
+ "bake": "tsx tools/bake.ts",
29
+ "validate:assets": "tsx tools/validate-assets.ts",
30
+ "benchmark": "tsx tools/benchmark.ts"
31
+ },
32
+ "peerDependencies": {
33
+ "@babylonjs/core": "^7.54.3"
34
+ },
35
+ "devDependencies": {
36
+ "@babylonjs/core": "7.54.3",
37
+ "@types/node": "^22.0.0",
38
+ "tsx": "^4.19.0",
39
+ "typescript": "^5.7.0",
40
+ "vite": "^6.0.0"
41
+ },
42
+ "bin": {
43
+ "pose-flow": "./dist/offline/tools/cli.js"
44
+ },
45
+ "dependencies": {
46
+ "@babylonjs/loaders": "7.54.3"
47
+ }
48
+ }