create-aura3d 1.1.2 → 1.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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/package.json +11 -9
- package/templates/{cartoon-channel → animation-channel}/README.md +11 -11
- package/templates/{cartoon-studio → animation-channel}/index.html +1 -1
- package/templates/{cartoon-channel → animation-channel}/package.json +2 -2
- package/templates/{cartoon-channel → animation-channel}/src/episode.ts +22 -22
- package/templates/{cartoon-channel → animation-channel}/src/experimental/README.md +5 -5
- package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.css +3 -3
- package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.ts +7 -7
- package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.css +4 -4
- package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.ts +6 -6
- package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.css +3 -3
- package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.ts +7 -7
- package/templates/{cartoon-channel → animation-channel}/src/main.ts +32 -32
- package/templates/{cartoon-channel → animation-channel}/src/render-plan.ts +13 -13
- package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.css +34 -34
- package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.ts +20 -20
- package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d.spec.ts +6 -6
- package/templates/{cartoon-channel → animation-channel}/tests/image-puppet-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/image-puppet.spec.ts +5 -5
- package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d.spec.ts +4 -4
- package/templates/{prompt-cartoon-channel → animation-channel}/tests/route-health.spec.ts +1 -1
- package/templates/{cartoon-channel → animation-channel}/tests/sample-episode-visual.spec.ts +5 -5
- package/templates/{cartoon-channel → animation-channel}/tests/storyboard-playback.spec.ts +7 -7
- package/templates/animation-studio/AGENTS.md +71 -0
- package/templates/{cartoon-studio → animation-studio}/ASSET-LICENSES.md +7 -7
- package/templates/{cartoon-studio → animation-studio}/README.md +6 -6
- package/templates/animation-studio/aura.assets.json +669 -0
- package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +10284 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
- package/templates/animation-studio/dist/generated/second-scene.document.json +1210 -0
- package/templates/animation-studio/dist/generated/studio-edited.document.json +1196 -0
- package/templates/animation-studio/dist/scene/inshader-accept.document.json +1 -0
- package/templates/animation-studio/dist/scene/inshader-test.document.json +1 -0
- package/templates/animation-studio/dist/scene/integ.document.json +1 -0
- package/templates/animation-studio/dist/scene/working.document.json +723 -0
- package/templates/animation-studio/dist/scene/working.history.json +1 -0
- package/templates/{cartoon-studio → animation-studio}/live-route.html +1 -1
- package/templates/{cartoon-studio → animation-studio}/package-lock.json +2 -2
- package/templates/animation-studio/package.json +32 -0
- package/templates/animation-studio/public/aura-assets/cast-a.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/cast-b.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/luma.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/luma2.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/miko.catalog.glb +0 -0
- package/templates/{cartoon-studio → animation-studio}/public/aura-assets/moonGarden.gltf +1 -1
- package/templates/animation-studio/public/aura-assets/mushroom.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/rusty.catalog.glb +0 -0
- package/templates/animation-studio/public/cast-library/cast-library.json +71 -0
- package/templates/animation-studio/public/clip-library/gesture.json +173903 -0
- package/templates/animation-studio/public/clip-library/idle.json +23035 -0
- package/templates/animation-studio/public/clip-library/manifest.json +154 -0
- package/templates/animation-studio/public/clip-library/nod.json +173903 -0
- package/templates/animation-studio/public/clip-library/point.json +69879 -0
- package/templates/animation-studio/public/clip-library/react.json +66084 -0
- package/templates/animation-studio/public/clip-library/run.json +9648 -0
- package/templates/animation-studio/public/clip-library/talk.json +69879 -0
- package/templates/animation-studio/public/clip-library/walk.json +8283 -0
- package/templates/animation-studio/public/hifi-cast/BrainStem.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/CesiumMan.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/RiggedFigure.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Barbarian.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Knight.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Mage.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Rogue.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Rogue_Hooded.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/khronos-report.json +295 -0
- package/templates/animation-studio/public/hifi-cast/quaternius-report.json +449 -0
- package/templates/animation-studio/public/hifi-cast/rpm/cesiumman.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm/fox.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm/riggedfigure.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm-report.json +301 -0
- package/templates/animation-studio/scripts/anim.ts +103 -0
- package/templates/animation-studio/scripts/animation-scene.ts +457 -0
- package/templates/animation-studio/scripts/asset-motion-probe.ts +450 -0
- package/templates/animation-studio/scripts/asset-render-probe.ts +152 -0
- package/templates/animation-studio/scripts/author-office-scene.ts +65 -0
- package/templates/animation-studio/scripts/build-cast-library.ts +97 -0
- package/templates/animation-studio/scripts/build-characters.ts +1543 -0
- package/templates/animation-studio/scripts/build-clip-library.ts +972 -0
- package/templates/animation-studio/scripts/clip-decision-cli.ts +51 -0
- package/templates/animation-studio/scripts/determinism-check.ts +48 -0
- package/templates/animation-studio/scripts/generate-scene.ts +95 -0
- package/templates/animation-studio/scripts/hifi-cast-validate.ts +572 -0
- package/templates/animation-studio/scripts/motion-evidence-cli.ts +150 -0
- package/templates/animation-studio/scripts/motion-evidence.ts +362 -0
- package/templates/animation-studio/scripts/preview-server.ts +52 -0
- package/templates/animation-studio/scripts/regen-manifest.ts +156 -0
- package/templates/animation-studio/scripts/render-core.ts +360 -0
- package/templates/animation-studio/scripts/render-live.ts +813 -0
- package/templates/animation-studio/scripts/render-parallel.ts +70 -0
- package/templates/animation-studio/scripts/render-server.ts +119 -0
- package/templates/animation-studio/scripts/resolve-asset.ts +669 -0
- package/templates/animation-studio/scripts/skeleton-overlay-cli.ts +82 -0
- package/templates/animation-studio/scripts/skeleton-overlay.ts +293 -0
- package/templates/animation-studio/scripts/studio-tools-demo.ts +45 -0
- package/templates/animation-studio/scripts/texture-bake.ts +297 -0
- package/templates/{cartoon-studio → animation-studio}/scripts/validate-characters.ts +0 -0
- package/templates/animation-studio/scripts/verify-extracted-overlay-cli.ts +264 -0
- package/templates/animation-studio/src/animation-episode-validator.ts +97 -0
- package/templates/animation-studio/src/animation-performance.ts +556 -0
- package/templates/animation-studio/src/aura-assets.ts +71 -0
- package/templates/animation-studio/src/character.ts +40 -0
- package/templates/animation-studio/src/director/compile-episode-document.ts +45 -0
- package/templates/animation-studio/src/director/director-heuristics.ts +697 -0
- package/templates/animation-studio/src/director/prompt-to-scene.ts +369 -0
- package/templates/animation-studio/src/empty-document.ts +77 -0
- package/templates/animation-studio/src/episode-document.ts +442 -0
- package/templates/animation-studio/src/examples/moon-garden.example.ts +183 -0
- package/templates/animation-studio/src/fidelity.ts +129 -0
- package/templates/animation-studio/src/graph.ts +14 -0
- package/templates/animation-studio/src/procedural-hdri.ts +243 -0
- package/templates/animation-studio/src/profile.ts +97 -0
- package/templates/animation-studio/src/render-live-route.ts +42 -0
- package/templates/animation-studio/src/render-modes.ts +137 -0
- package/templates/animation-studio/src/scene-player.ts +867 -0
- package/templates/animation-studio/src/set-templates.ts +361 -0
- package/templates/animation-studio/src/studio/episode-document-store.ts +72 -0
- package/templates/animation-studio/src/studio/scene-tool-registry.ts +86 -0
- package/templates/animation-studio/src/studio/scene-tools.ts +169 -0
- package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +938 -0
- package/templates/animation-studio/tsconfig.json +26 -0
- package/templates/{cartoon-studio → animation-studio}/vite.config.ts +6 -1
- package/templates/character-controller/README.md +33 -0
- package/templates/character-controller/index.html +19 -0
- package/templates/character-controller/package.json +23 -0
- package/templates/character-controller/playwright.config.ts +14 -0
- package/templates/character-controller/src/controller.ts +44 -0
- package/templates/character-controller/src/main.ts +62 -0
- package/templates/character-controller/tests/route-health.spec.ts +26 -0
- package/templates/character-controller/tests/screenshot.spec.ts +9 -0
- package/templates/character-controller/tsconfig.json +15 -0
- package/templates/cinematic-scene/package.json +1 -1
- package/templates/episode-builder/README.md +4 -4
- package/templates/episode-builder/package.json +1 -1
- package/templates/episode-builder/src/aura-assets.ts +1 -1
- package/templates/episode-builder/src/builder.ts +4 -4
- package/templates/episode-builder/src/episode.ts +21 -21
- package/templates/episode-builder/src/main.ts +20 -20
- package/templates/episode-builder/src/render-plan.ts +13 -13
- package/templates/episode-builder/tests/storyboard-playback.spec.ts +7 -7
- package/templates/fighting-game/package.json +1 -1
- package/templates/mini-game/package.json +1 -1
- package/templates/product-viewer/package.json +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/README.md +5 -5
- package/templates/{cartoon-channel → prompt-animation-channel}/index.html +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/package.json +2 -2
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/aura-assets.ts +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/episode.ts +22 -22
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/main.ts +28 -28
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/render-plan.ts +13 -13
- package/templates/{cartoon-channel → prompt-animation-channel}/tests/route-health.spec.ts +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tests/storyboard-playback.spec.ts +8 -8
- package/templates/cartoon-studio/aura.assets.json +0 -400
- package/templates/cartoon-studio/package.json +0 -33
- package/templates/cartoon-studio/public/aura-assets/luma.authored.glb +0 -0
- package/templates/cartoon-studio/public/aura-assets/miko.authored.glb +0 -0
- package/templates/cartoon-studio/scripts/build-characters.ts +0 -738
- package/templates/cartoon-studio/scripts/build-dialogue-audio.ts +0 -262
- package/templates/cartoon-studio/scripts/episode.ts +0 -323
- package/templates/cartoon-studio/scripts/render-live.ts +0 -873
- package/templates/cartoon-studio/src/aura-assets.ts +0 -64
- package/templates/cartoon-studio/src/characters.ts +0 -65
- package/templates/cartoon-studio/src/episode-renderer.ts +0 -605
- package/templates/cartoon-studio/src/episode.ts +0 -306
- package/templates/cartoon-studio/src/main.ts +0 -400
- package/templates/cartoon-studio/src/render-live-route.ts +0 -913
- package/templates/cartoon-studio/src/render-plan.ts +0 -567
- package/templates/cartoon-studio/src/review.ts +0 -99
- package/templates/cartoon-studio/src/studio.ts +0 -101
- package/templates/cartoon-studio/test-results/.last-run.json +0 -4
- package/templates/cartoon-studio/tests/episode-render.spec.ts +0 -44
- package/templates/cartoon-studio/tests/motion-quality.spec.ts +0 -45
- package/templates/cartoon-studio/tests/route-health.spec.ts +0 -7
- package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +0 -99
- package/templates/cartoon-studio/tests/visual-review.spec.ts +0 -21
- package/templates/cartoon-studio/tsconfig.json +0 -14
- package/templates/prompt-cartoon-channel/index.html +0 -13
- package/templates/prompt-cartoon-channel/src/sets.ts +0 -14
- /package/templates/{cartoon-channel → animation-channel}/aura.assets.json +0 -0
- /package/templates/{cartoon-channel → animation-channel}/playwright.config.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.047f5e5f.glb +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.thumb.svg +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.047f5e5f.glb +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.thumb.svg +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/moon-garden-feature-frame.png +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/aura-assets.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/characters.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/contract.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/sets.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/test-results/.last-run.json +0 -0
- /package/templates/{cartoon-channel → animation-channel}/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
- /package/templates/{cartoon-channel → animation-channel}/tsconfig.json +0 -0
- /package/templates/{cartoon-studio → animation-studio}/playwright.config.ts +0 -0
- /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/luma.thumb.svg +0 -0
- /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/miko.thumb.svg +0 -0
- /package/templates/{cartoon-studio → animation-studio}/src/contract.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/playwright.config.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/characters.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/contract.ts +0 -0
- /package/templates/{cartoon-studio → prompt-animation-channel}/src/sets.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tsconfig.json +0 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* animation-performance.ts — the Phase 2.5 KEYSTONE helper for `scene-player.ts`.
|
|
3
|
+
*
|
|
4
|
+
* It turns the SHARED, rig-neutral animation library (`@aura3d/animation`'s standard humanoid
|
|
5
|
+
* clip registry + performance state graph) into per-frame poses that drive ANY character GLB:
|
|
6
|
+
*
|
|
7
|
+
* beat clip intent ──▶ performance state graph ──▶ sample standard library clip
|
|
8
|
+
* │ (rig-neutral AnimationPose)
|
|
9
|
+
* ▼
|
|
10
|
+
* retargetHumanoidPose(pose, map) (→ GLB node names)
|
|
11
|
+
* │
|
|
12
|
+
* ▼
|
|
13
|
+
* actor.applyRetargetedPose(pose)
|
|
14
|
+
*
|
|
15
|
+
* The library + retargeting come from the FRESHLY-BUILT monorepo `dist/animation`. The template's
|
|
16
|
+
* own published `node_modules/@aura3d/animation` predates Phase 2.4/2.5 (no clip library / rig
|
|
17
|
+
* inference / state graph), and the render route only aliases a fixed list of `@aura3d/*` specifiers
|
|
18
|
+
* — NOT `@aura3d/animation`. So we import the in-repo build by relative path (the same monorepo dist
|
|
19
|
+
* the rest of the route is validated against; vite's workspace fs.allow covers it).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
23
|
+
// @ts-ignore — resolved at runtime to the freshly-built monorepo dist (has co-located .d.ts).
|
|
24
|
+
import {
|
|
25
|
+
inferHumanoidRig,
|
|
26
|
+
createHumanoidRetargetingMap,
|
|
27
|
+
retargetHumanoidPose,
|
|
28
|
+
createStandardHumanoidClipRegistry,
|
|
29
|
+
createStandardHumanoidClipDefinitions,
|
|
30
|
+
createPerformanceStateGraph,
|
|
31
|
+
AnimationClipRegistry,
|
|
32
|
+
AnimationTrack,
|
|
33
|
+
STANDARD_LIBRARY_RIG,
|
|
34
|
+
STANDARD_CLIP_IDS,
|
|
35
|
+
PERFORMANCE_GRAPH_PARAMETERS
|
|
36
|
+
} from "@aura3d/animation";
|
|
37
|
+
|
|
38
|
+
type StandardClipId = (typeof STANDARD_CLIP_IDS)[number];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* B1/B3 motion-source provenance. `extracted` = a real catalog-mocap clip from the
|
|
42
|
+
* universal `public/clip-library/<intent>.json`; `procedural` = the standard-library
|
|
43
|
+
* synthesized clip; `embedded` = a rich GLB clip shipped by the character itself;
|
|
44
|
+
* `idle-fallback` = the last-resort idle clip (no usable motion for the intent).
|
|
45
|
+
*/
|
|
46
|
+
export type ClipSource = "extracted" | "procedural" | "embedded" | "idle-fallback";
|
|
47
|
+
|
|
48
|
+
/** Canonical body bones a downstream gate measures (EXCLUDING mouth morph + caption + camera). */
|
|
49
|
+
export const BODY_BONES = [
|
|
50
|
+
"hips",
|
|
51
|
+
"spine",
|
|
52
|
+
"chest",
|
|
53
|
+
"upperChest",
|
|
54
|
+
"neck",
|
|
55
|
+
"head",
|
|
56
|
+
"leftShoulder",
|
|
57
|
+
"leftUpperArm",
|
|
58
|
+
"leftLowerArm",
|
|
59
|
+
"rightShoulder",
|
|
60
|
+
"rightUpperArm",
|
|
61
|
+
"rightLowerArm"
|
|
62
|
+
] as const;
|
|
63
|
+
export type BodyBoneName = (typeof BODY_BONES)[number];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One character/one-beat clip-decision record (B1). It is emitted per-frame from the
|
|
67
|
+
* performance rig and bubbled into the route seek proof + the render summary so a
|
|
68
|
+
* downstream gate can prove the BODY moves (not just the mouth morph).
|
|
69
|
+
*/
|
|
70
|
+
export interface ClipDecision {
|
|
71
|
+
/** The standard intent that was requested for this beat. */
|
|
72
|
+
readonly intent: StandardClipId;
|
|
73
|
+
/** The clip id actually sampled (may differ from intent when the graph auto-returns to idle). */
|
|
74
|
+
readonly clipId: StandardClipId;
|
|
75
|
+
/** Where the sampled motion came from (extracted catalog / procedural / embedded / idle fallback). */
|
|
76
|
+
readonly source: ClipSource;
|
|
77
|
+
/** Count of canonical body bones whose rotation amplitude this frame exceeds a tiny epsilon. */
|
|
78
|
+
readonly bonesTouched: number;
|
|
79
|
+
/** Largest per-body-bone rotation amplitude (radians) applied this frame. */
|
|
80
|
+
readonly maxRotAmplitudeRad: number;
|
|
81
|
+
/**
|
|
82
|
+
* Largest per-body-bone TRANSLATION amplitude (metres) applied this frame across ALL sampled bones
|
|
83
|
+
* (not just the hips root) — a body bone that physically displaces (weight shift, hip bob, hand
|
|
84
|
+
* reach) shows up here even when its rotation is small. B1 contract field.
|
|
85
|
+
*/
|
|
86
|
+
readonly maxTransAmplitude: number;
|
|
87
|
+
/** Per-body-bone rotation amplitude (radians) keyed by canonical bone (gate input, mouth-free). */
|
|
88
|
+
readonly bodyBoneRotationRad: Partial<Record<BodyBoneName, number>>;
|
|
89
|
+
/** Root (hips) translation magnitude applied this frame (locomotion / weight shift). */
|
|
90
|
+
readonly rootTranslation: number;
|
|
91
|
+
/**
|
|
92
|
+
* B1 — did this decision's pose actually reach the GLB skeleton at runtime? The library pose only
|
|
93
|
+
* proves the LIBRARY moved; this is set true by `scene-player.ts` once the retargeted pose (or an
|
|
94
|
+
* embedded clip) was applied to ≥1 real skinned skeleton node (`applyRetargetedPose` /
|
|
95
|
+
* `playClip` reported tracks applied). `poseFor` emits it `false`; the player overwrites it.
|
|
96
|
+
*/
|
|
97
|
+
readonly reachedGLBRuntime: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The 8 standard performance ids (idle/talk/gesture/point/nod/walk/run/react). */
|
|
101
|
+
export const STANDARD_INTENTS = STANDARD_CLIP_IDS as readonly StandardClipId[];
|
|
102
|
+
|
|
103
|
+
const INTENT_SET = new Set<string>(STANDARD_INTENTS);
|
|
104
|
+
|
|
105
|
+
/** Loop-vs-one-shot per standard clip (one-shots auto-return to idle in the graph). */
|
|
106
|
+
const ONE_SHOT = new Set<StandardClipId>(["gesture", "point", "nod", "react"]);
|
|
107
|
+
|
|
108
|
+
/** Plain pose transform shapes (match `@aura3d/animation`'s AnimationPose). */
|
|
109
|
+
interface PoseTransform {
|
|
110
|
+
position?: { x: number; y: number; z: number };
|
|
111
|
+
rotation?: { x: number; y: number; z: number; w: number };
|
|
112
|
+
}
|
|
113
|
+
interface SamplablePose {
|
|
114
|
+
bones: Record<string, PoseTransform>;
|
|
115
|
+
morphTargets?: Record<string, number>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface SamplableTrack {
|
|
119
|
+
readonly target: string;
|
|
120
|
+
readonly valueType: string;
|
|
121
|
+
sample(time: number): readonly number[];
|
|
122
|
+
}
|
|
123
|
+
interface LibraryClip {
|
|
124
|
+
readonly id: StandardClipId;
|
|
125
|
+
readonly duration: number;
|
|
126
|
+
readonly loop: boolean;
|
|
127
|
+
readonly tracks: readonly SamplableTrack[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Map an episode beat's clip field + motion state to one of the standard performance intents.
|
|
132
|
+
*
|
|
133
|
+
* - A director-built document already emits a STANDARD intent (`talk`, `react`, `walk`, …) — use it.
|
|
134
|
+
* - An AUTHORED document (e.g. Moon Garden) carries GLB-clip placeholder names (`Loops`, `Run`,
|
|
135
|
+
* `Idle`) that are NOT standard intents — derive the intent from speaking/movement so those
|
|
136
|
+
* characters still perform from the shared library instead of looping one embedded clip.
|
|
137
|
+
* - Movement always wins (walk/run) so a translating character animates its legs.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveIntent(input: {
|
|
140
|
+
readonly clip: string;
|
|
141
|
+
readonly moving: boolean;
|
|
142
|
+
readonly running: boolean;
|
|
143
|
+
readonly speaking: boolean;
|
|
144
|
+
readonly anyDialogue: boolean;
|
|
145
|
+
}): StandardClipId {
|
|
146
|
+
// Locomotion is VELOCITY-GATED, never name-gated: legs cycle ONLY while the root is actually
|
|
147
|
+
// translating. `moving`/`running` come from the per-frame waypoint velocity (sampleBlocking).
|
|
148
|
+
if (input.running) return "run";
|
|
149
|
+
if (input.moving) return "walk";
|
|
150
|
+
// Stationary now. A shot whose authored clip is walk/run must NOT keep walking in place once the
|
|
151
|
+
// character has reached its mark — that is the treadmill/moonwalk bug. So when stopped, IGNORE a
|
|
152
|
+
// locomotion clip name and fall through to the performance/idle state; honor only a non-locomotion
|
|
153
|
+
// performance clip name (gesture/point/nod/react/talk/idle).
|
|
154
|
+
const raw = input.clip.trim().toLowerCase();
|
|
155
|
+
const isLocomotion = raw === "walk" || raw === "run" || /run|sprint|walk|move|loco|jog|stride/.test(raw);
|
|
156
|
+
if (!isLocomotion) {
|
|
157
|
+
if (INTENT_SET.has(raw)) return raw as StandardClipId;
|
|
158
|
+
// Heuristic mapping for common embedded/authored names → standard intent.
|
|
159
|
+
if (/wave|gesture|emote/.test(raw)) return "gesture";
|
|
160
|
+
if (/point/.test(raw)) return "point";
|
|
161
|
+
if (/nod|agree/.test(raw)) return "nod";
|
|
162
|
+
if (/react|surprise|recoil/.test(raw)) return "react";
|
|
163
|
+
if (/talk|speak|dialog/.test(raw)) return "talk";
|
|
164
|
+
}
|
|
165
|
+
// Locomotion clip but stopped, or no usable name — fall back to the dialogue state.
|
|
166
|
+
if (input.speaking) return "talk";
|
|
167
|
+
if (input.anyDialogue) return "react";
|
|
168
|
+
return "idle";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** One character's shared-library performance driver (state graph + retarget map + clips). */
|
|
172
|
+
export interface PerformanceRig {
|
|
173
|
+
/** GLB node names this rig will write to (used to build a clean identity base each frame). */
|
|
174
|
+
readonly targetNodeNames: readonly string[];
|
|
175
|
+
/** Retarget map coverage (standard rig → this GLB rig); low coverage = sparse skeleton. */
|
|
176
|
+
readonly coverage: number;
|
|
177
|
+
/** True when the GLB's own clips are rich enough to be preferred over retargeting (fallback rule). */
|
|
178
|
+
readonly preferEmbedded: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Produce the GLB-node-keyed pose for `intent` at episode time `t`. Returns a pose whose `bones`
|
|
181
|
+
* keys are the GLB's node names — feed straight into `actor.applyRetargetedPose(pose)` — plus a
|
|
182
|
+
* B1 {@link ClipDecision} measuring the body-bone motion this frame (mouth/caption/camera-free).
|
|
183
|
+
*/
|
|
184
|
+
poseFor(intent: StandardClipId, t: number, morph?: Record<string, number>): {
|
|
185
|
+
readonly pose: SamplablePose;
|
|
186
|
+
readonly decision: ClipDecision;
|
|
187
|
+
};
|
|
188
|
+
/** Current state-graph state name (for the proof / debugging). */
|
|
189
|
+
readonly state: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface PerformanceRigOptions {
|
|
193
|
+
/** Flat list of the GLB's skeleton node names (for rig inference). */
|
|
194
|
+
readonly nodeNames: readonly string[];
|
|
195
|
+
/** The GLB's own embedded clip names (used by the fallback rule). */
|
|
196
|
+
readonly embeddedClips: readonly string[];
|
|
197
|
+
/** B3 per-intent motion source (extracted catalog vs procedural) from {@link loadSharedClipLibrary}. */
|
|
198
|
+
readonly sources?: Readonly<Record<StandardClipId, "extracted" | "procedural">>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Build the (procedural) shared clip registry once and reuse across characters. */
|
|
202
|
+
export function createSharedClipRegistry(): ReturnType<typeof createStandardHumanoidClipRegistry> {
|
|
203
|
+
return createStandardHumanoidClipRegistry();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A shared registry plus the resolved motion SOURCE per standard intent (B3). */
|
|
207
|
+
export interface SharedClipLibrary {
|
|
208
|
+
readonly registry: ReturnType<typeof createStandardHumanoidClipRegistry>;
|
|
209
|
+
/** Per-intent: did the first-choice EXTRACTED catalog clip load, or did we fall to procedural? */
|
|
210
|
+
readonly sources: Readonly<Record<StandardClipId, "extracted" | "procedural">>;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Shape of a `public/clip-library/<id>.json` file (mirrors build-clip-library.ts serialization). */
|
|
214
|
+
interface SerializedClipFile {
|
|
215
|
+
readonly id: StandardClipId;
|
|
216
|
+
readonly name: string;
|
|
217
|
+
readonly duration: number;
|
|
218
|
+
readonly frameRate: number;
|
|
219
|
+
readonly loop: boolean;
|
|
220
|
+
readonly tags?: readonly string[];
|
|
221
|
+
readonly source?: string;
|
|
222
|
+
readonly rig?: string;
|
|
223
|
+
readonly tracks: { target: string; valueType: string; keyframes: unknown[] }[];
|
|
224
|
+
readonly provenance?: { source?: string };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* B3 — load the shared clip registry with the EXTRACTED universal catalog library as the
|
|
229
|
+
* DEFAULT first choice. Mirrors `loadExtractedClipLibrary` from `scripts/build-clip-library.ts`,
|
|
230
|
+
* but uses `fetch` (the player runs in the browser, where `node:fs` is unavailable) to read the
|
|
231
|
+
* SAME `public/clip-library/<intent>.json` files that script writes. We start from the procedural
|
|
232
|
+
* standard clips and OVERRIDE each intent with its real extracted catalog clip when one exists;
|
|
233
|
+
* intents with no usable extracted clip keep the procedural baseline (and are logged as such).
|
|
234
|
+
*
|
|
235
|
+
* Fallback order per intent (logged): extracted catalog clip → procedural standard clip.
|
|
236
|
+
* (The embedded-GLB and idle fallbacks live at the per-character decision layer in `poseFor`.)
|
|
237
|
+
*/
|
|
238
|
+
export async function loadSharedClipLibrary(
|
|
239
|
+
baseUrl = "/clip-library"
|
|
240
|
+
): Promise<SharedClipLibrary> {
|
|
241
|
+
const registry = new AnimationClipRegistry(createStandardHumanoidClipDefinitions());
|
|
242
|
+
const sources: Record<StandardClipId, "extracted" | "procedural"> = Object.create(null);
|
|
243
|
+
for (const id of STANDARD_INTENTS) sources[id] = "procedural";
|
|
244
|
+
|
|
245
|
+
// MOTION-SOURCE POLICY (render-verified, TWICE). The extracted catalog mocap DISTORTS the real
|
|
246
|
+
// 21-joint character rig: with legs it removes/collapses them; even "upper-body only" it stretches
|
|
247
|
+
// the arms to the floor and compresses the lower body. Both failures were only caught in a real
|
|
248
|
+
// render (FK overlays + numeric checks passed). So the DEFAULT is the PROCEDURAL baseline — proven
|
|
249
|
+
// to render clean: natural standing, normal arm length, full intact legs. Extracted is opt-in for
|
|
250
|
+
// future per-rig validation only.
|
|
251
|
+
// - default (off) → pure procedural baseline (clean, stable) ← DEFAULT
|
|
252
|
+
// - AURA_EXTRACTED=upper-body → extracted upper-body + stripped legs (distorts arms; validation only)
|
|
253
|
+
// - AURA_EXTRACTED=full → keep leg tracks too (breaks legs; validation only)
|
|
254
|
+
const extractedMode = (typeof process !== "undefined" && process.env?.AURA_EXTRACTED) || "off";
|
|
255
|
+
if (extractedMode === "off") {
|
|
256
|
+
console.info("[animation-performance] extracted clips OFF → procedural baseline (clean rig; mocap distorts this rig).");
|
|
257
|
+
return { registry, sources };
|
|
258
|
+
}
|
|
259
|
+
const STRIP_LEGS = extractedMode !== "full";
|
|
260
|
+
const LEG_BONE = /(leg|knee|foot|toe|ankle|thigh|shin)/i;
|
|
261
|
+
|
|
262
|
+
await Promise.all(
|
|
263
|
+
STANDARD_INTENTS.map(async (id) => {
|
|
264
|
+
try {
|
|
265
|
+
// Locomotion (walk/run) IS the legs — stripping their leg tracks would leave the character
|
|
266
|
+
// gliding with no step. Keep the PROCEDURAL walk/run (real, stable leg cycle); use extracted
|
|
267
|
+
// only for the upper-body-dominant intents (talk/gesture/nod/point/react/idle).
|
|
268
|
+
if (STRIP_LEGS && (id === "walk" || id === "run")) {
|
|
269
|
+
console.info(`[animation-performance] clip-library/${id}.json → procedural (locomotion keeps its leg cycle).`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const res = await fetch(`${baseUrl}/${id}.json`, { cache: "no-cache" });
|
|
273
|
+
if (!res.ok) {
|
|
274
|
+
console.warn(`[animation-performance] clip-library/${id}.json HTTP ${res.status} → procedural baseline.`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const file = (await res.json()) as SerializedClipFile;
|
|
278
|
+
// Only override with a REAL extracted catalog clip; procedural-fallback files are identical
|
|
279
|
+
// to the baseline already registered, so registering them would just be noise.
|
|
280
|
+
if (file.provenance?.source !== "catalog-extracted") {
|
|
281
|
+
console.warn(`[animation-performance] clip-library/${id}.json is "${file.provenance?.source ?? "unknown"}" (not catalog-extracted) → procedural baseline.`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
// SANITY GATE: reject extracted clips whose root (hips) motion is in raw/un-normalized
|
|
285
|
+
// units or carries a collapsing static offset — those displace the character to the floor.
|
|
286
|
+
// Clean clips on the standard rig (meters) keep hips translation ≲1m and hips rotation
|
|
287
|
+
// near identity. Until the extraction pipeline normalizes units + rest pose, fall back to
|
|
288
|
+
// the (cranked) procedural baseline for any clip that fails this — so the default is never broken.
|
|
289
|
+
{
|
|
290
|
+
let maxTrans = 0;
|
|
291
|
+
let maxHipsRotOffset = 0;
|
|
292
|
+
// A clip with a baked axis-conversion rest offset (e.g. a constant 90° head/limb
|
|
293
|
+
// rotation from a Z-up source) is INVISIBLE to a range-based check — the bone never
|
|
294
|
+
// moves, it just sits rotated, and retargeting bakes that 90° onto the target rig so
|
|
295
|
+
// the character renders laid-down / contorted. Detect it by the bone that stays
|
|
296
|
+
// FARTHEST from rest the whole clip: for each rotation track take the MIN offset from
|
|
297
|
+
// identity across keyframes (the closest it ever gets to rest); genuine motion passes
|
|
298
|
+
// near rest, a baked offset never does. Reject if any bone's floor exceeds the limit.
|
|
299
|
+
const REST_FLOOR_LIMIT = 0.15; // ≈ a bone pinned ≥64° from rest for the entire clip
|
|
300
|
+
let worstRestFloor = 0;
|
|
301
|
+
let worstRestBone = "";
|
|
302
|
+
for (const tr of file.tracks) {
|
|
303
|
+
const isHipsT = /(^|[.\/])hips\.translation$/i.test(tr.target);
|
|
304
|
+
const isRot = /\.rotation$/i.test(tr.target);
|
|
305
|
+
const isHipsR = /(^|[.\/])hips\.rotation$/i.test(tr.target);
|
|
306
|
+
if (isHipsT) {
|
|
307
|
+
for (const kf of tr.keyframes as { value: number[] }[])
|
|
308
|
+
for (const c of kf.value ?? []) maxTrans = Math.max(maxTrans, Math.abs(c));
|
|
309
|
+
}
|
|
310
|
+
if (isRot) {
|
|
311
|
+
let restFloor = Infinity;
|
|
312
|
+
for (const kf of tr.keyframes as { value: number[] }[]) {
|
|
313
|
+
const v = kf.value ?? [];
|
|
314
|
+
if (v.length < 4) continue;
|
|
315
|
+
const offset = 1 - Math.abs(v[3]!); // |w| far from 1 ⇒ large rotation
|
|
316
|
+
restFloor = Math.min(restFloor, offset);
|
|
317
|
+
if (isHipsR) maxHipsRotOffset = Math.max(maxHipsRotOffset, offset);
|
|
318
|
+
}
|
|
319
|
+
if (restFloor !== Infinity && restFloor > worstRestFloor) {
|
|
320
|
+
worstRestFloor = restFloor;
|
|
321
|
+
worstRestBone = tr.target;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (maxTrans > 1.5 || maxHipsRotOffset > 0.25 || worstRestFloor > REST_FLOOR_LIMIT) {
|
|
326
|
+
console.warn(
|
|
327
|
+
`[animation-performance] clip-library/${id}.json rejected (hips trans ${maxTrans.toFixed(2)} / hips rotOffset ${maxHipsRotOffset.toFixed(2)} / ${worstRestBone || "—"} rest-floor ${worstRestFloor.toFixed(2)} — un-normalized or baked rest offset) → procedural baseline.`
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Strip leg/foot/toe tracks by default so the extracted mocap drives only the upper body;
|
|
333
|
+
// the legs stay at the clean rest pose (built fresh each frame), which keeps them STABLE on
|
|
334
|
+
// the real character rig. (Hips translation is also a leg-adjacent root signal — drop it too.)
|
|
335
|
+
const usableTracks = STRIP_LEGS
|
|
336
|
+
? file.tracks.filter((tr) => !LEG_BONE.test(tr.target) && !/hips\.translation$/i.test(tr.target))
|
|
337
|
+
: file.tracks;
|
|
338
|
+
const tracks = usableTracks.map(
|
|
339
|
+
(tr) =>
|
|
340
|
+
new AnimationTrack({
|
|
341
|
+
target: tr.target,
|
|
342
|
+
valueType: tr.valueType as never,
|
|
343
|
+
keyframes: tr.keyframes as never
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
registry.register(
|
|
347
|
+
{
|
|
348
|
+
id: file.id,
|
|
349
|
+
name: file.name ?? file.id,
|
|
350
|
+
duration: file.duration,
|
|
351
|
+
frameRate: file.frameRate ?? 30,
|
|
352
|
+
loop: file.loop ?? true,
|
|
353
|
+
tags: file.tags ?? [file.id, "performance", "extracted"],
|
|
354
|
+
source: file.source ?? "aura3d.catalog-extracted",
|
|
355
|
+
tracks: tracks as never,
|
|
356
|
+
metadata: { rig: file.rig ?? STANDARD_LIBRARY_RIG.id, extracted: true }
|
|
357
|
+
} as never,
|
|
358
|
+
{ replace: true }
|
|
359
|
+
);
|
|
360
|
+
sources[id] = "extracted";
|
|
361
|
+
console.info(`[animation-performance] clip-library/${id}.json → EXTRACTED catalog clip loaded (${tracks.length} tracks, ${file.duration.toFixed?.(2) ?? file.duration}s).`);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
console.warn(`[animation-performance] clip-library/${id}.json fetch failed (${(err as Error).message}) → procedural baseline.`);
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const extractedCount = STANDARD_INTENTS.filter((id) => sources[id] === "extracted").length;
|
|
369
|
+
console.info(`[animation-performance] shared clip library: ${extractedCount}/${STANDARD_INTENTS.length} intents from EXTRACTED universal catalog, rest procedural.`);
|
|
370
|
+
return { registry, sources };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Build a {@link PerformanceRig} for one character: infer its humanoid rig from its GLB node names,
|
|
375
|
+
* build a retarget map from the standard library rig onto it, and hold a performance state graph.
|
|
376
|
+
*
|
|
377
|
+
* Fallback rule (Phase 2.5): if the GLB ships rich, named, varied clips of its own (more than one
|
|
378
|
+
* usable motion clip beyond a single idle), we mark `preferEmbedded` so the caller plays the
|
|
379
|
+
* embedded clip whenever the intent matches one. Sparse rigs (one idle, or a skeleton too minimal
|
|
380
|
+
* to retarget well) ALWAYS get retargeted library motion.
|
|
381
|
+
*/
|
|
382
|
+
export function createPerformanceRig(
|
|
383
|
+
registry: ReturnType<typeof createStandardHumanoidClipRegistry>,
|
|
384
|
+
options: PerformanceRigOptions
|
|
385
|
+
): PerformanceRig {
|
|
386
|
+
const rig = inferHumanoidRig(options.nodeNames);
|
|
387
|
+
const map = createHumanoidRetargetingMap(STANDARD_LIBRARY_RIG, rig);
|
|
388
|
+
const targetNodeNames = Object.values(map.bindings)
|
|
389
|
+
.map((b: { target: { name: string } }) => b.target.name)
|
|
390
|
+
.filter((n): n is string => typeof n === "string" && n.length > 0);
|
|
391
|
+
|
|
392
|
+
const graph = createPerformanceStateGraph();
|
|
393
|
+
|
|
394
|
+
// Cache sampled clips by id (registry clips are immutable).
|
|
395
|
+
const clipCache = new Map<StandardClipId, LibraryClip>();
|
|
396
|
+
const getClip = (id: StandardClipId): LibraryClip | undefined => {
|
|
397
|
+
const cached = clipCache.get(id);
|
|
398
|
+
if (cached) return cached;
|
|
399
|
+
const clip = (registry.get?.(id) ?? registry.require?.(id)) as unknown as LibraryClip | undefined;
|
|
400
|
+
if (clip) clipCache.set(id, clip);
|
|
401
|
+
return clip;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// Build an identity rest transform for every target node so each frame starts clean — this
|
|
405
|
+
// prevents a bone touched by a previous intent (e.g. an arm raised in `talk`) from freezing when
|
|
406
|
+
// the next intent (e.g. `idle`) doesn't move it.
|
|
407
|
+
const identityBase = (): Record<string, PoseTransform> => {
|
|
408
|
+
const bones: Record<string, PoseTransform> = {};
|
|
409
|
+
for (const node of targetNodeNames) bones[node] = { rotation: { x: 0, y: 0, z: 0, w: 1 } };
|
|
410
|
+
return bones;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Body-bone amplitude is measured on the rig-neutral (canonical) source pose BEFORE retargeting,
|
|
414
|
+
// so the numbers describe the LIBRARY motion (mouth-morph/caption/camera-free) regardless of how
|
|
415
|
+
// sparsely the target GLB maps. A quaternion's rotation magnitude is 2*acos(|w|).
|
|
416
|
+
const rotationAmplitude = (rot?: { x: number; y: number; z: number; w: number }): number => {
|
|
417
|
+
if (!rot) return 0;
|
|
418
|
+
const w = Math.min(1, Math.abs(rot.w));
|
|
419
|
+
return 2 * Math.acos(w);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
let lastTime = 0;
|
|
423
|
+
let lastState = graph.currentState as string;
|
|
424
|
+
|
|
425
|
+
const poseFor = (
|
|
426
|
+
intent: StandardClipId,
|
|
427
|
+
t: number,
|
|
428
|
+
morph?: Record<string, number>
|
|
429
|
+
): { readonly pose: SamplablePose; readonly decision: ClipDecision } => {
|
|
430
|
+
// Drive the state graph deterministically from the resolved intent. We set every parameter so
|
|
431
|
+
// the graph crossfades into the requested state (one-shots are pulsed; loops are held).
|
|
432
|
+
const P = PERFORMANCE_GRAPH_PARAMETERS as Record<string, string>;
|
|
433
|
+
graph.setParameter(P.talk, intent === "talk");
|
|
434
|
+
graph.setParameter(P.walk, intent === "walk" || intent === "run");
|
|
435
|
+
graph.setParameter(P.run, intent === "run");
|
|
436
|
+
for (const oneShot of ["gesture", "point", "nod", "react"] as const) {
|
|
437
|
+
graph.setParameter(P[oneShot], intent === oneShot);
|
|
438
|
+
}
|
|
439
|
+
const delta = Math.max(0, t - lastTime);
|
|
440
|
+
lastTime = t;
|
|
441
|
+
graph.update(delta);
|
|
442
|
+
lastState = graph.currentState as string;
|
|
443
|
+
|
|
444
|
+
// The graph may have auto-returned a one-shot to idle; honor the requested intent for sampling
|
|
445
|
+
// so a beat that asks for `gesture` actually plays gesture (the graph still governs transitions
|
|
446
|
+
// for proof/state). For one-shots we sample the requested clip from its local start; for loops
|
|
447
|
+
// we sample at the wrapped episode time so idle/talk/walk cycle continuously.
|
|
448
|
+
const clipId: StandardClipId = INTENT_SET.has(graph.currentState) && !ONE_SHOT.has(intent)
|
|
449
|
+
? (graph.currentState as StandardClipId)
|
|
450
|
+
: intent;
|
|
451
|
+
|
|
452
|
+
// Sampling fallback order (logged via the decision.source): the requested clip, else idle.
|
|
453
|
+
let sampledId: StandardClipId = clipId;
|
|
454
|
+
let clip = getClip(clipId);
|
|
455
|
+
if (!clip) {
|
|
456
|
+
sampledId = "idle";
|
|
457
|
+
clip = getClip("idle");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const bones = identityBase();
|
|
461
|
+
const bodyBoneRotationRad: Partial<Record<BodyBoneName, number>> = {};
|
|
462
|
+
let rootTranslation = 0;
|
|
463
|
+
let maxTransAmplitude = 0;
|
|
464
|
+
if (clip) {
|
|
465
|
+
const duration = clip.duration > 0 ? clip.duration : 1;
|
|
466
|
+
const local = clip.loop ? t % duration : Math.min(t, duration);
|
|
467
|
+
// Sample the standard (rig-neutral) clip into an AnimationPose keyed by canonical bone names.
|
|
468
|
+
const sourceBones: Record<string, PoseTransform> = {};
|
|
469
|
+
for (const track of clip.tracks) {
|
|
470
|
+
const dot = track.target.lastIndexOf(".");
|
|
471
|
+
if (dot < 0) continue;
|
|
472
|
+
const bone = track.target.slice(0, dot);
|
|
473
|
+
const path = track.target.slice(dot + 1);
|
|
474
|
+
const v = track.sample(local);
|
|
475
|
+
const slot = (sourceBones[bone] ??= {});
|
|
476
|
+
if (path === "rotation") slot.rotation = { x: v[0]!, y: v[1]!, z: v[2]!, w: v[3]! };
|
|
477
|
+
else if (path === "translation") slot.position = { x: v[0]!, y: v[1]!, z: v[2]! };
|
|
478
|
+
}
|
|
479
|
+
// B1/B2: measure BODY-bone motion on the rig-neutral source pose (mouth/caption/camera-free).
|
|
480
|
+
for (const bone of BODY_BONES) {
|
|
481
|
+
const amp = rotationAmplitude(sourceBones[bone]?.rotation);
|
|
482
|
+
if (amp > 0) bodyBoneRotationRad[bone] = amp;
|
|
483
|
+
// B1 maxTransAmplitude: the largest per-body-bone translation magnitude (metres) this frame.
|
|
484
|
+
const pos = sourceBones[bone]?.position;
|
|
485
|
+
if (pos) maxTransAmplitude = Math.max(maxTransAmplitude, Math.hypot(pos.x, pos.y, pos.z));
|
|
486
|
+
}
|
|
487
|
+
const hipsPos = sourceBones.hips?.position;
|
|
488
|
+
if (hipsPos) rootTranslation = Math.hypot(hipsPos.x, hipsPos.y, hipsPos.z);
|
|
489
|
+
// Retarget rig-neutral → this GLB's node names, then overlay onto the clean identity base.
|
|
490
|
+
const retargeted = retargetHumanoidPose({ bones: sourceBones }, map) as SamplablePose;
|
|
491
|
+
for (const [node, transform] of Object.entries(retargeted.bones)) bones[node] = transform;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const amplitudes = Object.values(bodyBoneRotationRad);
|
|
495
|
+
// ~0.5° threshold: a bone is "touched" only if it actually rotated this frame (not numeric dust).
|
|
496
|
+
const EPS = 0.0087;
|
|
497
|
+
const bonesTouched = amplitudes.filter((a) => a > EPS).length;
|
|
498
|
+
const maxRotAmplitudeRad = amplitudes.length > 0 ? Math.max(...amplitudes) : 0;
|
|
499
|
+
|
|
500
|
+
// B3 source: which library this sampled clip's motion came from. If we had to fall back to idle
|
|
501
|
+
// (the requested intent had no clip at all) report `idle-fallback`; else the per-intent library
|
|
502
|
+
// source resolved at load time (extracted catalog vs procedural baseline).
|
|
503
|
+
const source: ClipSource =
|
|
504
|
+
sampledId !== clipId ? "idle-fallback" : (options.sources?.[sampledId] ?? "procedural");
|
|
505
|
+
|
|
506
|
+
const decision: ClipDecision = {
|
|
507
|
+
intent,
|
|
508
|
+
clipId: sampledId,
|
|
509
|
+
source,
|
|
510
|
+
bonesTouched,
|
|
511
|
+
maxRotAmplitudeRad: +maxRotAmplitudeRad.toFixed(5),
|
|
512
|
+
maxTransAmplitude: +maxTransAmplitude.toFixed(5),
|
|
513
|
+
bodyBoneRotationRad,
|
|
514
|
+
rootTranslation: +rootTranslation.toFixed(5),
|
|
515
|
+
// The library pose has not been applied to a GLB skeleton yet; scene-player flips this true
|
|
516
|
+
// once it applies the retargeted pose / embedded clip to ≥1 real skinned node.
|
|
517
|
+
reachedGLBRuntime: false
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
return { pose: { bones, ...(morph ? { morphTargets: morph } : {}) }, decision };
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Fallback rule: a GLB with >1 distinct non-idle embedded clip is considered "rich".
|
|
524
|
+
const nonIdle = options.embeddedClips.filter((c) => !/^idle$/i.test(c.trim()));
|
|
525
|
+
const preferEmbedded = nonIdle.length >= 2;
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
targetNodeNames,
|
|
529
|
+
coverage: map.coverage as number,
|
|
530
|
+
preferEmbedded,
|
|
531
|
+
poseFor,
|
|
532
|
+
get state() {
|
|
533
|
+
return lastState;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Does the GLB ship an embedded clip that satisfies this intent? Returns the matching embedded clip
|
|
540
|
+
* name (so the caller can `playClip` it) or undefined to fall through to retargeting.
|
|
541
|
+
*/
|
|
542
|
+
export function embeddedClipForIntent(intent: StandardClipId, embeddedClips: readonly string[]): string | undefined {
|
|
543
|
+
const patterns: Partial<Record<StandardClipId, RegExp>> = {
|
|
544
|
+
idle: /idle|breath|stand/i,
|
|
545
|
+
walk: /walk|move/i,
|
|
546
|
+
run: /run|sprint/i,
|
|
547
|
+
gesture: /wave|gesture|emote|hello/i,
|
|
548
|
+
point: /point/i,
|
|
549
|
+
talk: /talk|speak/i,
|
|
550
|
+
nod: /nod/i,
|
|
551
|
+
react: /react|surprise/i
|
|
552
|
+
};
|
|
553
|
+
const pat = patterns[intent];
|
|
554
|
+
if (!pat) return undefined;
|
|
555
|
+
return embeddedClips.find((c) => pat.test(c));
|
|
556
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defineAuraAssets } from "@aura3d/engine";
|
|
2
|
+
import { AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID } from "./contract";
|
|
3
|
+
|
|
4
|
+
export const assetContractId = AURAVOICE_AURA3D_PROMPT_ANIMATION_CONTRACT_ID;
|
|
5
|
+
|
|
6
|
+
export const animationStudioRequiredAssetKeys = ["miko", "luma", "moonGarden"] as const;
|
|
7
|
+
|
|
8
|
+
export const animationStudioOptionalAudioAssetKeys = [
|
|
9
|
+
"mikoDialogueStem",
|
|
10
|
+
"lumaDialogueStem",
|
|
11
|
+
"moonGardenMusic",
|
|
12
|
+
"moonGardenChimeSfx"
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
export const assets = defineAuraAssets({
|
|
16
|
+
miko: {
|
|
17
|
+
type: "model",
|
|
18
|
+
format: "glb",
|
|
19
|
+
url: "/aura-assets/miko.catalog.glb",
|
|
20
|
+
bounds: [1.015, 1.95, 0.369],
|
|
21
|
+
hash: "sha256-fcdc2b7502031e556259dfe942fdfbd7abe41963433f2046ed2522d27bce9d65",
|
|
22
|
+
metadata: {
|
|
23
|
+
contractId: assetContractId,
|
|
24
|
+
role: "animation-character",
|
|
25
|
+
license: "CC0-1.0",
|
|
26
|
+
attribution: "Miko — Aura3D-authored CC0 procedural humanoid",
|
|
27
|
+
source:
|
|
28
|
+
"Aura3D-authored rigged GLB generated by scripts/build-characters.ts (CC0). Full 21-joint humanoid rig (hips→spine→chest→neck→head, both arms with forearms+hands, both legs with lower-legs+feet+toes; graded-A) with embedded Idle/Wave/Walk/Talk clips and a facial mouthOpen blendshape morph target for lip-sync. Now ships a real UV-mapped 1024×1024 base-colour bitmap texture (skin tone gradients + cheek blush, woven clothing fabric, brushed accent trim, eye/iris falloff, edge ambient occlusion) instead of flat vertex colours.",
|
|
29
|
+
animationClips: ["Idle", "Wave", "Walk", "Talk"],
|
|
30
|
+
// Authored rig ships a real face morph (mouthOpen), so lip-sync drives a
|
|
31
|
+
// blendshape rather than the primitive mouth-card fallback.
|
|
32
|
+
mouthReadiness: "blendshape",
|
|
33
|
+
mouthMorphTargets: ["mouthOpen"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
luma: {
|
|
37
|
+
type: "model",
|
|
38
|
+
format: "glb",
|
|
39
|
+
url: "/aura-assets/luma2.catalog.glb",
|
|
40
|
+
bounds: [0.99, 1.95, 0.333],
|
|
41
|
+
hash: "sha256-911c60a074f6d89d0eaeefc1f1eecf4188873880b62aa11b3d02ec2c18a4e036",
|
|
42
|
+
metadata: {
|
|
43
|
+
contractId: assetContractId,
|
|
44
|
+
role: "animation-character",
|
|
45
|
+
license: "CC0-1.0",
|
|
46
|
+
attribution: "Luma — Aura3D-authored CC0 procedural humanoid",
|
|
47
|
+
source:
|
|
48
|
+
"Aura3D-authored rigged GLB generated by scripts/build-characters.ts (CC0). A warm-gold helper that matches Miko's style (coherent cast). Full 21-joint humanoid rig (graded-A) with embedded Idle/Wave/Walk/Talk clips and a facial mouthOpen blendshape morph target. Now ships a real UV-mapped 1024×1024 base-colour bitmap texture (skin tone gradients + cheek blush, woven clothing fabric, brushed accent trim, eye/iris falloff, edge ambient occlusion) instead of flat vertex colours.",
|
|
49
|
+
animationClips: ["Idle", "Wave", "Walk", "Talk"],
|
|
50
|
+
// Authored rig ships a real face morph (mouthOpen), so lip-sync drives a
|
|
51
|
+
// blendshape rather than the primitive mouth-card fallback.
|
|
52
|
+
mouthReadiness: "blendshape",
|
|
53
|
+
mouthMorphTargets: ["mouthOpen"]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
moonGarden: {
|
|
57
|
+
type: "model",
|
|
58
|
+
format: "gltf",
|
|
59
|
+
url: "/aura-assets/moonGarden.gltf",
|
|
60
|
+
bounds: [3.2, 0.6, 1.2],
|
|
61
|
+
hash: "sha256-b7b50e37cc4319df21fa2a598aab9e055d54eeb3f5511a3b3f0d4f6c5101c456",
|
|
62
|
+
metadata: {
|
|
63
|
+
contractId: assetContractId,
|
|
64
|
+
role: "animation-set",
|
|
65
|
+
license: "MIT",
|
|
66
|
+
source: "Aura3D animation-studio local generated starter set anchor",
|
|
67
|
+
walkable: true,
|
|
68
|
+
framingReady: true
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} as const);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Typed character definition for Animation Studio. Pure data + types (no engine import) so the
|
|
2
|
+
// profile pipeline (plan/profile/package/verify) runs in plain Node.
|
|
3
|
+
|
|
4
|
+
export type LocomotionAction = "idle" | "walk" | "run";
|
|
5
|
+
|
|
6
|
+
export interface AnimationStudioIkChain {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly root: string;
|
|
9
|
+
readonly mid: string;
|
|
10
|
+
readonly tip: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AnimationStudioCharacter {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
/** Typed asset key resolved from aura.assets.json (never a raw URL). */
|
|
17
|
+
readonly assetKey: string;
|
|
18
|
+
/** Locomotion clip map: action -> embedded GLB clip name. */
|
|
19
|
+
readonly clipMap: Readonly<Record<LocomotionAction, string>>;
|
|
20
|
+
/** Speeds (units/sec) at which walk and run clips reach full weight. */
|
|
21
|
+
readonly walkSpeed: number;
|
|
22
|
+
readonly runSpeed: number;
|
|
23
|
+
/** Two-bone IK chains the studio can solve (feet/hands). */
|
|
24
|
+
readonly ikChains: readonly AnimationStudioIkChain[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const requiredLocomotionActions: readonly LocomotionAction[] = ["idle", "walk", "run"];
|
|
28
|
+
|
|
29
|
+
export const heroCharacter: AnimationStudioCharacter = {
|
|
30
|
+
id: "hero",
|
|
31
|
+
name: "Hero",
|
|
32
|
+
assetKey: "hero",
|
|
33
|
+
clipMap: { idle: "Idle", walk: "Walk", run: "Run" },
|
|
34
|
+
walkSpeed: 1.6,
|
|
35
|
+
runSpeed: 4.4,
|
|
36
|
+
ikChains: [
|
|
37
|
+
{ id: "left-leg", root: "LeftUpLeg", mid: "LeftLeg", tip: "LeftFoot" },
|
|
38
|
+
{ id: "right-leg", root: "RightUpLeg", mid: "RightLeg", tip: "RightFoot" }
|
|
39
|
+
]
|
|
40
|
+
};
|