create-aura3d 1.1.2 → 1.3.1
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 +6 -4
- package/dist/index.js.map +1 -1
- package/package.json +10 -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,867 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scene-player.ts — the GENERIC animation scene player.
|
|
3
|
+
*
|
|
4
|
+
* It consumes an EpisodeDocument and renders it. It contains ZERO scene-specific
|
|
5
|
+
* constants — blocking, camera framing, the clip per beat, prop layout, the set + lights,
|
|
6
|
+
* and the dim→sparkle world-state all come from the document via the sampling functions.
|
|
7
|
+
* It can play ANY document; the Moon Garden specifics live in `moon-garden-document.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Dialogue/caption/viseme still come from the episode (`render-plan.ts`) — that timed
|
|
10
|
+
* track is the AuraVoice contract and is shared across the pipeline.
|
|
11
|
+
*
|
|
12
|
+
* Imports resolve from the published `@aura3d/engine` (advanced-runtime A3DRenderer +
|
|
13
|
+
* production-runtime typed GLB actors + rendering primitives), exactly as the old route.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { A3DRenderer, TextureBinding as EngineTextureBinding } from "@aura3d/engine/advanced-runtime";
|
|
17
|
+
import { createStudioLighting, createTypedGLBActor, type TypedGLBActor } from "@aura3d/engine/production-runtime";
|
|
18
|
+
import { createCameraPathFromPreset, sampleCameraPath, type CameraPath } from "@aura3d/engine";
|
|
19
|
+
// I1 clean-room correctness: import the rendering/scene PRIMITIVES from the engine's own subpaths
|
|
20
|
+
// (`@aura3d/engine/rendering` / `@aura3d/engine/scene`) — the SAME copy the A3DRenderer uses — NOT
|
|
21
|
+
// the standalone `@aura3d/rendering` / `@aura3d/scene` packages. In a clean-room (tarball) install
|
|
22
|
+
// those standalone packages are a SECOND copy of the rendering code, so a `Geometry`/`PBRMaterial`/
|
|
23
|
+
// `TextureBinding` built from them is a DIFFERENT class than the renderer's, and the renderer's
|
|
24
|
+
// `MaterialBinding.bind` (`value instanceof TextureBinding`) rejects EVERY PBR material with a
|
|
25
|
+
// `MaterialBindingError` ("u_environmentMapTexture must be texture2d"). Importing through the engine
|
|
26
|
+
// gives ONE shared copy, so the bindings match. (In the monorepo these resolve to the same deduped
|
|
27
|
+
// dist either way, so this never changes the monorepo render.)
|
|
28
|
+
import { Geometry, PBRMaterial, type CollectedLight, type RenderItem, type RenderSource } from "@aura3d/engine/rendering";
|
|
29
|
+
import { bakeSetHdri, type BakedHdri } from "./procedural-hdri";
|
|
30
|
+
import { composeMat4, multiplyMat4, perspectiveMat4, PointLight, quatFromEuler, type Mat4 } from "@aura3d/engine/scene";
|
|
31
|
+
import {
|
|
32
|
+
sampleBlocking,
|
|
33
|
+
sampleCaption,
|
|
34
|
+
sampleVisemeOpenness,
|
|
35
|
+
sampleWorldStateGlow,
|
|
36
|
+
shotAtTime,
|
|
37
|
+
type EpisodeDocument,
|
|
38
|
+
type SetPiece,
|
|
39
|
+
type Vec3
|
|
40
|
+
} from "./episode-document";
|
|
41
|
+
// I2 — GLOBAL BLEND/alpha ghost fix policy + cel/render-mode clarification (one canonical home).
|
|
42
|
+
import { forceOpaqueAcrossRenderItems } from "./render-modes";
|
|
43
|
+
import {
|
|
44
|
+
loadSharedClipLibrary,
|
|
45
|
+
createPerformanceRig,
|
|
46
|
+
resolveIntent,
|
|
47
|
+
embeddedClipForIntent,
|
|
48
|
+
type PerformanceRig,
|
|
49
|
+
type ClipDecision,
|
|
50
|
+
type ClipSource
|
|
51
|
+
} from "./animation-performance";
|
|
52
|
+
// B6 — rig-capability + true-foot-IK helpers. These come from the SAME freshly-built monorepo
|
|
53
|
+
// dist that `animation-performance.ts` imports (vite aliases @aura3d/* to dist; this relative path
|
|
54
|
+
// is the one specifier the route does NOT alias — @aura3d/animation — so we reach it by path, just
|
|
55
|
+
// like animation-performance.ts does). `gradeRig` tells us whether a rig actually HAS the
|
|
56
|
+
// leg/knee/ankle/foot chain a two-bone foot-IK solve needs; `createFootIkRig` is the engine's real
|
|
57
|
+
// two-bone solver (used only for rigs that have the chain). On sparse/mascot rigs we keep the
|
|
58
|
+
// honest `rootGrounding` (root pinned to the ground mark — NOT IK).
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
60
|
+
// @ts-ignore — resolved at runtime to the freshly-built monorepo dist (has co-located .d.ts).
|
|
61
|
+
import {
|
|
62
|
+
gradeRig,
|
|
63
|
+
inferHumanoidRig,
|
|
64
|
+
createFootIkRig,
|
|
65
|
+
type RigQualityReport,
|
|
66
|
+
type FootIkRig,
|
|
67
|
+
type GroundRaycaster
|
|
68
|
+
} from "@aura3d/animation";
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Proof shapes (unchanged from the old route so the capture script keeps working).
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
interface LiveRouteSeekProof {
|
|
74
|
+
readonly time: number;
|
|
75
|
+
readonly drawCalls: number;
|
|
76
|
+
readonly skinnedRenderItems: number;
|
|
77
|
+
readonly gardenGlow: number;
|
|
78
|
+
readonly shot: {
|
|
79
|
+
readonly shotId: string;
|
|
80
|
+
readonly presetId: string;
|
|
81
|
+
readonly episodeTime: number;
|
|
82
|
+
readonly cameraPosition: Vec3;
|
|
83
|
+
readonly fov: number;
|
|
84
|
+
};
|
|
85
|
+
readonly caption: { readonly text: string; readonly speakerId: string; readonly captionId: string };
|
|
86
|
+
/**
|
|
87
|
+
* B1 debug overlay — the current clip + intent + source for EVERY character at this frame,
|
|
88
|
+
* inspectable per-seek (e.g. "miko: talk←talk [extracted] 9 bones / 0.412rad").
|
|
89
|
+
*/
|
|
90
|
+
readonly debugOverlay: readonly string[];
|
|
91
|
+
readonly characters: readonly {
|
|
92
|
+
readonly id: string;
|
|
93
|
+
readonly clip: string;
|
|
94
|
+
readonly position: Vec3;
|
|
95
|
+
readonly sweeping: boolean;
|
|
96
|
+
readonly tracksApplied: number;
|
|
97
|
+
readonly skinningPalettesUpdated: number;
|
|
98
|
+
readonly skinningBindingCount: number;
|
|
99
|
+
readonly mouthOpenness: number;
|
|
100
|
+
readonly visemeId: string;
|
|
101
|
+
readonly mouthMorphWeight: number;
|
|
102
|
+
readonly mouthMorphIndex: number;
|
|
103
|
+
readonly primitiveMouthOpen: number;
|
|
104
|
+
/** B1 per-character/per-beat clip-decision record (intent/clipId/source/bonesTouched/…). */
|
|
105
|
+
readonly clipDecision: ClipDecision & { readonly characterId: string; readonly time: number };
|
|
106
|
+
}[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface LiveRouteReadyProof {
|
|
110
|
+
readonly ready: true;
|
|
111
|
+
readonly backend: string;
|
|
112
|
+
readonly characters: readonly {
|
|
113
|
+
readonly id: string;
|
|
114
|
+
readonly url: string;
|
|
115
|
+
readonly clip: string;
|
|
116
|
+
readonly clips: readonly string[];
|
|
117
|
+
readonly skinningBindingCount: number;
|
|
118
|
+
readonly skinnedRenderItemCount: number;
|
|
119
|
+
}[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface LiveSeekOptions {
|
|
123
|
+
readonly mouthOverride?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type LiveRouteWindow = Window & {
|
|
127
|
+
__AURA_LIVE_ROUTE_READY__?: LiveRouteReadyProof;
|
|
128
|
+
__auraSeek__?: (time: number, options?: LiveSeekOptions) => LiveRouteSeekProof;
|
|
129
|
+
__AURA_LIVE_ROUTE_HEADLESS__?: boolean;
|
|
130
|
+
__AURA_LIVE_ROUTE_ERROR__?: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
interface LiveCharacter {
|
|
134
|
+
readonly id: string;
|
|
135
|
+
readonly actor: TypedGLBActor;
|
|
136
|
+
readonly defaultClip: string;
|
|
137
|
+
readonly availableClips: readonly string[];
|
|
138
|
+
readonly scale: number;
|
|
139
|
+
readonly mouthMorphIndex: number;
|
|
140
|
+
readonly morphRenderables: { morphWeights: number[] }[];
|
|
141
|
+
/** Shared-library performance driver (state graph + retarget map) for this character (Phase 2.5). */
|
|
142
|
+
readonly perf: PerformanceRig;
|
|
143
|
+
/**
|
|
144
|
+
* B6 — body-acting suitability grade for this character's inferred rig (A/B/C/D). Drives the
|
|
145
|
+
* HONEST foot-handling decision: only rigs that actually HAVE a leg/knee/ankle/foot chain
|
|
146
|
+
* (grade A/B with ankles) get true two-bone foot IK; everything else keeps `rootGrounding`.
|
|
147
|
+
*/
|
|
148
|
+
readonly rigGrade: RigQualityReport;
|
|
149
|
+
/**
|
|
150
|
+
* B6 — the engine's real two-bone foot-IK rig, present ONLY when this character's rig has the
|
|
151
|
+
* ankle chain. `undefined` ⇒ no true IK is available; the player uses `rootGrounding` and SAYS so.
|
|
152
|
+
*/
|
|
153
|
+
readonly footIk?: FootIkRig;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Shared base geometry (built once; the document scales them via modelMatrix).
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
const BASE_GEOMETRY: Record<SetPiece["geometry"], Geometry> = {
|
|
160
|
+
cube: Geometry.litCube(1),
|
|
161
|
+
sphere: Geometry.uvSphere(0.5, 24, 16),
|
|
162
|
+
cylinder: Geometry.cylinder({ radius: 0.5, height: 1, segments: 20, capped: true })
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve a requested clip to the GLB's real clip list (exact → substring). Returns `undefined`
|
|
167
|
+
* when NO real clip matches — an unknown/fictional clip name must FALL THROUGH to the shared
|
|
168
|
+
* library (B3), NOT silently collapse to clip #0 (which is how a whole cast ended up looping one
|
|
169
|
+
* embedded idle). Callers treat `undefined` as "no embedded clip; use the library pose."
|
|
170
|
+
*/
|
|
171
|
+
function pickClip(requested: string, available: readonly string[]): string | undefined {
|
|
172
|
+
if (available.length === 0) return undefined;
|
|
173
|
+
const want = requested.toLowerCase();
|
|
174
|
+
const exact = available.find((c) => c.toLowerCase() === want);
|
|
175
|
+
if (exact) return exact;
|
|
176
|
+
return available.find((c) => c.toLowerCase().includes(want) || want.includes(c.toLowerCase()));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Right-handed look-at view matrix (column-major, matches perspectiveMat4). */
|
|
180
|
+
function lookAtViewMatrix(eye: Vec3, target: Vec3, up: Vec3 = [0, 1, 0]): Mat4 {
|
|
181
|
+
const fx = target[0] - eye[0];
|
|
182
|
+
const fy = target[1] - eye[1];
|
|
183
|
+
const fz = target[2] - eye[2];
|
|
184
|
+
const fl = Math.hypot(fx, fy, fz) || 1;
|
|
185
|
+
const f: [number, number, number] = [fx / fl, fy / fl, fz / fl];
|
|
186
|
+
let sx = f[1] * up[2] - f[2] * up[1];
|
|
187
|
+
let sy = f[2] * up[0] - f[0] * up[2];
|
|
188
|
+
let sz = f[0] * up[1] - f[1] * up[0];
|
|
189
|
+
const sl = Math.hypot(sx, sy, sz) || 1;
|
|
190
|
+
sx /= sl;
|
|
191
|
+
sy /= sl;
|
|
192
|
+
sz /= sl;
|
|
193
|
+
const ux = sy * f[2] - sz * f[1];
|
|
194
|
+
const uy = sz * f[0] - sx * f[2];
|
|
195
|
+
const uz = sx * f[1] - sy * f[0];
|
|
196
|
+
return [
|
|
197
|
+
sx, ux, -f[0], 0,
|
|
198
|
+
sy, uy, -f[1], 0,
|
|
199
|
+
sz, uz, -f[2], 0,
|
|
200
|
+
-(sx * eye[0] + sy * eye[1] + sz * eye[2]),
|
|
201
|
+
-(ux * eye[0] + uy * eye[1] + uz * eye[2]),
|
|
202
|
+
f[0] * eye[0] + f[1] * eye[1] + f[2] * eye[2],
|
|
203
|
+
1
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cameraViewProjection(
|
|
208
|
+
path: CameraPath,
|
|
209
|
+
time: number,
|
|
210
|
+
aspect: number
|
|
211
|
+
): { readonly viewProjectionMatrix: Mat4; readonly position: Vec3; readonly fov: number } {
|
|
212
|
+
const sample = sampleCameraPath(path, time);
|
|
213
|
+
const view = lookAtViewMatrix(sample.position as Vec3, sample.target as Vec3);
|
|
214
|
+
const projection = perspectiveMat4((sample.fov * Math.PI) / 180, aspect, 0.1, 100);
|
|
215
|
+
return {
|
|
216
|
+
viewProjectionMatrix: multiplyMat4(projection, view),
|
|
217
|
+
position: sample.position as Vec3,
|
|
218
|
+
fov: sample.fov
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const SHADOW_MATERIAL = new PBRMaterial({
|
|
223
|
+
name: "live-route-contact-shadow",
|
|
224
|
+
baseColor: [0.03, 0.08, 0.07, 1],
|
|
225
|
+
metallic: 0,
|
|
226
|
+
roughness: 1,
|
|
227
|
+
emissiveColor: [0, 0, 0],
|
|
228
|
+
emissiveStrength: 0
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Mount the generic player for `doc`. Loads its assets, builds its set/lights/props,
|
|
233
|
+
* exposes the headless `__auraSeek__` hook + ready proof, and renders.
|
|
234
|
+
*/
|
|
235
|
+
export async function mountScenePlayer(doc: EpisodeDocument): Promise<void> {
|
|
236
|
+
const root = document.querySelector<HTMLDivElement>("#app");
|
|
237
|
+
if (!root) throw new Error("scene-player: missing #app root element.");
|
|
238
|
+
root.innerHTML = `
|
|
239
|
+
<main style="margin:0;background:#0b0f1a;min-height:100vh;display:grid;place-items:center;position:relative;">
|
|
240
|
+
<canvas id="live-canvas" width="960" height="540"
|
|
241
|
+
style="width:960px;height:540px;display:block;background:#0b0f1a;"></canvas>
|
|
242
|
+
<pre id="aura-debug-overlay" aria-hidden="true" style="
|
|
243
|
+
position:absolute;left:8px;top:8px;margin:0;padding:6px 8px;max-width:944px;
|
|
244
|
+
font:11px/1.35 ui-monospace,SFMono-Regular,Menlo,monospace;color:#9effa2;
|
|
245
|
+
background:rgba(8,12,22,0.72);border:1px solid rgba(158,255,162,0.35);border-radius:6px;
|
|
246
|
+
white-space:pre-wrap;pointer-events:none;display:none;"></pre>
|
|
247
|
+
</main>
|
|
248
|
+
`;
|
|
249
|
+
const canvas = root.querySelector<HTMLCanvasElement>("#live-canvas");
|
|
250
|
+
if (!canvas) throw new Error("scene-player: missing #live-canvas.");
|
|
251
|
+
|
|
252
|
+
// B1 — per-character clip + intent debug overlay, gated behind AURA_DEBUG_OVERLAY=1 (surfaced to
|
|
253
|
+
// the browser route via Vite as VITE_AURA_DEBUG_OVERLAY). Off by default; when on, the proof DOM
|
|
254
|
+
// shows the live clip←intent [source] + body-motion numbers per character per frame.
|
|
255
|
+
const debugOverlayEnabled =
|
|
256
|
+
String(
|
|
257
|
+
(import.meta as unknown as { env?: Record<string, string> }).env?.VITE_AURA_DEBUG_OVERLAY ?? ""
|
|
258
|
+
).trim() === "1";
|
|
259
|
+
const debugOverlayEl = root.querySelector<HTMLPreElement>("#aura-debug-overlay");
|
|
260
|
+
if (debugOverlayEl && debugOverlayEnabled) debugOverlayEl.style.display = "block";
|
|
261
|
+
|
|
262
|
+
const liveWindow = window as LiveRouteWindow;
|
|
263
|
+
const width = canvas.width;
|
|
264
|
+
const height = canvas.height;
|
|
265
|
+
const aspect = width / height;
|
|
266
|
+
|
|
267
|
+
// 1. Characters.
|
|
268
|
+
// Shared, rig-neutral clip library (idle/talk/gesture/point/nod/walk/run/react) built ONCE and
|
|
269
|
+
// reused by every character's performance rig. B3: the EXTRACTED universal catalog library
|
|
270
|
+
// (public/clip-library/<intent>.json) is the DEFAULT first choice; intents with no usable
|
|
271
|
+
// extracted clip fall back to the procedural standard baseline (both logged).
|
|
272
|
+
const sharedLibrary = await loadSharedClipLibrary();
|
|
273
|
+
const sharedClipRegistry = sharedLibrary.registry;
|
|
274
|
+
const characters: LiveCharacter[] = [];
|
|
275
|
+
for (const spec of doc.assets.characters) {
|
|
276
|
+
const actor = await createTypedGLBActor({ asset: { url: spec.url }, id: spec.id, name: spec.id, width, height });
|
|
277
|
+
// FIX (ghost characters): many catalog GLBs — especially Sketchfab/FBX exports — set
|
|
278
|
+
// alphaMode=BLEND on an OPAQUE (opacity 1) material, so the textured character renders
|
|
279
|
+
// as a translucent white silhouette. The GLOBAL ghost-fix policy (render-modes.ts) forces
|
|
280
|
+
// each opaque-but-BLENDed material OPAQUE (blend off, depth write/test on) so its real
|
|
281
|
+
// textures show solid. The SAME policy runs on props + set dressing below, so the fix is
|
|
282
|
+
// provably global (not character-only).
|
|
283
|
+
forceOpaqueAcrossRenderItems(actor.collectRenderItems());
|
|
284
|
+
const snapshot = actor.snapshot();
|
|
285
|
+
if (snapshot.clips.length < 1) throw new Error(`scene-player: ${spec.id} GLB exposes no animation clips.`);
|
|
286
|
+
const scene = actor.pipeline.resources.scene;
|
|
287
|
+
const morphRenderables: { morphWeights: number[] }[] = scene
|
|
288
|
+
.collectRenderables()
|
|
289
|
+
.map((entry) => entry.renderable)
|
|
290
|
+
.filter((renderable) => renderable.morphWeights.length > 0);
|
|
291
|
+
const mouthMorphIndex = spec.mouthMorphIndex ?? (morphRenderables.length > 0 ? 0 : -1);
|
|
292
|
+
|
|
293
|
+
// Phase 2.5: infer this GLB's humanoid rig from its skeleton node names and build a performance
|
|
294
|
+
// rig (retarget map standard-library-rig → this rig + a performance state graph). This is what
|
|
295
|
+
// lets EVERY character — even a catalog GLB that ships only a single embedded idle — talk,
|
|
296
|
+
// gesture, walk and react from the SHARED clip library instead of looping one embedded clip.
|
|
297
|
+
const nodeNames: string[] = [];
|
|
298
|
+
scene.root.traverse((node) => {
|
|
299
|
+
if (node.name) nodeNames.push(node.name);
|
|
300
|
+
});
|
|
301
|
+
const perf = createPerformanceRig(sharedClipRegistry, {
|
|
302
|
+
nodeNames,
|
|
303
|
+
embeddedClips: snapshot.clips,
|
|
304
|
+
sources: sharedLibrary.sources
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// B6 — HONEST foot handling. Grade the inferred rig: only rigs that actually carry a
|
|
308
|
+
// leg/knee/ankle/foot chain (grade A/B with `hasAnkles`) can run TRUE two-bone foot IK.
|
|
309
|
+
// Sparse/mascot rigs (grade C — head+torso, stub arms, no ankle chain) keep `rootGrounding`
|
|
310
|
+
// (root pinned to the ground mark — NOT IK) and we LOG exactly that, never claiming IK.
|
|
311
|
+
const rig = inferHumanoidRig(nodeNames);
|
|
312
|
+
const rigGrade = gradeRig(rig);
|
|
313
|
+
let footIk: FootIkRig | undefined;
|
|
314
|
+
const canFootIk = rigGrade.hasLegs && rigGrade.hasKnees && rigGrade.hasAnkles;
|
|
315
|
+
if (canFootIk) {
|
|
316
|
+
try {
|
|
317
|
+
// Flat ground at y=0 (the staged ground mark): a downward ray returns the floor under
|
|
318
|
+
// each ankle. This is a REAL two-bone solve (hip→knee→ankle) with foot-lock, not a pin.
|
|
319
|
+
const flatGround: GroundRaycaster = {
|
|
320
|
+
raycastDown(origin, maxDistance) {
|
|
321
|
+
const distance = origin[1] - 0;
|
|
322
|
+
if (distance < 0 || distance > maxDistance) return undefined;
|
|
323
|
+
return { point: [origin[0], 0, origin[2]], normal: [0, 1, 0], distance };
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
// Rest-pose leg chains in the standard (meters) frame; per-frame ankle targets come from
|
|
327
|
+
// the staged ground in `solveFootPlacement`. Hip≈0.9m, knee≈0.5m, ankle≈0.08m.
|
|
328
|
+
footIk = createFootIkRig({
|
|
329
|
+
raycaster: flatGround,
|
|
330
|
+
legs: [
|
|
331
|
+
{ side: "left", hip: [-0.1, 0.9, 0], knee: [-0.1, 0.5, 0.02], ankle: [-0.1, 0.08, 0] },
|
|
332
|
+
{ side: "right", hip: [0.1, 0.9, 0], knee: [0.1, 0.5, 0.02], ankle: [0.1, 0.08, 0] }
|
|
333
|
+
]
|
|
334
|
+
});
|
|
335
|
+
console.info(`[scene-player] ${spec.id}: true two-bone foot IK enabled (rig grade ${rigGrade.grade}: has leg/knee/ankle chain).`);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
footIk = undefined;
|
|
338
|
+
console.warn(`[scene-player] ${spec.id}: foot-IK rig construction failed (${(err as Error).message}) → rootGrounding.`);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
console.info(
|
|
342
|
+
`[scene-player] ${spec.id}: rootGrounding (rig grade ${rigGrade.grade}: no ankle chain → true foot IK unavailable).`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
characters.push({
|
|
347
|
+
id: spec.id,
|
|
348
|
+
actor,
|
|
349
|
+
defaultClip: spec.defaultClip,
|
|
350
|
+
availableClips: snapshot.clips,
|
|
351
|
+
scale: spec.scale,
|
|
352
|
+
mouthMorphIndex,
|
|
353
|
+
morphRenderables,
|
|
354
|
+
perf,
|
|
355
|
+
rigGrade,
|
|
356
|
+
footIk
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 2. Props (loaded once; instanced per set-dressing placement).
|
|
361
|
+
const propActors = new Map<string, TypedGLBActor>();
|
|
362
|
+
for (const prop of doc.assets.props) {
|
|
363
|
+
// A prop with no url is a prompt-derived object with no resolved mesh — recorded in the doc for
|
|
364
|
+
// provenance, but there is nothing to load. Skip it (procedural set dressing fills the space)
|
|
365
|
+
// instead of 404-ing on a fictional GLB.
|
|
366
|
+
if (!prop.url) continue;
|
|
367
|
+
const propActor = await createTypedGLBActor({ asset: { url: prop.url }, id: prop.id, name: prop.id, width, height });
|
|
368
|
+
// Same GLOBAL ghost fix as characters: catalog prop GLBs can also ship opaque-but-BLENDed
|
|
369
|
+
// materials that render as translucent ghosts; force them opaque here too.
|
|
370
|
+
forceOpaqueAcrossRenderItems(propActor.collectRenderItems());
|
|
371
|
+
propActors.set(prop.id, propActor);
|
|
372
|
+
}
|
|
373
|
+
function collectSetDressing(): RenderItem[] {
|
|
374
|
+
const items: RenderItem[] = [];
|
|
375
|
+
for (const placement of doc.setDressing) {
|
|
376
|
+
const actor = propActors.get(placement.propId);
|
|
377
|
+
if (!actor) continue;
|
|
378
|
+
actor.pipeline.resources.scene.root.transform
|
|
379
|
+
.setPosition(placement.position[0], placement.feetOffset * placement.scale, placement.position[2])
|
|
380
|
+
.setRotation(0, 0, 0, 1)
|
|
381
|
+
.setScale(placement.scale, placement.scale, placement.scale);
|
|
382
|
+
items.push(...actor.collectRenderItems());
|
|
383
|
+
}
|
|
384
|
+
return items;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 3. Set pieces (primitive geometry + materials from the document) + glow registry.
|
|
388
|
+
// M2 — IN-SHADER CEL BY DEFAULT for STYLIZED renders. NON-GLOW pieces render with the engine's
|
|
389
|
+
// REAL AnimationToonMaterial (GPU banded N·L + Fresnel rim). Glow pieces stay on PBR because the
|
|
390
|
+
// dim→sparkle world-state drives their emissive uniform.
|
|
391
|
+
//
|
|
392
|
+
// Default policy (M2): stylized is the DEFAULT, so cel is ON unless the document/flag picks PBR.
|
|
393
|
+
// - `doc.set.inShaderCel === false` → explicit PBR opt-out (realistic asset / PBR mode).
|
|
394
|
+
// - `import.meta.env.VITE_AURA_RENDER_STYLE === "pbr"` → flag opt-out (mirrors the server-side
|
|
395
|
+
// AURA_RENDER_STYLE=toon|pbr; the browser route reads it via Vite's import.meta.env).
|
|
396
|
+
// - otherwise (undefined / true) → stylized → cel ON.
|
|
397
|
+
// M2 — resolve the REAL engine cel material LAZILY + NON-FATALLY: if the aliased @aura3d/rendering
|
|
398
|
+
// build predates AnimationToonMaterial the import yields undefined and we fall back to PBR — never
|
|
399
|
+
// a hard module-link crash.
|
|
400
|
+
const renderStylePbr =
|
|
401
|
+
((import.meta as unknown as { env?: Record<string, string> }).env?.VITE_AURA_RENDER_STYLE ?? "")
|
|
402
|
+
.trim()
|
|
403
|
+
.toLowerCase() === "pbr";
|
|
404
|
+
const inShaderCel = doc.set.inShaderCel !== false && !renderStylePbr;
|
|
405
|
+
const celKeyDir: Vec3 = [0.4, 0.82, 0.45]; // upper-front key, matching the softbox/3-point key
|
|
406
|
+
type CelCtor = new (o: { name: string; baseColor: [number, number, number, number]; bands: number; shadowFloor: number; lightDirection: Vec3; rimColor?: [number, number, number]; rimPower?: number; rimIntensity: number }) => RenderItem["material"];
|
|
407
|
+
let CelMaterial: CelCtor | undefined;
|
|
408
|
+
if (inShaderCel) {
|
|
409
|
+
try {
|
|
410
|
+
const mod = (await import("@aura3d/rendering")) as unknown as { AnimationToonMaterial?: CelCtor };
|
|
411
|
+
CelMaterial = mod.AnimationToonMaterial;
|
|
412
|
+
if (!CelMaterial) console.warn("[scene-player] inShaderCel (stylized default) requested but AnimationToonMaterial absent from the build; falling back to PBR.");
|
|
413
|
+
else console.info("[scene-player] inShaderCel ON (stylized default): non-glow set pieces use AnimationToonMaterial.");
|
|
414
|
+
} catch (err) {
|
|
415
|
+
console.warn(`[scene-player] AnimationToonMaterial import failed (${(err as Error).message}) → PBR set pieces.`);
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
console.info(`[scene-player] inShaderCel OFF → PBR set pieces (${renderStylePbr ? "VITE_AURA_RENDER_STYLE=pbr" : "doc.set.inShaderCel=false"}).`);
|
|
419
|
+
}
|
|
420
|
+
const glowPieces: { material: PBRMaterial; dim: number; full: number }[] = [];
|
|
421
|
+
const setItems: RenderItem[] = doc.set.pieces.map((piece) => {
|
|
422
|
+
const rgba = [...piece.baseColor];
|
|
423
|
+
const material: RenderItem["material"] =
|
|
424
|
+
CelMaterial && !piece.glow
|
|
425
|
+
? new CelMaterial({
|
|
426
|
+
name: `set-${piece.id}`,
|
|
427
|
+
baseColor: [rgba[0] ?? 0, rgba[1] ?? 0, rgba[2] ?? 0, rgba[3] ?? 1],
|
|
428
|
+
// M2 cel tuning for a CLEAN look: 4 bands give crisp, readable cel steps without the
|
|
429
|
+
// posterized banding 5+ produces on smooth primitives; shadowFloor 0.32 keeps the
|
|
430
|
+
// darkest band off pure black so shapes stay readable in shadow; a soft, tight rim
|
|
431
|
+
// (cool color, power 3) pops the silhouette without haloing.
|
|
432
|
+
bands: 4,
|
|
433
|
+
shadowFloor: 0.32,
|
|
434
|
+
lightDirection: celKeyDir,
|
|
435
|
+
rimColor: [0.7, 0.82, 1],
|
|
436
|
+
rimPower: 3,
|
|
437
|
+
rimIntensity: 0.4
|
|
438
|
+
})
|
|
439
|
+
: new PBRMaterial({
|
|
440
|
+
name: `set-${piece.id}`,
|
|
441
|
+
baseColor: [...piece.baseColor],
|
|
442
|
+
metallic: piece.metallic ?? 0,
|
|
443
|
+
roughness: piece.roughness ?? 0.85,
|
|
444
|
+
emissiveColor: piece.emissiveColor ? [...piece.emissiveColor] : [0, 0, 0],
|
|
445
|
+
emissiveStrength: piece.glow ? piece.glow.dim : piece.emissiveStrength ?? 0
|
|
446
|
+
});
|
|
447
|
+
if (piece.glow && material instanceof PBRMaterial) glowPieces.push({ material, dim: piece.glow.dim, full: piece.glow.full });
|
|
448
|
+
return {
|
|
449
|
+
label: piece.id,
|
|
450
|
+
geometry: BASE_GEOMETRY[piece.geometry],
|
|
451
|
+
material,
|
|
452
|
+
modelMatrix: composeMat4([...piece.position], quatFromEuler(0, 0, piece.roll ?? 0), [...piece.scale]) as Mat4,
|
|
453
|
+
includeInAutoFrame: piece.includeInAutoFrame ?? false
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
/** Drive every glow piece's emissive from the eased dim→sparkle world-state. */
|
|
457
|
+
function applyWorldGlow(eased: number): void {
|
|
458
|
+
for (const g of glowPieces) g.material.setParameter("u_emissiveStrength", g.dim + (g.full - g.dim) * eased);
|
|
459
|
+
}
|
|
460
|
+
applyWorldGlow(sampleWorldStateGlow(doc, 0));
|
|
461
|
+
|
|
462
|
+
// 4. Lights: M3 — a REAL 3-point rig (key/fill/rim) balanced for characters + the document's
|
|
463
|
+
// point rig. The `inspection` preset is a true 3-point setup (warm front key with shadows, cool
|
|
464
|
+
// side fill, warm back rim) that reads as intentional studio lighting and separates the character
|
|
465
|
+
// from the backdrop; the softbox preset previously used had NO rim (flat). The key (and only the
|
|
466
|
+
// key) casts shadow maps so we get one clean shadow, not three.
|
|
467
|
+
//
|
|
468
|
+
// M3 — SHADOWS DEFAULT-ON for the studio render unless a flag disables. Real shadow maps ground
|
|
469
|
+
// the character (cast/receive) and add depth. Disable paths:
|
|
470
|
+
// - `doc.set.realShadows === false` → explicit document opt-out.
|
|
471
|
+
// - `import.meta.env.VITE_AURA_SHADOWS === "off"` → flag opt-out.
|
|
472
|
+
// The cheap contact-shadow blobs below still ground every character regardless.
|
|
473
|
+
const shadowsDisabledByFlag =
|
|
474
|
+
((import.meta as unknown as { env?: Record<string, string> }).env?.VITE_AURA_SHADOWS ?? "")
|
|
475
|
+
.trim()
|
|
476
|
+
.toLowerCase() === "off";
|
|
477
|
+
const realShadows = doc.set.realShadows !== false && !shadowsDisabledByFlag;
|
|
478
|
+
console.info(`[scene-player] real shadow maps ${realShadows ? "ON (studio key casts shadows)" : "OFF"}; 3-point key/fill/rim lighting rig.`);
|
|
479
|
+
const lights: readonly CollectedLight[] = [
|
|
480
|
+
...createStudioLighting({ preset: "inspection", shadows: realShadows, intensityScale: doc.set.studioLightingScale }),
|
|
481
|
+
...doc.set.lights.map((l): CollectedLight => {
|
|
482
|
+
const source = new PointLight(l.id);
|
|
483
|
+
source.intensity = l.intensity;
|
|
484
|
+
source.range = l.range;
|
|
485
|
+
return {
|
|
486
|
+
kind: "point",
|
|
487
|
+
color: l.color,
|
|
488
|
+
intensity: l.intensity,
|
|
489
|
+
position: l.position,
|
|
490
|
+
direction: [0, -1, 0],
|
|
491
|
+
range: l.range,
|
|
492
|
+
spotAngle: 0,
|
|
493
|
+
penumbra: 0,
|
|
494
|
+
castsShadow: false,
|
|
495
|
+
layerMask: 0xffffffff,
|
|
496
|
+
source
|
|
497
|
+
};
|
|
498
|
+
})
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
// 5. Contact shadows (one per character, at its staged ground position).
|
|
502
|
+
const shadowState = new Map<string, { position: Vec3; scale: number }>();
|
|
503
|
+
function collectCharacterShadows(): RenderItem[] {
|
|
504
|
+
const items: RenderItem[] = [];
|
|
505
|
+
for (const [, s] of shadowState) {
|
|
506
|
+
const r = 0.4 * s.scale;
|
|
507
|
+
items.push({
|
|
508
|
+
label: "contact-shadow",
|
|
509
|
+
geometry: BASE_GEOMETRY.sphere,
|
|
510
|
+
material: SHADOW_MATERIAL,
|
|
511
|
+
modelMatrix: composeMat4([s.position[0], 0.02, s.position[2] + 0.05], quatFromEuler(0, 0, 0), [r, 0.01, r * 0.7]) as Mat4,
|
|
512
|
+
includeInAutoFrame: false
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return items;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 6. Per-shot camera paths (framing from the document).
|
|
519
|
+
const cameraPathByShot = new Map<string, CameraPath>(
|
|
520
|
+
doc.shots.map((shot) => [
|
|
521
|
+
shot.shotId,
|
|
522
|
+
createCameraPathFromPreset({
|
|
523
|
+
id: `live-cam:${shot.shotId}`,
|
|
524
|
+
presetId: shot.presetId,
|
|
525
|
+
startTime: shot.startTime,
|
|
526
|
+
endTime: shot.endTime,
|
|
527
|
+
subjectPosition: [...shot.cameraSubject]
|
|
528
|
+
})
|
|
529
|
+
])
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
// 7. Renderer + render source.
|
|
533
|
+
const renderer = await A3DRenderer.create({
|
|
534
|
+
canvas,
|
|
535
|
+
width,
|
|
536
|
+
height,
|
|
537
|
+
backend: "webgl2",
|
|
538
|
+
alpha: false,
|
|
539
|
+
clearColor: [...doc.set.clearColor]
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// M3 IBL — if the set carries an `hdri` spec, BAKE a 1024×512 equirectangular environment image
|
|
543
|
+
// and wire it as the engine's real `environmentMapTexture` (image-based lighting for PBR mode). It
|
|
544
|
+
// composes ON TOP of the procedural ambient: the procedural map stays the floor, the sampled HDRI
|
|
545
|
+
// adds image-based diffuse/specular + a directional sun highlight. Absent → procedural map only.
|
|
546
|
+
let bakedHdri: BakedHdri | undefined;
|
|
547
|
+
const hdriSpec = doc.set.environment.hdri;
|
|
548
|
+
if (hdriSpec) {
|
|
549
|
+
bakedHdri = bakeSetHdri(doc.id, hdriSpec);
|
|
550
|
+
console.info(
|
|
551
|
+
`[scene-player] IBL ON — baked 1024×512 equirect HDRI (env map intensity ${bakedHdri.intensity}, specular ${bakedHdri.specularIntensity})${hdriSpec.sun ? " + sun disc" : ""}.`
|
|
552
|
+
);
|
|
553
|
+
} else {
|
|
554
|
+
console.info("[scene-player] IBL — procedural environment map only (no per-set HDRI bitmap).");
|
|
555
|
+
}
|
|
556
|
+
// I1 clean-room hardening: the baked HDRI env map is an OPTIONAL enhancement on top of the
|
|
557
|
+
// procedural ambient — it must NEVER be a hard render requirement. The binding MUST degrade
|
|
558
|
+
// gracefully (warn + continue without IBL) instead of crashing the render.
|
|
559
|
+
//
|
|
560
|
+
// Root cause of the clean-room crash (diagnosed by rendering it): in the monorepo a SINGLE deduped
|
|
561
|
+
// @aura3d/engine is loaded, so every `TextureBinding` — ours, the engine's auto-generated env-map
|
|
562
|
+
// mip chain, and its BRDF-LUT — is the SAME class and the renderer's `MaterialBinding.bind`
|
|
563
|
+
// (`value instanceof TextureBinding`, see rendering/MaterialBinding.ts) accepts them. In a CLEAN-ROOM
|
|
564
|
+
// install from tarballs the engine resolves into SEVERAL Vite-prebundled chunks: the app's
|
|
565
|
+
// `@aura3d/rendering` (which builds our env map) and `@aura3d/engine/advanced-runtime` (the renderer,
|
|
566
|
+
// which builds the internal env-map/BRDF-LUT bindings) get DIFFERENT `TextureBinding` class objects,
|
|
567
|
+
// so the `instanceof` check fails and the renderer throws `MaterialBindingError`
|
|
568
|
+
// ("u_environmentMapTexture must be texture2d" / "Unsupported uniform value"). This is unfixable from
|
|
569
|
+
// the template (the engine's OWN internal BRDF-LUT binding crosses the chunk boundary too), and a
|
|
570
|
+
// failed render permanently poisons the engine's environment resources — so we must decide BEFORE any
|
|
571
|
+
// render and never bind the env map when the engine is multi-chunk.
|
|
572
|
+
//
|
|
573
|
+
// The reliable, NON-POISONING signal is exactly that class-identity divergence: if our baked binding
|
|
574
|
+
// (from `@aura3d/rendering`) is NOT `instanceof` the engine renderer's own `TextureBinding` (from
|
|
575
|
+
// `@aura3d/engine/advanced-runtime`), the engine is multi-chunk and its internal env bindings will be
|
|
576
|
+
// rejected too → skip IBL. When they ARE the same class (the monorepo / a correctly-deduped install)
|
|
577
|
+
// IBL stays on, so this never changes the monorepo render's success.
|
|
578
|
+
let iblBinding:
|
|
579
|
+
| {
|
|
580
|
+
readonly environmentMapTexture: BakedHdri["binding"];
|
|
581
|
+
readonly environmentMapIntensity: number;
|
|
582
|
+
readonly environmentMapSpecularIntensity: number;
|
|
583
|
+
readonly environmentMapEncoding: "srgb";
|
|
584
|
+
readonly environmentMapMipCount: number;
|
|
585
|
+
}
|
|
586
|
+
| undefined;
|
|
587
|
+
if (bakedHdri) {
|
|
588
|
+
const engineWillAcceptBinding = bakedHdri.binding instanceof EngineTextureBinding;
|
|
589
|
+
if (engineWillAcceptBinding) {
|
|
590
|
+
iblBinding = {
|
|
591
|
+
environmentMapTexture: bakedHdri.binding,
|
|
592
|
+
environmentMapIntensity: bakedHdri.intensity,
|
|
593
|
+
environmentMapSpecularIntensity: bakedHdri.specularIntensity,
|
|
594
|
+
environmentMapEncoding: "srgb" as const,
|
|
595
|
+
environmentMapMipCount: 1
|
|
596
|
+
};
|
|
597
|
+
} else {
|
|
598
|
+
console.warn(
|
|
599
|
+
`[scene-player] IBL DISABLED — the baked HDRI env map cannot be bound by the renderer ` +
|
|
600
|
+
`(its TextureBinding is a different class than the engine renderer's, i.e. @aura3d/engine resolved ` +
|
|
601
|
+
`into multiple chunks). Continuing with procedural ambient lighting only; the scene still renders ` +
|
|
602
|
+
`WITHOUT image-based lighting. (Expected in a clean-room install; IBL is an enhancement, not a requirement.)`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const source: RenderSource = {
|
|
608
|
+
collectRenderItems: () => [
|
|
609
|
+
...setItems,
|
|
610
|
+
...collectSetDressing(),
|
|
611
|
+
...collectCharacterShadows(),
|
|
612
|
+
...characters.flatMap((c) => c.actor.collectRenderItems())
|
|
613
|
+
],
|
|
614
|
+
cameraPolicy: "auto-frame",
|
|
615
|
+
cameraFrameBounds: { min: [-2.6, -0.1, -1.6], max: [2.6, 3.4, 1.6] },
|
|
616
|
+
collectedLights: lights,
|
|
617
|
+
environmentLighting: {
|
|
618
|
+
color: [...doc.set.environment.color],
|
|
619
|
+
intensity: doc.set.environment.intensity,
|
|
620
|
+
proceduralMap: {
|
|
621
|
+
skyColor: [...doc.set.environment.proceduralMap.skyColor],
|
|
622
|
+
horizonColor: [...doc.set.environment.proceduralMap.horizonColor],
|
|
623
|
+
groundColor: [...doc.set.environment.proceduralMap.groundColor],
|
|
624
|
+
specularColor: [...doc.set.environment.proceduralMap.specularColor],
|
|
625
|
+
intensity: doc.set.environment.proceduralMap.intensity,
|
|
626
|
+
specularIntensity: doc.set.environment.proceduralMap.specularIntensity
|
|
627
|
+
},
|
|
628
|
+
// Real IBL: equirect 2D environment map sampled for diffuse irradiance + specular reflection.
|
|
629
|
+
// `iblBinding` is set above ONLY when the baked env map is the engine's own `TextureBinding`
|
|
630
|
+
// class (so the renderer's `instanceof` validator accepts it); otherwise it is undefined and
|
|
631
|
+
// IBL is skipped, leaving the procedural ambient as the lighting floor.
|
|
632
|
+
...(iblBinding ?? {})
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// 8. Pose at episode time `t`, drive camera + lip-sync + world-state, render.
|
|
637
|
+
const poseAt = (time: number, options: LiveSeekOptions = {}): LiveRouteSeekProof => {
|
|
638
|
+
const shot = shotAtTime(doc, time);
|
|
639
|
+
const gardenGlow = sampleWorldStateGlow(doc, time);
|
|
640
|
+
applyWorldGlow(gardenGlow);
|
|
641
|
+
|
|
642
|
+
// Resolve the active speaker (the AuraVoice dialogue contract) once per frame: it drives both
|
|
643
|
+
// the per-character intent (talk vs react/listen) and the look-at (everyone faces the speaker).
|
|
644
|
+
const captionNow = sampleCaption(doc, time);
|
|
645
|
+
const activeSpeakerId = captionNow.speakerId;
|
|
646
|
+
const anyDialogue = activeSpeakerId.length > 0;
|
|
647
|
+
// Stage each character's ground mark first so look-at can aim non-speakers at the speaker.
|
|
648
|
+
const staged = characters.map((character) => ({
|
|
649
|
+
character,
|
|
650
|
+
blocking: sampleBlocking(doc, character.id, time, { position: [0, 0, 0], yaw: 0, clip: character.defaultClip })
|
|
651
|
+
}));
|
|
652
|
+
const speakerMark = staged.find((s) => s.character.id === activeSpeakerId)?.blocking.position;
|
|
653
|
+
|
|
654
|
+
// B6 — per-character foot-handling status for this frame (surfaced in the free-form debug
|
|
655
|
+
// overlay below). `rootGrounding` for sparse rigs; `footIK(n grounded)` where true IK ran.
|
|
656
|
+
const footStatus = new Map<string, string>();
|
|
657
|
+
|
|
658
|
+
const characterProofs: LiveRouteSeekProof["characters"] = staged.map(({ character, blocking }) => {
|
|
659
|
+
const speaking = character.id === activeSpeakerId;
|
|
660
|
+
// Run when traversing a long distance (the standard `run` intent); else walk while moving.
|
|
661
|
+
const running = blocking.moving && /run|sprint/i.test(blocking.clip);
|
|
662
|
+
// B7 — speaking performance: a talking beat must move MORE than the mouth. `resolveIntent`
|
|
663
|
+
// returns `talk` for the active speaker (independent of whether the rig even HAS a mouth
|
|
664
|
+
// morph), so `poseFor("talk")` below plays the standard talk clip — head + torso/shoulder
|
|
665
|
+
// body motion — for EVERY speaker, including mouthless rigs (mouthMorphIndex < 0). The mouth
|
|
666
|
+
// morph (when present) is layered ON TOP, never a substitute for the body performance.
|
|
667
|
+
const intent = resolveIntent({
|
|
668
|
+
clip: blocking.clip,
|
|
669
|
+
moving: blocking.moving,
|
|
670
|
+
running,
|
|
671
|
+
speaking,
|
|
672
|
+
anyDialogue
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
// 2.6 / B7 lip-sync: mouth-openness from the dialogue/viseme track (speaker only). The morph
|
|
676
|
+
// (if the rig has one) is driven below. B7 — prevent a long STATIC mouth-open hold: the viseme
|
|
677
|
+
// must PULSE with the dialogue. `sampleVisemeOpenness` oscillates but never closes (min ≈0.18),
|
|
678
|
+
// so a held line would read as a frozen open mouth. We gate it with a faster syllable-cadence
|
|
679
|
+
// pulse (≈3.3 Hz) that dips toward closed between syllables — only while this character is the
|
|
680
|
+
// active speaker — so the mouth visibly opens AND closes across the line instead of holding open.
|
|
681
|
+
const viseme = sampleVisemeOpenness(doc, time, character.id);
|
|
682
|
+
let mouthOpenness = options.mouthOverride ?? viseme.mouthOpenness;
|
|
683
|
+
if (options.mouthOverride === undefined && speaking && mouthOpenness > 0) {
|
|
684
|
+
// Syllable gate in [0,1]: a raised-cosine pulse that returns near 0 between syllables, so the
|
|
685
|
+
// mouth closes (not just dims) between beats. ~3.3 syllables/sec ≈ natural speaking cadence.
|
|
686
|
+
const syllableGate = 0.5 - 0.5 * Math.cos(time * Math.PI * 2 * 3.3);
|
|
687
|
+
// Map the (never-closing) sampled openness through the gate: full open at a syllable peak,
|
|
688
|
+
// near-closed in the trough — eliminating the static open hold while keeping lip motion lively.
|
|
689
|
+
mouthOpenness = mouthOpenness * (0.18 + 0.82 * syllableGate);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// B3 motion-source order (explicit + logged via decision.source):
|
|
693
|
+
// (1) extracted universal-catalog clip for this intent → (2) procedural standard clip
|
|
694
|
+
// — both produced by `poseFor` (the library is the DEFAULT first choice),
|
|
695
|
+
// (3) a RICH, genuinely-matching embedded GLB clip ONLY when the library pose is empty
|
|
696
|
+
// (idle-fallback) or carries no real body motion,
|
|
697
|
+
// (4) idle last resort (already handled inside `poseFor`).
|
|
698
|
+
const libraryResult = character.perf.poseFor(intent, time);
|
|
699
|
+
let decision: ClipDecision = libraryResult.decision;
|
|
700
|
+
|
|
701
|
+
// A rich embedded clip is a fallback, not the default: only consider it when the library gave
|
|
702
|
+
// us nothing usable for this intent (no body motion) AND the GLB ships a non-trivial match.
|
|
703
|
+
const libraryWeak = decision.source === "idle-fallback" || decision.bonesTouched === 0;
|
|
704
|
+
const embedded =
|
|
705
|
+
libraryWeak && character.perf.preferEmbedded
|
|
706
|
+
? pickClip(embeddedClipForIntent(intent, character.availableClips) ?? "", character.availableClips)
|
|
707
|
+
: undefined;
|
|
708
|
+
|
|
709
|
+
let apply: ReturnType<TypedGLBActor["playClip"]>;
|
|
710
|
+
let resolvedClip: string;
|
|
711
|
+
if (embedded) {
|
|
712
|
+
apply = character.actor.playClip(embedded, time);
|
|
713
|
+
resolvedClip = embedded;
|
|
714
|
+
// We played the GLB's own embedded clip; record that as the motion source. (Per-bone body
|
|
715
|
+
// amplitude isn't measured for embedded playback here — the gate treats embedded as a
|
|
716
|
+
// distinct, lower-confidence source than the rig-neutral library pose.)
|
|
717
|
+
decision = { ...decision, clipId: intent, source: "embedded" as ClipSource };
|
|
718
|
+
} else {
|
|
719
|
+
const pose = libraryResult.pose;
|
|
720
|
+
// GLTFScenePose accepts {x,y,z}/{x,y,z,w} transforms; same shape the retargeter emits.
|
|
721
|
+
apply = character.actor.applyRetargetedPose(pose as never, time);
|
|
722
|
+
resolvedClip = intent;
|
|
723
|
+
}
|
|
724
|
+
// B1 — the pose/clip actually reached the GLB skeleton at runtime when the actor reported it
|
|
725
|
+
// wrote ≥1 skinned track (tracksApplied) or updated ≥1 skinning palette. A decision whose
|
|
726
|
+
// motion never touched a real skeleton node (tracksApplied===0) is a SILENT no-op and must NOT
|
|
727
|
+
// be trusted by the gate as "the body moved" — `reachedGLBRuntime:false` surfaces that.
|
|
728
|
+
decision = {
|
|
729
|
+
...decision,
|
|
730
|
+
reachedGLBRuntime: apply.tracksApplied > 0 || apply.skinningPalettesUpdated > 0
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// 2.7 look-at: non-speakers turn to face the active speaker; otherwise honor the directed yaw.
|
|
734
|
+
let yaw = blocking.yaw;
|
|
735
|
+
if (!speaking && speakerMark && !blocking.moving) {
|
|
736
|
+
const dx = speakerMark[0] - blocking.position[0];
|
|
737
|
+
const dz = speakerMark[2] - blocking.position[2];
|
|
738
|
+
if (Math.hypot(dx, dz) > 0.05) yaw = Math.atan2(dx, dz);
|
|
739
|
+
}
|
|
740
|
+
const rotation = quatFromEuler(0, yaw, 0);
|
|
741
|
+
// B6 — `rootGrounding` (NOT true IK): pin the staged root at y from the blocking mark
|
|
742
|
+
// (ground = 0) so the retargeted hip-bob / walk cycle animates the body WITHOUT the
|
|
743
|
+
// character floating off the floor. This is honest root grounding — it does NOT solve the
|
|
744
|
+
// leg chain. It is the baseline for EVERY character and the ONLY foot handling for sparse /
|
|
745
|
+
// mascot rigs (grade C, no ankle chain). The contact-shadow blob tracks the same mark below.
|
|
746
|
+
let rootY = blocking.position[1];
|
|
747
|
+
// B6 — TRUE foot IK: ONLY for rigs that actually carry the leg/knee/ankle/foot chain (set up
|
|
748
|
+
// at load time as `character.footIk`). The engine's real two-bone solver drops the hip so the
|
|
749
|
+
// lower foot reaches the ground and foot-locks a planted foot in world space (no slide). We
|
|
750
|
+
// apply the solved hip offset on top of rootGrounding; sparse rigs skip this entirely.
|
|
751
|
+
let footIkGroundedFeet = -1; // -1 ⇒ rootGrounding only (no true IK on this rig).
|
|
752
|
+
if (character.footIk) {
|
|
753
|
+
try {
|
|
754
|
+
const ik = character.footIk.solveFootPlacement();
|
|
755
|
+
rootY += ik.hipOffset; // hipOffset ≤ 0: lower the hip so the planted foot reaches ground.
|
|
756
|
+
footIkGroundedFeet = ik.groundedFeet;
|
|
757
|
+
} catch {
|
|
758
|
+
// Non-fatal: fall back to rootGrounding for this frame if the solve throws.
|
|
759
|
+
footIkGroundedFeet = -1;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
character.actor.pipeline.resources.scene.root.transform
|
|
763
|
+
.setPosition(blocking.position[0], rootY, blocking.position[2])
|
|
764
|
+
.setRotation(rotation[0], rotation[1], rotation[2], rotation[3])
|
|
765
|
+
.setScale(character.scale, character.scale, character.scale);
|
|
766
|
+
footStatus.set(
|
|
767
|
+
character.id,
|
|
768
|
+
footIkGroundedFeet >= 0
|
|
769
|
+
? `footIK(grade ${character.rigGrade.grade}, ${footIkGroundedFeet} grounded)`
|
|
770
|
+
: `rootGrounding(grade ${character.rigGrade.grade})`
|
|
771
|
+
);
|
|
772
|
+
shadowState.set(character.id, { position: blocking.position, scale: character.scale });
|
|
773
|
+
|
|
774
|
+
let mouthMorphWeight = -1;
|
|
775
|
+
if (character.mouthMorphIndex >= 0) {
|
|
776
|
+
mouthMorphWeight = mouthOpenness;
|
|
777
|
+
for (const renderable of character.morphRenderables) {
|
|
778
|
+
while (renderable.morphWeights.length <= character.mouthMorphIndex) renderable.morphWeights.push(0);
|
|
779
|
+
renderable.morphWeights[character.mouthMorphIndex] = mouthMorphWeight;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return {
|
|
784
|
+
id: character.id,
|
|
785
|
+
clip: embedded ? `${resolvedClip} (embedded:${intent})` : `${intent} (${decision.source})`,
|
|
786
|
+
position: blocking.position,
|
|
787
|
+
sweeping: blocking.sweeping,
|
|
788
|
+
tracksApplied: apply.tracksApplied,
|
|
789
|
+
skinningPalettesUpdated: apply.skinningPalettesUpdated,
|
|
790
|
+
skinningBindingCount: character.actor.snapshot().skinningBindingCount,
|
|
791
|
+
mouthOpenness,
|
|
792
|
+
visemeId: viseme.visemeId,
|
|
793
|
+
mouthMorphWeight,
|
|
794
|
+
mouthMorphIndex: character.mouthMorphIndex,
|
|
795
|
+
primitiveMouthOpen: 0,
|
|
796
|
+
clipDecision: { ...decision, characterId: character.id, time: +time.toFixed(3) }
|
|
797
|
+
};
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
const captionCue = sampleCaption(doc, time);
|
|
801
|
+
const caption = { text: captionCue.text, speakerId: captionCue.speakerId, captionId: captionCue.lineId };
|
|
802
|
+
|
|
803
|
+
const path = cameraPathByShot.get(shot.shotId)!;
|
|
804
|
+
const camera = cameraViewProjection(path, time, aspect);
|
|
805
|
+
const diagnostics = renderer.render(source, { viewProjectionMatrix: camera.viewProjectionMatrix });
|
|
806
|
+
|
|
807
|
+
const skinnedRenderItems = characters.reduce(
|
|
808
|
+
(sum, character) => sum + character.actor.collectRenderItems().filter((item) => item.skinning).length,
|
|
809
|
+
0
|
|
810
|
+
);
|
|
811
|
+
// B1 debug overlay: one inspectable line per character with clip←intent [source] + body motion.
|
|
812
|
+
const debugOverlay = characterProofs.map((c) => {
|
|
813
|
+
const d = c.clipDecision;
|
|
814
|
+
const foot = footStatus.get(c.id) ?? "rootGrounding";
|
|
815
|
+
const glb = d.reachedGLBRuntime ? "GLB✓" : "GLB✗";
|
|
816
|
+
return `${c.id}: ${d.clipId}←${d.intent} [${d.source}] ${d.bonesTouched}bones ${d.maxRotAmplitudeRad.toFixed(3)}rad transΔ${d.maxTransAmplitude.toFixed(3)} rootΔ${d.rootTranslation.toFixed(3)} ${glb} ${foot}`;
|
|
817
|
+
});
|
|
818
|
+
// B1 — paint the overlay into the proof DOM each frame (only when AURA_DEBUG_OVERLAY=1). The
|
|
819
|
+
// overlay element is always present so a test can assert it exists; it stays hidden otherwise.
|
|
820
|
+
if (debugOverlayEl && debugOverlayEnabled) {
|
|
821
|
+
debugOverlayEl.textContent = `t=${time.toFixed(2)}s shot=${shot.shotId}\n${debugOverlay.join("\n")}`;
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
time,
|
|
825
|
+
drawCalls: diagnostics.drawCalls,
|
|
826
|
+
skinnedRenderItems,
|
|
827
|
+
gardenGlow,
|
|
828
|
+
shot: {
|
|
829
|
+
shotId: shot.shotId,
|
|
830
|
+
presetId: String(shot.presetId),
|
|
831
|
+
episodeTime: (shot.startTime + shot.endTime) / 2,
|
|
832
|
+
cameraPosition: camera.position,
|
|
833
|
+
fov: camera.fov
|
|
834
|
+
},
|
|
835
|
+
caption,
|
|
836
|
+
debugOverlay,
|
|
837
|
+
characters: characterProofs
|
|
838
|
+
};
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
liveWindow.__auraSeek__ = poseAt;
|
|
842
|
+
liveWindow.__AURA_LIVE_ROUTE_READY__ = {
|
|
843
|
+
ready: true,
|
|
844
|
+
backend: renderer.device.kind,
|
|
845
|
+
characters: characters.map((character) => {
|
|
846
|
+
const snapshot = character.actor.snapshot();
|
|
847
|
+
return {
|
|
848
|
+
id: character.id,
|
|
849
|
+
url: character.actor.asset.url,
|
|
850
|
+
clip: pickClip(character.defaultClip, character.availableClips) ?? character.defaultClip,
|
|
851
|
+
clips: snapshot.clips,
|
|
852
|
+
skinningBindingCount: snapshot.skinningBindingCount,
|
|
853
|
+
skinnedRenderItemCount: character.actor.evidence.skinnedRenderItemCount
|
|
854
|
+
};
|
|
855
|
+
})
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
// Live preview loop (skipped during headless seek capture).
|
|
859
|
+
let start = performance.now();
|
|
860
|
+
const loop = (): void => {
|
|
861
|
+
if (!liveWindow.__AURA_LIVE_ROUTE_HEADLESS__) poseAt((performance.now() - start) / 1000);
|
|
862
|
+
window.requestAnimationFrame(loop);
|
|
863
|
+
};
|
|
864
|
+
poseAt(0);
|
|
865
|
+
start = performance.now();
|
|
866
|
+
window.requestAnimationFrame(loop);
|
|
867
|
+
}
|