create-aura3d 1.1.2 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/package.json +10 -9
- package/templates/{cartoon-channel → animation-channel}/README.md +11 -11
- package/templates/{cartoon-studio → animation-channel}/index.html +1 -1
- package/templates/{cartoon-channel → animation-channel}/package.json +2 -2
- package/templates/{cartoon-channel → animation-channel}/src/episode.ts +22 -22
- package/templates/{cartoon-channel → animation-channel}/src/experimental/README.md +5 -5
- package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.css +3 -3
- package/templates/{cartoon-channel → animation-channel}/src/experimental/concept-episode-2-5d.ts +7 -7
- package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.css +4 -4
- package/templates/{cartoon-channel → animation-channel}/src/experimental/image-puppet-episode.ts +6 -6
- package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.css +3 -3
- package/templates/{cartoon-channel → animation-channel}/src/experimental/puppet-episode-2d.ts +7 -7
- package/templates/{cartoon-channel → animation-channel}/src/main.ts +32 -32
- package/templates/{cartoon-channel → animation-channel}/src/render-plan.ts +13 -13
- package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.css +34 -34
- package/templates/{cartoon-channel → animation-channel}/src/sample-episode-visual.ts +20 -20
- package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/concept-2-5d.spec.ts +6 -6
- package/templates/{cartoon-channel → animation-channel}/tests/image-puppet-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/image-puppet.spec.ts +5 -5
- package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d-animation.spec.ts +3 -3
- package/templates/{cartoon-channel → animation-channel}/tests/puppet-2d.spec.ts +4 -4
- package/templates/{prompt-cartoon-channel → animation-channel}/tests/route-health.spec.ts +1 -1
- package/templates/{cartoon-channel → animation-channel}/tests/sample-episode-visual.spec.ts +5 -5
- package/templates/{cartoon-channel → animation-channel}/tests/storyboard-playback.spec.ts +7 -7
- package/templates/animation-studio/AGENTS.md +71 -0
- package/templates/{cartoon-studio → animation-studio}/ASSET-LICENSES.md +7 -7
- package/templates/{cartoon-studio → animation-studio}/README.md +6 -6
- package/templates/animation-studio/aura.assets.json +669 -0
- package/templates/animation-studio/dist/episodes/scene/episode-3d.webm +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/action.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/dialogue.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/final.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/first.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-closed.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/frames/mouth-open.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/render-live-summary.json +10284 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-1.png +0 -0
- package/templates/animation-studio/dist/episodes/scene/skeleton-overlays/worker-2.png +0 -0
- package/templates/animation-studio/dist/generated/second-scene.document.json +1210 -0
- package/templates/animation-studio/dist/generated/studio-edited.document.json +1196 -0
- package/templates/animation-studio/dist/scene/inshader-accept.document.json +1 -0
- package/templates/animation-studio/dist/scene/inshader-test.document.json +1 -0
- package/templates/animation-studio/dist/scene/integ.document.json +1 -0
- package/templates/animation-studio/dist/scene/working.document.json +723 -0
- package/templates/animation-studio/dist/scene/working.history.json +1 -0
- package/templates/{cartoon-studio → animation-studio}/live-route.html +1 -1
- package/templates/{cartoon-studio → animation-studio}/package-lock.json +2 -2
- package/templates/animation-studio/package.json +32 -0
- package/templates/animation-studio/public/aura-assets/cast-a.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/cast-b.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/luma.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/luma2.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/miko.catalog.glb +0 -0
- package/templates/{cartoon-studio → animation-studio}/public/aura-assets/moonGarden.gltf +1 -1
- package/templates/animation-studio/public/aura-assets/mushroom.catalog.glb +0 -0
- package/templates/animation-studio/public/aura-assets/rusty.catalog.glb +0 -0
- package/templates/animation-studio/public/cast-library/cast-library.json +71 -0
- package/templates/animation-studio/public/clip-library/gesture.json +173903 -0
- package/templates/animation-studio/public/clip-library/idle.json +23035 -0
- package/templates/animation-studio/public/clip-library/manifest.json +154 -0
- package/templates/animation-studio/public/clip-library/nod.json +173903 -0
- package/templates/animation-studio/public/clip-library/point.json +69879 -0
- package/templates/animation-studio/public/clip-library/react.json +66084 -0
- package/templates/animation-studio/public/clip-library/run.json +9648 -0
- package/templates/animation-studio/public/clip-library/talk.json +69879 -0
- package/templates/animation-studio/public/clip-library/walk.json +8283 -0
- package/templates/animation-studio/public/hifi-cast/BrainStem.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/CesiumMan.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/RiggedFigure.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Barbarian.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Knight.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Mage.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Rogue.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/kaykit/Rogue_Hooded.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/khronos-report.json +295 -0
- package/templates/animation-studio/public/hifi-cast/quaternius-report.json +449 -0
- package/templates/animation-studio/public/hifi-cast/rpm/cesiumman.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm/fox.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm/riggedfigure.glb +0 -0
- package/templates/animation-studio/public/hifi-cast/rpm-report.json +301 -0
- package/templates/animation-studio/scripts/anim.ts +103 -0
- package/templates/animation-studio/scripts/animation-scene.ts +457 -0
- package/templates/animation-studio/scripts/asset-motion-probe.ts +450 -0
- package/templates/animation-studio/scripts/asset-render-probe.ts +152 -0
- package/templates/animation-studio/scripts/author-office-scene.ts +65 -0
- package/templates/animation-studio/scripts/build-cast-library.ts +97 -0
- package/templates/animation-studio/scripts/build-characters.ts +1543 -0
- package/templates/animation-studio/scripts/build-clip-library.ts +972 -0
- package/templates/animation-studio/scripts/clip-decision-cli.ts +51 -0
- package/templates/animation-studio/scripts/determinism-check.ts +48 -0
- package/templates/animation-studio/scripts/generate-scene.ts +95 -0
- package/templates/animation-studio/scripts/hifi-cast-validate.ts +572 -0
- package/templates/animation-studio/scripts/motion-evidence-cli.ts +150 -0
- package/templates/animation-studio/scripts/motion-evidence.ts +362 -0
- package/templates/animation-studio/scripts/preview-server.ts +52 -0
- package/templates/animation-studio/scripts/regen-manifest.ts +156 -0
- package/templates/animation-studio/scripts/render-core.ts +360 -0
- package/templates/animation-studio/scripts/render-live.ts +813 -0
- package/templates/animation-studio/scripts/render-parallel.ts +70 -0
- package/templates/animation-studio/scripts/render-server.ts +119 -0
- package/templates/animation-studio/scripts/resolve-asset.ts +669 -0
- package/templates/animation-studio/scripts/skeleton-overlay-cli.ts +82 -0
- package/templates/animation-studio/scripts/skeleton-overlay.ts +293 -0
- package/templates/animation-studio/scripts/studio-tools-demo.ts +45 -0
- package/templates/animation-studio/scripts/texture-bake.ts +297 -0
- package/templates/{cartoon-studio → animation-studio}/scripts/validate-characters.ts +0 -0
- package/templates/animation-studio/scripts/verify-extracted-overlay-cli.ts +264 -0
- package/templates/animation-studio/src/animation-episode-validator.ts +97 -0
- package/templates/animation-studio/src/animation-performance.ts +556 -0
- package/templates/animation-studio/src/aura-assets.ts +71 -0
- package/templates/animation-studio/src/character.ts +40 -0
- package/templates/animation-studio/src/director/compile-episode-document.ts +45 -0
- package/templates/animation-studio/src/director/director-heuristics.ts +697 -0
- package/templates/animation-studio/src/director/prompt-to-scene.ts +369 -0
- package/templates/animation-studio/src/empty-document.ts +77 -0
- package/templates/animation-studio/src/episode-document.ts +442 -0
- package/templates/animation-studio/src/examples/moon-garden.example.ts +183 -0
- package/templates/animation-studio/src/fidelity.ts +129 -0
- package/templates/animation-studio/src/graph.ts +14 -0
- package/templates/animation-studio/src/procedural-hdri.ts +243 -0
- package/templates/animation-studio/src/profile.ts +97 -0
- package/templates/animation-studio/src/render-live-route.ts +42 -0
- package/templates/animation-studio/src/render-modes.ts +137 -0
- package/templates/animation-studio/src/scene-player.ts +867 -0
- package/templates/animation-studio/src/set-templates.ts +361 -0
- package/templates/animation-studio/src/studio/episode-document-store.ts +72 -0
- package/templates/animation-studio/src/studio/scene-tool-registry.ts +86 -0
- package/templates/animation-studio/src/studio/scene-tools.ts +169 -0
- package/templates/animation-studio/tests/reports/animation-studio/hifi-cast-validation.json +938 -0
- package/templates/animation-studio/tsconfig.json +26 -0
- package/templates/{cartoon-studio → animation-studio}/vite.config.ts +6 -1
- package/templates/character-controller/README.md +33 -0
- package/templates/character-controller/index.html +19 -0
- package/templates/character-controller/package.json +23 -0
- package/templates/character-controller/playwright.config.ts +14 -0
- package/templates/character-controller/src/controller.ts +44 -0
- package/templates/character-controller/src/main.ts +62 -0
- package/templates/character-controller/tests/route-health.spec.ts +26 -0
- package/templates/character-controller/tests/screenshot.spec.ts +9 -0
- package/templates/character-controller/tsconfig.json +15 -0
- package/templates/cinematic-scene/package.json +1 -1
- package/templates/episode-builder/README.md +4 -4
- package/templates/episode-builder/package.json +1 -1
- package/templates/episode-builder/src/aura-assets.ts +1 -1
- package/templates/episode-builder/src/builder.ts +4 -4
- package/templates/episode-builder/src/episode.ts +21 -21
- package/templates/episode-builder/src/main.ts +20 -20
- package/templates/episode-builder/src/render-plan.ts +13 -13
- package/templates/episode-builder/tests/storyboard-playback.spec.ts +7 -7
- package/templates/fighting-game/package.json +1 -1
- package/templates/mini-game/package.json +1 -1
- package/templates/product-viewer/package.json +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/README.md +5 -5
- package/templates/{cartoon-channel → prompt-animation-channel}/index.html +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/package.json +2 -2
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/aura-assets.ts +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/episode.ts +22 -22
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/main.ts +28 -28
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/render-plan.ts +13 -13
- package/templates/{cartoon-channel → prompt-animation-channel}/tests/route-health.spec.ts +1 -1
- package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tests/storyboard-playback.spec.ts +8 -8
- package/templates/cartoon-studio/aura.assets.json +0 -400
- package/templates/cartoon-studio/package.json +0 -33
- package/templates/cartoon-studio/public/aura-assets/luma.authored.glb +0 -0
- package/templates/cartoon-studio/public/aura-assets/miko.authored.glb +0 -0
- package/templates/cartoon-studio/scripts/build-characters.ts +0 -738
- package/templates/cartoon-studio/scripts/build-dialogue-audio.ts +0 -262
- package/templates/cartoon-studio/scripts/episode.ts +0 -323
- package/templates/cartoon-studio/scripts/render-live.ts +0 -873
- package/templates/cartoon-studio/src/aura-assets.ts +0 -64
- package/templates/cartoon-studio/src/characters.ts +0 -65
- package/templates/cartoon-studio/src/episode-renderer.ts +0 -605
- package/templates/cartoon-studio/src/episode.ts +0 -306
- package/templates/cartoon-studio/src/main.ts +0 -400
- package/templates/cartoon-studio/src/render-live-route.ts +0 -913
- package/templates/cartoon-studio/src/render-plan.ts +0 -567
- package/templates/cartoon-studio/src/review.ts +0 -99
- package/templates/cartoon-studio/src/studio.ts +0 -101
- package/templates/cartoon-studio/test-results/.last-run.json +0 -4
- package/templates/cartoon-studio/tests/episode-render.spec.ts +0 -44
- package/templates/cartoon-studio/tests/motion-quality.spec.ts +0 -45
- package/templates/cartoon-studio/tests/route-health.spec.ts +0 -7
- package/templates/cartoon-studio/tests/storyboard-playback.spec.ts +0 -99
- package/templates/cartoon-studio/tests/visual-review.spec.ts +0 -21
- package/templates/cartoon-studio/tsconfig.json +0 -14
- package/templates/prompt-cartoon-channel/index.html +0 -13
- package/templates/prompt-cartoon-channel/src/sets.ts +0 -14
- /package/templates/{cartoon-channel → animation-channel}/aura.assets.json +0 -0
- /package/templates/{cartoon-channel → animation-channel}/playwright.config.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.047f5e5f.glb +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/luma.thumb.svg +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.047f5e5f.glb +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/miko.thumb.svg +0 -0
- /package/templates/{cartoon-channel → animation-channel}/public/aura-assets/moon-garden-feature-frame.png +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/aura-assets.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/characters.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/contract.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/src/sets.ts +0 -0
- /package/templates/{cartoon-channel → animation-channel}/test-results/.last-run.json +0 -0
- /package/templates/{cartoon-channel → animation-channel}/tests/reports/prompt-animation/cartoon-sample-episode.png +0 -0
- /package/templates/{cartoon-channel → animation-channel}/tsconfig.json +0 -0
- /package/templates/{cartoon-studio → animation-studio}/playwright.config.ts +0 -0
- /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/luma.thumb.svg +0 -0
- /package/templates/{cartoon-studio → animation-studio}/public/aura-assets/miko.thumb.svg +0 -0
- /package/templates/{cartoon-studio → animation-studio}/src/contract.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/playwright.config.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/characters.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/src/contract.ts +0 -0
- /package/templates/{cartoon-studio → prompt-animation-channel}/src/sets.ts +0 -0
- /package/templates/{prompt-cartoon-channel → prompt-animation-channel}/tsconfig.json +0 -0
|
@@ -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
|
-
});
|