[m7-A] non-mixamo rig support + world-integrated backdrops (+4 sync7 render fixes)

Lane A: bone-alias layer so CC_Base_*/Unreal-HumanIK/Rigify/Rokoko/Biped rigs
fold onto the canonical mixamo names — lady.glb pairs 52/102 bones and
retargets (53 tracks) where she previously threw 'missing hips'; mixamo→mixamo
pairing asserted bit-identical. Backdrops gain frustum auto-fit, floor tint
sampled from the plate, and FogExp2 in the plate's colour via a keyable
param. Also caught: _buildLight ran _refreshDefaults before registration, so
scene lights never dimmed the built-in hemi (every preset was washed out).

Orchestrator SYNC-7 fixes (found only by rendering actual frames):
- editor chrome (gizmo/grid/helpers/cam proxies) was baked into output mp4s
- timeline's per-tick rest transform dragged fitted plates out of frame
- fit used the viewport aspect, output renders at its own → letterboxed plates
- plate offset put its top edge on the lens axis → void above centre-frame;
  now params.fitAnchor (default 0.75) picks the row that sits at eye level

Verified: 5 suites green; 1280x720 render shows a CC character dancing a mixamo
clip against an edge-to-edge Flow plate with a horizon and matched ground.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-07-25 22:21:37 +10:00
parent fe3d5404a9
commit 7fea880286
10 changed files with 1220 additions and 24 deletions

View File

@ -217,3 +217,28 @@ videoTex.needsUpdate — r160 VideoTexture rides requestVideoFrameCallback which
never fires in hidden tabs → black plates in renders. Backlog carried: honor
audio in/out at mux (C), walk-to-mark prevPos pre-apply (A), live viseme asset
still owed by John.
### 2026-07-25 — M7-A ACCEPTED at SYNC 7. Outstanding work; four orchestrator fixes to absorb.
Verified live: `?selftest=1` PASS+alias ok; **lady.glb (CC_Base rig) retargets — 53 tracks
where she used to throw**; man.fbx unchanged (66); both clips land; fog FogExp2 #4b3320 from
the plate; floor tinted #452f1d; old `sync2-sunset` (no `fit`) loads unchanged. The
`_refreshDefaults` catch was real — presets read far stronger now.
Four things only a rendered frame could show, fixed by the orchestrator in stage.js — absorb,
don't revert:
1. **Editor chrome was baked into the mp4** — a green translate gizmo across every frame.
`_chrome(false)` now hides gizmo/grid/helpers/camera proxies whenever `_paused` (which is
exactly "the caller is rendering output"); camera proxies carry `userData.chrome`.
2. **The timeline dragged fitted plates out of frame**`evaluate()` re-applies each entity's
rest transform every tick, undoing the fit. `setTransform` now re-fits instead for
`fit:'frustum'` backdrops (the lens owns them; keyframe a plate only with fit:'free').
3. **Plates letterboxed in every render** — fit used the LIVE viewport aspect while output
renders at its own (1280×720 etc). `_render` re-fits when the aspect it's about to draw at
differs. Pure algebra, no re-sampling.
4. **`root.position.y = -height/2` put the plate's TOP EDGE on the lens axis**, so everything
above centre-frame was void; centring instead put the road at eye level. Now
`params.fitAnchor` (default 0.75) = which fraction of the image sits at eye level. Expose it
in the inspector next to fitDist.
NEXT for Lane A (not urgent): expose `fitAnchor`; the hard plate/floor horizon seam could use a
soft gradient; and lady's hips-travel `ratio` 0 (Z-up CC export) still means CC clips play in
place — fine for machinima (keyframe the walk) but worth a look if a walk cycle ever needs root motion.

View File

@ -295,3 +295,119 @@ NEXT / orchestrator SYNC checklist (browser):
4. finalRender a 2s scene with the plate — frames must show advancing video (syncVideos is
awaited by render.js already).
5. NL box stays hidden until /director exists (probe is GET → expect 405 to reveal).
## 2026-07-25 session 6 (M7-A1 non-mixamo rigs + M7-A2 world-integrated backdrops)
DONE — both M7 items, headless-verified (no browser, no servers touched, per orchestrator order):
**M7-A1 — bone aliasing.** `canon()` used to strip a mixamorig prefix and nothing else, so every
non-mixamo skeleton paired 0 bones and died at "missing hips" (lady.glb has never been animatable).
- `room3d.js:70` legacy strip is now `rawCanon`; `room3d.js:136` `canon = raw ∈ CANON ? raw : alias(n) || raw`.
The CANON short-circuit is what makes mixamo→mixamo **bit-identical** — a name that already canons
to a mixamo bone never touches the alias tables (test asserts all 65 man.fbx bones × 5 namespace
spellings against an inlined copy of the old one-liner).
- Tables (room3d.js:77125): `CANON` (the 54 mixamo names, generated), `EXACT` side-less bones,
`STEMS` sided stems + generated finger stems, `SIDES` side decompositions. `alias()` walks 4
progressively-normalised candidates — raw → prefix-stripped → index-stripped → both — and takes the
first hit, so **`CC_Base_Spine01` (2nd spine bone) beats the generic `spine_01` (1st) reading**.
Structure handled: namespaces (`mixamorig4:`, `Armature|`), rig prefixes (`CC_Base_`, `DEF-`/`ORG-`/
`MCH-`, `Bip01 `), CC's per-export numeric tail (`_02`, `_0100`), and side spellings
`L_`/`_l`/`.L`/`Left`. A decomposition only counts when the leftover stem is known, so `lowerarm`
is never read as left-"owerarm" and `shoulder` is never read as "shoulde"+right.
- Covered: Character Creator (bone names read out of the REAL lady.glb JSON chunk, not guessed),
Unreal/HumanIK, Blender Rigify, Unity-humanoid/VRM/Rokoko long names, plain lowercase, 3ds Max Biped.
Twist / share / toe-detail / facial / breast / eye bones deliberately stay UNMAPPED — the pair map
already tolerates missing pairs, and a guessed mapping is worse than an unbaked bone.
- Guardrail: pairing moved out of `bakeRetarget` into three-free `pairBones()` (room3d.js:146). Under
`min(8, boneCount)` matches it throws e.g.
`retarget: only 1/54 bones matched (target looks like 'CC_Base', source looks like 'mixamo'; first
unmatched bone 'CC_Base_BoneRoot_01'); unsupported skeleton`. `rigStyle()` (room3d.js:127) does the
detection. The missing-hips error (room3d.js:176) now names both styles too. `min(8, …)` keeps
`_selftest`'s 3-bone synthetic skeletons legal.
- **Result on the real asset: lady.glb pairs 52/102 bones** — core 6 + both arms/legs + all 30 finger
bones, `CC_Base_Hip` wins `hips` and `CC_Base_Pelvis` folds onto the same key and is left unbaked
(dedupe does the right thing). Reverse direction (CC as the clip SOURCE onto a mixamo body) is 52/52.
**M7-A2 — world-integrated backdrops.** A plate read as a billboard in the void: gap under it, alien
floor, no air. Three fixes, all recomputed on load/param/camera change, never per frame (stage.js:340415).
- `params.fit:'frustum'``_fitBackdrop` scales `en.root` to COVER the active camera's frustum at
`params.fitDist` (default 14m) with 6% bleed, drops the plate's centre onto the wrapper origin, parks
the wrapper on the camera axis and `lookAt`s the camera. Because the plate now extends BELOW y=0 the
floor cuts it at a clean horizon — the gap is gone. `fitScale()` (stage.js:62) is a pure export so
node can assert the algebra. Recompute triggers: `addEntity`, `_rebuild`, `setParam(fov)` on the
ACTIVE cam, `setActiveCamera`, `_resize`, end of `applyState`, and the inspector's `⤢ refit`.
Absent `fit` = old behaviour (saved scenes unchanged); dock-added plates default to `'frustum'`.
- `params.groundTint` (default on) — `_plateGround` draws the plate's map (image, poster, or video
frame) into a 24×24 2D canvas and averages the bottom 7 rows, once per load/param change; the stage
floor takes that colour ×0.85 and the grid helper fades to 0.12 opacity while a plate is present.
- Atmosphere — `FogExp2` coloured from the same sample, density from a **keyable `fog` param on the
ambient light entity** (same trick as `bg`: Lane B's params track drives it with zero schema change).
Default 0.02, off with no plate. Plate/dome materials get `fog:false` so atmosphere sits between the
camera and the plate instead of washing it out. Fog object is mutated, not reallocated, when keyed.
- dock.js: inspector gains `fit` select, `fit dist`, `⤢ refit to camera`, `ground tint` checkbox
(new `_chk` helper, dock.js:250) and `fog` on ambient lights; new `_chk` styling in style.css:52.
VERIFICATION (headless only):
- `node scenegod/web/room3d_test.mjs``ALL PASS (26 CC bones paired against mixamo)`. NEW file; it
loads the REAL room3d.js with the three imports stripped and imported as a data: URL, so the shipped
tables are what's asserted (no drift-prone copy). Covers: 65 mixamo bones × 5 namespaces identical to
the legacy canon; 20 CC name→canonical assertions; 8 CC twist/share/facial bones must stay unmapped;
36 Unreal/Rigify/Unity/Biped assertions; side-less limbs must stay unmapped; CC↔mixamo pairing = 26
on the sample; mixamo→mixamo pairing 65/65 unchanged; the guardrail throws with style + unmatched
root in the message; 3-bone `_selftest` skeletons still legal; `rigStyle` on 5 rigs.
- `node scenegod/web/stage_test.mjs` → `ALL PASS (fit s=2.186 → plate 21.9×12.3m over a 20.6×11.6m
frame at 14m)`. NEW file, same import trick on stage.js: frustum size, cover-on-both-axes, bleed
factor, portrait/letterbox plates scaling on the tight axis, distance-invariance of framing, longer
lens → smaller plate.
- `node scenegod/web/presets_test.mjs` (M5) and `timeline_test.mjs` (Lane B) still pass unchanged.
- `node --check` clean on room3d.js / stage.js / dock.js / presets.js / grammar.js and on index.html's
inline module.
DECISIONS:
- Aliasing lives in `canon()` behind a CANON short-circuit rather than a new module — Lane A owns 5
files and the baker is the only consumer. `pairBones`/`rigStyle`/`fitScale` are exported THREE-free
so node can test the parts that actually encode judgement.
- Ambiguity resolved by qualification, not by guessing: CC's 3-bone torso (Waist/Spine01/Spine02) maps
to mixamo Spine/Spine1/Spine2 via `ccbase*` keys; UE's `spine_01/02/03` maps to the same three via
generic keys. Chains longer than mixamo's 3 (UE `spine_04/05`, rigify `spine.003+`) stay unmapped.
- **Known limit, marked `ponytail:` at room3d.js:190**: lady.glb hangs its hip height off LOCAL Z
(Z-up CC export), so the hips-travel `ratio` lands 0 and clips play IN PLACE. That is the safe read —
the source's travel delta is in the SOURCE's axes and scaling it into a Z-up hip would fly the
character. Rotations are unaffected (they transfer through world space), so she dances correctly.
Proper fix = rotate the travel delta through both rests' parent world quats; deliberately not done,
since it would perturb the verified mixamo→mixamo path for a case nobody has asked for yet.
- Auto-fit OWNS the backdrop's wrapper transform (marked `ponytail:` at stage.js:355) — keyframe a
plate only with `fit:'free'`. Fit is NOT recomputed on camera *movement* (only fov/cut/resize):
following a dolly would glue the plate to the lens and kill parallax.
- **Absorbed bug fix you should know about (visual delta):** `_buildLight` ran `_refreshDefaults()`
BEFORE the entity was registered in `_entities`, so adding a scene light never dimmed the built-in
hemi (2.2) / key — every M5 lighting preset was being washed out by studio defaults. Now refreshed
after registration (stage.js:197). Expect noir/night/golden_hour to read much stronger than in the
SYNC-6 screenshots; RoomEnvironment IBL still keeps nothing pitch black.
BLOCKED/REQUESTS:
- @orchestrator (PLAN §4.1 amendment, no code impact): backdrop `params` gains `fit:"frustum"|"free"`,
`fitDist` (m, default 14) and `groundTint` (bool, default true); `light` params gain `fog` (FogExp2
density, keyable, lives on the ambient entity next to `bg`). All round-trip through
captureState/applyState today.
- @Lane B (free win, not blocking): `fog` behaves exactly like `bg` — a params-track key on the ambient
light entity animates the atmosphere. A "haze up over 2s" key is now a one-liner for the DIRECT panel.
- Still owed by John: a viseme-bearing test character (carried from session 4).
NEXT / orchestrator SYNC checklist (browser, :8020):
1. `?selftest=1` → header badge must read `selftest: PASS (maxErr=…, alias ok)` — the alias check is
new (index.html:33), it canons CC + Unreal + namespaced-mixamo names in-page.
2. **Lady dances (the headline):** CHARACTERS → lady → ANIMATIONS → hiphop.fbx (drag onto her, or
select her and click the card) → block lands on the timeline → press play. Before today this threw
`retarget: missing hips (mixamorig skeleton?)`. Console proof if you want it:
`stage.prepareClip('e1','animations/test/hiphop.fbx',0).then(c=>console.log(c.tracks.length))`
expect **53** tracks (52 bone quats + 1 hips position). She dances in place by design (see the
ponytail note above); man.fbx is unchanged and still travels.
3. **Plate fills frame + ground + fog:** BACKDROPS → the golden-hour video plate (or street.jpg) →
it should land already filling the view with no gap under it, the floor should go warm/orange, the
grid should nearly vanish. Inspector shows `fit: frustum`, `fit dist 14`, `ground tint ✓`. Move the
director cam and hit `⤢ refit to camera` to re-fill. Add an ambient light → its inspector `fog`
field (0.02 default) — crank to 0.08 to see the haze eat the far floor, 0 to kill it.
4. Cut to a scene camera (◉ set active camera) — the plate refits to THAT lens; change its fov and it
refits again. PiP should show the same filled frame.
5. Regression watch: existing saved scenes (`sync2-sunset`) have no `fit` param → plates must load at
their old size/place. Lighting will look stronger than before (see the `_refreshDefaults` note).

View File

@ -36,3 +36,663 @@ INFO: 127.0.0.1:57176 - "GET /director HTTP/1.1" 405 Method Not Allowed
INFO: 127.0.0.1:57177 - "GET /web/timeline.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:57177 - "GET /web/tlui.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:57221 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK
INFO: 127.0.0.1:56254 - "GET /?selftest=1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56254 - "GET /web/style.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:56254 - "GET /web/stage.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56255 - "GET /web/dock.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56255 - "GET /web/room3d.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56255 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56254 - "GET /web/presets.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56255 - "GET /assets/tree HTTP/1.1" 200 OK
INFO: 127.0.0.1:56255 - "GET /web/grammar.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56255 - "GET /director HTTP/1.1" 405 Method Not Allowed
INFO: 127.0.0.1:56254 - "GET /web/timeline.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56254 - "GET /web/tlui.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56323 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK
INFO: 127.0.0.1:56323 - "GET /assets/thumb?path=backdrops%2Fvideo%2Fscenegod_plates%2Fsuburb_strip_goldenhour.mp4 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56502 - "GET /scenes/sync2-sunset HTTP/1.1" 200 OK
INFO: 127.0.0.1:56502 - "GET /assets/file?path=backdrops%2Ftest%2Fstreet.jpg HTTP/1.1" 200 OK
INFO: 127.0.0.1:56502 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK
INFO: 127.0.0.1:56606 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:56606 - "GET /web/style.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:56607 - "GET /web/dock.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56606 - "GET /web/stage.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56606 - "GET /web/room3d.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56608 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:56607 - "GET /web/presets.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56606 - "GET /assets/tree HTTP/1.1" 200 OK
INFO: 127.0.0.1:56607 - "GET /web/grammar.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56607 - "GET /director HTTP/1.1" 405 Method Not Allowed
INFO: 127.0.0.1:56606 - "GET /web/timeline.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56606 - "GET /web/tlui.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "GET /web/render.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/begin HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/0 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/2 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/3 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/4 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/5 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/6 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/8 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/9 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/11 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/12 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/13 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/14 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/15 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/16 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/17 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/18 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/19 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/20 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/21 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/22 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/23 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/24 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/25 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/28 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/29 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/30 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/31 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/32 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/33 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/34 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/35 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/36 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/37 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/38 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/39 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/40 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/41 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/42 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/43 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/44 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/45 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/46 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/47 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/48 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/49 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/50 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/51 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/52 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/53 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/54 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/55 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/56 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/57 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/58 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/59 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/60 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/61 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/62 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/63 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/64 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/65 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/66 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/67 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/68 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/69 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/70 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/71 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/72 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/73 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/74 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/75 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/76 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/77 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/78 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/79 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/80 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/81 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/82 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/83 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/84 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/85 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/86 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/87 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/88 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/frame/89 HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "POST /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/end HTTP/1.1" 200 OK
INFO: 127.0.0.1:56730 - "GET /render/3a5ca564eb7840ceae8ae1c5dcd9bfe3/status HTTP/1.1" 200 OK
INFO: 127.0.0.1:57926 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:57926 - "GET /web/style.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:57928 - "GET /web/dock.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:57927 - "GET /web/stage.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:57927 - "GET /web/room3d.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:57928 - "GET /web/presets.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:57926 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:57927 - "GET /assets/tree HTTP/1.1" 200 OK
INFO: 127.0.0.1:57927 - "GET /web/grammar.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:57927 - "GET /director HTTP/1.1" 405 Method Not Allowed
INFO: 127.0.0.1:57926 - "GET /web/timeline.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:57926 - "GET /web/tlui.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "GET /web/render.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/begin HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/0 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/2 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/3 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/4 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/5 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/6 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/8 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/9 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/11 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/12 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/13 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/14 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/15 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/16 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/17 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/18 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/19 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/20 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/21 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/22 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/23 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/24 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/25 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/28 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/29 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/30 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/31 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/32 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/33 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/34 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/35 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/36 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/37 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/38 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/39 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/40 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/41 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/42 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/43 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/44 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/45 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/46 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/47 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/48 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/49 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/50 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/51 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/52 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/53 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/54 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/55 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/56 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/57 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/58 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/59 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/60 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/61 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/62 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/63 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/64 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/65 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/66 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/67 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/68 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/69 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/70 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/71 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/72 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/73 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/74 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/75 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/76 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/77 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/78 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/79 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/80 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/81 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/82 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/83 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/84 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/85 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/86 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/87 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/88 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/frame/89 HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "POST /render/a219ad7a213f499f9630c36a62e6c90f/end HTTP/1.1" 200 OK
INFO: 127.0.0.1:58008 - "GET /render/a219ad7a213f499f9630c36a62e6c90f/status HTTP/1.1" 200 OK
INFO: 127.0.0.1:59022 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:59022 - "GET /web/style.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:59023 - "GET /web/stage.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59024 - "GET /web/dock.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59024 - "GET /web/room3d.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59023 - "GET /web/presets.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59024 - "GET /assets/tree HTTP/1.1" 200 OK
INFO: 127.0.0.1:59022 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:59023 - "GET /web/grammar.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59023 - "GET /director HTTP/1.1" 405 Method Not Allowed
INFO: 127.0.0.1:59022 - "GET /web/timeline.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59022 - "GET /web/tlui.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "GET /web/render.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/begin HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/0 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/2 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/3 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/4 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/5 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/6 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/8 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/9 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/11 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/12 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/13 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/14 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/15 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/16 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/17 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/18 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/19 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/20 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/21 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/22 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/23 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/24 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/25 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/28 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/29 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/30 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/31 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/32 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/33 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/34 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/35 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/36 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/37 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/38 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/39 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/40 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/41 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/42 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/43 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/44 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/45 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/46 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/47 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/48 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/49 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/50 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/51 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/52 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/53 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/54 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/55 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/56 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/57 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/58 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/59 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/60 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/61 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/62 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/63 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/64 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/65 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/66 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/67 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/68 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/69 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/70 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/71 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/72 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/73 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/74 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/75 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/76 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/77 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/78 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/79 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/80 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/81 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/82 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/83 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/84 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/85 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/86 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/87 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/88 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/frame/89 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "POST /render/203d2bb3faa24225a1a6f42eec25a555/end HTTP/1.1" 200 OK
INFO: 127.0.0.1:59090 - "GET /render/203d2bb3faa24225a1a6f42eec25a555/status HTTP/1.1" 200 OK
INFO: 127.0.0.1:59731 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:59731 - "GET /web/style.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:59731 - "GET /web/stage.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59732 - "GET /web/dock.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59732 - "GET /web/room3d.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59731 - "GET /web/presets.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59732 - "GET /assets/tree HTTP/1.1" 200 OK
INFO: 127.0.0.1:59733 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:59731 - "GET /web/grammar.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59731 - "GET /director HTTP/1.1" 405 Method Not Allowed
INFO: 127.0.0.1:59733 - "GET /web/timeline.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59733 - "GET /web/tlui.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "GET /web/render.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/begin HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/0 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/2 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/3 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/4 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/5 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/6 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/8 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/9 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/11 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/12 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/13 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/14 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/15 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/16 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/17 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/18 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/19 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/20 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/21 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/22 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/23 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/24 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/25 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/28 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/29 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/30 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/31 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/32 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/33 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/34 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/35 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/36 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/37 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/38 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/39 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/40 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/41 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/42 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/43 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/44 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/45 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/46 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/47 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/48 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/49 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/50 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/51 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/52 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/53 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/54 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/55 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/56 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/57 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/58 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/59 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/60 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/61 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/62 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/63 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/64 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/65 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/66 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/67 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/68 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/69 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/70 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/71 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/72 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/73 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/74 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/75 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/76 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/77 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/78 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/79 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/80 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/81 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/82 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/83 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/84 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/85 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/86 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/87 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/88 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/frame/89 HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "POST /render/a1ecf39dda9d46179a3ae1cc4eb413f8/end HTTP/1.1" 200 OK
INFO: 127.0.0.1:59798 - "GET /render/a1ecf39dda9d46179a3ae1cc4eb413f8/status HTTP/1.1" 200 OK
INFO: 127.0.0.1:60685 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:60685 - "GET /web/style.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:60687 - "GET /web/dock.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:60686 - "GET /web/stage.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:60686 - "GET /web/room3d.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:60687 - "GET /web/presets.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:60686 - "GET /assets/tree HTTP/1.1" 200 OK
INFO: 127.0.0.1:60686 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:60686 - "GET /web/grammar.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:60686 - "GET /director HTTP/1.1" 405 Method Not Allowed
INFO: 127.0.0.1:60687 - "GET /web/timeline.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:60687 - "GET /web/tlui.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "GET /web/render.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/begin HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/0 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/2 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/3 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/4 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/5 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/6 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/8 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/9 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/11 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/12 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/13 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/14 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/15 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/16 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/17 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/18 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/19 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/20 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/21 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/22 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/23 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/24 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/25 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/28 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/29 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/30 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/31 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/32 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/33 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/34 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/35 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/36 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/37 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/38 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/39 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/40 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/41 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/42 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/43 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/44 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/45 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/46 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/47 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/48 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/49 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/50 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/51 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/52 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/53 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/54 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/55 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/56 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/57 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/58 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/59 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/60 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/61 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/62 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/63 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/64 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/65 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/66 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/67 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/68 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/69 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/70 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/71 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/72 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/73 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/74 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/75 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/76 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/77 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/78 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/79 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/80 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/81 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/82 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/83 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/84 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/85 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/86 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/87 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/88 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/frame/89 HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "POST /render/8794e7fdcd1d4551b1c030ac66c53675/end HTTP/1.1" 200 OK
INFO: 127.0.0.1:60784 - "GET /render/8794e7fdcd1d4551b1c030ac66c53675/status HTTP/1.1" 200 OK
INFO: 127.0.0.1:61453 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:61516 - "GET / HTTP/1.1" 200 OK
INFO: 127.0.0.1:61516 - "GET /web/style.css HTTP/1.1" 200 OK
INFO: 127.0.0.1:61517 - "GET /web/stage.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:61518 - "GET /web/dock.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:61517 - "GET /web/room3d.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:61517 - "GET /assets/tree HTTP/1.1" 200 OK
INFO: 127.0.0.1:61516 - "GET /favicon.ico HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:61518 - "GET /web/presets.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:61518 - "GET /web/grammar.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:61518 - "GET /director HTTP/1.1" 405 Method Not Allowed
INFO: 127.0.0.1:61516 - "GET /web/timeline.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:61516 - "GET /web/tlui.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "GET /assets/file?path=characters%2Ftest%2Flady.glb HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "GET /web/render.js HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/begin HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/0 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/1 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/2 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/3 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/4 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/5 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/6 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/7 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/8 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/9 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/10 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/11 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/12 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/13 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/14 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/15 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/16 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/17 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/18 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/19 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/20 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/21 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/22 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/23 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/24 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/25 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/26 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/27 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/28 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/29 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/30 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/31 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/32 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/33 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/34 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/35 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/36 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/37 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/38 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/39 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/40 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/41 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/42 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/43 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/44 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/45 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/46 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/47 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/48 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/49 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/50 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/51 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/52 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/53 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/54 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/55 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/56 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/57 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/58 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/59 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/60 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/61 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/62 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/63 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/64 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/65 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/66 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/67 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/68 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/69 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/70 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/71 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/72 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/73 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/74 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/75 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/76 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/77 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/78 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/79 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/80 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/81 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/82 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/83 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/84 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/85 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/86 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/87 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/88 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/frame/89 HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "POST /render/b727c44ecdd846aba6da356db2b7afa2/end HTTP/1.1" 200 OK
INFO: 127.0.0.1:61602 - "GET /render/b727c44ecdd846aba6da356db2b7afa2/status HTTP/1.1" 200 OK

View File

@ -74,7 +74,8 @@ export class Dock {
const kind = tab === 'characters' ? 'character' : tab === 'props' ? 'prop' : 'backdrop';
const desc = { kind, label: name, source:{ type:'assets', path: video || path } };
if(kind === 'backdrop') desc.params = video
? { mode:'video', image:video, width:12 } : { mode:'plane', image:path, width:10 };
? { mode:'video', image:video, width:12, fit:'frustum' }
: { mode:'plane', image:path, width:10, fit:'frustum' };
else desc.transform = { pos: this.stage.screenToFloor(e.clientX, e.clientY), rot:[0,0,0], scale:1 };
const en = await this.stage.addEntity(desc);
this.stage.select(en.id);
@ -133,7 +134,9 @@ export class Dock {
const desc = { kind, label: this._name(it), source: { type:'assets', path } };
if(kind === 'backdrop'){
const vp = this._videoPath(it); // video plate → video plane; else image plane
desc.params = vp ? { mode:'video', image:vp, width:12 } : { mode:'plane', image:path, width:10 };
// M7: new plates auto-fit the camera frustum (saved scenes without `fit` keep their sizing)
desc.params = vp ? { mode:'video', image:vp, width:12, fit:'frustum' }
: { mode:'plane', image:path, width:10, fit:'frustum' };
if(vp) desc.source = { type:'assets', path: vp };
}
this._toast('loading ' + this._name(it) + '…');
@ -210,10 +213,22 @@ export class Dock {
if(en.kind === 'light'){
el.appendChild(this._num('intensity', en.params.intensity ?? 1.5, v => this.stage.setParam(en.id, 'intensity', v)));
el.appendChild(this._color('color', en.params.color || '#ffffff', v => this.stage.setParam(en.id, 'color', v)));
if(en.params.type === 'ambient') // M7: keyable atmosphere (0 = off), coloured from the plate
el.appendChild(this._num('fog', en.params.fog ?? 0.02, v => this.stage.setParam(en.id, 'fog', v)));
}
if(en.kind === 'backdrop'){
el.appendChild(this._sel('mode', ['plane','corner','dome','video'], en.params.mode || 'plane',
v => this.stage.setParam(en.id, 'mode', v))); // stage rebuilds the mesh on change
el.appendChild(this._sel('fit', ['frustum','free'], en.params.fit || 'free',
v => { this.stage.setParam(en.id, 'fit', v); this.renderInspector(en); }));
if(en.params.fit === 'frustum'){
el.appendChild(this._num('fit dist', en.params.fitDist ?? 14, v => this.stage.setParam(en.id, 'fitDist', v)));
const re = document.createElement('button'); re.textContent = '⤢ refit to camera'; re.className = 'wide';
re.onclick = () => this.stage.setParam(en.id, 'fit', 'frustum'); // after moving the camera
el.appendChild(re);
}
el.appendChild(this._chk('ground tint', en.params.groundTint ?? true,
v => this.stage.setParam(en.id, 'groundTint', v)));
if(en.video) el.appendChild(this._num('vid start', en.params.videoStart ?? 0,
v => this.stage.setParam(en.id, 'videoStart', v)));
}
@ -238,6 +253,8 @@ export class Dock {
i.oninput = () => cb(parseFloat(i.value) || 0); return this._field(label, i); }
_color(label, val, cb){ const i = document.createElement('input'); i.type='color'; i.value=val;
i.oninput = () => cb(i.value); return this._field(label, i); }
_chk(label, val, cb){ const i = document.createElement('input'); i.type='checkbox'; i.checked=!!val;
i.onchange = () => cb(i.checked); return this._field(label, i); }
_sel(label, opts, val, cb){ const s = document.createElement('select');
for(const o of opts){ const op = document.createElement('option'); op.value=op.textContent=o; if(o===val) op.selected=true; s.appendChild(op); }
s.onchange = () => cb(s.value); return this._field(label, s); }
@ -260,12 +277,12 @@ export class Dock {
if(/\.(png|jpe?g|webp)$/.test(nm)){
const url = URL.createObjectURL(f);
const en = await this.stage.addEntity({ kind:'backdrop', label:f.name,
source:{ type:'upload', path:f.name, _url:url }, params:{ mode:'plane' } });
source:{ type:'upload', path:f.name, _url:url }, params:{ mode:'plane', fit:'frustum' } });
this.stage.select(en.id);
} else if(/\.(mp4|webm|mov)$/.test(nm)){ // M6: dropped video → video plate (upload, warn on save)
const url = URL.createObjectURL(f);
const en = await this.stage.addEntity({ kind:'backdrop', label:f.name,
source:{ type:'upload', path:f.name, _url:url }, params:{ mode:'video', width:12 } });
source:{ type:'upload', path:f.name, _url:url }, params:{ mode:'video', width:12, fit:'frustum' } });
this.stage.select(en.id);
} else if(/\.(glb|gltf|fbx|obj|bvh)$/.test(nm)){
const buf = await f.arrayBuffer();

View File

@ -30,12 +30,15 @@ import { Dock } from './web/dock.js';
// retarget self-check (?selftest=1) — proves the crown-jewel bake before anything else
if(new URLSearchParams(location.search).has('selftest')){
const { _selftest } = await import('./web/room3d.js');
const { _selftest, canon } = await import('./web/room3d.js');
const r = _selftest();
// M7 bone aliasing: non-mixamo rigs must fold onto the canonical names (node: room3d_test.mjs)
const alias = canon('CC_Base_Hip_02') === 'hips' && canon('CC_Base_L_Upperarm_050') === 'leftarm'
&& canon('upperarm_l') === 'leftarm' && canon('mixamorig4:LeftHand') === 'lefthand';
const el = document.getElementById('selftest');
el.textContent = `selftest: ${r.ok ? 'PASS' : 'FAIL'} (maxErr=${r.maxErr.toExponential(2)})`;
el.className = r.ok ? 'ok' : 'bad';
console.log('[room3d selftest]', r);
el.textContent = `selftest: ${r.ok && alias ? 'PASS' : 'FAIL'} (maxErr=${r.maxErr.toExponential(2)}, alias ${alias ? 'ok' : 'BAD'})`;
el.className = r.ok && alias ? 'ok' : 'bad';
console.log('[room3d selftest]', r, 'alias', alias);
}
const stage = new Stage(document.getElementById('view'));

View File

@ -67,12 +67,96 @@ export function disposeRoot(root){
// canonical bone key: "mixamorig:LeftUpLeg" / "mixamorig_LeftUpLeg" / "mixamorig4LeftUpLeg" → "leftupleg"
// (mixamo numbers the namespace per-download — strip digits too). Encodes real Mixamo quirks; keep as-is.
export const canon = n => n.replace(/^.*?mixamorig\d*[:_]?/i,'').replace(/[^a-z0-9]/gi,'').toLowerCase();
const rawCanon = n => n.replace(/^.*?mixamorig\d*[:_]?/i,'').replace(/[^a-z0-9]/gi,'').toLowerCase();
// ---- bone aliasing (M7) -------------------------------------------------------------------------
// The bake is name-driven, so a NON-mixamo rig (Character Creator, Unreal/HumanIK, Rigify, Rokoko,
// Unity/VRM) used to pair zero bones and die at "missing hips". canon() now folds foreign names onto
// the canonical mixamo names the baker already speaks. Anything that already canons to a mixamo bone
// is returned untouched → mixamo→mixamo pairing is bit-identical (room3d_test.mjs asserts all 65).
const LIMBS = ['shoulder','arm','forearm','hand','upleg','leg','foot','toebase'];
const FINGS = ['handthumb','handindex','handmiddle','handring','handpinky'];
export const CANON = new Set(['hips','spine','spine1','spine2','neck','head',
...['left','right'].flatMap(s => [...LIMBS.map(b => s+b),
...FINGS.flatMap(f => [1,2,3].map(i => s+f+i))])]);
// side-less bones. CC-qualified keys are listed where the generic reading differs: UE `spine_01` is
// the FIRST spine bone, CC's `CC_Base_Spine01` is the SECOND (CC_Base_Waist is first). Spine chains
// longer than mixamo's 3 (UE spine_04/05, rigify spine.003+) are deliberately left unmapped.
const EXACT = {
hip:'hips', pelvis:'hips',
waist:'spine', spine01:'spine', abdomen:'spine', ccbasewaist:'spine',
chest:'spine1', spine02:'spine1', spine001:'spine1', abdomen2:'spine1', ccbasespine01:'spine1',
upperchest:'spine2', spine03:'spine2', spine002:'spine2', ccbasespine02:'spine2',
neck01:'neck', neck1:'neck', necktwist01:'neck', ccbasenecktwist01:'neck',
};
// sided stems → canonical suffix (side prefix/suffix is peeled off first)
const STEMS = { shoulder:'shoulder', clavicle:'shoulder', collar:'shoulder',
arm:'arm', upperarm:'arm', armupper:'arm',
forearm:'forearm', lowerarm:'forearm', armlower:'forearm',
hand:'hand', wrist:'hand',
upleg:'upleg', thigh:'upleg', upperleg:'upleg', legupper:'upleg',
leg:'leg', calf:'leg', shin:'leg', lowerleg:'leg', leglower:'leg',
foot:'foot', ankle:'foot',
toebase:'toebase', toe:'toebase', toes:'toebase', ball:'toebase' };
for(const [k, v] of Object.entries({ thumb:'handthumb', index:'handindex', findex:'handindex',
mid:'handmiddle', middle:'handmiddle', fmiddle:'handmiddle', ring:'handring', fring:'handring',
pinky:'handpinky', little:'handpinky', fpinky:'handpinky' }))
for(const i of [1,2,3]){ STEMS[k+i] = v+i; STEMS[k+'0'+i] = v+i; }
// longest side spellings first; a decomposition only counts when the leftover stem is known
// (so "lowerarm" isn't read as left-"owerarm", and "shoulder" isn't read as "shoulde"-right).
const SIDES = [['left',/^left(.+)/], ['right',/^right(.+)/], ['left',/(.+)left$/], ['right',/(.+)right$/],
['left',/^l(.+)/], ['right',/^r(.+)/], ['left',/(.+)l$/], ['right',/(.+)r$/]];
const nsStrip = s => s.replace(/^.*[:|]/, ''); // "mixamorig:X", "Armature|X"
const pfxStrip = s => s.replace(/^(cc[_ ]?base|def|org|mch|bip\d*|biped)[._\-| ]+/i, '');
const idxStrip = s => s.replace(/[._\- ]\d+$/, ''); // CC's per-export "_02" tail
const flat = s => s.replace(/[^a-z0-9]/gi, '').toLowerCase();
function alias(name){
const b = nsStrip(String(name));
// most-qualified first: "ccbasespine01" must beat the generic "spine01" reading
for(const s of [b, pfxStrip(b), idxStrip(b), pfxStrip(idxStrip(b))]){
const k = flat(s);
if(CANON.has(k)) return k;
if(EXACT[k]) return EXACT[k];
for(const [side, re] of SIDES){ const m = k.match(re), stem = m && STEMS[m[1]];
if(stem) return side + stem; }
}
return null; // never fabricate — an unmapped bone just stays unbaked
}
// what kind of skeleton is this? (error messages only — the mapping itself is per-bone)
export function rigStyle(names){
const s = (names || []).join('\n');
for(const [re, label] of [[/mixamorig/i,'mixamo'], [/CC_Base/i,'CC_Base'], [/^(DEF|ORG|MCH)-/m,'Rigify'],
[/(upperarm|lowerarm|thigh|calf|clavicle|ball)_[lr]\b/i,'Unreal/HumanIK'], [/Bip\d/,'3dsMax Biped'],
[/(LeftUpLeg|LeftForeArm|LeftToeBase)/i,'mixamo-style']])
if(re.test(s)) return label;
return 'unknown';
}
export const canon = n => { const raw = rawCanon(n); return CANON.has(raw) ? raw : (alias(n) || raw); };
export function boneMap(root){
const m={}; root.traverse(o=>{ if(o.isBone && !(canon(o.name) in m)) m[canon(o.name)] = o; }); return m;
}
const hipsOf = map => map['hips'] || null;
// target bone → source bone, by canonical name. Dedupe by canon key: fbx files often carry a
// DUPLICATE hierarchy, and aliased rigs can fold two bones onto one key (CC Hip + Pelvis → hips) —
// bind only the first, which is what boneMap picks. THREE-free so node can test it.
const MINPAIR = 8; // below this a "retarget" is garbage — fail loudly instead of animating soup
export function pairBones(tOrder, srcMap, hasRest = () => true){
const pairs = new Map(), used = new Set();
for(const tb of tOrder){ const k = canon(tb.name), sb = srcMap[k];
if(sb && hasRest(sb) && !used.has(k)){ pairs.set(tb, sb); used.add(k); } }
if(pairs.size < Math.min(MINPAIR, tOrder.length)){
const un = tOrder.find(tb => !pairs.has(tb));
throw new Error(`retarget: only ${pairs.size}/${tOrder.length} bones matched (target looks like `
+ `'${rigStyle(tOrder.map(b => b.name))}', source looks like `
+ `'${rigStyle(Object.values(srcMap).map(b => b.name))}'; first unmatched bone `
+ `'${un ? un.name : '?'}'); unsupported skeleton`);
}
return pairs;
}
// bind-pose snapshot — retargets are world-space deltas against THIS. Capture BEFORE any clip poses it.
export function captureRest(root){
root.updateMatrixWorld(true);
@ -89,15 +173,18 @@ export function captureRest(root){
export function bakeRetarget(clip, srcRoot, srcRest, tgtRoot, tgtRest, opts={}){
const tgt = boneMap(tgtRoot), src = boneMap(srcRoot);
const tHips = hipsOf(tgt), sHips = hipsOf(src);
if(!tHips || !sHips) throw new Error('retarget: missing hips (mixamorig skeleton?)');
const sRest = srcRest.rest, tRest = tgtRest.rest, tOrder = tgtRest.order;
const pairs = new Map(); // target bone → source bone. Dedupe by canon key: fbx files often
const used = new Set(); // carry a DUPLICATE hierarchy — bind only the first (what boneMap picks)
for(const tb of tOrder){ const k=canon(tb.name), sb=src[k];
if(sb && sRest.has(sb) && !used.has(k)){ pairs.set(tb, sb); used.add(k); } }
if(!pairs.size) throw new Error('retarget: no matching bones');
if(!tHips || !sHips) throw new Error(`retarget: no bone maps to 'hips' on the `
+ `${tHips ? 'source' : 'target'} (target looks like '${rigStyle(tOrder.map(b => b.name))}', `
+ `source looks like '${rigStyle(srcRest.order.map(b => b.name))}'); unsupported skeleton`);
const pairs = pairBones(tOrder, src, sb => sRest.has(sb));
let ratio=1; // hips travel: local hip-height ratio (cm fbx vs m glb sorts itself out)
if(sHips.position.y > 1e-6) ratio = tHips.position.y / sHips.position.y;
// ponytail: axis-swapped rigs (lady.glb hangs its hip height off LOCAL Z, not Y — CC via a Z-up
// exporter) land ratio 0, so the clip plays IN PLACE. That is the safe read: the source's travel
// delta is expressed in the SOURCE's axes, and scaling it into a Z-up hip would fly the character.
// Rotations are unaffected (they transfer through world space). Proper fix = rotate the travel
// delta through both rests' parent world quats — not needed until a CC rig has to walk a mark.
const fps=30, n=Math.max(2, Math.ceil(clip.duration*fps)+1);
const times = new Float32Array(n);
const qData = new Map([...pairs.keys()].map(tb=>[tb, new Float32Array(n*4)]));

View File

@ -0,0 +1,116 @@
// room3d_test.mjs — Lane A M7-A1 check: bone aliasing + the retarget guardrail.
// Runs under plain `node`, no deps and no three.js: it loads the REAL room3d.js with the three
// imports stripped (nothing at module top level touches THREE), so the tables under test are the
// shipped ones — not a copy that can drift.
// node scenegod/web/room3d_test.mjs
import assert from 'node:assert/strict';
import fs from 'node:fs';
const src = fs.readFileSync(new URL('./room3d.js', import.meta.url), 'utf8')
.replace(/^import .*from ['"]three(\/[^'"]*)?['"];$/gm, '');
const { canon, pairBones, rigStyle } = await import(
'data:text/javascript;base64,' + Buffer.from(src).toString('base64'));
// ---- 1. BACKWARD COMPAT: every mixamo bone canons exactly as the pre-M7 one-liner did ----------
const legacy = n => n.replace(/^.*?mixamorig\d*[:_]?/i, '').replace(/[^a-z0-9]/gi, '').toLowerCase();
const MIXAMO = ['Hips','Spine','Spine1','Spine2','Neck','Head','HeadTop_End',
...['Left','Right'].flatMap(s => [s+'Shoulder', s+'Arm', s+'ForeArm', s+'Hand', s+'UpLeg', s+'Leg',
s+'Foot', s+'ToeBase', s+'Toe_End',
...['Thumb','Index','Middle','Ring','Pinky'].flatMap(f => [1,2,3,4].map(i => `${s}Hand${f}${i}`))])];
assert.equal(MIXAMO.length, 65, 'the man.fbx bone list');
for(const n of MIXAMO) for(const ns of ['mixamorig:', 'mixamorig_', 'mixamorig4:', 'mixamorig1_', ''])
assert.equal(canon(ns + n), legacy(ns + n), `mixamo canon changed: ${ns + n}`);
assert.equal(canon('mixamorig4:LeftHand'), 'lefthand', 'per-download namespace still collapses');
assert.equal(canon('mixamorig:Hips'), 'hips');
assert.equal(canon('mixamorig:LeftToe_End'), 'lefttoeend', 'end effectors stay legacy (unmapped)');
// ---- 2. Character Creator (the real lady.glb names, verified out of the GLB's JSON chunk) -------
const CC = ['CC_Base_BoneRoot_01','CC_Base_Hip_02','CC_Base_Pelvis_03','CC_Base_L_Thigh_04',
'CC_Base_L_Calf_05','CC_Base_L_Foot_06','CC_Base_L_ToeBaseShareBone_07','CC_Base_L_ToeBase_08',
'CC_Base_L_CalfTwist01_014','CC_Base_R_Thigh_018','CC_Base_R_Calf_019','CC_Base_R_Foot_021',
'CC_Base_R_ToeBase_022','CC_Base_Waist_033','CC_Base_Spine01_034','CC_Base_Spine02_035',
'CC_Base_NeckTwist01_036','CC_Base_NeckTwist02_037','CC_Base_Head_038','CC_Base_L_Eye_046',
'CC_Base_L_Clavicle_049','CC_Base_L_Upperarm_050','CC_Base_L_Forearm_051',
'CC_Base_L_ForearmTwist01_052','CC_Base_L_Hand_055','CC_Base_L_Pinky1_056','CC_Base_L_Mid1_062',
'CC_Base_L_Thumb1_068','CC_Base_L_Breast_074','CC_Base_R_Clavicle_077','CC_Base_R_Upperarm_078',
'CC_Base_R_Forearm_079','CC_Base_R_Hand_083','CC_Base_R_Index3_095',
'CC_Base_R_UpperarmTwist02_0100'];
for(const [name, want] of [
['CC_Base_Hip_02','hips'], ['CC_Base_L_Thigh_04','leftupleg'], ['CC_Base_L_Calf_05','leftleg'],
['CC_Base_L_Foot_06','leftfoot'], ['CC_Base_L_ToeBase_08','lefttoebase'],
['CC_Base_R_Thigh_018','rightupleg'], ['CC_Base_Waist_033','spine'],
['CC_Base_Spine01_034','spine1'], ['CC_Base_Spine02_035','spine2'],
['CC_Base_NeckTwist01_036','neck'], ['CC_Base_Head_038','head'],
['CC_Base_L_Clavicle_049','leftshoulder'], ['CC_Base_L_Upperarm_050','leftarm'],
['CC_Base_L_Forearm_051','leftforearm'], ['CC_Base_L_Hand_055','lefthand'],
['CC_Base_R_Hand_083','righthand'], ['CC_Base_L_Thumb1_068','lefthandthumb1'],
['CC_Base_L_Mid1_062','lefthandmiddle1'], ['CC_Base_R_Index3_095','righthandindex3'],
['CC_Base_L_Pinky1_056','lefthandpinky1']])
assert.equal(canon(name), want, `CC ${name}${canon(name)}, want ${want}`);
// twist / share / cosmetic bones have no mixamo counterpart: they must stay UNMAPPED, not guess
for(const n of ['CC_Base_L_CalfTwist01_014','CC_Base_L_ToeBaseShareBone_07','CC_Base_L_Eye_046',
'CC_Base_L_Breast_074','CC_Base_L_ForearmTwist01_052','CC_Base_R_UpperarmTwist02_0100',
'CC_Base_BoneRoot_01','CC_Base_NeckTwist02_037'])
assert.ok(!canon(n).match(/^(hips|spine|neck|head|left|right)/), `${n} must stay unmapped, got ${canon(n)}`);
// ---- 3. Unreal / HumanIK, Rigify, Unity-VRM / Rokoko, plain lowercase --------------------------
for(const [name, want] of [
// Unreal mannequin / HumanIK
['pelvis','hips'], ['spine_01','spine'], ['spine_02','spine1'], ['spine_03','spine2'],
['neck_01','neck'], ['head','head'], ['clavicle_l','leftshoulder'], ['upperarm_l','leftarm'],
['lowerarm_l','leftforearm'], ['hand_r','righthand'], ['thigh_r','rightupleg'],
['calf_l','leftleg'], ['foot_r','rightfoot'], ['ball_l','lefttoebase'],
['thumb_01_l','lefthandthumb1'], ['index_03_r','righthandindex3'],
// Blender Rigify (deform bones are what gets skinned)
['DEF-spine','spine'], ['DEF-spine.001','spine1'], ['DEF-spine.002','spine2'],
['DEF-shoulder.L','leftshoulder'], ['DEF-upper_arm.L','leftarm'], ['DEF-forearm.R','rightforearm'],
['DEF-hand.L','lefthand'], ['DEF-thigh.R','rightupleg'], ['DEF-shin.L','leftleg'],
['DEF-foot.L','leftfoot'], ['DEF-toe.R','righttoebase'], ['DEF-thumb.01.L','lefthandthumb1'],
['DEF-f_index.02.R','righthandindex2'], ['ORG-upper_arm.R','rightarm'],
// Unity humanoid / VRM / Rokoko-style long names + plain lowercase
['LeftUpperArm','leftarm'], ['RightLowerArm','rightforearm'], ['LeftUpperLeg','leftupleg'],
['RightLowerLeg','rightleg'], ['Chest','spine1'], ['UpperChest','spine2'], ['Hips','hips'],
['leftshoulder','leftshoulder'], ['l_hand','lefthand'], ['R_Foot','rightfoot'],
['Bip01 Pelvis','hips'], ['Bip01 L Clavicle','leftshoulder']])
assert.equal(canon(name), want, `${name}${canon(name)}, want ${want}`);
// side-less limbs stay unmapped (guessing a side would silently mirror a character)
for(const n of ['thigh','upperarm','hand','shoulder','lowerarm','toe'])
assert.ok(!canon(n).match(/^(left|right)/), `side-less "${n}" must stay unmapped, got ${canon(n)}`);
// ---- 4. pairing: a CC target against a mixamo source ------------------------------------------
const boneList = names => names.map(name => ({ name }));
const mapOf = names => { const m = {};
for(const b of boneList(names)) if(!(canon(b.name) in m)) m[canon(b.name)] = b; return m; };
const mixamoSrc = mapOf(MIXAMO.map(n => 'mixamorig:' + n));
const ccPairs = pairBones(boneList(CC), mixamoSrc);
assert.ok(ccPairs.size >= 8, `CC target paired only ${ccPairs.size} bones`);
assert.equal(ccPairs.size, 26, 'every mappable bone in the CC sample pairs exactly once');
// (the full 102-bone lady.glb skeleton pairs 52: core + both arms/legs + all 30 finger bones —
// every twist/share/toe-detail/facial bone correctly stays unmapped)
// CC_Base_Hip wins 'hips'; CC_Base_Pelvis folds onto the same key and is left unbaked (not doubled)
const paired = [...ccPairs.keys()].map(b => b.name);
assert.ok(paired.includes('CC_Base_Hip_02') && !paired.includes('CC_Base_Pelvis_03'), 'hips dedupe');
// mixamo→mixamo is unaffected
assert.equal(pairBones(boneList(MIXAMO.map(n => 'mixamorig4:' + n)), mixamoSrc).size, 65);
// ---- 5. the guardrail: an unmappable rig fails loudly instead of baking soup -------------------
const junk = boneList(['CC_Base_BoneRoot_01', ...Array.from({length: 53}, (_, i) => `joint_x${i}`)]);
assert.throws(() => pairBones(junk, mixamoSrc), err => {
assert.match(err.message, /only \d+\/54 bones matched/, err.message);
assert.match(err.message, /CC_Base/, 'names the detected skeleton style');
assert.match(err.message, /first unmatched bone 'joint_x0'|first unmatched bone 'CC_Base_BoneRoot_01'/);
assert.match(err.message, /unsupported skeleton/);
return true;
}, 'a 0-8 match rig must throw');
// tiny synthetic skeletons (room3d _selftest builds a 3-bone pair) must still bake
assert.equal(pairBones(boneList(['mixamorig4_Hips','mixamorig4_Spine','mixamorig4_Head']),
mapOf(['mixamorig1_Hips','mixamorig1_Spine','mixamorig1_Head'])).size, 3, '_selftest skeletons');
// ---- 6. style detection -----------------------------------------------------------------------
assert.equal(rigStyle(CC), 'CC_Base');
assert.equal(rigStyle(MIXAMO.map(n => 'mixamorig:' + n)), 'mixamo');
assert.equal(rigStyle(['pelvis','spine_01','upperarm_l','thigh_r']), 'Unreal/HumanIK');
assert.equal(rigStyle(['DEF-spine','DEF-upper_arm.L']), 'Rigify');
assert.equal(rigStyle(['bone_a','bone_b']), 'unknown');
console.log(`room3d_test: ALL PASS (${ccPairs.size} CC bones paired against mixamo)`);

View File

@ -56,6 +56,14 @@ export function visemeSelfCheck(){
return { ok: detectOk && driveOk, found: Object.keys(visemes).sort().join(','), visemes };
}
// M7 backdrop auto-fit, kept pure so node can check the algebra (stage_test.mjs): the frustum is
// `fh` tall at `dist`, and the plate scales to COVER it (either axis, whichever is tighter) + bleed.
export const BLEED = 1.06;
export function fitScale(fovDeg, aspect, dist, planeW, planeH){
const fh = 2 * Math.tan(fovDeg * Math.PI / 360) * dist, fw = fh * aspect;
return { fh, fw, s: Math.max(fw / planeW, fh / planeH) * BLEED };
}
export class Stage {
constructor(viewEl){
this.view = viewEl;
@ -95,10 +103,12 @@ export class Stage {
this._defHemi = new THREE.HemisphereLight(0xffffff, 0x555a66, 2.2); scene.add(this._defHemi);
this._defKey = new THREE.DirectionalLight(0xffffff, 1.8); this._defKey.position.set(3, 5, 2); scene.add(this._defKey);
const floor = new THREE.Mesh(new THREE.CircleGeometry(30, 64).rotateX(-Math.PI/2),
const floor = this._floor = new THREE.Mesh(new THREE.CircleGeometry(30, 64).rotateX(-Math.PI/2),
new THREE.MeshStandardMaterial({color:0x141a20, roughness:.97}));
floor.receiveShadow = true; scene.add(floor);
scene.add(new THREE.GridHelper(60, 60, 0x2a323b, 0x1c232b));
this._floorBase = new THREE.Color(0x141a20);
const grid = this._grid = new THREE.GridHelper(60, 60, 0x2a323b, 0x1c232b);
grid.material.transparent = true; scene.add(grid);
// gizmo
const tc = this._tc = new TransformControls(cam, r.domElement);
@ -132,7 +142,9 @@ export class Stage {
this.renderer.setSize(w, h, false); this._dirCam.aspect = w/h; this._dirCam.updateProjectionMatrix();
// size the inset here — CSS aspect-ratio/% is unreliable on a <canvas>
const pw = Math.max(180, Math.min(340, w*0.24));
this._pip.style.width = Math.round(pw)+'px'; this._pip.style.height = Math.round(pw*9/16)+'px'; }
this._pip.style.width = Math.round(pw)+'px'; this._pip.style.height = Math.round(pw*9/16)+'px';
this.fitBackdrops(); // frame aspect changed → auto-fit plates no longer fill it
}
// ---- callbacks ----
onSelect(cb){ this._selCbs.push(cb); }
@ -151,7 +163,8 @@ export class Stage {
const noAsset = desc.kind === 'camera' || desc.kind === 'light'; // rig entities have no file
const en = { id, kind: desc.kind, label: desc.label || desc.kind,
source: desc.source || (noAsset ? {type:'none'} : null), params: {...(desc.params||{})},
wrapper, root:null, mixer:null, rest:null, cam:null, light:null, morphs:null, visemes:null, video:null };
wrapper, root:null, mixer:null, rest:null, cam:null, light:null, morphs:null, visemes:null,
video:null, ground:null };
if(desc.kind === 'character' || desc.kind === 'prop'){
const { root } = await this._loadAsset(desc);
@ -170,7 +183,8 @@ export class Stage {
wrapper.add(en.cam);
const proxy = new THREE.Mesh(new THREE.ConeGeometry(0.18, 0.35, 4).rotateX(-Math.PI/2),
new THREE.MeshBasicMaterial({color:0x43c0cf, wireframe:true}));
proxy.position.z = 0.18; wrapper.add(proxy);
proxy.position.z = 0.18; proxy.userData.chrome = true; // SYNC7: editor-only, never in a render
wrapper.add(proxy); en.proxy = proxy;
} else if(desc.kind === 'light'){
this._buildLight(en);
} else throw new Error('unknown kind: ' + desc.kind);
@ -178,6 +192,10 @@ export class Stage {
this.scene.add(wrapper);
this._entities.set(id, en);
if(desc.transform) this.setTransform(id, desc.transform);
if(en.kind === 'backdrop'){ this._fitBackdrop(en); this._sampleGround(en); } // M7: world tie-in
// _buildLight ran BEFORE the entity was registered, so its _refreshDefaults couldn't see it —
// without this the built-in hemi stayed at 2.2 and washed every lighting preset out.
if(en.kind === 'light'){ this._refreshDefaults(); this._applyWorld(); }
this._fireChange(en); // tlui/dock build a row per entity off onChange (incl. applyState loads)
return en;
}
@ -192,6 +210,7 @@ export class Stage {
this._entities.delete(id);
for(const k of [...this._baked.keys()]) if(k.startsWith(id+'|')) this._baked.delete(k);
if(en.kind === 'light') this._refreshDefaults();
if(en.kind === 'backdrop' || en.kind === 'light') this._applyWorld(); // floor/grid/fog follow the plates
this._fireChange(en); // trigger a row resync so the deleted entity drops out
}
@ -247,11 +266,12 @@ export class Stage {
}
const w = p.width || 10, h = w / aspect;
const grp = new THREE.Group(), mats = [];
// fog:false — the plate IS the distance; atmosphere is for what stands in front of it
if(p.mode === 'dome'){
const dm = new THREE.MeshBasicMaterial({map:tex, side:THREE.BackSide}); mats.push(dm);
const dm = new THREE.MeshBasicMaterial({map:tex, side:THREE.BackSide, fog:false}); mats.push(dm);
grp.add(new THREE.Mesh(new THREE.SphereGeometry(40, 40, 24), dm));
} else {
const mat = new THREE.MeshStandardMaterial({map:tex, roughness:1, metalness:0}); mats.push(mat);
const mat = new THREE.MeshStandardMaterial({map:tex, roughness:1, metalness:0, fog:false}); mats.push(mat);
const wall = new THREE.Mesh(new THREE.PlaneGeometry(w, h), mat);
wall.position.y = h/2; wall.receiveShadow = true; grp.add(wall);
if(p.mode === 'corner'){ // cheap "L": duplicate lower third laid flat as ground
@ -318,6 +338,90 @@ export class Stage {
en.video.pause(); en.video.removeAttribute('src'); en.video.load(); en.video = null;
}
// ---- world-integrated backdrops (M7) ---------------------------------------------------------
// A plate used to read as a billboard in the void: a gap between its bottom edge and the grid, a
// floor that belongs to a different scene, no air between camera and horizon. Three cheap fixes,
// all recomputed on load / param change / camera change — NEVER per frame.
// params.fit:'frustum' size+place the plate so it fills the active camera's frustum (+bleed)
// params.groundTint tint the stage floor to the plate's bottom rows (default on)
// params `fog` on the ambient light keyable FogExp2 density, coloured from the plate
_activeAspect(){ const c = this._activeCam();
return c.aspect || (this.view.clientWidth / this.view.clientHeight) || 16/9; }
fitBackdrops(){ for(const en of this._entities.values())
if(en.kind === 'backdrop') this._fitBackdrop(en); }
// Scale/park the plate on the active camera's axis so it covers the frame at params.fitDist.
// ponytail: fit OWNS the backdrop's wrapper transform — keyframe a plate only with fit:'free'.
_fitBackdrop(en){
const p = en.params, root = en.root;
if(!root || p.fit !== 'frustum') return;
if(p.mode === 'dome' || p.mode === 'corner') return; // dome already wraps; corner needs its floor flat
const mesh = root.children[0], g = mesh && mesh.geometry && mesh.geometry.parameters;
if(!g || !g.width) return;
const cam = this._activeCam(); cam.updateMatrixWorld(true);
const d = p.fitDist || 14; // far enough to read as background
const { s } = fitScale(cam.fov, this._activeAspect(), d, g.width, g.height);
root.scale.setScalar(s);
// SYNC7: which row of the plate sits at eye level. 1.0 parked the plate's TOP EDGE on the
// camera axis (void above centre-frame); 0.5 put the image's middle — road tarmac, for a
// street plate — at eye height. params.fitAnchor is the fraction of image height at the
// lens axis: 0.75 lands a typical plate's horizon near eye level and still fills upward.
const anchor = p.fitAnchor == null ? 0.75 : p.fitAnchor;
root.position.set(0, (0.5 - anchor) * g.height * s, 0);
const camPos = cam.getWorldPosition(new THREE.Vector3());
const fwd = new THREE.Vector3(0, 0, -1).applyQuaternion(cam.getWorldQuaternion(new THREE.Quaternion()));
en.wrapper.position.copy(camPos).addScaledVector(fwd, d);
en.wrapper.lookAt(camPos); // plane normal is +Z
en.wrapper.scale.setScalar(1);
}
_unfitBackdrop(en){ if(en.root){ en.root.scale.setScalar(1); en.root.position.set(0,0,0); } }
// average colour of the plate's bottom rows — the ground the character should be standing on.
// Works for images and for video (poster or current frame); same-origin, so the canvas is clean.
_plateGround(en){
const mesh = en.root && en.root.children[0], tex = mesh && mesh.material && mesh.material.map;
const img = tex && tex.image;
if(!img || !(img.width || img.videoWidth)) return null;
if(img.videoWidth && img.readyState < 2) return null; // no decoded frame yet → sample black
try {
const c = document.createElement('canvas'); c.width = c.height = 24;
const g = c.getContext('2d', { willReadFrequently: true });
g.drawImage(img, 0, 0, 24, 24);
const d = g.getImageData(0, 17, 24, 7).data; // bottom ~quarter
let r = 0, gr = 0, b = 0; const n = d.length / 4;
for(let i = 0; i < d.length; i += 4){ r += d[i]; gr += d[i+1]; b += d[i+2]; }
return new THREE.Color().setRGB(r/n/255, gr/n/255, b/n/255, THREE.SRGBColorSpace);
} catch(e){ return null; } // tainted canvas / no frame yet
}
_sampleGround(en){
const col = this._plateGround(en);
if(col) en.ground = col;
else if(en.video) // no decoded frame yet — take the first one that lands
en.video.addEventListener('loadeddata', () => this._sampleGround(en), { once:true });
this._applyWorld(); // grid/fog follow the plate even when sampling came back empty
}
// floor tint + grid fade + fog, derived from whatever plates are on stage
_applyWorld(){
const plates = this.entities().filter(e => e.kind === 'backdrop');
const tint = plates.filter(e => e.ground && (e.params.groundTint ?? true)).pop();
const col = tint ? tint.ground : null;
this._floor.material.color.copy(col ? col.clone().multiplyScalar(0.85) : this._floorBase);
this._grid.material.opacity = plates.length ? 0.12 : 1; // fade the studio grid out under a plate
const amb = this.entities().find(e => e.kind === 'light' && e.params.type === 'ambient');
const dens = amb && amb.params.fog != null ? amb.params.fog : (col ? 0.02 : 0); // subtle default
const had = !!this.scene.fog;
if(col && dens > 0){ // Lane B can key `fog` per tick — mutate, don't reallocate
if(had){ this.scene.fog.color.copy(col); this.scene.fog.density = dens; }
else this.scene.fog = new THREE.FogExp2(col.getHex(), dens);
} else this.scene.fog = null;
// toggling fog on/off changes the shader program — materials must recompile (once, not per frame)
if(had !== !!this.scene.fog) this.scene.traverse(o => { const m = o.material;
if(m) (Array.isArray(m) ? m : [m]).forEach(x => x.needsUpdate = true); });
}
_buildLight(en){
const p = en.params;
if(p.type === 'ambient'){
@ -377,7 +481,11 @@ export class Stage {
scale: w.scale.x };
}
setTransform(id, {pos, rot, scale}){
const w = this._entities.get(id)?.wrapper; if(!w) return;
const en = this._entities.get(id); const w = en?.wrapper; if(!w) return;
// SYNC7: a frustum-fitted plate is placed BY THE LENS, not by the user. The timeline
// re-applies every entity's rest transform each tick, which dragged the plate back to
// its authored pos and left a black bar at frame top. Camera owns it; ignore the rest.
if(en.kind === 'backdrop' && en.params && en.params.fit === 'frustum'){ this._fitBackdrop(en); return; }
if(pos) w.position.set(pos[0], pos[1], pos[2]);
if(rot) w.rotation.set(rot[0], rot[1], rot[2]);
if(scale != null) w.scale.setScalar(scale);
@ -385,12 +493,19 @@ export class Stage {
setParam(id, key, value){
const en = this._entities.get(id); if(!en) return;
en.params[key] = value;
if(en.cam && key === 'fov'){ en.cam.fov = value; en.cam.updateProjectionMatrix(); }
if(en.cam && key === 'fov'){ en.cam.fov = value; en.cam.updateProjectionMatrix();
if(en.id === this._activeCamId) this.fitBackdrops(); } // the frame changed shape
if(en.light){
if(key === 'intensity') en.light.intensity = value;
if(key === 'color') en.light.color.set(value);
if(key === 'castShadow' && en.light.isDirectionalLight) en.light.castShadow = !!value;
if(key === 'bg') this.scene.background = new THREE.Color(value); // bg rides the ambient entity (keyable, persists)
if(key === 'fog') this._applyWorld(); // keyable atmosphere, same pattern as bg
}
if(en.kind === 'backdrop'){
if(key === 'fit') value === 'frustum' ? this._fitBackdrop(en) : this._unfitBackdrop(en);
if(key === 'fitDist') this._fitBackdrop(en);
if(key === 'groundTint') this._applyWorld();
}
const rebuild = () => this._rebuild(en).catch(e => console.warn('media rebuild failed:', en.id, e));
if(en.kind === 'backdrop' && (key === 'mode' || key === 'image' || key === 'width'))
@ -408,6 +523,7 @@ export class Stage {
en.root = en.kind === 'screen' ? await this._buildScreen(en) : await this._buildBackdrop(en);
en.wrapper.add(en.root);
if(old){ en.wrapper.remove(old); disposeRoot(old); }
if(en.kind === 'backdrop'){ this._fitBackdrop(en); en.ground = null; this._sampleGround(en); }
}
// ---- clips (bake is expensive — cache per path parse and per (entity,path,index) bake) ----
@ -466,17 +582,35 @@ export class Stage {
setActiveCamera(id){
this._activeCamId = id;
this._pip.style.display = (id && this._entities.get(id)?.cam) ? 'block' : 'none';
this.fitBackdrops(); // auto-fit plates belong to whichever camera is shooting
}
_activeCam(){ const en = this._activeCamId && this._entities.get(this._activeCamId);
return en && en.cam ? en.cam : this._dirCam; }
// render `cam` to the main WebGL canvas at the target's aspect; if `canvas` given, blit there and
// restore the cam's aspect (so using a cam for the inset never distorts it in the main view).
// SYNC7: gizmo + helpers were being BAKED INTO THE FINAL MP4 (a green translate
// arrow across every frame). Anything that's editor chrome hides while paused —
// pause() is exactly "the caller is rendering output now".
_chrome(visible){
const set = o => { if(o) o.visible = visible; };
set(this._tc); set(this._grid);
this.scene.traverse(o => { if(o.userData.chrome || o.isTransformControls || o.type === 'GridHelper'
|| o.isCameraHelper || o.isDirectionalLightHelper || o.isBox3Helper || o.isSkeletonHelper) o.visible = visible; });
for(const en of this._entities.values()) if(en.helper) en.helper.visible = visible;
}
_render(cam, canvas){
const w = canvas ? canvas.width : this.renderer.domElement.width;
const h = canvas ? canvas.height : this.renderer.domElement.height;
const prev = cam.aspect;
if(cam.isPerspectiveCamera){ cam.aspect = w/h; cam.updateProjectionMatrix(); }
// SYNC7: plates are fitted to the LIVE viewport aspect, but output renders at their own
// (960x540 etc). A narrower output frame is taller than what the plate was sized for →
// black bars top and bottom of every rendered film. Refit to the aspect we're about to
// draw at. Pure algebra (no texture sampling), so per-frame is fine.
if(cam.isPerspectiveCamera && Math.abs((prev || 0) - cam.aspect) > 1e-4) this.fitBackdrops();
if(this._paused) this._chrome(false);
this.renderer.render(this.scene, cam);
if(this._paused) this._chrome(true);
if(canvas){ canvas.getContext('2d').drawImage(this.renderer.domElement, 0, 0, canvas.width, canvas.height);
if(cam.isPerspectiveCamera && cam.aspect !== prev){ cam.aspect = prev; cam.updateProjectionMatrix(); } }
}
@ -561,5 +695,6 @@ export class Stage {
console.warn('skipping upload entity (no bytes):', d.id); continue; }
try { await this.addEntity(d); } catch(err){ console.error('addEntity failed', d.id, err); }
}
this.fitBackdrops(); // cameras usually load after their plates
}
}

View File

@ -0,0 +1,36 @@
// stage_test.mjs — Lane A M7-A2 check: the backdrop auto-fit algebra. Plain `node`, no three.js:
// loads the REAL stage.js with its three/room3d imports stripped (nothing at module top level
// touches them), so the shipped formula is what gets asserted.
// node scenegod/web/stage_test.mjs
import assert from 'node:assert/strict';
import fs from 'node:fs';
const src = fs.readFileSync(new URL('./stage.js', import.meta.url), 'utf8')
.replace(/^import .*from ['"](three(\/[^'"]*)?|\.\/room3d\.js)['"];$/gm, '');
const { fitScale, BLEED } = await import(
'data:text/javascript;base64,' + Buffer.from(src).toString('base64'));
// a 16:9 plate built 10m wide (dock default) seen by a 45° camera 14m away (fitDist default)
const camA = 16/9, fov = 45, d = 14, pw = 10, ph = 10 / (16/9);
const f = fitScale(fov, camA, d, pw, ph);
assert.ok(Math.abs(f.fh - 2 * Math.tan(fov * Math.PI/360) * d) < 1e-12, 'frustum height at dist');
assert.ok(Math.abs(f.fw - f.fh * camA) < 1e-12, 'frustum width from aspect');
// the scaled plate COVERS the frame with bleed on both axes — no void showing at the edges
assert.ok(pw * f.s >= f.fw * 1.0599 && ph * f.s >= f.fh * 0.9999, 'covers the frame');
assert.ok(Math.abs(pw * f.s / f.fw - BLEED) < 1e-9, 'matched aspect → exactly the bleed factor');
// a portrait plate in a wide frame must scale on WIDTH (the tighter axis), and vice-versa
const port = fitScale(fov, camA, d, 4, 9);
assert.ok(4 * port.s >= port.fw && 9 * port.s > port.fh * 1.5, 'portrait plate covers width, overshoots height');
const wide = fitScale(fov, 1.0, d, 20, 2);
assert.ok(2 * wide.s >= wide.fh && 20 * wide.s > wide.fw, 'letterbox plate covers height');
// framing is scale-free in distance: doubling fitDist doubles the plate, so it fills the same frame
const near = fitScale(fov, camA, 7, pw, ph), far = fitScale(fov, camA, 14, pw, ph);
assert.ok(Math.abs(far.s / near.s - 2) < 1e-9, 'distance only changes parallax, not framing');
// a longer lens (smaller fov) needs a smaller plate at the same distance
assert.ok(fitScale(20, camA, d, pw, ph).s < f.s, 'tighter fov → smaller plate');
console.log('stage_test: ALL PASS (fit s=' + f.s.toFixed(3) + ' → plate '
+ (pw * f.s).toFixed(1) + '×' + (ph * f.s).toFixed(1) + 'm over a '
+ f.fw.toFixed(1) + '×' + f.fh.toFixed(1) + 'm frame at ' + d + 'm)');

View File

@ -49,6 +49,7 @@ header .tag{font-family:var(--mono);font-size:10px;color:var(--teal);letter-spac
.fld input,.fld select{flex:1;min-width:0;background:var(--panel);border:1px solid var(--line2);color:var(--ink);
border-radius:6px;padding:5px 7px;font-family:var(--mono);font-size:11px}
.fld input[type=color]{padding:2px;height:26px}
.fld input[type=checkbox]{flex:0 0 auto;width:15px;height:15px;accent-color:var(--teal)}
.fld.vec input{width:100%}
.fld.vec{align-items:center}
.ibtns{display:flex;gap:8px;margin-top:14px}