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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fidelity.ts — PRD Phase M7: honest quality tiering.
|
|
3
|
+
*
|
|
4
|
+
* Surfaces a single, user-facing FIDELITY GRADE (A / B / C) per character and per
|
|
5
|
+
* scene, derived from the REAL signals the pipeline already records. The grade is
|
|
6
|
+
* how we keep the product honest: a C scene is "previz", never sold as finished.
|
|
7
|
+
*
|
|
8
|
+
* The grade is a deliberate DOWNGRADE-only combination of the inputs the PRD lists:
|
|
9
|
+
*
|
|
10
|
+
* A — a curated/uploaded properly-rigged character (rig grade A) playing real
|
|
11
|
+
* (mocap/extracted) motion, rendered with cel-or-PBR shading AND shadows.
|
|
12
|
+
* B — a catalog rig that graded OK (rig grade A/B) but is missing one of the
|
|
13
|
+
* A ingredients (procedural motion, or shadows off): a watchable catalog take.
|
|
14
|
+
* C — a mascot / minimal rig (rig grade C/D) OR an authored placeholder: PREVIZ.
|
|
15
|
+
* Never present a C as a finished shot.
|
|
16
|
+
*
|
|
17
|
+
* The function is PURE (no IO) so it can run identically in the render-time resolver
|
|
18
|
+
* report and in the browser UI. Both sides import the SAME grading rules.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export type FidelityGrade = "A" | "B" | "C";
|
|
22
|
+
|
|
23
|
+
/** Rig grade as produced by the resolver (`RigGrade`) / `gradeRig` (A best … D unusable). */
|
|
24
|
+
export type FidelityRigGrade = "A" | "B" | "C" | "D";
|
|
25
|
+
|
|
26
|
+
/** Where a character's geometry/rig came from — drives the mascot/previz floor. */
|
|
27
|
+
export type FidelityProvenance = "curated" | "user-uploaded" | "catalog-resolved" | "authored-fallback";
|
|
28
|
+
|
|
29
|
+
/** The motion actually played — real extracted/mocap clips vs the procedural baseline. */
|
|
30
|
+
export type FidelityMotionSource = "mocap" | "extracted" | "embedded" | "procedural" | "idle" | "talk" | "fallback" | "unknown";
|
|
31
|
+
|
|
32
|
+
export interface CharacterFidelityInput {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
/** Rig grade from the resolver / `gradeRig`. Absent → treated as the previz floor. */
|
|
35
|
+
readonly rigGrade?: FidelityRigGrade;
|
|
36
|
+
readonly provenance: FidelityProvenance;
|
|
37
|
+
/** Dominant motion source the render actually played for this character. */
|
|
38
|
+
readonly motionSource?: FidelityMotionSource;
|
|
39
|
+
/** Shading mode the scene rendered with (cel/pbr both count; "none" does not). */
|
|
40
|
+
readonly shading?: "cel" | "pbr" | "none";
|
|
41
|
+
/** Whether real shadow maps were enabled (grounding + depth). */
|
|
42
|
+
readonly shadows?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CharacterFidelity {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly grade: FidelityGrade;
|
|
48
|
+
/** True for grade C — the UI must label this output "previz". */
|
|
49
|
+
readonly previz: boolean;
|
|
50
|
+
/** Short human reason the grade landed where it did (shown in the report + UI tooltip). */
|
|
51
|
+
readonly reason: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SceneFidelity {
|
|
55
|
+
readonly grade: FidelityGrade;
|
|
56
|
+
readonly previz: boolean;
|
|
57
|
+
readonly characters: readonly CharacterFidelity[];
|
|
58
|
+
readonly reason: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const MASCOT_PROVENANCE = new Set<FidelityProvenance>(["authored-fallback"]);
|
|
62
|
+
const REAL_MOTION = new Set<FidelityMotionSource>(["mocap", "extracted", "embedded"]);
|
|
63
|
+
const CURATED_PROVENANCE = new Set<FidelityProvenance>(["curated", "user-uploaded"]);
|
|
64
|
+
|
|
65
|
+
/** Grade ONE character. Pure; downgrade-only from the A ingredients. */
|
|
66
|
+
export function gradeCharacterFidelity(input: CharacterFidelityInput): CharacterFidelity {
|
|
67
|
+
const rig = input.rigGrade ?? "D";
|
|
68
|
+
const provenance = input.provenance;
|
|
69
|
+
const motion = input.motionSource ?? "unknown";
|
|
70
|
+
const shading = input.shading ?? "none";
|
|
71
|
+
const shadows = input.shadows ?? false;
|
|
72
|
+
|
|
73
|
+
// PREVIZ FLOOR (C): a mascot/authored placeholder, or a rig too sparse to act (C/D),
|
|
74
|
+
// is previz regardless of the other ingredients — its body acting is unreliable.
|
|
75
|
+
if (MASCOT_PROVENANCE.has(provenance) || rig === "C" || rig === "D") {
|
|
76
|
+
const why =
|
|
77
|
+
MASCOT_PROVENANCE.has(provenance)
|
|
78
|
+
? "authored placeholder / mascot rig"
|
|
79
|
+
: `sparse rig (grade ${rig}) — crude retarget only`;
|
|
80
|
+
return { id: input.id, grade: "C", previz: true, reason: `previz: ${why}` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A: curated/uploaded rig (grade A) + real motion + shading + shadows — the full stack.
|
|
84
|
+
const realMotion = REAL_MOTION.has(motion);
|
|
85
|
+
const shaded = shading === "cel" || shading === "pbr";
|
|
86
|
+
if (CURATED_PROVENANCE.has(provenance) && rig === "A" && realMotion && shaded && shadows) {
|
|
87
|
+
return {
|
|
88
|
+
id: input.id,
|
|
89
|
+
grade: "A",
|
|
90
|
+
previz: false,
|
|
91
|
+
reason: "curated/uploaded grade-A rig with real motion, shading and shadows"
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// B: a catalog (or curated) rig that graded OK (A/B) but is missing one A ingredient
|
|
96
|
+
// (procedural motion, shadows off, or unshaded) — a watchable catalog take, not previz.
|
|
97
|
+
const missing: string[] = [];
|
|
98
|
+
if (!realMotion) missing.push("procedural motion");
|
|
99
|
+
if (!shaded) missing.push("no cel/PBR shading");
|
|
100
|
+
if (!shadows) missing.push("shadows off");
|
|
101
|
+
if (!CURATED_PROVENANCE.has(provenance)) missing.push("catalog rig");
|
|
102
|
+
return {
|
|
103
|
+
id: input.id,
|
|
104
|
+
grade: "B",
|
|
105
|
+
previz: false,
|
|
106
|
+
reason: missing.length ? `catalog-grade: ${missing.join(", ")}` : "graded-ok rig"
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Grade a whole SCENE: the floor of its characters (worst grade wins; empty scene → C/previz). */
|
|
111
|
+
export function gradeSceneFidelity(characters: readonly CharacterFidelityInput[]): SceneFidelity {
|
|
112
|
+
const graded = characters.map(gradeCharacterFidelity);
|
|
113
|
+
if (graded.length === 0) {
|
|
114
|
+
return { grade: "C", previz: true, characters: graded, reason: "previz: no characters in the scene" };
|
|
115
|
+
}
|
|
116
|
+
const order: Record<FidelityGrade, number> = { A: 3, B: 2, C: 1 };
|
|
117
|
+
let worst: FidelityGrade = "A";
|
|
118
|
+
for (const g of graded) if (order[g.grade] < order[worst]) worst = g.grade;
|
|
119
|
+
const previz = worst === "C";
|
|
120
|
+
const reason = previz
|
|
121
|
+
? `previz: lowest character grade is C (${graded.filter((g) => g.grade === "C").map((g) => g.id).join(", ")})`
|
|
122
|
+
: `scene grade ${worst} (floor of ${graded.length} character grade(s))`;
|
|
123
|
+
return { grade: worst, previz, characters: graded, reason };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Human one-word tier label for a grade (used by report + UI badge). */
|
|
127
|
+
export function fidelityLabel(grade: FidelityGrade): string {
|
|
128
|
+
return grade === "C" ? "Previz" : `Grade ${grade}`;
|
|
129
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Runtime locomotion composition for the preview route. Uses the public @aura3d/animation
|
|
2
|
+
// locomotion kit (BlendTree1D + locomotion state graph) built from the typed character.
|
|
3
|
+
import { createLocomotionKit, type LocomotionKit } from "@aura3d/animation";
|
|
4
|
+
import { heroCharacter, type AnimationStudioCharacter } from "./character.js";
|
|
5
|
+
|
|
6
|
+
export function buildLocomotionKit(character: AnimationStudioCharacter = heroCharacter): LocomotionKit {
|
|
7
|
+
return createLocomotionKit({
|
|
8
|
+
idleClip: character.clipMap.idle,
|
|
9
|
+
walkClip: character.clipMap.walk,
|
|
10
|
+
runClip: character.clipMap.run,
|
|
11
|
+
walkSpeed: character.walkSpeed,
|
|
12
|
+
runSpeed: character.runSpeed
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* procedural-hdri.ts — bake a per-set equirectangular HDRI and wire it as real IBL (Phase M3).
|
|
3
|
+
*
|
|
4
|
+
* M3's remaining optional item was "load an HDRI *bitmap* per set" for image-based lighting in PBR
|
|
5
|
+
* mode. Rather than ship licensed .hdr files, we GENERATE an equirectangular environment IMAGE
|
|
6
|
+
* procedurally from each set's `EnvironmentHdriSpec` (a sky→horizon→ground vertical gradient plus an
|
|
7
|
+
* optional sun disc) and build a real `@aura3d/rendering` `Texture` + `TextureBinding` from it. The
|
|
8
|
+
* engine's forward pass already samples `environmentMapTexture` as a 2D equirect map for both diffuse
|
|
9
|
+
* irradiance and specular reflection (`u_environmentMapTexture*` uniforms in ForwardPass), so this is
|
|
10
|
+
* genuine IBL — not just a richer procedural ambient. The baked map composes ON TOP of the existing
|
|
11
|
+
* `proceduralMap` ambient: the procedural map remains the floor, the sampled HDRI adds image-based
|
|
12
|
+
* specular/diffuse detail and a directional sun highlight.
|
|
13
|
+
*
|
|
14
|
+
* The bitmap is generated in-memory (1024×512 RGBA8) so there is no asset to ship or fetch; it is
|
|
15
|
+
* deterministic from the spec. A PNG encoder is included so a set's HDRI can optionally be written to
|
|
16
|
+
* `public/hdri/<id>.png` for inspection/debug, but the render path uses the in-memory pixels directly.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// I1 clean-room correctness: import the rendering primitives (Sampler/Texture/TextureBinding) from
|
|
20
|
+
// the engine's OWN rendering subpath — the same copy the A3DRenderer validates against — NOT the
|
|
21
|
+
// standalone `@aura3d/rendering` package. In a clean-room (tarball) install the standalone package is
|
|
22
|
+
// a SECOND copy, so a `TextureBinding` built from it is a different class than the renderer's and is
|
|
23
|
+
// rejected by `MaterialBinding.bind` (`value instanceof TextureBinding`). Going through the engine
|
|
24
|
+
// keeps the baked HDRI's binding the same class as the renderer's, so IBL binds across the install.
|
|
25
|
+
import { Sampler, Texture, TextureBinding } from "@aura3d/engine/rendering";
|
|
26
|
+
import type { EnvironmentHdriSpec, Vec3 } from "./episode-document";
|
|
27
|
+
|
|
28
|
+
export const HDRI_WIDTH = 1024;
|
|
29
|
+
export const HDRI_HEIGHT = 512;
|
|
30
|
+
|
|
31
|
+
function clamp01(x: number): number {
|
|
32
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Linear interpolate between two RGB triples. */
|
|
36
|
+
function mixRgb(a: Vec3, b: Vec3, t: number): Vec3 {
|
|
37
|
+
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Convert a linear 0..1+ channel to an 8-bit sRGB-ish byte (gamma 2.2), clamped. */
|
|
41
|
+
function toByte(linear: number): number {
|
|
42
|
+
const g = Math.pow(clamp01(linear), 1 / 2.2);
|
|
43
|
+
return Math.round(clamp01(g) * 255);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bake the equirectangular RGBA8 pixel buffer for a set HDRI. Row 0 is the zenith (sky), the middle
|
|
48
|
+
* row is the horizon, the bottom row is the nadir (ground bounce). The sun disc, if present, is drawn
|
|
49
|
+
* into the sky hemisphere as a soft bright spot for a directional specular highlight.
|
|
50
|
+
*/
|
|
51
|
+
export function bakeEquirectHdriPixels(spec: EnvironmentHdriSpec): Uint8Array {
|
|
52
|
+
const data = new Uint8Array(HDRI_WIDTH * HDRI_HEIGHT * 4);
|
|
53
|
+
const sun = spec.sun;
|
|
54
|
+
// Precompute the sun's world direction (unit) so each texel can dot against it.
|
|
55
|
+
let sunDir: Vec3 | null = null;
|
|
56
|
+
if (sun) {
|
|
57
|
+
const ce = Math.cos(sun.elevation);
|
|
58
|
+
sunDir = [ce * Math.sin(sun.azimuth), Math.sin(sun.elevation), ce * Math.cos(sun.azimuth)];
|
|
59
|
+
}
|
|
60
|
+
const cosSunRadius = sun ? Math.cos(sun.angularRadius) : 1;
|
|
61
|
+
|
|
62
|
+
for (let y = 0; y < HDRI_HEIGHT; y++) {
|
|
63
|
+
// v: 0 at top (zenith) → 1 at bottom (nadir). Equirect latitude = (0.5 - v) * π.
|
|
64
|
+
const v = (y + 0.5) / HDRI_HEIGHT;
|
|
65
|
+
const lat = (0.5 - v) * Math.PI; // +π/2 zenith, 0 horizon, -π/2 nadir.
|
|
66
|
+
// Vertical gradient: sky→horizon over the top hemisphere, horizon→ground over the bottom.
|
|
67
|
+
let base: Vec3;
|
|
68
|
+
if (lat >= 0) {
|
|
69
|
+
base = mixRgb(spec.horizonColor, spec.skyColor, clamp01(lat / (Math.PI / 2)));
|
|
70
|
+
} else {
|
|
71
|
+
base = mixRgb(spec.horizonColor, spec.groundColor, clamp01(-lat / (Math.PI / 2)));
|
|
72
|
+
}
|
|
73
|
+
const sinLat = Math.sin(lat);
|
|
74
|
+
const cosLat = Math.cos(lat);
|
|
75
|
+
for (let x = 0; x < HDRI_WIDTH; x++) {
|
|
76
|
+
// u: 0..1 around the sphere; longitude = (u - 0.5) * 2π.
|
|
77
|
+
const u = (x + 0.5) / HDRI_WIDTH;
|
|
78
|
+
const lon = (u - 0.5) * 2 * Math.PI;
|
|
79
|
+
let r = base[0];
|
|
80
|
+
let g = base[1];
|
|
81
|
+
let b = base[2];
|
|
82
|
+
if (sunDir && lat > -0.05) {
|
|
83
|
+
// Texel direction on the unit sphere.
|
|
84
|
+
const dx = cosLat * Math.sin(lon);
|
|
85
|
+
const dy = sinLat;
|
|
86
|
+
const dz = cosLat * Math.cos(lon);
|
|
87
|
+
const dot = dx * sunDir[0] + dy * sunDir[1] + dz * sunDir[2];
|
|
88
|
+
if (dot > cosSunRadius) {
|
|
89
|
+
// Inside the disc: full sun. Add a soft glow falloff just outside via smoothstep.
|
|
90
|
+
const k = sun!.intensity;
|
|
91
|
+
r += sun!.color[0] * k;
|
|
92
|
+
g += sun!.color[1] * k;
|
|
93
|
+
b += sun!.color[2] * k;
|
|
94
|
+
} else if (dot > cosSunRadius - 0.04) {
|
|
95
|
+
const t = (dot - (cosSunRadius - 0.04)) / 0.04;
|
|
96
|
+
const halo = t * t * 0.5 * sun!.intensity;
|
|
97
|
+
r += sun!.color[0] * halo;
|
|
98
|
+
g += sun!.color[1] * halo;
|
|
99
|
+
b += sun!.color[2] * halo;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const i = (y * HDRI_WIDTH + x) * 4;
|
|
103
|
+
data[i] = toByte(r);
|
|
104
|
+
data[i + 1] = toByte(g);
|
|
105
|
+
data[i + 2] = toByte(b);
|
|
106
|
+
data[i + 3] = 255;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface BakedHdri {
|
|
113
|
+
readonly texture: Texture;
|
|
114
|
+
readonly binding: TextureBinding;
|
|
115
|
+
readonly intensity: number;
|
|
116
|
+
readonly specularIntensity: number;
|
|
117
|
+
readonly pixels: Uint8Array;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Bake a set's HDRI spec into a real engine IBL: an equirect `Texture` + a `TextureBinding`
|
|
122
|
+
* (`u_environmentMapTexture`, equirect → repeat U / clamp V like the engine's parity preset). The
|
|
123
|
+
* returned `intensity`/`specularIntensity` feed the engine's `environmentMapIntensity` /
|
|
124
|
+
* `environmentMapSpecularIntensity`.
|
|
125
|
+
*/
|
|
126
|
+
export function bakeSetHdri(id: string, spec: EnvironmentHdriSpec): BakedHdri {
|
|
127
|
+
const pixels = bakeEquirectHdriPixels(spec);
|
|
128
|
+
const texture = new Texture({
|
|
129
|
+
width: HDRI_WIDTH,
|
|
130
|
+
height: HDRI_HEIGHT,
|
|
131
|
+
colorSpace: "srgb",
|
|
132
|
+
label: `animation-studio-hdri-${id}`,
|
|
133
|
+
data: pixels
|
|
134
|
+
});
|
|
135
|
+
const binding = new TextureBinding({
|
|
136
|
+
name: "u_environmentMapTexture",
|
|
137
|
+
texture,
|
|
138
|
+
// Equirect: wrap horizontally (longitude is periodic), clamp vertically (poles).
|
|
139
|
+
sampler: new Sampler({ minFilter: "linear", magFilter: "linear", addressU: "repeat", addressV: "clamp-to-edge" }),
|
|
140
|
+
expectedColorSpace: "srgb",
|
|
141
|
+
expectedDimension: "2d",
|
|
142
|
+
required: true
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
texture,
|
|
146
|
+
binding,
|
|
147
|
+
intensity: spec.intensity ?? 1,
|
|
148
|
+
specularIntensity: spec.specularIntensity ?? 0.4,
|
|
149
|
+
pixels
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Minimal PNG encoder (debug only) — lets `public/hdri/<id>.png` be written for inspection. The
|
|
155
|
+
// render path does NOT depend on this; it samples the in-memory pixels directly.
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
function crc32(bytes: Uint8Array): number {
|
|
159
|
+
let crc = 0xffffffff;
|
|
160
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
161
|
+
crc ^= bytes[i]!;
|
|
162
|
+
for (let k = 0; k < 8; k++) {
|
|
163
|
+
crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function adler32(bytes: Uint8Array): number {
|
|
170
|
+
let a = 1;
|
|
171
|
+
let b = 0;
|
|
172
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
173
|
+
a = (a + bytes[i]!) % 65521;
|
|
174
|
+
b = (b + a) % 65521;
|
|
175
|
+
}
|
|
176
|
+
return ((b << 16) | a) >>> 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function u32(n: number): Uint8Array {
|
|
180
|
+
return new Uint8Array([(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff]);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function chunk(type: string, payload: Uint8Array): Uint8Array {
|
|
184
|
+
const typeBytes = new Uint8Array([type.charCodeAt(0), type.charCodeAt(1), type.charCodeAt(2), type.charCodeAt(3)]);
|
|
185
|
+
const body = new Uint8Array(typeBytes.length + payload.length);
|
|
186
|
+
body.set(typeBytes, 0);
|
|
187
|
+
body.set(payload, typeBytes.length);
|
|
188
|
+
const out = new Uint8Array(4 + body.length + 4);
|
|
189
|
+
out.set(u32(payload.length), 0);
|
|
190
|
+
out.set(body, 4);
|
|
191
|
+
out.set(u32(crc32(body)), 4 + body.length);
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Encode an RGBA8 buffer to an (uncompressed-deflate) PNG. For debug export only. */
|
|
196
|
+
export function encodePng(width: number, height: number, rgba: Uint8Array): Uint8Array {
|
|
197
|
+
// Raw scanlines with a 0 (none) filter byte per row.
|
|
198
|
+
const raw = new Uint8Array(height * (1 + width * 4));
|
|
199
|
+
for (let y = 0; y < height; y++) {
|
|
200
|
+
const dst = y * (1 + width * 4);
|
|
201
|
+
raw[dst] = 0;
|
|
202
|
+
raw.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), dst + 1);
|
|
203
|
+
}
|
|
204
|
+
// zlib stream: 0x78 0x01 header, stored (uncompressed) deflate blocks, adler32 trailer.
|
|
205
|
+
const blocks: Uint8Array[] = [];
|
|
206
|
+
const MAX = 0xffff;
|
|
207
|
+
for (let off = 0; off < raw.length; off += MAX) {
|
|
208
|
+
const len = Math.min(MAX, raw.length - off);
|
|
209
|
+
const final = off + len >= raw.length ? 1 : 0;
|
|
210
|
+
const header = new Uint8Array([final, len & 0xff, (len >>> 8) & 0xff, ~len & 0xff, (~len >>> 8) & 0xff]);
|
|
211
|
+
blocks.push(header, raw.subarray(off, off + len));
|
|
212
|
+
}
|
|
213
|
+
let zlibLen = 2 + 4;
|
|
214
|
+
for (const blk of blocks) zlibLen += blk.length;
|
|
215
|
+
const zlib = new Uint8Array(zlibLen);
|
|
216
|
+
zlib[0] = 0x78;
|
|
217
|
+
zlib[1] = 0x01;
|
|
218
|
+
let p = 2;
|
|
219
|
+
for (const blk of blocks) {
|
|
220
|
+
zlib.set(blk, p);
|
|
221
|
+
p += blk.length;
|
|
222
|
+
}
|
|
223
|
+
zlib.set(u32(adler32(raw)), p);
|
|
224
|
+
|
|
225
|
+
const ihdr = new Uint8Array(13);
|
|
226
|
+
ihdr.set(u32(width), 0);
|
|
227
|
+
ihdr.set(u32(height), 4);
|
|
228
|
+
ihdr[8] = 8; // bit depth
|
|
229
|
+
ihdr[9] = 6; // colour type RGBA
|
|
230
|
+
// 10,11,12 = compression/filter/interlace = 0.
|
|
231
|
+
|
|
232
|
+
const sig = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
233
|
+
const idat = chunk("IDAT", zlib);
|
|
234
|
+
const ihdrChunk = chunk("IHDR", ihdr);
|
|
235
|
+
const iend = chunk("IEND", new Uint8Array(0));
|
|
236
|
+
const out = new Uint8Array(sig.length + ihdrChunk.length + idat.length + iend.length);
|
|
237
|
+
let q = 0;
|
|
238
|
+
for (const part of [sig, ihdrChunk, idat, iend]) {
|
|
239
|
+
out.set(part, q);
|
|
240
|
+
q += part.length;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Pure Animation Studio profile pipeline: builds the exportable animation-profile.json structure
|
|
2
|
+
// and validates character readiness. No engine/browser import — runs in plain Node so
|
|
3
|
+
// `anim:plan/profile/package/verify` work without a renderer.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
requiredLocomotionActions,
|
|
7
|
+
type AnimationStudioCharacter,
|
|
8
|
+
type LocomotionAction
|
|
9
|
+
} from "./character.js";
|
|
10
|
+
|
|
11
|
+
export interface AnimationStudioReadiness {
|
|
12
|
+
readonly ok: boolean;
|
|
13
|
+
readonly characterId: string;
|
|
14
|
+
readonly requiredActions: readonly LocomotionAction[];
|
|
15
|
+
readonly missingActions: readonly LocomotionAction[];
|
|
16
|
+
readonly errors: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AnimationStudioStateGraphDescriptor {
|
|
20
|
+
readonly states: readonly string[];
|
|
21
|
+
readonly parameters: readonly string[];
|
|
22
|
+
readonly transitions: ReadonlyArray<{ readonly from: string; readonly to: string; readonly when: string }>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AnimationStudioBlendTreeDescriptor {
|
|
26
|
+
readonly parameter: "speed";
|
|
27
|
+
readonly children: ReadonlyArray<{ readonly clip: string; readonly threshold: number }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AnimationProfile {
|
|
31
|
+
readonly schema: "aura-animation-profile/v1";
|
|
32
|
+
readonly characterId: string;
|
|
33
|
+
readonly characterName: string;
|
|
34
|
+
readonly assetKey: string;
|
|
35
|
+
readonly clipMap: Readonly<Record<LocomotionAction, string>>;
|
|
36
|
+
readonly stateGraph: AnimationStudioStateGraphDescriptor;
|
|
37
|
+
readonly blendTree: AnimationStudioBlendTreeDescriptor;
|
|
38
|
+
readonly ikChains: AnimationStudioCharacter["ikChains"];
|
|
39
|
+
readonly rootMotion: { readonly suppressed: boolean; readonly note: string };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateAnimationStudioCharacter(character: AnimationStudioCharacter): AnimationStudioReadiness {
|
|
43
|
+
const errors: string[] = [];
|
|
44
|
+
const missingActions: LocomotionAction[] = [];
|
|
45
|
+
for (const action of requiredLocomotionActions) {
|
|
46
|
+
const clip = character.clipMap[action];
|
|
47
|
+
if (!clip || clip.trim().length === 0) {
|
|
48
|
+
missingActions.push(action);
|
|
49
|
+
errors.push(`Required locomotion action "${action}" has no clip mapped.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!character.assetKey || character.assetKey.trim().length === 0) {
|
|
53
|
+
errors.push("Character must reference a typed assetKey, not a raw URL.");
|
|
54
|
+
}
|
|
55
|
+
if (!(character.runSpeed > character.walkSpeed)) {
|
|
56
|
+
errors.push(`runSpeed (${character.runSpeed}) must be greater than walkSpeed (${character.walkSpeed}).`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
ok: errors.length === 0,
|
|
60
|
+
characterId: character.id,
|
|
61
|
+
requiredActions: requiredLocomotionActions,
|
|
62
|
+
missingActions,
|
|
63
|
+
errors
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createAnimationProfile(character: AnimationStudioCharacter): AnimationProfile {
|
|
68
|
+
return {
|
|
69
|
+
schema: "aura-animation-profile/v1",
|
|
70
|
+
characterId: character.id,
|
|
71
|
+
characterName: character.name,
|
|
72
|
+
assetKey: character.assetKey,
|
|
73
|
+
clipMap: character.clipMap,
|
|
74
|
+
stateGraph: {
|
|
75
|
+
states: ["idle", "walk", "run"],
|
|
76
|
+
parameters: ["isMoving", "isRunning"],
|
|
77
|
+
transitions: [
|
|
78
|
+
{ from: "idle", to: "walk", when: "isMoving" },
|
|
79
|
+
{ from: "idle", to: "run", when: "isRunning" },
|
|
80
|
+
{ from: "walk", to: "run", when: "isRunning" },
|
|
81
|
+
{ from: "walk", to: "idle", when: "!isMoving" },
|
|
82
|
+
{ from: "run", to: "walk", when: "!isRunning" },
|
|
83
|
+
{ from: "run", to: "idle", when: "!isMoving" }
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
blendTree: {
|
|
87
|
+
parameter: "speed",
|
|
88
|
+
children: [
|
|
89
|
+
{ clip: character.clipMap.idle, threshold: 0 },
|
|
90
|
+
{ clip: character.clipMap.walk, threshold: character.walkSpeed },
|
|
91
|
+
{ clip: character.clipMap.run, threshold: character.runSpeed }
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
ikChains: character.ikChains,
|
|
95
|
+
rootMotion: { suppressed: true, note: "Locomotion uses controller velocity; in-place clips with root motion suppressed." }
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-live-route.ts — the live 3D route ENTRY (the generic player).
|
|
3
|
+
*
|
|
4
|
+
* This file contains NO scene-specific content. It loads whatever EpisodeDocument is
|
|
5
|
+
* injected (a Director-GENERATED scene, the working document, or an explicitly chosen
|
|
6
|
+
* example) and hands it to the GENERIC `scene-player`, which can render ANY document.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: there is NO content fixture as the default. When nothing is injected the
|
|
9
|
+
* route plays a neutral EMPTY placeholder and prints a clear "no scene loaded" message.
|
|
10
|
+
* The Moon Garden scene is now an EXAMPLE (`src/examples/moon-garden.example.ts`) and is
|
|
11
|
+
* used ONLY when explicitly requested — never as the default render path.
|
|
12
|
+
*
|
|
13
|
+
* This is the bridge for both project goals:
|
|
14
|
+
* generalization = a Director GENERATES documents; studio = an agent EDITS documents.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { mountScenePlayer } from "./scene-player";
|
|
18
|
+
import { emptyDocument, EMPTY_DOCUMENT_NOTICE } from "./empty-document";
|
|
19
|
+
import type { EpisodeDocument } from "./episode-document";
|
|
20
|
+
|
|
21
|
+
// The capture script / studio injects the scene to render (any document) via this global.
|
|
22
|
+
// If nothing is injected we fall back to the generic EMPTY placeholder — never a fixture.
|
|
23
|
+
const injected = (window as Window & { __AURA_EPISODE_DOCUMENT__?: EpisodeDocument }).__AURA_EPISODE_DOCUMENT__;
|
|
24
|
+
const documentToPlay = injected ?? emptyDocument;
|
|
25
|
+
if (!injected) {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.warn(EMPTY_DOCUMENT_NOTICE);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
void mountScenePlayer(documentToPlay).catch((error: unknown) => {
|
|
31
|
+
const diagnostics = (error as { diagnostics?: readonly string[] })?.diagnostics;
|
|
32
|
+
const baseMessage = error instanceof Error ? error.message : String(error);
|
|
33
|
+
const message = diagnostics && diagnostics.length > 0 ? `${baseMessage} :: ${diagnostics.join(" | ")}` : baseMessage;
|
|
34
|
+
const liveWindow = window as Window & { __AURA_LIVE_ROUTE_ERROR__?: string };
|
|
35
|
+
liveWindow.__AURA_LIVE_ROUTE_ERROR__ = message;
|
|
36
|
+
const root = document.querySelector<HTMLDivElement>("#app");
|
|
37
|
+
if (root) {
|
|
38
|
+
root.innerHTML = `<pre style="color:#f88;background:#0b0f1a;padding:24px;white-space:pre-wrap;">scene-player failed: ${message}</pre>`;
|
|
39
|
+
}
|
|
40
|
+
// eslint-disable-next-line no-console
|
|
41
|
+
console.error("scene-player failed", error);
|
|
42
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* render-modes.ts — PRD Phase I2 (rendering honesty). The SINGLE source of truth for:
|
|
3
|
+
*
|
|
4
|
+
* 1. WHICH cel is which. There are TWO distinct "cel" mechanisms and conflating them is the
|
|
5
|
+
* dishonesty this module exists to prevent:
|
|
6
|
+
* • IN-SHADER CEL (GPU): the engine's real `AnimationToonMaterial` (banded N·L + Fresnel
|
|
7
|
+
* rim), assigned to non-glow SET PIECES at scene-build time in `scene-player.ts`. This is
|
|
8
|
+
* a true material shader — it shades geometry on the GPU before any pixels are read back.
|
|
9
|
+
* • CPU TOON POST-PASS: `applyToonTreatment` in `scripts/render-core.ts`, run on the
|
|
10
|
+
* CAPTURED RGBA pixels after read-back (quantize luma + Sobel ink + grade). This is a
|
|
11
|
+
* 2D image filter — it never touches geometry or lighting.
|
|
12
|
+
* `renderModeNotes()` returns this contract verbatim so the studio/report can show it and the
|
|
13
|
+
* two are never described as the same thing.
|
|
14
|
+
*
|
|
15
|
+
* 2. The RENDER MODE (toon / wireframe / storyboard) and its verification. These are the three
|
|
16
|
+
* user-facing stage views; this module resolves the requested mode from a flag/env and
|
|
17
|
+
* verifies a request is one of the supported modes (no silent fallback to a dead mode).
|
|
18
|
+
*
|
|
19
|
+
* 3. The GLOBAL BLEND/alpha GHOST FIX. Many catalog GLBs set `alphaMode=BLEND` on an OPAQUE
|
|
20
|
+
* (opacity 1) material, so a textured mesh renders as a translucent white "ghost". The fix —
|
|
21
|
+
* force such materials opaque (blend off, depth write/test on) — must apply to EVERY render
|
|
22
|
+
* item (characters, props, set dressing), not just characters. `forceOpaqueIfGhost` is the one
|
|
23
|
+
* policy both `scene-player.ts` and any prop/set path call, so the fix is provably global.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The three user-facing stage render modes (mirrors the UI `ViewMode`). */
|
|
27
|
+
export type RenderMode = "toon" | "wireframe" | "storyboard";
|
|
28
|
+
|
|
29
|
+
/** Shading style WITHIN the rendered (toon) mode: cel look vs realistic PBR. */
|
|
30
|
+
export type RenderStyle = "toon" | "pbr";
|
|
31
|
+
|
|
32
|
+
export const RENDER_MODES: readonly RenderMode[] = ["toon", "wireframe", "storyboard"];
|
|
33
|
+
export const RENDER_STYLES: readonly RenderStyle[] = ["toon", "pbr"];
|
|
34
|
+
|
|
35
|
+
/** True when `mode` is a real, supported render mode (not a dead/legacy flag). */
|
|
36
|
+
export function isRenderMode(mode: string): mode is RenderMode {
|
|
37
|
+
return (RENDER_MODES as readonly string[]).includes(mode);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True when `style` is a real, supported render style. */
|
|
41
|
+
export function isRenderStyle(style: string): style is RenderStyle {
|
|
42
|
+
return (RENDER_STYLES as readonly string[]).includes(style);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResolvedRenderMode {
|
|
46
|
+
readonly mode: RenderMode;
|
|
47
|
+
readonly style: RenderStyle;
|
|
48
|
+
/** True when the cel image post-pass + in-shader cel apply (only in `toon` mode + `toon` style). */
|
|
49
|
+
readonly celApplies: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolve the effective render mode + style from explicit values, else env, else the defaults
|
|
54
|
+
* (`toon` mode, `toon`/cel style). VERIFIES the request: an unknown mode/style THROWS rather than
|
|
55
|
+
* silently falling back, so a typo can never quietly render the wrong view (PRD: "render-mode
|
|
56
|
+
* verification; no dead flags").
|
|
57
|
+
*/
|
|
58
|
+
export function resolveRenderMode(opts: {
|
|
59
|
+
readonly mode?: string;
|
|
60
|
+
readonly style?: string;
|
|
61
|
+
readonly env?: Record<string, string | undefined>;
|
|
62
|
+
} = {}): ResolvedRenderMode {
|
|
63
|
+
const env = opts.env ?? (typeof process !== "undefined" ? process.env : {});
|
|
64
|
+
const rawMode = (opts.mode ?? env.AURA_RENDER_MODE ?? "toon").trim().toLowerCase();
|
|
65
|
+
const rawStyle = (opts.style ?? env.AURA_RENDER_STYLE ?? "toon").trim().toLowerCase();
|
|
66
|
+
if (!isRenderMode(rawMode)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Unknown AURA_RENDER_MODE "${rawMode}". Supported: ${RENDER_MODES.join(", ")} (no dead/legacy modes).`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (!isRenderStyle(rawStyle)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Unknown AURA_RENDER_STYLE "${rawStyle}". Supported: ${RENDER_STYLES.join(", ")}.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
// Cel (both the in-shader material and the CPU post-pass) only applies in the rendered TOON mode
|
|
77
|
+
// with the TOON style. Wireframe/storyboard are line/flat views; PBR style is realistic.
|
|
78
|
+
const celApplies = rawMode === "toon" && rawStyle === "toon";
|
|
79
|
+
return { mode: rawMode, style: rawStyle, celApplies };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The honest cel-vs-post-pass contract, returned verbatim for the resolver report / UI. */
|
|
83
|
+
export function renderModeNotes(): {
|
|
84
|
+
readonly inShaderCel: string;
|
|
85
|
+
readonly cpuToonPostPass: string;
|
|
86
|
+
readonly modes: readonly RenderMode[];
|
|
87
|
+
readonly styles: readonly RenderStyle[];
|
|
88
|
+
} {
|
|
89
|
+
return {
|
|
90
|
+
inShaderCel:
|
|
91
|
+
"GPU material (AnimationToonMaterial): banded N·L + Fresnel rim, assigned to non-glow set pieces in scene-player.ts. Shades geometry before read-back.",
|
|
92
|
+
cpuToonPostPass:
|
|
93
|
+
"CPU 2D filter (render-core.ts applyToonTreatment): luma quantize + Sobel ink + grade on captured pixels. Never touches geometry/lighting.",
|
|
94
|
+
modes: RENDER_MODES,
|
|
95
|
+
styles: RENDER_STYLES
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A material's render state subset relevant to the ghost fix (the engine's `renderState`). */
|
|
100
|
+
export interface GhostFixableRenderState {
|
|
101
|
+
blend?: boolean;
|
|
102
|
+
depthWrite?: boolean;
|
|
103
|
+
depthTest?: boolean;
|
|
104
|
+
}
|
|
105
|
+
export interface GhostFixableMaterial {
|
|
106
|
+
readonly renderState?: GhostFixableRenderState;
|
|
107
|
+
/** Opacity, when the material exposes it — opacity 1 + blend on is the ghost signature. */
|
|
108
|
+
readonly opacity?: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* GLOBAL ghost fix. If a material is BLENDed yet effectively opaque (opacity ≥ 1 or unspecified),
|
|
113
|
+
* force it opaque (blend off, depth write + test on) so its real textures render solid instead of
|
|
114
|
+
* washing into the background. Returns true when it changed something (for proof/logging). Safe to
|
|
115
|
+
* call on ANY render item — characters, props, set dressing — which is how the fix stays global.
|
|
116
|
+
*/
|
|
117
|
+
export function forceOpaqueIfGhost(material: GhostFixableMaterial | undefined | null): boolean {
|
|
118
|
+
const rs = material?.renderState;
|
|
119
|
+
if (!rs || !rs.blend) return false;
|
|
120
|
+
// A genuinely translucent material (opacity < 1) is left alone; only opaque-but-BLENDed ghosts
|
|
121
|
+
// are corrected.
|
|
122
|
+
const opacity = typeof material?.opacity === "number" ? material.opacity : 1;
|
|
123
|
+
if (opacity < 1) return false;
|
|
124
|
+
rs.blend = false;
|
|
125
|
+
rs.depthWrite = true;
|
|
126
|
+
rs.depthTest = true;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Apply the global ghost fix across a list of render items; returns the count of fixed materials. */
|
|
131
|
+
export function forceOpaqueAcrossRenderItems(
|
|
132
|
+
items: readonly { readonly material?: GhostFixableMaterial }[]
|
|
133
|
+
): number {
|
|
134
|
+
let fixed = 0;
|
|
135
|
+
for (const item of items) if (forceOpaqueIfGhost(item.material)) fixed += 1;
|
|
136
|
+
return fixed;
|
|
137
|
+
}
|