The transient settle at Z 78 was the first physics-asset recreation; repeating the
delete + rebuild + assign cycle flipped the pawn to simulating=0 and stuck at spawn,
with the same box centre -18 that had settled. The delete+recreate is the failure,
not the box: delete_asset does not fully release the package, so NewObject reuses
stale state and the rebuilt body stops simulating.
- Plugin enablement (5e328f5) stays the solid fix (4 wheels).
- The physics asset needs a proper fix: set PhysicsType=PhysType_Simulated and update
in place, not delete+recreate.
- ue_vehicle_import.py SOURCE is stale (/tmp/gigline/hatch vs /tmp/gigline/vehicle for
the skeletal FBX), so the wired regeneration is never reached.
- Driving is still untested.
The Chaos vehicle never existed: ChaosVehiclesPlugin is EnabledByDefault=false and
was absent from GigLine.uproject, so FChaosVehicleManager was null and
OnCreatePhysicsState skipped CreateVehicle() (read from the engine source). The
pawn stayed at setups=4 wheels=0 output=null and the mesh fell back to the regular
rigid-body solver, which is what teleported upward.
- Enable ChaosVehiclesPlugin in GigLine.uproject. CreateVehicle() now runs:
'wheels 4', 'output present', 'setups=4 wheels=4'.
- Drop the constructor's SetSimulatePhysics(true) / collision-profile override: the
Chaos movement component drives the mesh through async physics, and forcing the
regular solver kept the body out of the vehicle solver.
The plugin fix turns the teleport into a stable 'float at Z 545' (540 cm above the
floor) in both probe maps - suspension tuning is the next bug.
Rebuilt and re-ran the vehicle probe workflow:
- Control (no terrain) falls and settles (300 -> 78 cm).
- Terrain rises (313 -> 545 cm) with body velocity pinned to 0 and zero overlaps.
- Turning physics OFF at 3 s stops the rise, so the Chaos vehicle solver is
teleporting the body (transform set directly, no velocity) - not an external
SetActorLocation.
- 0 wheels in both runs, so the missing wheels are not the trigger; the terrain is
what flips fall to rise.
Ruled out the asset pipeline with commandlets: the FBX, the imported
SK_Vehicle_Sedan (476x228x144 cm), the physics asset (452x205x40) and Terrain_DEM
(380x210 m) are all correct; the logged '1 cm bounds' is a stale component read,
not a bad mesh.
Next: bisect step 2 - a stock AWheeledVehiclePawn beside AGigLineVehicle in the
terrain probe map.
The only main-side change was the skills documentation (266fc1b), which already
exists on this branch as 19ed4fe with the same content. MEMORY.md conflicted
because the two skills commits were appended to different bases; the branch's
version is a strict superset (it also carries the vehicle/site/tools/hero entries),
so it is kept.
Banner lettering (JIMMY KIMMEL LIVE): new Tools/gen/generate_banner_texture.py
draws the exact title with Pillow + Liberation Sans Bold - an image model would
mangle a trademark string, and Qwen-Image 2.1 is research-only. Gold JIMMY /
KIMMEL / LIVE stacked on a crimson ground. ue_hero_facade_import.py generalises
import_albedo to import_texture and wires the banner into M_Facade_Banner_Ref as
Base Color (tiling 1.0), replacing the flat dark tint with a brighter red per the
owner's 'less dark, more bright'.
Re-rendered all three ReviewMap staging shots (front, oblique, portals) and
updated their Screenshots/README.md status rows.
The hero facade rework is now closed at 'surface detail that reads at 45 m'. The
remaining elevation work - volute capital depth, lattice grille depth, and the
full realism pass - is deferred to 0.9.0 per the owner, and the 0.9.0 hero item
records the hand-off.
The motto band and moulded surround had the same burial as the medallions: offset
0.06 left their front face at Y -0.44, inside the 1.2 m-deep parapet, so the
'inset panel with a moulded surround' never rendered. Only the inscription (moved
to 0.40 in the previous commit) read.
- Band recessed at offset 0.34 (Y -0.16), surround proud at 0.43 (Y -0.07),
inscription stays 0.40 (Y -0.10).
- Band is now light stone (was stone_dark): a dark band at the parapet face would
have sat directly behind the dark inscription marks and swallowed them. The
reference is dark incised lettering on a pale band.
- Removed the dead duplicate 'Hero_MottoBand 0.40' entry - the earlier 0.06 match
won, so the 0.40 was silently ignored.
Rendered and judged; the advisory judge no longer flags the band.
The medallions were not just flat - they were buried. seat_on_front put
Hero_Medallion at offset 0.08 (front Y -0.42) while the 1.2 m-deep parapet stands
at Y -0.15, so both discs sat inside the parapet and never rendered. The motto
band and surround have the same burial (offset 0.06) and are a separate follow-up.
- Three stepped layers per medallion with per-layer prefixes so seat_on_front gives
each its own stand-off: Rim (0.55 m, dark, offset 0.36 -> Y -0.14), Field
(0.38 m, light, offset 0.43 -> Y -0.07), Boss (0.15 m, dark, offset 0.50 -> Y 0.00).
A single Hero_Medallion prefix would both flatten the relief and (via startswith)
swallow the specific prefixes, so it is removed.
- Moved from +/-6.2 (inside the inscription text, which spans +/-6.87) to +/-9.8.
- Fixed Tools/review/hero_iterate.sh, which still had the pre-tools-move
'cd dirname/..' and landed in Tools/ instead of the repo root.
Rendered and judged; the advisory two-model judge (no, 0.93) no longer flags the
medallions. Owner review pending.
The reference's most distinctive architectural feature is three lines of incised capitals across the attic.
inscription() lays 112 letter marks in three lines, in the shadow stone so they read as dark incisions at 45 m
rather than disappearing into a same-colour relief - which is exactly what the first attempt did.
Positioning was the lesson: the parapet is 1.2 m deep and stands in FRONT of the wall plane, so marks seated on
FRONT_Y were hidden behind it and rendered invisible. They are now seated on the parapet face (FRONT_Y + 0.40),
which is where the reference puts the lettering too.
Still open in the architectural pass, recorded in TODO.md: medallion relief (the discs are flush and empty),
banner lettering (needs a texture, not geometry), and depth on the capitals and window grilles.
Rows 4-6 re-rendered and pending.
The owner rejected the previous pass on three counts - too saturated, too dark, and the albedo was wrong for the
building. All three were true, and the reason the colour work kept missing is that the albedo never reached the
material: import_albedo() resolved the repository root one level short after the tools move, so every 'stone'
render was the flat tint alone. That is fixed first.
Then measured instead of guessed: the reference's sunlit stone samples sRGB ~0.49 (linear ~0.19), attic
highlight ~0.75, shaded recess ~0.43, and it is nearly neutral, not warm. New albedo generated for a pale
low-contrast marble-limestone ashlar (two checkpoints compared, AlbedoBase won) and the tint moved to
(0.47, 0.455, 0.43) with the shadow at (0.30, 0.29, 0.27), which lands lit stone around 0.62 sRGB - brighter
than the last pass and close to the photographs.
The elevation now reads as pale, low-saturation concrete/marble with thin-jointed ashlar. Row 4-6 re-rendered
and pending. The architectural pass the owner also asked for - carved inscription, medallions, banner lettering,
capital and grille depth - is untouched and recorded as the next step.
THIRD_PARTY_NOTICES carries the new seed and prompt.
The generated ashlar albedo never showed for two reasons that had nothing to do with the material graph,
which was verified correct the whole time (TextureSampleParameter2D -> Multiply -> Base Color, colour sampler,
texture set, 6x UV tiling):
1. **The map overrode every hero slot.** The generic per-slot material pass in ue_build_demo_map.py assigned
the facade KIT's flat stone to every actor whose slot names looked like stone, banner, glass - including
Hero_Facade. Whatever the import authored was replaced at map build time. The hero is now excluded and gets
its own materials.
2. **Mesh slot assignment does not persist.** Assigning to mesh.get_editor_property('static_materials') returns a
copy; after a reload the mesh still pointed at the original imported materials (measured twice). Component
level overrides do persist in the saved map, so apply_hero_materials() sets them in both builders.
The elevation now renders as coursed, grainy warm limestone with banners, window bays and recessed portals.
Rows 4-6 re-rendered and pending review; the comparison plate is regenerated. Still missing against the
reference: carved inscription, medallion relief, banner lettering, crimson banner tone. The two-model judge
returns no/no (0.90/0.95) - it wants the text and detail that are still absent, which is at least the right
complaint this time.
- New 'Toolchain - end-to-end asset check (done 2026-11-10)' section: what the e2e command runs, the last PASS,
and the seven steps that got it there. Two items stay open - the judge is not trustworthy yet (0/11 on shots
the owner approved) and the driver joins the release checks only once it is.
- Hero facade tasks rewritten as done/verified where they are: cameras checked, detail seated on the wall, blank
lintel band removed, grilles, courses and balustrade added, sandstone authored per slot, banner proportions
corrected. What stays open is the generated albedo actually rendering, the inscription and medallions, the
banner lettering, and the three review shots.
- Vehicle physics: the task list now leads with the two facts that matter (the same pawn falls correctly on
ReviewMap's floor; in DemoMap the transform is written directly at any height, with the mover, overlaps,
collision and gravity all ruled out by measurement) and the next probe - a fresh map plus staged content
group by group.
- 0.3.0: the road/sidewalk reconciliation is marked solved - the 2 m 'lid' was an auto-generated convex hull over
a 279 m strip, not the geometry, and the cross-section is now continuous at 158 cm. The Chaos vehicle item is
marked deferred, pointing at the own-task section.
Records what cost time today, with the evidence:
- the four traps of the folder move: sibling imports (sys.path needs both the tools root AND the script's own
folder), shell scripts resolving REPO relative to themselves, stale result files passing as success, and
Unreal hanging rather than failing when a second editor is alive - plus the pkill-matches-its-own-command-line
gotcha;
- measured proof that LevelEditorSubsystem.new_level() cannot replace a map in one session (create -> delete ->
create returns False, and delete_asset leaves the .umap), so emptying actors is the workable step; and that a
falling actor count (600 -> 409) was the fix that audits clean;
- two-model judging: the reconciliation rule, the 404 that retired claude-3.5-haiku, and the observation that
this judge is consistently pessimistic on our content so the model disagreement is the signal;
- local ComfyUI: it is a Stability Matrix install nothing here starts, the documented graphs are UI format that
/prompt rejects, albedo-only output, no seamless sampler in 0.44, and per-model licence rules;
- hero facade: detail was modelled inside the wall, the hero's materials are not the kit's, Unreal keeps a
material on re-import, materials must be saved themselves, and project_dir() is not the repo root;
- the render chain is build -> import -> render, and e2e_asset_check.py is the command that runs it all.
Investigated the count difference from the e2e run and it is the opposite of a regression. The rebuilt map
holds 409 actors: 72 site meshes, 310 facade-kit pieces (the kit tool reports 'placed 310 kit pieces'), six
frontage buildings, lights, cameras and the player start. The previous 600 carried the duplicated staging that
the prefix-only clear used to leave behind; ue_audit_scene.py on the new map reports 0 failures and no
duplicate actor labels.
Captures were re-rendered against this map.
The e2e asset check now completes: PASS on all six steps - empty maps 9 s, demo build 10 s, review build 9 s,
captures 80 s, judge 388 s, review file. Four breakages from the folder move had to be fixed first:
- ue_placement and ue_common are siblings of the scripts that import them, but the rewrite put only the tools
root on sys.path, so every Unreal commandlet died with ModuleNotFoundError. Both paths are inserted now.
- render_screenshots.sh resolved the repository root one level up, so commandlet paths became Tools/Tools/...
and the capture step failed in 0 s.
- the driver treated a result file left by an earlier run as success, which is how a ten-second 'demo build'
passed without building. Result files are unlinked before each step and child output streams to a per-step
log file rather than a pipe.
- two editors on one project hang instead of failing, so the driver refuses to start while one is running.
Also corrects ue_recreate_maps.py: LevelEditorSubsystem.new_level() refuses a path whose package is still in
memory and delete_asset() leaves the file on disk, measured create->delete->create returning False. The step
now loads each map, destroys every actor, saves, and reports the count it left (0), which is the property the
builders actually need.
Open question recorded in TODO.md: the rebuilt map holds 409 actors and 613 KB against 600 and 886 KB before,
and the ~190 missing ones look like the facade-kit and theatre-trim placements that run as separate steps.
The e2e check recreates the maps as empty levels by design, and the run was cancelled while it was still
rebuilding DemoMap. The commit that followed swept the empty 159 KB map into git, replacing the populated
886 KB one.
Both maps are restored from f681d83, the last commit that had them built. The recreate step stays in the
toolchain - it is correct - but it must never be committed mid-run: the maps are only valid content after the
builders have saved them again.
One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary:
Tools/unreal/ commandlets and the two map builders, with ue_common.py and ue_placement.py as the
shared imports (they need the engine)
Tools/blender/ builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots
(they need the Blender Flatpak)
Tools/review/ the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py,
build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py,
visual_targets.json
Tools/gen/ image and texture generation: the ComfyUI driver, deterministic texture writers,
reference fetching
Tools/input/ OS-level input injection
root _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/
Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on
sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five
folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver,
hero_iterate.sh and the comparison generator were repointed.
51 README rows now link to their new location, the README gains a Layout section naming each folder's
sandbox, and the architecture notes and Blender README follow the moves.
Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new
homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is
being rerun against the new layout.
Used the workflow integration from the previous step for real: generated a warm limestone ashlar albedo
with two profiles compared (AlbedoBase 17.04 vs Krea-2 13.90, AlbedoBase won), saved it as
Art/textures/generated/T_FacadeAshlar_AI.png beside its provenance sidecar, and imported it to
/Game/Hollywood/Textures.
ue_hero_facade_import.py now imports that albedo and, for the two stone slots, builds a material with
TextureSampleParameter2D -> Multiply(tint) -> Base Color, tiling the UVs 6x, so the map carries the coursing
and the tint keeps the value inside the reference's range.
Two real bugs fixed on the way:
- the albedo path was resolved against unreal.Paths.project_dir() (Game/GigLine), not the repository root, so
the import silently skipped;
- the authored materials were never saved - only the mesh was - so after a reload the slots pointed at
nothing and the elevation fell back to one default colour.
Honest status: the nodes are verified present and saved (Constant3Vector, TextureSampleParameter2D,
TextureCoordinate, Multiply) and the source mesh has UVs, but the render still shows the flat tint. Recorded
in TODO.md with the next probe: read the compiled Base Color pin instead of the node list.
THIRD_PARTY_NOTICES.md gains the AlbedoBase row (commercial use, credit required).
Re-read /home/zwitschi/StabilityMatrix/Data/README.md - it now documents 141 GB of models and four
verified, core-node image/texture graphs - and wired the ones that matter into the toolchain.
comfyui_generate.py gains a 'texture' subcommand that rebuilds the API graph for each documented workflow,
because the files in Workflows/ are ComfyUI UI format and /prompt cannot accept them:
albedobase AlbedoBase XL v3.1, 30 steps, CFG 6.0, dpmpp_2m/karras, baked VAE
krea2 Krea-2 Turbo NVFP4, 8 steps, CFG 1.0, euler/simple, ConditioningZeroOut, texture-fix VAE
qwen21 Qwen-Image 2.1 int8, 25 steps, CFG 1.0, euler/simple, TextEncodeQwenImage21, texture-fix VAE
Default pair is albedobase + krea2 - both commercially usable - generated from the same prompt and seed,
scored by high-frequency detail, and compared before the winner takes --out. Smoke-tested at 512 px: both
rendered, albedobase won 18.91 against 17.51, and the resulting limestone albedo is a plausible material
sheet.
Tools/README.md documents the profiles with their settings, a licence row per profile (Qwen-Image 2.1 is
research-only and must not ship), and the two limits the workflows carry: they produce albedo only, and
ComfyUI 0.44 has no seamless sampler, so tiling has to be checked by offsetting the image 50%. Five tests
cover the graph shapes; the suite is at 108.
Tools/README.md now covers the generative tools next to each other:
- comfyui_generate.py drives the LOCAL ComfyUI at 127.0.0.1:8188, which this repository does not start. A new
subsection says so and says what to do: start Stability Matrix, launch ComfyUI, re-run. It points at
/home/zwitschi/StabilityMatrix/Data/README.md for the package version, the launch arguments and the shared
80 GB model library, and notes that THIRD_PARTY_NOTICES.md must carry the model and licence before a
generated texture ships.
- generate_facade_textures.py and generate_character_textures.py are listed as the deterministic procedural
path: no model, no network, byte-identical on every run.
- The tool's own docstring carries the same Stability Matrix instruction.
The default second checkpoint was sd_xl_base_1.0.safetensors, which is NOT installed; it is now
epicrealismXL_pureFix.safetensors, verified against the running server. The pair was smoke-tested end to end
at 512 px: both checkpoints rendered, both scored, juggernautXL won at 20.37 against 12.93, and the winner
landed on --out with the loser kept beside it.
A single vision model has been wrong in both directions on this project - it missed real defects and
disputed frames that were correct - so agreement between different model families is worth more than one
confident opinion.
judge_screenshot.py now runs two models by default (openai/gpt-4o-mini + qwen/qwen3.7-flash, both served
on OpenRouter; claude-3.5-haiku is gone, which the first attempt discovered with a 404). combine()
reconciles them: agreement carries the verdict with the mean confidence, disagreement reports 'uncertain'
and keeps both reasons rather than letting the more confident model win. --model still forces a single
judgement, and every line prints which model said what.
comfyui_generate.py generates the same prompt and seed with two checkpoints, scores both by
high-frequency detail (mean absolute Laplacian, which catches blurry or flat results), keeps the loser as
<out>__<model>.png with its sidecar, and moves the winner onto --out. --alt-model restores the single
run.
Tests cover the reconciliation - agreement, disagreement, unknown verdicts, and no verdicts at all - and the
suite is at 103.
Iterated the loop five times against Art/reference/hero-masonic/. What took the iterations was the material,
not the geometry:
- M_FacadeStone in /Game/Hollywood/Site is the KIT stone; the hero does not use it, so warming it changed
nothing.
- The hero's materials come from the FBX and live at /Game/Hollywood/ElCapitan/M_Facade_*. Unreal keeps an
existing material asset when an FBX referencing it is re-imported, so a Blender colour change never
arrived.
- Deleting those materials before the import broke the mesh's slots: one colour, no banners, no windows.
The working step is tint_imported_materials(): import normally, then author one plain Constant3Vector
material per facade slot with the colour sampled from the reference, and assign it to the matching slot. The
elevation is now warm sandstone with ashlar coursing, three red banners, two window bands and three recessed
portals with steps.
Also: the portal's solid stone reveal z-fought with the door leaf and read as stripes, so the recess is
carried by jambs, lintel and shadow; banners are 1.35 x 6.2 m hung from 7.2 m instead of 1.55 x 5.4.
Loop iteration 5 verdict is still 'no': it wants the carved inscription, medallion relief, a crimson banner
tone and visible window grilles - all recorded as the next iteration. Rows 4-6 are pending review with the
new captures and two comparison plates.
The 94 symlinks in ~/.agents/skills and the full second copy in
~/.pi/agent/skills/blender-skills were the same packs under two roots, so Pi
reported a collision for every one of them on every start and silently dropped
the loser.
- The 93 upstream packs, the shared references/ notes and the LICENSE now live
once, flat and one level down, in the common agent folder ~/.pi/agent/skills/.
- The repository keeps only the project-specific pack:
.agents/skills/blender-skills/blender-director/. Its ../references/ links now
point at ~/.pi/agent/skills/references/ because it no longer sits beside them.
Its SKILL.md declared metadata.role twice (specialist, orchestrator), which is
fatal YAML - the pack had never actually loaded.
- AGENTS.md and Docs/toolchain.md describe the split and warn against copying the
common packs back into the repository.
Verified by walking all three discovery roots and parsing every frontmatter:
97 skills loaded, 0 malformed, 0 collisions.
Tools/hero_iterate.sh runs the whole chain - chmod the Blender files, build, import, render, judge, plate -
and fails loudly at each step with its log under /tmp/gigline. Tools/hero_compare.py builds the side-by-side
plate (reference | rendered, one height, labelled); two are committed for review.
This pass: the stone is warmer in Blender (0.66/0.60/0.48 against the near-white blockout) and the banners are
1.35 x 6.2 m hung from 7.2 m, matching the reference proportion instead of the 1.55 x 5.4 that sat too low and
too fat.
Iteration 1 judge verdict, confidence 0.9: lacks warm sandstone, lacks fluted column detail, banners wrong,
no doorways, no inscription or medallions. Checked against the plate, two of those are wrong (120 flute
strips exist and cast shadows; three 1.2 m deep doorways with steps exist and read black) and three are real.
The split is recorded in TODO.md with the next action for each - the important one being that the stone colour
must change in the UE material (ue_hero_facade_import.py), not in the Blender material, which the render does
not use.
Every .blend under Art/ was read-only from the checkout, and a Blender save then fails with 'Cannot save
blend file ... is not writable'. In Tools/hero_facade_build.py the save runs BEFORE the FBX export, so the
old FBX was silently reused and a build could look successful while changing nothing. All 20 .blend and
.blend1 files are now writable and 'find Art -name *.blend ! -perm -u+w' returns nothing.
Re-rendered the full capture set afterwards. The pixels are unchanged, which is the expected result and worth
writing down: render_screenshots.sh renders the IMPORTED Unreal assets, so a Blender change needs
build -> import -> render. Chmod alone cannot change an image.
Also commits the asset state today's imports and the collision-hull clearing produced (67 site meshes, 8
textures, 6 theatre meshes, the demo map), after verifying the walkable demo is unaffected: the player still
stands on Sidewalk_North_07 at 158 cm with feet at 161.
Art/reference/hero-masonic/ (15 images, git-ignored) replaces the single Wikimedia photo as the source of
truth. Two front elevations drive this pass: 02-masonictemple01.jpg, a clean street-level frontal, and
06-el-capitan-theater-frontal-fachade.jpg; 05-hollywoodmasonictemple-1922.jpg records the original masonry.
Changes:
- the blank Hero_Stylobate_LintelBand across the portals is removed - the reference has open doorways with
steps, and they now read as three dark openings with banners above them;
- rustication(): five ashlar courses from the pavement to the portal heads, 4 cm joints proud of the wall;
- balustrade(): bottom rail, top rail and 38 balusters along the roof edge, where the reference shows a
cresting and railing.
Still missing against the same references, and recorded: warm sandstone instead of flat pale stone, lattice
grilles in the window bays, the carved inscription, and medallions that do not read at distance.
The blend is read-only after a checkout - chmod u+w first or the save fails silently and the FBX is never
written, which happened once here.
Rows 4-6 re-rendered and pending review.
The three hero captures showed a plain block with six columns because every applied detail sat BEHIND the
wall's front face, inside the solid mass:
Hero_Banner_1 y -1.08..-1.02 (55 cm inside)
Hero_Portal_1 y -2.35..-1.15 (2 m inside)
Hero_Window_1_1 y -0.60..-0.52 (2 cm inside)
The colonnade, cornice and dentils happened to protrude, which is exactly what the shots showed.
The depth is now stated once, in hero_facade_build.py: FRONT_Y is the wall's front face and
seat_on_front() moves every applied part until its street-facing face sits at FRONT_Y + offset - 8 cm for
banners and jambs, 5 cm for entablature and cornice, 3 cm for window panes, 4 cm for mullions, 10 cm for
portal signs. It reports what it moved, 99 parts.
After re-export, re-import and re-render the front elevation now shows the three red banners, the two
window bands, the dentil course and the portals. What is still missing against the reference: rusticated
banding on the stone, lettering on the motto band, grilles in the windows, a balustrade on the parapet, and
the blank lintel band across the portals that the reference does not have.
Rows 4, 5 and 6 are back to pending for review because the images changed.
Measured the Y positions in the source and the imported mesh:
blend scene Y -12.50 .. +0.13
Hero_Mass y = -BODY_D/2 - 0.5, so it spans -12.50 .. -0.50
Hero_Banner_1 -1.08 .. -1.02, i.e. 55 cm BEHIND the wall's front face
imported bounds y -13 .. +1250 cm
So the banners - and by the same construction the window bays and portal surrounds - sit inside the mass and
never render. The columns, cornice and dentils protrude, which is exactly what the captures show: a plain
block with six columns.
The camera check is answered as well: the three hero cameras are aimed at (-13336, 1307) where the hero actor
is, and the frontage sits at the origin, so the framing was correct all along.
Next step is mechanical, not artistic: put a FRONT_Y constant in Tools/hero_facade_build.py, place every
elevation detail relative to it, re-export and re-render. The model does not need remodelling before its
detail can be seen.
Started the hero facade task the way it has to start - by comparing the reference photograph with the
current render rather than with memory of it.
Reference: el-capitan-facade-day-wikimedia-2009-10-27.jpg, north elevation, daylight.
The model (SM_HeroFacade, 4 materials) is much further along than the review captures suggest: six fluted
columns with capitals and abaci, architrave, entablature, cornice with 52 dentils, parapet, motto band with
surround and two medallions, three banners, five window pairs, and three portals with jambs, lintels,
mullions and five steps each.
What it genuinely lacks: surface. Rusticated banding, carved lettering on the motto band, window grilles,
and a balustrade with finials on the parapet.
And one thing to check before any modelling: the three hero captures show a plain block with six columns and
nothing else, while the mesh carries banners, windows and portals and is staged at (-13336, 1307, 0). The
likely explanation is that the cameras frame Frontage_SM_ElCapitanTheatre at the origin, or the wrong side
of the hero - in which case the rejection is describing a camera error. One render aimed at Hero_Facade from
Y -4000 decides it, and three new tasks are recorded with that first.
A shared heading string made one edit land in two sections, so the hero-facade task carried a copy of the
vehicle's bisect evidence. The section is back to its own two steps.
Also re-ran the gates on the tree as it stands after today's ground and placement work:
Tools/ue_audit_scene.py 593 mesh actors, every mesh has a material, building X ranges as expected,
no duplicate actor labels, 0 failures
prettier --check all Markdown clean
unittest 99 tests pass
Third fix tried and failed: the terrain staged in the probe map with collision disabled entirely. The pawn
still rises (Z 315 -> 421), exactly as with collision enabled.
With that, the terrain's convex hull, its collision channel and its collision itself are all eliminated, and
the failure survives in a map whose only difference is the presence of a 279 m static mesh.
The player is unaffected throughout - re-checked after every attempt, still standing on Sidewalk_North_07
at 157 cm - so nothing in the walkable demo regressed.
Deferred rather than guessed, per AGENTS.md: the task is marked deferred with the three failed fixes, the
probe workflow and switches a fresh session inherits, and the next question framed precisely - why a map
containing a collisionless 279 m static mesh changes a physics body's behaviour, which points at streaming,
memory layout or the broadphase rather than at collision authoring.
Generated probe levels deleted; the tools stay.
Built the map-content bisect the task asked for: Tools/ue_vehicle_probe_map.py duplicates ReviewMap,
optionally adds one demo mesh and the pawn, and saves it as VehicleProbe.
control (no extra mesh) Z 300 -> 297 rests on the review floor
+ Terrain_DEM Z 300 -> 498 thrown upward
So Terrain_DEM alone reproduces the fault in a map that works. Two fixes were tried:
- Tools/ue_clear_simple_collision.py cleared the auto-generated convex hulls from all 76 site/street
meshes - the terrain had ONE convex element over 279 m - and set them to complex-as-simple. Still rises.
- The terrain component set to QueryOnly, so traces hit it and physics ignores it. Still rises.
Neither changed the behaviour, so the ejection is neither the convex hull nor the collision channel. The
next probe is the complex triangles themselves: give Terrain_DEM no collision at all in the probe map and
see whether the pawn falls. If it falls, the triangle winding is pushing bodies out of the ground mesh -
the same inverted-winding family that made the sidewalk unstandable earlier.
Both tools have README rows; the pawn is parked again.
Two more probes, both negative:
--novehmover movement component tick disabled from the constructor -> the pawn still rises
overlaps GetOverlappingComponents at spawn -> 0 components
So it is not the Chaos movement component, and the body is not inside anything, at any height. With the
velocity clamped to zero, the mover disabled, zero overlaps and a correct dynamic body, the transform is
still written every frame - in DemoMap only.
That is the limit of targeted probing. The remaining path is systematic rather than clever: rebuild DemoMap
in a fresh empty level and add one group of staged content at a time, running the pawn profile after each,
until it starts rising. Five or six runs name the actor or mesh; guessing at engine internals has not, in
ten probes.
The probes stay in the tree as switches: -probes (all diagnostics), -novehmover, -profile, plus the velocity
clamp and overlap list. They cost nothing when unused.
The decisive clamp: zero linear and angular velocity every frame for two seconds.
t=0.25 Z=3220 vZ=0 | t=0.75 Z=3683 vZ=0 | t=1.25 Z=4197 vZ=0
It rises at ~1200 cm/s with velocity pinned to zero, so this is not a force, an impulse, or a bad initial
state - all of those the clamp would have removed. Something calls a transform setter on the vehicle every
frame.
Our own source contains exactly one direct transform write outside the character (the shot actor's camera),
so the writer is engine-side: the Chaos vehicle component driving UpdatedComponent from a vehicle state that
was never created, or a skeletal-mesh transform update.
Also diffed the two maps, since the pawn works in ReviewMap and not in DemoMap: kill_z, gravity, lights and
partitioning are identical; DemoMap adds 593 static mesh actors, a height fog and a post-process volume,
ReviewMap adds a shot actor and the character. Nothing in either list can write a transform, so the map
difference is not in its actors or settings.
Ran the bisect the task asked for. The same pawn, unchanged, in two maps:
ReviewMap, flat floor Z 200 -> 188 -> 174 -> 157 -> 87 vZ -208 -> -663 falls correctly
DemoMap, on the street Z 486 -> 920 -> 1126 -> 1385 vZ ~0 teleports up
DemoMap, 60 m away Z 291 -> 467 -> 920 -> 1343 vZ +1595 launches
DemoMap, 25 m up Z 2496 -> 2664 -> 3068 -> 3434 vZ +1509 launches at once
So the pawn, its physics asset, its wheels, its mass and its movement component all work: on a plain floor
it falls and settles. Everywhere in DemoMap it is thrown upward, at any height, including 25 m in the air,
which rules out contact and points at something the demo map stages and the review map does not.
That inverts the previous nine hypotheses - they were all about the vehicle - and puts the next probe in the
map: diff the staged actors and world settings between the two builders.
Pawn parked; the bisect numbers are in the task.
The dynamics probe rules out the last structural explanation:
simulate=1 instance=yes profile=PhysicsActor
A normal dynamic actor - and it still travels ~700 cm/s upward with vZ oscillating around zero. So the
teleport is applied to a genuinely dynamic body, which is why not one of the force-shaped settings
(gravity, mass, penetration, spawn height) changed anything.
Recorded with it: the bisect plan for the next pass, because guessing at Chaos internals has stopped being
the cheap option. Spawn the pawn in ReviewMap's empty floor to separate asset from world, and spawn a stock
AWheeledVehiclePawn beside it to separate our setup from the engine's.
Pawn parked; captures unchanged.
Instrumented the movement component so the write is caught at its source rather than inferred from the
actor: a UGigLineVehicleMovement subclass is substituted for the stock component through
AWheeledVehiclePawn::VehicleMovementComponentName, and it logs CreateVehicle(), the first UpdateState and
the component tick.
mover tick: Z 661 -> 1143 -> 1443 -> 1718 ..., output null, wheels 0
physics OFF at 3 s: Z 2247, and it does not move for a whole second afterwards
profile: t=0.25 Z=515 vZ=-8 | t=0.75 Z=920 vZ=-22 | t=1.25 Z=1287 vZ=173
Two conclusions:
1. The mesh's physics state IS what moves the actor - switching physics off freezes it at once.
2. It moves WITHOUT VELOCITY: position rises ~700 cm/s while vZ oscillates around zero. That is a
kinematic teleport, not an impulse and not a force - which is exactly why gravity, mass and
penetration all measured clean. The vehicle-state output is null throughout, so the teleport comes
from the Chaos layer driving the body kinematically while the vehicle itself was never created.
Pawn parked, captures regenerated so the demo stays clean.
Sampled the actor transform immediately before and after Super::Tick on every frame: it never changes
there. The 'inside Super::Tick' line never fires, yet the actor climbs 400-1400 cm/s.
With the earlier results that leaves exactly one consistent candidate: the Chaos movement component's own
tick or its async physics step, outside APawn::Tick. That also explains why SetComponentTickEnabled(false)
did not stop the climb and why the body's reported velocity is ~0 while the actor moves - the movement
component is writing a transform it took from a vehicle state that was never created.
Next step is a subclass of UChaosWheeledVehicleMovementComponent that logs UpdateState /
OnUpdateTransform together with the vehicle-state pointer, so the write is caught at its source rather
than inferred from the actor.
Three more measured facts:
bones VehicleRig root Wheel_FL Wheel_FR Wheel_RL Wheel_RR
setups Wheel_FL Wheel_FR Wheel_RL Wheel_RR -> exact match, mesh suspect ruled out
mover updated=VehicleMesh ticking=1
stop tick disabled at 2.5 s with the actor at Z 2001; at 3.0 s it is at Z 2258
terrain CTF_USE_COMPLEX_AS_SIMPLE with 0 box elements -> no invisible lid to eject from
So the wheel bones are right, there is no proxy collision to fight, and disabling the mover's tick did not
stop the climb. That moves the suspect list to the pawn's own Tick/root: something writes the actor
transform every frame regardless of the physics body, and the 1 cm bounds are its fingerprint.
Pawn parked again; the diagnostic stays.
The previous edit to this section never applied - a substring that no longer existed - so the task
still described only the first launch observation. It now records everything measured this session:
box 452x205x40 centre 0,0,-18 | bounds extent 1,1,1
velocity -24,-182,-28 cm/s | mass 1500 | sim=1 | setups=4 wheels=0
The conclusion is that the body is nearly still while the actor is nine metres up, so this is a
transform problem rather than a force one, and the suspects are ordered: the component hierarchy, the
Chaos movement component's transform updates, and then the skeletal mesh's bone layout - which would
explain CreateVehicle() bailing before it ever builds wheels.
Ruled out with a run each: spawn penetration at four heights, panel collision, chassis box dimensions,
world gravity, and mass.
The motion probe splits the problem in two:
velocity -24, -182, -28 cm/s (a body that cannot climb 9 m in 1.5 s)
mass 1500 kg (the override DID apply)
box 452 x 205 x 40 (correct)
bounds extent 1,1,1 (still degenerate)
actor Z 1393, spawned at 458
The physics body is nearly still while the actor is nine metres above its spawn. So this is not a
force problem at all - it is a transform problem: something writes the actor's transform that is
neither the body's velocity nor our code, which is also what the degenerate bounds are telling us from
the other side.
Next: find who writes that transform. Suspects are the Chaos movement component's UpdateState /
OnUpdateTransform and the component hierarchy - a pawn whose root is the mesh while the physics state
belongs elsewhere behaves exactly like this.
Pawn parked again so the demo stays clean.
The last open row (DemoMap vehicle) is approved, so every capture in the table has a verdict: 20
approved and 3 hero-facade rows excluded from this pass and tracked as their own task.
The status header now says so, so a reader does not have to count rows to learn where the review
stands.
Owner approved the last three mannequin rows, so every ReviewMap capture is now approved except the
three hero-facade shots, which live in their own task.
Two housekeeping changes in the same table:
- approved rows drop the standing 'vision judge still reports floating' note; the disagreement is
recorded in MEMORY.md, and the table is for approval state, not for a second copy of it.
- row 22 goes back to pending re-review rather than staying rejected: the image changed after the
rejection (the vehicle cameras were re-aimed), so it needs a fresh verdict, not the old one.
Owner review: rows 1, 2 and 3 show the mannequin about 2 cm too high; rows 8 and 9 show nothing wrong.
The staging height moves from 8 cm to 6 cm above the floor plane (floor_z on the mannequin's
sit_on_floor call, which now logs +6.0), and rows 8 and 9 are approved. Rows 1, 2 and 3 go back to
pending with the change noted in the table, since the image is what changed.
Captures regenerated.
Four more things measured instead of guessed:
chassis box 452 x 205 x 40, centre 0,0,-18 -> correct for the car
world gravity global_gravity_z = 0.0, and the character falls normally
spawn height 5 m up: the pawn still climbs, so penetration is not the cause
mass override SetMassOverrideInKg at 0.5 s never logged
The one number that does not fit: the pawn's mesh component reports bounds extent 1,1,1 while
simulating and carrying a 452 cm collision box, and the actor drifts 4.8 m sideways while climbing
with no input. A body that simulates with correct collision but degenerate bounds is not the body the
movement component is driving, so the next question is which component owns it and whether
CreateVehicle() runs at all.
The pawn is parked again; the diagnostic logging stays (it is the tool that produced this), and the
evidence is in the 'Vehicle physics' task.
Three hypotheses tested and ruled out, each with its own run:
spawn level with the road -> actor climbs to Z 1993
spawn 1.2 m above the road -> climbs to 2131
visual panels' collision removed -> climbs to 2066
So neither the spawn penetration nor the panels' overlap is pushing the pawn up; the force is inside
the vehicle. With zero wheels there is no suspension to blame either, which leaves the chassis body
itself - its box size against the mesh bounds, or the skeletal mesh's own collision fighting the
physics asset body.
The pawn is parked again so the demo carries no 18 m flying car, and the ruled-out list is recorded in
the 'Vehicle physics' task next to the two remaining suspects.