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.
Files changed (215) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +6 -4
  4. package/dist/index.js.map +1 -1
  5. package/package.json +10 -9
  6. package/templates/{cartoon-channel → animation-channel}/README.md +11 -11
  7. package/templates/{cartoon-studio → animation-channel}/index.html +1 -1
  8. package/templates/{cartoon-channel → animation-channel}/package.json +2 -2
  9. package/templates/{cartoon-channel → animation-channel}/src/episode.ts +22 -22
  10. package/templates/{cartoon-channel → animation-channel}/src/experimental/README.md +5 -5
  11. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.css +3 -3
  12. package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.ts +7 -7
  13. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.css +4 -4
  14. package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.ts +6 -6
  15. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.css +3 -3
  16. package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.ts +7 -7
  17. package/templates/{cartoon-channel → animation-channel}/src/main.ts +32 -32
  18. package/templates/{cartoon-channel → animation-channel}/src/render-plan.ts +13 -13
  19. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.css +34 -34
  20. package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.ts +20 -20
  21. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d-animation.spec.ts +3 -3
  22. package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d.spec.ts +6 -6
  23. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet-animation.spec.ts +3 -3
  24. package/templates/{cartoon-channel → animation-channel}/tests/image-puppet.spec.ts +5 -5
  25. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d-animation.spec.ts +3 -3
  26. package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d.spec.ts +4 -4
  27. package/templates/{prompt-cartoon-channel → animation-channel}/tests/route-health.spec.ts +1 -1
  28. package/templates/{cartoon-channel → animation-channel}/tests/sample-episode-visual.spec.ts +5 -5
  29. package/templates/{cartoon-channel → animation-channel}/tests/storyboard-playback.spec.ts +7 -7
  30. package/templates/animation-studio/AGENTS.md +71 -0
  31. package/templates/{cartoon-studio → animation-studio}/ASSET-LICENSES.md +7 -7
  32. package/templates/{cartoon-studio → animation-studio}/README.md +6 -6
  33. package/templates/animation-studio/aura.assets.json +669 -0
  34. package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
  35. package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
  36. package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
  37. package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
  38. package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
  39. package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
  40. package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
  41. package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +10284 -0
  42. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
  43. package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
  44. package/templates/animation-studio/dist/generated/second-scene.document.json +1210 -0
  45. package/templates/animation-studio/dist/generated/studio-edited.document.json +1196 -0
  46. package/templates/animation-studio/dist/scene/inshader-accept.document.json +1 -0
  47. package/templates/animation-studio/dist/scene/inshader-test.document.json +1 -0
  48. package/templates/animation-studio/dist/scene/integ.document.json +1 -0
  49. package/templates/animation-studio/dist/scene/working.document.json +723 -0
  50. package/templates/animation-studio/dist/scene/working.history.json +1 -0
  51. package/templates/{cartoon-studio → animation-studio}/live-route.html +1 -1
  52. package/templates/{cartoon-studio → animation-studio}/package-lock.json +2 -2
  53. package/templates/animation-studio/package.json +32 -0
  54. package/templates/animation-studio/public/aura-assets/cast-a.catalog.glb +0 -0
  55. package/templates/animation-studio/public/aura-assets/cast-b.catalog.glb +0 -0
  56. package/templates/animation-studio/public/aura-assets/luma.catalog.glb +0 -0
  57. package/templates/animation-studio/public/aura-assets/luma2.catalog.glb +0 -0
  58. package/templates/animation-studio/public/aura-assets/miko.catalog.glb +0 -0
  59. package/templates/{cartoon-studio → animation-studio}/public/aura-assets/moonGarden.gltf +1 -1
  60. package/templates/animation-studio/public/aura-assets/mushroom.catalog.glb +0 -0
  61. package/templates/animation-studio/public/aura-assets/rusty.catalog.glb +0 -0
  62. package/templates/animation-studio/public/cast-library/cast-library.json +71 -0
  63. package/templates/animation-studio/public/clip-library/gesture.json +173903 -0
  64. package/templates/animation-studio/public/clip-library/idle.json +23035 -0
  65. package/templates/animation-studio/public/clip-library/manifest.json +154 -0
  66. package/templates/animation-studio/public/clip-library/nod.json +173903 -0
  67. package/templates/animation-studio/public/clip-library/point.json +69879 -0
  68. package/templates/animation-studio/public/clip-library/react.json +66084 -0
  69. package/templates/animation-studio/public/clip-library/run.json +9648 -0
  70. package/templates/animation-studio/public/clip-library/talk.json +69879 -0
  71. package/templates/animation-studio/public/clip-library/walk.json +8283 -0
  72. package/templates/animation-studio/public/hifi-cast/BrainStem.glb +0 -0
  73. package/templates/animation-studio/public/hifi-cast/CesiumMan.glb +0 -0
  74. package/templates/animation-studio/public/hifi-cast/RiggedFigure.glb +0 -0
  75. package/templates/animation-studio/public/hifi-cast/kaykit/Barbarian.glb +0 -0
  76. package/templates/animation-studio/public/hifi-cast/kaykit/Knight.glb +0 -0
  77. package/templates/animation-studio/public/hifi-cast/kaykit/Mage.glb +0 -0
  78. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue.glb +0 -0
  79. package/templates/animation-studio/public/hifi-cast/kaykit/Rogue_Hooded.glb +0 -0
  80. package/templates/animation-studio/public/hifi-cast/khronos-report.json +295 -0
  81. package/templates/animation-studio/public/hifi-cast/quaternius-report.json +449 -0
  82. package/templates/animation-studio/public/hifi-cast/rpm/cesiumman.glb +0 -0
  83. package/templates/animation-studio/public/hifi-cast/rpm/fox.glb +0 -0
  84. package/templates/animation-studio/public/hifi-cast/rpm/riggedfigure.glb +0 -0
  85. package/templates/animation-studio/public/hifi-cast/rpm-report.json +301 -0
  86. package/templates/animation-studio/scripts/anim.ts +103 -0
  87. package/templates/animation-studio/scripts/animation-scene.ts +457 -0
  88. package/templates/animation-studio/scripts/asset-motion-probe.ts +450 -0
  89. package/templates/animation-studio/scripts/asset-render-probe.ts +152 -0
  90. package/templates/animation-studio/scripts/author-office-scene.ts +65 -0
  91. package/templates/animation-studio/scripts/build-cast-library.ts +97 -0
  92. package/templates/animation-studio/scripts/build-characters.ts +1543 -0
  93. package/templates/animation-studio/scripts/build-clip-library.ts +972 -0
  94. package/templates/animation-studio/scripts/clip-decision-cli.ts +51 -0
  95. package/templates/animation-studio/scripts/determinism-check.ts +48 -0
  96. package/templates/animation-studio/scripts/generate-scene.ts +95 -0
  97. package/templates/animation-studio/scripts/hifi-cast-validate.ts +572 -0
  98. package/templates/animation-studio/scripts/motion-evidence-cli.ts +150 -0
  99. package/templates/animation-studio/scripts/motion-evidence.ts +362 -0
  100. package/templates/animation-studio/scripts/preview-server.ts +52 -0
  101. package/templates/animation-studio/scripts/regen-manifest.ts +156 -0
  102. package/templates/animation-studio/scripts/render-core.ts +360 -0
  103. package/templates/animation-studio/scripts/render-live.ts +813 -0
  104. package/templates/animation-studio/scripts/render-parallel.ts +70 -0
  105. package/templates/animation-studio/scripts/render-server.ts +119 -0
  106. package/templates/animation-studio/scripts/resolve-asset.ts +669 -0
  107. package/templates/animation-studio/scripts/skeleton-overlay-cli.ts +82 -0
  108. package/templates/animation-studio/scripts/skeleton-overlay.ts +293 -0
  109. package/templates/animation-studio/scripts/studio-tools-demo.ts +45 -0
  110. package/templates/animation-studio/scripts/texture-bake.ts +297 -0
  111. package/templates/{cartoon-studio → animation-studio}/scripts/validate-characters.ts +0 -0
  112. package/templates/animation-studio/scripts/verify-extracted-overlay-cli.ts +264 -0
  113. package/templates/animation-studio/src/animation-episode-validator.ts +97 -0
  114. package/templates/animation-studio/src/animation-performance.ts +556 -0
  115. package/templates/animation-studio/src/aura-assets.ts +71 -0
  116. package/templates/animation-studio/src/character.ts +40 -0
  117. package/templates/animation-studio/src/director/compile-episode-document.ts +45 -0
  118. package/templates/animation-studio/src/director/director-heuristics.ts +697 -0
  119. package/templates/animation-studio/src/director/prompt-to-scene.ts +369 -0
  120. package/templates/animation-studio/src/empty-document.ts +77 -0
  121. package/templates/animation-studio/src/episode-document.ts +442 -0
  122. package/templates/animation-studio/src/examples/moon-garden.example.ts +183 -0
  123. package/templates/animation-studio/src/fidelity.ts +129 -0
  124. package/templates/animation-studio/src/graph.ts +14 -0
  125. package/templates/animation-studio/src/procedural-hdri.ts +243 -0
  126. package/templates/animation-studio/src/profile.ts +97 -0
  127. package/templates/animation-studio/src/render-live-route.ts +42 -0
  128. package/templates/animation-studio/src/render-modes.ts +137 -0
  129. package/templates/animation-studio/src/scene-player.ts +867 -0
  130. package/templates/animation-studio/src/set-templates.ts +361 -0
  131. package/templates/animation-studio/src/studio/episode-document-store.ts +72 -0
  132. package/templates/animation-studio/src/studio/scene-tool-registry.ts +86 -0
  133. package/templates/animation-studio/src/studio/scene-tools.ts +169 -0
  134. package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +938 -0
  135. package/templates/animation-studio/tsconfig.json +26 -0
  136. package/templates/{cartoon-studio → animation-studio}/vite.config.ts +6 -1
  137. package/templates/character-controller/README.md +33 -0
  138. package/templates/character-controller/index.html +19 -0
  139. package/templates/character-controller/package.json +23 -0
  140. package/templates/character-controller/playwright.config.ts +14 -0
  141. package/templates/character-controller/src/controller.ts +44 -0
  142. package/templates/character-controller/src/main.ts +62 -0
  143. package/templates/character-controller/tests/route-health.spec.ts +26 -0
  144. package/templates/character-controller/tests/screenshot.spec.ts +9 -0
  145. package/templates/character-controller/tsconfig.json +15 -0
  146. package/templates/cinematic-scene/package.json +1 -1
  147. package/templates/episode-builder/README.md +4 -4
  148. package/templates/episode-builder/package.json +1 -1
  149. package/templates/episode-builder/src/aura-assets.ts +1 -1
  150. package/templates/episode-builder/src/builder.ts +4 -4
  151. package/templates/episode-builder/src/episode.ts +21 -21
  152. package/templates/episode-builder/src/main.ts +20 -20
  153. package/templates/episode-builder/src/render-plan.ts +13 -13
  154. package/templates/episode-builder/tests/storyboard-playback.spec.ts +7 -7
  155. package/templates/fighting-game/package.json +1 -1
  156. package/templates/mini-game/package.json +1 -1
  157. package/templates/product-viewer/package.json +1 -1
  158. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/README.md +5 -5
  159. package/templates/{cartoon-channel → prompt-animation-channel}/index.html +1 -1
  160. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/package.json +2 -2
  161. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/aura-assets.ts +1 -1
  162. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/episode.ts +22 -22
  163. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/main.ts +28 -28
  164. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/render-plan.ts +13 -13
  165. package/templates/{cartoon-channel → prompt-animation-channel}/tests/route-health.spec.ts +1 -1
  166. package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tests/storyboard-playback.spec.ts +8 -8
  167. package/templates/cartoon-studio/aura.assets.json +0 -400
  168. package/templates/cartoon-studio/package.json +0 -33
  169. package/templates/cartoon-studio/public/aura-assets/luma.authored.glb +0 -0
  170. package/templates/cartoon-studio/public/aura-assets/miko.authored.glb +0 -0
  171. package/templates/cartoon-studio/scripts/build-characters.ts +0 -738
  172. package/templates/cartoon-studio/scripts/build-dialogue-audio.ts +0 -262
  173. package/templates/cartoon-studio/scripts/episode.ts +0 -323
  174. package/templates/cartoon-studio/scripts/render-live.ts +0 -873
  175. package/templates/cartoon-studio/src/aura-assets.ts +0 -64
  176. package/templates/cartoon-studio/src/characters.ts +0 -65
  177. package/templates/cartoon-studio/src/episode-renderer.ts +0 -605
  178. package/templates/cartoon-studio/src/episode.ts +0 -306
  179. package/templates/cartoon-studio/src/main.ts +0 -400
  180. package/templates/cartoon-studio/src/render-live-route.ts +0 -913
  181. package/templates/cartoon-studio/src/render-plan.ts +0 -567
  182. package/templates/cartoon-studio/src/review.ts +0 -99
  183. package/templates/cartoon-studio/src/studio.ts +0 -101
  184. package/templates/cartoon-studio/test-results/.last-run.json +0 -4
  185. package/templates/cartoon-studio/tests/episode-render.spec.ts +0 -44
  186. package/templates/cartoon-studio/tests/motion-quality.spec.ts +0 -45
  187. package/templates/cartoon-studio/tests/route-health.spec.ts +0 -7
  188. package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +0 -99
  189. package/templates/cartoon-studio/tests/visual-review.spec.ts +0 -21
  190. package/templates/cartoon-studio/tsconfig.json +0 -14
  191. package/templates/prompt-cartoon-channel/index.html +0 -13
  192. package/templates/prompt-cartoon-channel/src/sets.ts +0 -14
  193. /package/templates/{cartoon-channel → animation-channel}/aura.assets.json +0 -0
  194. /package/templates/{cartoon-channel → animation-channel}/playwright.config.ts +0 -0
  195. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.047f5e5f.glb +0 -0
  196. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.thumb.svg +0 -0
  197. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.047f5e5f.glb +0 -0
  198. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.thumb.svg +0 -0
  199. /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/moon-garden-feature-frame.png +0 -0
  200. /package/templates/{cartoon-channel → animation-channel}/src/aura-assets.ts +0 -0
  201. /package/templates/{cartoon-channel → animation-channel}/src/characters.ts +0 -0
  202. /package/templates/{cartoon-channel → animation-channel}/src/contract.ts +0 -0
  203. /package/templates/{cartoon-channel → animation-channel}/src/sets.ts +0 -0
  204. /package/templates/{cartoon-channel → animation-channel}/test-results/.last-run.json +0 -0
  205. /package/templates/{cartoon-channel → animation-channel}/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
  206. /package/templates/{cartoon-channel → animation-channel}/tsconfig.json +0 -0
  207. /package/templates/{cartoon-studio → animation-studio}/playwright.config.ts +0 -0
  208. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/luma.thumb.svg +0 -0
  209. /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/miko.thumb.svg +0 -0
  210. /package/templates/{cartoon-studio → animation-studio}/src/contract.ts +0 -0
  211. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/playwright.config.ts +0 -0
  212. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/characters.ts +0 -0
  213. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/contract.ts +0 -0
  214. /package/templates/{cartoon-studio → prompt-animation-channel}/src/sets.ts +0 -0
  215. /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tsconfig.json +0 -0
@@ -1,913 +0,0 @@
1
- /**
2
- * render-live-route.ts — a REAL 3D render path for Cartoon Studio.
3
- *
4
- * This is an ADDITIVE, second route (it does not touch `main.ts` or any existing
5
- * `tests/`). Where the `createAuraApp` storyboard route cannot skin GLB
6
- * characters, this route proves the renderer CAN: it loads two real rigged GLB
7
- * characters, builds a skinning-capable animation runtime for each, renders them
8
- * on a ground plane with the advanced-runtime `A3DRenderer`, and exposes a seek
9
- * hook so a headless capturer can pose any animation time and read back pixels
10
- * that visibly change as the skeleton moves.
11
- *
12
- * Imports resolve from the template's own `@aura3d/engine` (published build):
13
- * - `A3DRenderer` via `@aura3d/engine/advanced-runtime`
14
- * - `createTypedGLBActor` via `@aura3d/engine/production-runtime`
15
- * - render primitives via `@aura3d/engine/rendering`
16
- * All three exist in the published engine, so the in-browser skinned render works
17
- * with the template's shipped dependencies.
18
- *
19
- * TOON SHADING NOTE (honest): the published `@aura3d/engine` build does NOT ship
20
- * `CartoonToonMaterial` or register the `aura3d/cartoon-toon` GLSL program, so a
21
- * banded toon MATERIAL cannot compile in-browser from the template's deps. The
22
- * toon treatment is therefore applied as a real pixel post-pass in the capture
23
- * script (`scripts/render-live.ts`), which resolves the monorepo source build of
24
- * `@aura3d/rendering` (banded ramp + Sobel outline + storybook grade). This file
25
- * focuses on the half only the renderer can do: skinned GLB animation in pixels.
26
- */
27
-
28
- import { A3DRenderer } from "@aura3d/engine/advanced-runtime";
29
- import { createStudioLighting, createTypedGLBActor, type TypedGLBActor } from "@aura3d/engine/production-runtime";
30
- import {
31
- captionCueAtTime,
32
- createCameraPathFromPreset,
33
- sampleCameraPath,
34
- sampleVisemeTrack,
35
- type CameraPath,
36
- type CameraPresetId
37
- } from "@aura3d/engine";
38
- // Import render types from `@aura3d/rendering` (not `@aura3d/engine/rendering`):
39
- // `A3DRenderer` and `createTypedGLBActor` are typed against `@aura3d/rendering`,
40
- // and the published install keeps those as a distinct type identity, so using the
41
- // same package here keeps `RenderItem` / `RenderSource` / `Geometry` compatible.
42
- import {
43
- Geometry,
44
- PBRMaterial,
45
- type CollectedLight,
46
- type RenderItem,
47
- type RenderSource
48
- } from "@aura3d/rendering";
49
- import { composeMat4, multiplyMat4, perspectiveMat4, PointLight, quatFromEuler, type Mat4 } from "@aura3d/scene";
50
- import { visemeTrack } from "./render-plan";
51
- import { episode } from "./episode";
52
-
53
- interface LiveCharacter {
54
- readonly actor: TypedGLBActor;
55
- /** Default/fallback clip from the spec (used when a beat's staged clip is absent). */
56
- readonly clip: string;
57
- /** Every animation clip this GLB exposes (used to validate per-beat staged clips). */
58
- readonly availableClips: readonly string[];
59
- /** Default/fallback opening position + yaw (used when no beat staging is defined). */
60
- readonly position: readonly [number, number, number];
61
- readonly yaw: number;
62
- readonly scale: number;
63
- readonly tint: readonly [number, number, number, number];
64
- /** Scene-graph node whose world transform tracks the head (for mouth placement). */
65
- readonly headNode: { readonly transform: { readonly worldMatrix: Mat4 } } | undefined;
66
- /**
67
- * Renderables that carry GLB morph weights, if any. miko has 3 (robot-expressive
68
- * Angry/Surprised/Sad); luma (Mixamo soldier) has none — see MORPH HONESTY below.
69
- */
70
- readonly morphRenderables: { morphWeights: number[] }[];
71
- /** Index of the morph weight driven for mouth-open (Surprised on miko), or -1. */
72
- readonly mouthMorphIndex: number;
73
- }
74
-
75
- interface LiveRouteSeekProof {
76
- readonly time: number;
77
- readonly drawCalls: number;
78
- readonly skinnedRenderItems: number;
79
- /** Eased dim→sparkle world-state at this seek time (0 = dim beat, 1 = full twinkle). */
80
- readonly gardenGlow: number;
81
- readonly shot: {
82
- readonly shotId: string;
83
- readonly presetId: string;
84
- readonly episodeTime: number;
85
- readonly cameraPosition: readonly [number, number, number];
86
- readonly fov: number;
87
- };
88
- /**
89
- * Active burned-in caption for this beat (sampled from the episode caption track
90
- * at the shot's episode time). The capture script draws `text` into the captured
91
- * frame pixels so the exported video has visible captions, not just a DOM overlay.
92
- */
93
- readonly caption: {
94
- readonly text: string;
95
- readonly speakerId: string;
96
- readonly captionId: string;
97
- };
98
- readonly characters: readonly {
99
- readonly id: string;
100
- readonly clip: string;
101
- /** Staged world position this beat (proves per-beat blocking / sweep staging). */
102
- readonly position: readonly [number, number, number];
103
- /** True when this character is staged at the broom playing the sweep stand-in clip. */
104
- readonly sweeping: boolean;
105
- readonly tracksApplied: number;
106
- readonly skinningPalettesUpdated: number;
107
- readonly skinningBindingCount: number;
108
- /** AuraVoice mouth-open (0..1) sampled at this shot's episode time. */
109
- readonly mouthOpenness: number;
110
- readonly visemeId: string;
111
- /** GLB morph weight actually written onto the renderables (miko only; -1 if no morphs). */
112
- readonly mouthMorphWeight: number;
113
- readonly mouthMorphIndex: number;
114
- /** Open height of the primitive mouth indicator quad (the VISIBLE lip-sync). */
115
- readonly primitiveMouthOpen: number;
116
- }[];
117
- }
118
-
119
- interface LiveRouteReadyProof {
120
- readonly ready: true;
121
- readonly backend: string;
122
- readonly characters: readonly {
123
- readonly id: string;
124
- readonly url: string;
125
- readonly clip: string;
126
- readonly clips: readonly string[];
127
- readonly skinningBindingCount: number;
128
- readonly skinnedRenderItemCount: number;
129
- }[];
130
- }
131
-
132
- interface LiveSeekOptions {
133
- /**
134
- * Optional override forcing every character's mouth openness to a fixed value
135
- * (0..1), ignoring the viseme track. Used by the capture script to render an
136
- * isolated mouth-open vs mouth-closed A/B pair at an otherwise identical pose +
137
- * camera, proving the lip-sync indicator alone changes pixels.
138
- */
139
- readonly mouthOverride?: number;
140
- }
141
-
142
- type LiveRouteWindow = Window & {
143
- __AURA_LIVE_ROUTE_READY__?: LiveRouteReadyProof;
144
- /** Headless seek hook: pose every character at `time` seconds and render one frame. */
145
- __auraSeek__?: (time: number, options?: LiveSeekOptions) => LiveRouteSeekProof;
146
- };
147
-
148
- const GROUND_Y = 0;
149
-
150
- /**
151
- * MOON-GARDEN WORLD-STATE (HONEST: primitive/emissive geometry, NOT authored GLB
152
- * art). The episode's setting — "two robots in a glowing moon garden" — is built
153
- * here from the same shapes/positions/emissive colors as the cartoon-channel
154
- * PRIMITIVE reference set (`cartoon-channel/src/main.ts createAuraRenderedCartoonScene`
155
- * + `sets.ts`), translated into the A3DRenderer scene-source mesh format this live
156
- * route already uses (Geometry + PBRMaterial + modelMatrix RenderItems).
157
- *
158
- * It contains: a crater/garden floor + mound, a glowing moon portal orb + cyan rim
159
- * backdrop, several glow stones, moon lilies (emissive stem + bloom), a broom prop
160
- * (handle + bristles), and two colored night-garden point lights.
161
- *
162
- * The story's dim→sparkle world-state change (lilies "losing their sparkle" → the
163
- * garden "twinkling again") is driven by `setGardenGlow(t01)` below: every glow
164
- * material's emissive strength is scaled from a dim floor up to full twinkle, so
165
- * frames captured early (beat-open) vs late (beat-finish) visibly differ in glow.
166
- */
167
-
168
- /** An emissive garden material whose strength is driven by the dim→sparkle state. */
169
- interface GlowMaterial {
170
- readonly material: PBRMaterial;
171
- /** Emissive strength at the dim "losing their sparkle" beat. */
172
- readonly dim: number;
173
- /** Emissive strength at the full "twinkling again" beat. */
174
- readonly full: number;
175
- }
176
-
177
- const glowMaterials: GlowMaterial[] = [];
178
-
179
- /** Captured clip-time span over which the garden ramps dim→sparkle. Matches the
180
- * `clipSpan` the shot plan uses, so beat-open≈0 (dim) and beat-finish≈end (full). */
181
- const GARDEN_GLOW_SPAN = 2.1;
182
-
183
- function makeGlowMaterial(
184
- name: string,
185
- baseColor: readonly [number, number, number, number],
186
- emissiveColor: readonly [number, number, number],
187
- dim: number,
188
- full: number
189
- ): PBRMaterial {
190
- const material = new PBRMaterial({
191
- name,
192
- baseColor,
193
- metallic: 0,
194
- roughness: 0.85,
195
- emissiveColor,
196
- emissiveStrength: dim
197
- });
198
- glowMaterials.push({ material, dim, full });
199
- return material;
200
- }
201
-
202
- /**
203
- * Drive the dim→sparkle world-state. `t01` is the normalized seek progress through
204
- * the captured timeline (0 at beat-open, 1 at beat-finish). Every registered glow
205
- * material's emissive strength is lerped from its dim floor to its full twinkle, so
206
- * the captured early vs late frames differ in glow brightness. A gentle eased curve
207
- * keeps the brighten reduced-motion-safe (no abrupt flash). */
208
- function setGardenGlow(t01: number): number {
209
- const clamped = t01 < 0 ? 0 : t01 > 1 ? 1 : t01;
210
- // Smoothstep for a soft, non-flashing ramp.
211
- const eased = clamped * clamped * (3 - 2 * clamped);
212
- for (const glow of glowMaterials) {
213
- // `emissiveStrength` has no public setter; drive the same uniform the
214
- // constructor seeds (`u_emissiveStrength`) so the forward PBR pass picks it up.
215
- glow.material.setParameter("u_emissiveStrength", glow.dim + (glow.full - glow.dim) * eased);
216
- }
217
- return eased;
218
- }
219
-
220
- // Two distinct, AURA3D-AUTHORED rigged GLBs (procedurally generated by
221
- // `scripts/build-characters.ts`, CC0). Both are real skinned cartoon helper robots
222
- // with a small skeleton (root/spine/head + 2 arms + 2 legs), three animation clips
223
- // (Idle/Wave/Walk), and ONE real `mouthOpen` face blendshape that drops the lower
224
- // lip — verified to load through the engine loader (skin + morph parse, palettes
225
- // update) by `scripts/validate-characters.ts`. These REPLACE the reused three.js
226
- // RobotExpressive + Mixamo Soldier placeholders. miko = rounded/cyan, luma =
227
- // taller/warm-gold (visibly different shapes + colors).
228
- const CHARACTER_SPECS = [
229
- {
230
- id: "miko",
231
- url: "/aura-assets/miko.authored.glb",
232
- clip: "Walk",
233
- position: [-0.95, GROUND_Y, 0] as const,
234
- yaw: Math.PI * 0.12,
235
- // Authored ~1.6u tall; scale up so it reads at full body height in frame.
236
- scale: 1.15,
237
- tint: [0.36, 0.82, 0.92, 1] as const
238
- },
239
- {
240
- id: "luma",
241
- url: "/aura-assets/luma.authored.glb",
242
- clip: "Walk",
243
- position: [1.0, GROUND_Y, 0] as const,
244
- yaw: -Math.PI * 0.12,
245
- // luma is authored taller; a touch smaller scale keeps the two heights paired.
246
- scale: 1.0,
247
- tint: [0.96, 0.74, 0.34, 1] as const
248
- }
249
- ] as const;
250
-
251
- // REAL BLENDSHAPE LIP-SYNC: both authored GLBs expose exactly ONE morph target,
252
- // `mouthOpen` (index 0), whose POSITION deltas drop the lower-mouth panel ~0.12u to
253
- // open a mouth. Driving this weight 0..1 from the sampled viseme mouthOpenness now
254
- // moves real geometry (verified maxAbsDelta > 0.1u through the loader), so the
255
- // mouth visibly opens/closes in pixels — no primitive mouth-card fallback needed.
256
- const MOUTH_MORPH_INDEX = 0;
257
-
258
- // STAGED PERFORMANCE / BLOCKING (HONEST: the "sweep" clip is a STAND-IN — these
259
- // GLBs ship no authored sweep/broom animation, so we reuse an available body clip
260
- // as a sweep-like motion and MOVE the character to the broom prop so the staging
261
- // reads in pixels). The broom prop's hero owner is miko (see episode.props
262
- // glow-broom ownerCharacterId). The broom HANDLE world position is below; miko
263
- // walks from her opening mark toward the broom during the teamwork beat and plays
264
- // a sweep stand-in clip, so frames captured at the teamwork beat visibly show miko
265
- // AT the broom vs. her opening mark. luma walks in to help.
266
- const BROOM_HANDLE_POSITION = [-0.55, GROUND_Y, 0.25] as const;
267
- // miko stands just to the broom's working side (slightly screen-left of the handle,
268
- // facing it) so the body + the broom prop frame together at the teamwork beat.
269
- const MIKO_SWEEP_MARK = [BROOM_HANDLE_POSITION[0] + 0.18, GROUND_Y, BROOM_HANDLE_POSITION[2] - 0.02] as const;
270
-
271
- /** Per-beat staging for one character: where it stands and which clip it plays. The
272
- * sweep clip is a STAND-IN (no authored sweep exists on these rigs). */
273
- interface CharacterStaging {
274
- readonly position: readonly [number, number, number];
275
- readonly yaw: number;
276
- readonly clip: string;
277
- /** True for the teamwork sweep mark (used by the seek proof to flag the staged sweep). */
278
- readonly sweeping: boolean;
279
- }
280
-
281
- // Per-character, per-beat staging. Keyed by shotId. miko: opens at her mark with a
282
- // neutral walk, crosses to the broom for the teamwork sweep (stand-in clip), then
283
- // settles for the goodnight wave. luma mirrors with its own walk/run clips. Clips
284
- // are validated against each GLB's real clip list at mount (fallback to clip[0]).
285
- const STAGING_BY_CHARACTER: Record<string, Record<string, CharacterStaging>> = {
286
- miko: {
287
- "shot-moon-garden-open": { position: [-0.95, GROUND_Y, 0], yaw: Math.PI * 0.12, clip: "Walk", sweeping: false },
288
- // Sweep stand-in: "Wave" is the closest authored arm-driven clip on this rig.
289
- "shot-glow-stone-teamwork": { position: MIKO_SWEEP_MARK, yaw: -Math.PI * 0.5, clip: "Wave", sweeping: true },
290
- "shot-moon-garden-finish": { position: [-0.65, GROUND_Y, 0], yaw: Math.PI * 0.12, clip: "Wave", sweeping: false }
291
- },
292
- luma: {
293
- "shot-moon-garden-open": { position: [1.0, GROUND_Y, 0], yaw: -Math.PI * 0.12, clip: "Walk", sweeping: false },
294
- // luma walks in toward the broom to help during teamwork.
295
- "shot-glow-stone-teamwork": { position: [0.25, GROUND_Y, 0.2], yaw: -Math.PI * 0.62, clip: "Walk", sweeping: false },
296
- "shot-moon-garden-finish": { position: [0.68, GROUND_Y, 0], yaw: -Math.PI * 0.12, clip: "Idle", sweeping: false }
297
- }
298
- };
299
-
300
- /** Resolve the staging for `characterId` at `shotId`, validated against the GLB's
301
- * real clip list (falls back to the spec clip, then clip[0]). */
302
- function stagingFor(
303
- characterId: string,
304
- shotId: string,
305
- availableClips: readonly string[],
306
- fallbackClip: string,
307
- fallbackPosition: readonly [number, number, number],
308
- fallbackYaw: number
309
- ): CharacterStaging {
310
- const staged = STAGING_BY_CHARACTER[characterId]?.[shotId];
311
- if (!staged) {
312
- return { position: fallbackPosition, yaw: fallbackYaw, clip: fallbackClip, sweeping: false };
313
- }
314
- const clip = availableClips.includes(staged.clip) ? staged.clip : fallbackClip;
315
- return { ...staged, clip };
316
- }
317
-
318
- // Per-shot camera framing. The live route captures animation-clip time (0..~2s),
319
- // while the episode shot timeline / viseme track live on the 0..60s episode
320
- // timeline. We map clip-capture time onto the three episode shots so consecutive
321
- // captured frames cut between distinct framings (wide → two-shot → push-in) AND
322
- // sample the viseme track at a real dialogue beat. `episodeTime` is chosen inside
323
- // each shot's dialogue window so a speaking character's mouth is open.
324
- interface LiveShot {
325
- readonly shotId: string;
326
- readonly presetId: CameraPresetId;
327
- /** Clip-time window (seconds) of the captures assigned to this shot. */
328
- readonly clipTimeStart: number;
329
- readonly clipTimeEnd: number;
330
- /** Episode time the camera is framed at (stable per shot). */
331
- readonly episodeTime: number;
332
- /** Episode-time window swept for viseme sampling so the mouth opens AND closes. */
333
- readonly visemeStart: number;
334
- readonly visemeEnd: number;
335
- }
336
-
337
- function buildShotPlan(): readonly LiveShot[] {
338
- const shots = episode.shotTimeline.shots;
339
- // Distinct framings, one per episode shot. Falls back gracefully if shot count differs.
340
- const presets: CameraPresetId[] = ["establishing", "two-shot", "close-up"];
341
- const clipSpan = 2.1; // total captured clip-time span (see CAPTURE_TIMES in render-live.ts)
342
- const slot = clipSpan / Math.max(1, shots.length);
343
- return shots.map((shot, index) => {
344
- // The full episode-time span of this shot's dialogue cues (covers open + closed).
345
- const shotCues = visemeTrack.cues.filter((cue) => (cue.lineId ?? "").startsWith(`${shot.shotId}:`));
346
- const visemeStart = shotCues.length > 0 ? Math.min(...shotCues.map((cue) => cue.startTime)) : shot.startTime;
347
- const visemeEnd = shotCues.length > 0 ? Math.max(...shotCues.map((cue) => cue.endTime)) : shot.endTime;
348
- // Frame the camera mid-dialogue.
349
- const episodeTime = (visemeStart + visemeEnd) / 2;
350
- return {
351
- shotId: shot.shotId,
352
- presetId: presets[index] ?? presets[presets.length - 1]!,
353
- clipTimeStart: index * slot,
354
- clipTimeEnd: (index + 1) * slot,
355
- episodeTime,
356
- visemeStart,
357
- visemeEnd
358
- };
359
- });
360
- }
361
-
362
- function shotForClipTime(plan: readonly LiveShot[], clipTime: number): LiveShot {
363
- let chosen = plan[0]!;
364
- for (const shot of plan) {
365
- if (clipTime >= shot.clipTimeStart) chosen = shot;
366
- }
367
- return chosen;
368
- }
369
-
370
- /** Episode time to sample visemes at: sweeps the shot's dialogue window so the
371
- * mouth animates open→closed across the captures assigned to this shot. */
372
- function visemeTimeForShot(shot: LiveShot, clipTime: number): number {
373
- const span = Math.max(1e-3, shot.clipTimeEnd - shot.clipTimeStart);
374
- const fraction = Math.min(1, Math.max(0, (clipTime - shot.clipTimeStart) / span));
375
- return shot.visemeStart + fraction * (shot.visemeEnd - shot.visemeStart);
376
- }
377
-
378
- /** Right-handed look-at view matrix (column-major, matches perspectiveMat4). */
379
- function lookAtViewMatrix(
380
- eye: readonly [number, number, number],
381
- target: readonly [number, number, number],
382
- up: readonly [number, number, number] = [0, 1, 0]
383
- ): Mat4 {
384
- const fx = target[0] - eye[0];
385
- const fy = target[1] - eye[1];
386
- const fz = target[2] - eye[2];
387
- const fl = Math.hypot(fx, fy, fz) || 1;
388
- const f: [number, number, number] = [fx / fl, fy / fl, fz / fl];
389
- // s = normalize(cross(f, up))
390
- let sx = f[1] * up[2] - f[2] * up[1];
391
- let sy = f[2] * up[0] - f[0] * up[2];
392
- let sz = f[0] * up[1] - f[1] * up[0];
393
- const sl = Math.hypot(sx, sy, sz) || 1;
394
- sx /= sl;
395
- sy /= sl;
396
- sz /= sl;
397
- // u = cross(s, f)
398
- const ux = sy * f[2] - sz * f[1];
399
- const uy = sz * f[0] - sx * f[2];
400
- const uz = sx * f[1] - sy * f[0];
401
- return [
402
- sx, ux, -f[0], 0,
403
- sy, uy, -f[1], 0,
404
- sz, uz, -f[2], 0,
405
- -(sx * eye[0] + sy * eye[1] + sz * eye[2]),
406
- -(ux * eye[0] + uy * eye[1] + uz * eye[2]),
407
- f[0] * eye[0] + f[1] * eye[1] + f[2] * eye[2],
408
- 1
409
- ];
410
- }
411
-
412
- function cameraViewProjection(
413
- path: CameraPath,
414
- episodeTime: number,
415
- aspect: number
416
- ): { readonly viewProjectionMatrix: Mat4; readonly position: readonly [number, number, number]; readonly fov: number } {
417
- const sample = sampleCameraPath(path, episodeTime);
418
- const view = lookAtViewMatrix(sample.position, sample.target);
419
- const projection = perspectiveMat4((sample.fov * Math.PI) / 180, aspect, 0.1, 100);
420
- return {
421
- viewProjectionMatrix: multiplyMat4(projection, view),
422
- position: sample.position,
423
- fov: sample.fov
424
- };
425
- }
426
-
427
- export async function mountLiveRenderRoute(): Promise<void> {
428
- const root = document.querySelector<HTMLDivElement>("#app");
429
- if (!root) throw new Error("render-live-route: missing #app root element.");
430
- root.innerHTML = `
431
- <main style="margin:0;background:#0b0f1a;min-height:100vh;display:grid;place-items:center;">
432
- <canvas id="live-canvas" width="960" height="540"
433
- style="width:960px;height:540px;display:block;background:#0b0f1a;"></canvas>
434
- </main>
435
- `;
436
- const canvas = root.querySelector<HTMLCanvasElement>("#live-canvas");
437
- if (!canvas) throw new Error("render-live-route: missing #live-canvas.");
438
-
439
- const liveWindow = window as LiveRouteWindow;
440
- const width = canvas.width;
441
- const height = canvas.height;
442
-
443
- // 1. Load each rigged GLB into a skinning-capable typed actor.
444
- const characters: LiveCharacter[] = [];
445
- for (const spec of CHARACTER_SPECS) {
446
- const actor = await createTypedGLBActor({
447
- asset: { url: spec.url },
448
- id: spec.id,
449
- name: spec.id,
450
- width,
451
- height,
452
- tint: { baseColor: spec.tint, emissiveColor: [spec.tint[0] * 0.18, spec.tint[1] * 0.18, spec.tint[2] * 0.18], emissiveStrength: 0.12 }
453
- });
454
- const snapshot = actor.snapshot();
455
- if (snapshot.skinningBindingCount < 1) {
456
- throw new Error(`render-live-route: ${spec.id} GLB bound no skinning palettes (skinningBindingCount=${snapshot.skinningBindingCount}).`);
457
- }
458
- const clip = snapshot.clips.includes(spec.clip) ? spec.clip : snapshot.clips[0];
459
- if (!clip) throw new Error(`render-live-route: ${spec.id} GLB has no animation clips.`);
460
-
461
- // Discover the head-ish node (for mouth placement) and morph-bearing renderables.
462
- const scene = actor.pipeline.resources.scene;
463
- let headNode: LiveCharacter["headNode"];
464
- scene.traverse((node) => {
465
- if (!headNode && /head/i.test(node.name)) headNode = node as LiveCharacter["headNode"];
466
- });
467
- const morphRenderables: { morphWeights: number[] }[] = scene
468
- .collectRenderables()
469
- .map((entry) => entry.renderable)
470
- .filter((renderable) => renderable.morphWeights.length > 0);
471
- // Both authored GLBs expose the `mouthOpen` morph, so drive index 0 on each.
472
- const mouthMorphIndex = morphRenderables.length > 0 ? MOUTH_MORPH_INDEX : -1;
473
-
474
- characters.push({
475
- actor,
476
- clip,
477
- availableClips: snapshot.clips,
478
- position: spec.position,
479
- yaw: spec.yaw,
480
- scale: spec.scale,
481
- tint: spec.tint,
482
- headNode,
483
- morphRenderables,
484
- mouthMorphIndex
485
- });
486
- }
487
-
488
- // Build the per-shot camera plan + a camera path per shot (distinct framings).
489
- const shotPlan = buildShotPlan();
490
- const aspect = width / height;
491
- // Per-shot subject the camera centers on. Wider shots frame the two-robot
492
- // midpoint; the close-up push-in centers on miko's FACE (screen-left, the
493
- // morph/lip-sync subject) so the authored `mouthOpen` blendshape reads in frame.
494
- // The authored characters are shorter than the preset's human eyeline (target
495
- // y≈1.45), so the close-up offsets the subject DOWN to sit the mouth (world
496
- // y≈1.1) at frame center.
497
- const mikoX = CHARACTER_SPECS.find((spec) => spec.id === "miko")?.position[0] ?? -0.95;
498
- const subjectFor = (preset: CameraPresetId): [number, number, number] =>
499
- preset === "close-up" ? [mikoX, -0.35, 0] : [0, 0.4, 0];
500
- const cameraPathByShot = new Map<string, CameraPath>(
501
- shotPlan.map((shot) => [
502
- shot.shotId,
503
- createCameraPathFromPreset({
504
- id: `live-cam:${shot.shotId}`,
505
- presetId: shot.presetId,
506
- startTime: shot.episodeTime - 0.5,
507
- endTime: shot.episodeTime + 0.5,
508
- subjectPosition: subjectFor(shot.presetId)
509
- })
510
- ])
511
- );
512
-
513
- // 2. Create the advanced-runtime renderer (real WebGL2 forward pass + skinning).
514
- const renderer = await A3DRenderer.create({
515
- canvas,
516
- width,
517
- height,
518
- backend: "webgl2",
519
- alpha: false,
520
- clearColor: [0.043, 0.058, 0.101, 1]
521
- });
522
-
523
- // 3. Primitive moon-garden set (crater floor + mound, glowing moon orb + rim,
524
- // glow stones, moon lilies, broom prop) so the characters stand IN the episode's
525
- // setting. HONEST: primitive/emissive geometry, not authored GLB art.
526
- const gardenItems = createGardenItems();
527
- // Seed the dim "losing their sparkle" state for the very first frame.
528
- setGardenGlow(0);
529
-
530
- // Real directional key/fill/rim rig from the published production runtime, PLUS
531
- // two colored night-garden point lights (cyan key + warm gold rim).
532
- const lights: readonly CollectedLight[] = [
533
- ...createStudioLighting({ preset: "softbox", shadows: false, intensityScale: 0.7 }),
534
- ...createGardenLights()
535
- ];
536
-
537
- const source: RenderSource = {
538
- collectRenderItems: () => [
539
- ...gardenItems,
540
- ...characters.flatMap(collectCharacterItems)
541
- ],
542
- // Fallback only; an explicit per-shot camera is passed to render() below, so
543
- // auto-frame does not run during normal operation.
544
- cameraPolicy: "auto-frame",
545
- cameraFrameBounds: { min: [-2.6, -0.1, -1.6], max: [2.6, 3.4, 1.6] },
546
- collectedLights: lights,
547
- // A procedural environment map is required so the PBR materials get their
548
- // env-map + BRDF LUT bindings (the forward pass validates these uniforms).
549
- environmentLighting: {
550
- color: [0.4, 0.46, 0.58],
551
- intensity: 0.5,
552
- proceduralMap: {
553
- skyColor: [0.16, 0.22, 0.34],
554
- horizonColor: [0.22, 0.26, 0.32],
555
- groundColor: [0.05, 0.06, 0.08],
556
- specularColor: [0.85, 0.9, 1],
557
- intensity: 0.6,
558
- specularIntensity: 0.85
559
- }
560
- }
561
- };
562
-
563
- // Pose every character at `time`, drive the per-shot camera + lip-sync, render.
564
- const poseAt = (time: number, options: LiveSeekOptions = {}): LiveRouteSeekProof => {
565
- const shot = shotForClipTime(shotPlan, time);
566
-
567
- // WORLD-STATE dim→sparkle: normalize seek time across the captured span (the
568
- // three episode beats map onto clip-time 0..GARDEN_GLOW_SPAN; see buildShotPlan).
569
- // Early frames (beat-open) render dim "losing their sparkle"; late frames
570
- // (beat-finish) render full "twinkling again". This updates emissive strength on
571
- // every glow material so captured early vs late frames differ in glow brightness.
572
- const gardenGlow = setGardenGlow(time / GARDEN_GLOW_SPAN);
573
-
574
- // Sweep the viseme sample time across the shot's dialogue as clip time advances
575
- // within the shot, so consecutive captured frames show the mouth open AND close
576
- // (rather than a single frozen mouth state per shot).
577
- const visemeTime = visemeTimeForShot(shot, time);
578
-
579
- const characterProofs: LiveRouteSeekProof["characters"] = characters.map((character) => {
580
- // STAGED PERFORMANCE: resolve this character's blocking + clip for THIS beat.
581
- // miko crosses to the broom and plays a sweep stand-in clip at the teamwork
582
- // beat; positions differ per beat so captured frames visibly move the body.
583
- const staging = stagingFor(
584
- character.actor.id,
585
- shot.shotId,
586
- character.availableClips,
587
- character.clip,
588
- character.position,
589
- character.yaw
590
- );
591
- const apply = character.actor.playClip(staging.clip, time);
592
- const root = character.actor.pipeline.resources.scene.root;
593
- const rotation = quatFromEuler(0, staging.yaw, 0);
594
- root.transform
595
- .setPosition(staging.position[0], staging.position[1], staging.position[2])
596
- .setRotation(rotation[0], rotation[1], rotation[2], rotation[3])
597
- .setScale(character.scale, character.scale, character.scale);
598
-
599
- // Lip-sync: sample AuraVoice visemes at the swept episode time for this beat.
600
- const viseme = sampleVisemeTrack(visemeTrack, visemeTime, character.actor.id);
601
- const mouthOpenness = options.mouthOverride ?? viseme.mouthOpenness;
602
-
603
- // REAL BLENDSHAPE LIP-SYNC: drive the authored `mouthOpen` morph weight from
604
- // the sampled viseme mouthOpenness. The authored GLB's morph carries real
605
- // POSITION deltas (lower-lip drop ~0.12u), so this moves actual geometry and
606
- // the mouth visibly opens/closes in pixels. This flows through the renderer's
607
- // morph path (collectRenderItems emits morphTargets + morphWeights, ForwardPass
608
- // applies them); no primitive mouth-card is used anymore.
609
- //
610
- // The clips authored here carry no `weights` track, so `playClip` above does
611
- // not overwrite this weight; we set it after playing the body clip.
612
- let mouthMorphWeight = -1;
613
- if (character.mouthMorphIndex >= 0) {
614
- mouthMorphWeight = mouthOpenness;
615
- for (const renderable of character.morphRenderables) {
616
- while (renderable.morphWeights.length <= character.mouthMorphIndex) renderable.morphWeights.push(0);
617
- renderable.morphWeights[character.mouthMorphIndex] = mouthMorphWeight;
618
- }
619
- }
620
- // `primitiveMouthOpen` is retained in the proof shape for the capture script's
621
- // logging, but is no longer the visible driver (kept at 0 — the morph drives it).
622
- const primitiveMouthOpen = 0;
623
-
624
- return {
625
- id: character.actor.id,
626
- clip: apply.clipName,
627
- position: staging.position,
628
- sweeping: staging.sweeping,
629
- tracksApplied: apply.tracksApplied,
630
- skinningPalettesUpdated: apply.skinningPalettesUpdated,
631
- skinningBindingCount: character.actor.snapshot().skinningBindingCount,
632
- mouthOpenness,
633
- visemeId: viseme.visemeId,
634
- mouthMorphWeight,
635
- mouthMorphIndex: character.mouthMorphIndex,
636
- primitiveMouthOpen
637
- };
638
- });
639
-
640
- // BURNED-IN CAPTION: sample the active caption cue from the episode caption
641
- // track at this shot's episode time (one-to-one with the AuraVoice dialogue).
642
- // The text is reported here; the capture script draws it INTO the frame pixels
643
- // (see render-live.ts) so the exported video carries visible captions.
644
- const captionCue = captionCueAtTime(episode.captionTrack, shot.episodeTime);
645
- const caption = {
646
- text: captionCue?.text ?? "",
647
- speakerId: captionCue?.speakerId ?? "",
648
- captionId: captionCue?.captionId ?? ""
649
- };
650
-
651
- // Drive the camera explicitly from this shot's preset path.
652
- const path = cameraPathByShot.get(shot.shotId)!;
653
- const camera = cameraViewProjection(path, shot.episodeTime, aspect);
654
- const diagnostics = renderer.render(source, { viewProjectionMatrix: camera.viewProjectionMatrix });
655
-
656
- const skinnedRenderItems = characters.reduce(
657
- (sum, character) => sum + character.actor.collectRenderItems().filter((item) => item.skinning).length,
658
- 0
659
- );
660
- return {
661
- time,
662
- drawCalls: diagnostics.drawCalls,
663
- skinnedRenderItems,
664
- gardenGlow,
665
- shot: {
666
- shotId: shot.shotId,
667
- presetId: String(shot.presetId),
668
- episodeTime: shot.episodeTime,
669
- cameraPosition: camera.position,
670
- fov: camera.fov
671
- },
672
- caption,
673
- characters: characterProofs
674
- };
675
- };
676
-
677
- // Expose the headless seek hook + a ready proof for the capturer.
678
- liveWindow.__auraSeek__ = poseAt;
679
- liveWindow.__AURA_LIVE_ROUTE_READY__ = {
680
- ready: true,
681
- backend: renderer.device.kind,
682
- characters: characters.map((character) => {
683
- const snapshot = character.actor.snapshot();
684
- return {
685
- id: character.actor.id,
686
- url: character.actor.asset.url,
687
- clip: character.clip,
688
- clips: snapshot.clips,
689
- skinningBindingCount: snapshot.skinningBindingCount,
690
- skinnedRenderItemCount: character.actor.evidence.skinnedRenderItemCount
691
- };
692
- })
693
- };
694
-
695
- // Live preview loop (skipped during headless seek capture, which drives poseAt
696
- // directly). Provides a moving on-screen render when opened in a browser.
697
- let start = performance.now();
698
- const loop = (): void => {
699
- if (!liveWindow.__AURA_LIVE_ROUTE_HEADLESS__) {
700
- poseAt((performance.now() - start) / 1000);
701
- }
702
- window.requestAnimationFrame(loop);
703
- };
704
- // Render one deterministic frame immediately so the canvas is never blank.
705
- poseAt(0);
706
- start = performance.now();
707
- window.requestAnimationFrame(loop);
708
- }
709
-
710
- function collectCharacterItems(character: LiveCharacter): RenderItem[] {
711
- return character.actor.collectRenderItems();
712
- }
713
-
714
- // NOTE: the former primitive mouth-indicator quad has been removed — the authored
715
- // GLBs carry a real `mouthOpen` blendshape that is the visible lip-sync now.
716
-
717
- // Shared garden geometry (built once, reused across RenderItems each frame).
718
- const GARDEN_CUBE = Geometry.litCube(1);
719
- const GARDEN_SPHERE = Geometry.uvSphere(0.5, 24, 16);
720
- const GARDEN_CYLINDER = Geometry.cylinder({ radius: 0.5, height: 1, segments: 20, capped: true });
721
- const GARDEN_TORUS_RIM = Geometry.cylinder({ radius: 0.5, height: 1, segments: 28, capped: false });
722
-
723
- /**
724
- * Build the primitive moon garden around the characters. HONEST: these are
725
- * primitive/emissive shapes (sphere/cylinder/box), not authored GLB art. Shapes,
726
- * positions, and emissive colors mirror the cartoon-channel reference set
727
- * (`createAuraRenderedCartoonScene`). Glow-stone/lily/orb/broom emissive strengths
728
- * are registered via `makeGlowMaterial`, so `setGardenGlow(t01)` can drive the
729
- * dim→sparkle world-state on every captured frame.
730
- *
731
- * NOTE: garden materials are built ONCE and reused, so the glowMaterials registry
732
- * is populated exactly once even if this is somehow called again.
733
- */
734
- function createGardenItems(): RenderItem[] {
735
- // --- Static (non-glow) night-garden geometry ---
736
- const gardenFloor = new PBRMaterial({
737
- name: "live-route-garden-floor",
738
- baseColor: [0.09, 0.29, 0.245, 1], // #17493e crater garden
739
- metallic: 0.02,
740
- roughness: 0.78,
741
- emissiveColor: [0.03, 0.09, 0.08],
742
- emissiveStrength: 0.18
743
- });
744
- const craterRim = new PBRMaterial({
745
- name: "live-route-crater-rim",
746
- baseColor: [0.07, 0.21, 0.18, 1],
747
- metallic: 0.02,
748
- roughness: 0.82,
749
- emissiveColor: [0.02, 0.06, 0.06],
750
- emissiveStrength: 0.12
751
- });
752
- const skyPlate = new PBRMaterial({
753
- name: "live-route-garden-sky",
754
- baseColor: [0.04, 0.16, 0.255, 1], // #0a2941 night sky plate
755
- metallic: 0,
756
- roughness: 1,
757
- emissiveColor: [0.06, 0.28, 0.4],
758
- emissiveStrength: 0.55
759
- });
760
- const broomHandle = new PBRMaterial({
761
- name: "live-route-broom-handle",
762
- baseColor: [0.55, 0.42, 0.26, 1],
763
- metallic: 0.1,
764
- roughness: 0.7,
765
- emissiveColor: [0.12, 0.1, 0.06],
766
- emissiveStrength: 0.1
767
- });
768
-
769
- // --- Glow materials (driven by setGardenGlow dim→sparkle) ---
770
- // Moon portal orb + cyan rim backdrop.
771
- const moonOrb = makeGlowMaterial(
772
- "live-route-moon-orb",
773
- [0.72, 0.95, 1, 1], // #b7f4ff
774
- [0.49, 0.886, 1], // #7de2ff cyan glow
775
- 0.45,
776
- 1.55
777
- );
778
- const moonRim = makeGlowMaterial("live-route-moon-rim", [0.49, 0.886, 1, 1], [0.49, 0.886, 1], 0.4, 1.7);
779
- // Glow stones (warm gold + cyan) — the "wake the glow stones" payoff.
780
- const stoneGold = makeGlowMaterial("live-route-stone-gold", [1, 0.882, 0.557, 1], [1, 0.882, 0.557], 0.12, 1.85);
781
- const stoneCyan = makeGlowMaterial("live-route-stone-cyan", [0.49, 0.886, 1, 1], [0.49, 0.886, 1], 0.1, 1.7);
782
- // Moon lilies — "losing their sparkle" (dim) → "twinkling again" (full).
783
- const lilyStem = makeGlowMaterial("live-route-lily-stem", [0.25, 1, 0.75, 1], [0.25, 1, 0.75], 0.15, 1.05);
784
- const lilyBloom = makeGlowMaterial("live-route-lily-bloom", [0.917, 0.988, 1, 1], [0.843, 0.984, 1], 0.1, 1.35);
785
- // Broom bristles get a soft glow tip.
786
- const broomBristles = makeGlowMaterial("live-route-broom-bristles", [1, 0.882, 0.557, 1], [1, 0.882, 0.557], 0.18, 1.0);
787
-
788
- return [
789
- // Crater/garden floor + mound (characters stand on this).
790
- groundItem("garden-floor", GARDEN_CUBE, gardenFloor, [0, -0.06, -0.2], [6, 0.12, 4]),
791
- groundItem("garden-mound", GARDEN_CYLINDER, craterRim, [0, 0.0, -0.55], [3.0, 0.16, 1.4]),
792
- // Night-sky plate + glowing moon portal orb and cyan rim backdrop.
793
- groundItem("garden-sky", GARDEN_CUBE, skyPlate, [0, 2.0, -2.6], [9.0, 4.4, 0.12]),
794
- glowItem("moon-orb", GARDEN_SPHERE, moonOrb, [0, 2.05, -2.42], [1.55, 1.55, 0.16]),
795
- glowItem("moon-rim", GARDEN_TORUS_RIM, moonRim, [0, 2.05, -2.46], [1.95, 1.95, 0.1]),
796
- // Glow stones scattered across the crater floor.
797
- glowItem("glow-stone-l", GARDEN_SPHERE, stoneGold, [-1.7, 0.12, 0.45], [0.26, 0.13, 0.2]),
798
- glowItem("glow-stone-c", GARDEN_SPHERE, stoneCyan, [-0.15, 0.1, 0.35], [0.2, 0.1, 0.16]),
799
- glowItem("glow-stone-r", GARDEN_SPHERE, stoneGold, [1.75, 0.12, 0.4], [0.26, 0.13, 0.2]),
800
- glowItem("glow-stone-back", GARDEN_SPHERE, stoneCyan, [0.55, 0.1, -0.7], [0.2, 0.1, 0.16]),
801
- // Moon lilies (emissive stem + bloom) flanking the robots.
802
- glowItem("lily-stem-l", GARDEN_CYLINDER, lilyStem, [-2.05, 0.32, -0.2], [0.05, 0.62, 0.05]),
803
- glowItem("lily-bloom-l", GARDEN_SPHERE, lilyBloom, [-2.05, 0.66, -0.2], [0.34, 0.16, 0.26]),
804
- glowItem("lily-stem-r", GARDEN_CYLINDER, lilyStem, [2.1, 0.32, -0.3], [0.05, 0.62, 0.05]),
805
- glowItem("lily-bloom-r", GARDEN_SPHERE, lilyBloom, [2.1, 0.66, -0.3], [0.34, 0.16, 0.26]),
806
- // Broom prop (handle + glowing bristles), miko's hero prop.
807
- broomItem("broom-handle", GARDEN_CYLINDER, broomHandle, [-0.55, 0.5, 0.25], [0.045, 1.0, 0.045], -0.55),
808
- broomItem("broom-bristles", GARDEN_CUBE, broomBristles, [-0.82, 0.18, 0.27], [0.32, 0.18, 0.12], -0.2)
809
- ];
810
- }
811
-
812
- /** Two colored night-garden point lights (cyan key + warm gold rim), mirroring the
813
- * reference set's `lights.point` rig. Backed by real `PointLight` source nodes so
814
- * they satisfy `CollectedLight`. These complement the emissive glow geometry. */
815
- function createGardenLights(): CollectedLight[] {
816
- const make = (
817
- name: string,
818
- color: readonly [number, number, number],
819
- position: readonly [number, number, number],
820
- intensity: number,
821
- range: number
822
- ): CollectedLight => {
823
- const source = new PointLight(name);
824
- source.intensity = intensity;
825
- source.range = range;
826
- return {
827
- kind: "point",
828
- color,
829
- intensity,
830
- position,
831
- direction: [0, -1, 0],
832
- range,
833
- spotAngle: 0,
834
- penumbra: 0,
835
- castsShadow: false,
836
- layerMask: 0xffffffff,
837
- source
838
- };
839
- };
840
- return [
841
- make("garden-cyan-key", [0.49, 0.886, 1], [-1.8, 2.4, 1.2], 2.8, 12),
842
- make("garden-warm-rim", [1, 0.882, 0.557], [1.8, 1.7, 0.8], 2.0, 10)
843
- ];
844
- }
845
-
846
- function glowItem(
847
- label: string,
848
- geometry: Geometry,
849
- material: PBRMaterial,
850
- position: readonly [number, number, number],
851
- scale: readonly [number, number, number]
852
- ): RenderItem {
853
- return {
854
- label,
855
- geometry,
856
- material,
857
- modelMatrix: composeMat4([...position], quatFromEuler(0, 0, 0), [...scale]) as Mat4,
858
- includeInAutoFrame: false
859
- };
860
- }
861
-
862
- function broomItem(
863
- label: string,
864
- geometry: Geometry,
865
- material: PBRMaterial,
866
- position: readonly [number, number, number],
867
- scale: readonly [number, number, number],
868
- rollZ: number
869
- ): RenderItem {
870
- return {
871
- label,
872
- geometry,
873
- material,
874
- modelMatrix: composeMat4([...position], quatFromEuler(0, 0, rollZ), [...scale]) as Mat4,
875
- includeInAutoFrame: false
876
- };
877
- }
878
-
879
- function groundItem(
880
- label: string,
881
- geometry: Geometry,
882
- material: PBRMaterial,
883
- position: readonly [number, number, number],
884
- scale: readonly [number, number, number]
885
- ): RenderItem {
886
- return {
887
- label,
888
- geometry,
889
- material,
890
- modelMatrix: composeMat4([...position], quatFromEuler(0, 0, 0), [...scale]) as Mat4,
891
- includeInAutoFrame: true
892
- };
893
- }
894
-
895
- declare global {
896
- interface Window {
897
- __AURA_LIVE_ROUTE_HEADLESS__?: boolean;
898
- }
899
- }
900
-
901
- void mountLiveRenderRoute().catch((error: unknown) => {
902
- const diagnostics = (error as { diagnostics?: readonly string[] })?.diagnostics;
903
- const baseMessage = error instanceof Error ? error.message : String(error);
904
- const message = diagnostics && diagnostics.length > 0 ? `${baseMessage} :: ${diagnostics.join(" | ")}` : baseMessage;
905
- const liveWindow = window as LiveRouteWindow & { __AURA_LIVE_ROUTE_ERROR__?: string };
906
- liveWindow.__AURA_LIVE_ROUTE_ERROR__ = message;
907
- const root = document.querySelector<HTMLDivElement>("#app");
908
- if (root) {
909
- root.innerHTML = `<pre style="color:#f88;background:#0b0f1a;padding:24px;white-space:pre-wrap;">render-live-route failed: ${message}</pre>`;
910
- }
911
- // eslint-disable-next-line no-console
912
- console.error("render-live-route failed", error);
913
- });