diff --git a/lanes/A-stage.md b/lanes/A-stage.md index b05ad5d..212243d 100644 --- a/lanes/A-stage.md +++ b/lanes/A-stage.md @@ -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. diff --git a/logs/laneA.md b/logs/laneA.md index b78242e..dbac020 100644 --- a/logs/laneA.md +++ b/logs/laneA.md @@ -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:77–125): `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:340–415). +- `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). diff --git a/scenegod-8020.log b/scenegod-8020.log index 7cdc35b..c747910 100644 --- a/scenegod-8020.log +++ b/scenegod-8020.log @@ -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 diff --git a/scenegod/web/dock.js b/scenegod/web/dock.js index a419d4b..b9ff34a 100644 --- a/scenegod/web/dock.js +++ b/scenegod/web/dock.js @@ -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(); diff --git a/scenegod/web/index.html b/scenegod/web/index.html index ca92283..6dabfda 100644 --- a/scenegod/web/index.html +++ b/scenegod/web/index.html @@ -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')); diff --git a/scenegod/web/room3d.js b/scenegod/web/room3d.js index 2b610b0..a5af9ca 100644 --- a/scenegod/web/room3d.js +++ b/scenegod/web/room3d.js @@ -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)])); diff --git a/scenegod/web/room3d_test.mjs b/scenegod/web/room3d_test.mjs new file mode 100644 index 0000000..c16f207 --- /dev/null +++ b/scenegod/web/room3d_test.mjs @@ -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)`); diff --git a/scenegod/web/stage.js b/scenegod/web/stage.js index 03e60cc..f7979b9 100644 --- a/scenegod/web/stage.js +++ b/scenegod/web/stage.js @@ -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 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 } } diff --git a/scenegod/web/stage_test.mjs b/scenegod/web/stage_test.mjs new file mode 100644 index 0000000..c3a15d9 --- /dev/null +++ b/scenegod/web/stage_test.mjs @@ -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)'); diff --git a/scenegod/web/style.css b/scenegod/web/style.css index 3e12aaf..e5d718b 100644 --- a/scenegod/web/style.css +++ b/scenegod/web/style.css @@ -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}