Commit Graph
42 Commits
Author SHA1 Message Date
zwitschi 8c34eaece0 feat(hero): banner lettering, re-render the three staging shots; defer the rest to 0.9.0
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.
2026-11-10 15:27:08 +01:00
zwitschi bc9fed8e11 fix(hero): seat the motto band and surround on the parapet face
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.
2026-11-10 15:11:52 +01:00
zwitschi 33f8139cb2 feat(hero): stepped medallion relief, seated on the parapet face
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.
2026-11-10 15:04:48 +01:00
zwitschi 1d57ed477d feat(hero): carved inscription across the attic
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.
2026-11-10 14:25:04 +01:00
zwitschi 4d27109668 refactor(tools): group the sixty scripts by what they talk to
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.
2026-11-10 12:40:30 +01:00
zwitschi f681d83736 feat(hero): generate the stone albedo with the new texture tool, import it, wire it into the material
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).
2026-11-10 11:58:39 +01:00
zwitschi bf7dfe1052 feat(hero): sandstone, banners and recessed portals - the material was the missing link
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.
2026-11-10 09:05:48 +01:00
zwitschi 61dd28bc89 feat(hero): one-command iteration loop, first judged pass, comparison plates
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.
2026-11-10 08:28:06 +01:00
zwitschi 03ef4dc945 feat(hero): first pass from the masonic-temple reference folder
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.
2026-11-10 08:08:01 +01:00
zwitschi 7b9a7a3867 fix(hero): seat the façade detail on the wall, so it renders at all
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.
2026-11-10 07:59:51 +01:00
zwitschi 1b4b46e957 fix(site): slice the road too - the 2 m lid was a convex hull, not geometry
The road was never mis-modelled: a 279 m strip imports with ONE auto-generated convex hull, and a
convex hull's top is the strip's maximum height. A downward trace therefore landed at 366 cm - a flat
lid about 2 m above the visible road, which sits at roughly 152 cm there. That single fact explains
every failed sidewalk fix, the 'road above its own sidewalk' reading, and why the player always ended
up on Road_Hollywood.

Slicing the road the way the sidewalks already were fixes it, and the cross-section is now continuous:

  Terrain_DEM       154
  Sidewalk_North_07 158
  Road_Hollywood_07 158   (was 366)

Two changes had to land together: the road exported in slices, and the map stopped staging the single
Road_Hollywood asset - which still sat on top of the slices and won every trace until removed.

All 23 captures regenerated; the player and the car both stand on a continuous ground plane.
2026-11-09 19:15:10 +01:00
zwitschi cbdb8e6e3a test(site): the source is right, the engine is wrong about where the road is
Regenerated the street blend from OSM and the DEM (the code was already correct: 279.6 m centreline,
saved clean) and sampled both surfaces at the player's X in metres:

  Road_Hollywood    y -9.0..-9.1,  z 0.87..0.94
  Sidewalk_North    y +11.9..+12.0, z 1.51..1.59

The road is the south band at about 0.9 m and the north sidewalk is about 1.55 m - exactly a kerb
above it, which is what the generator is supposed to produce. The source is correct.

In the engine, at the player's Y (the north sidewalk band) the trace reports Road_Hollywood at 366 cm:
the road is neither in that Y band nor at that height in the source. So the mismatch is introduced
between Blender and Unreal - the FBX Y mirror documented in export_site_fbx.py, or the importer's
transform - not by the geometry or the placement code.

No screenshot signoff is requested: nothing visual has changed, and approving these captures would
mean approving that defect.
2026-11-09 18:24:48 +01:00
zwitschi d0d87e8fda fix(site): the slice cuts now keep the middle, and the surfaces do not agree
Two findings, one a bug and one the real cause.

Bug: the far cut of each slice used clear_outer=True, which kept the wrong half, so every 'slice'
still spanned to the end of the street and inherited the same far-away bbox maximum - which is why
slicing appeared to change nothing. Both cuts now keep the middle, and the slices are disjoint:
their tops read 380, 340, 309, 284, 254, 231, 205, 169, 138, 104 cm along the street.

Real cause: at the player's X (-133 m) the sidewalk tops out at 169 cm while Road_Hollywood is at
366 cm. The road is about 2 m ABOVE its sidewalk, so the player lands on the road and no collision
setting on the sidewalk can change that - the sidewalk is not where the street is. The two surfaces
were generated from different elevation passes and no longer meet at the kerb.

That is site-pipeline data, not placement. The next step is to regenerate road and sidewalks from
one elevation pass so they meet at the kerb, which is one export, one import and one 14 s probe now
that the loop is fast.
2026-11-09 18:19:39 +01:00
zwitschi 1916321761 feat(site): slice the ground into 20 m segments - and the slice hypothesis dies with them
The site exporter now splits sidewalks and kerbs into ~20 m pieces
(SEGMENTED / SEGMENT_M in Art/blender/site/export_site_fbx.py), 56 slices in all, so each has a
bounding box that means something where it stands instead of reporting the highest point on a
279 m slope. They import and stage (63 site meshes in the demo map).

The hypothesis behind it is dead, and that is the useful part: with the invisible sidewalk
stopgap retired, the player now falls to Road_Hollywood at 366 - the slices do not block either.
So the 279 m aspect ratio was never the cause; a 20 m slice of the same mesh fails identically.
The sidewalk geometry itself is what does not collide, and the next place to look is its shape -
an open surface with no volume, inverted winding or zero-thickness triangles - not the import
flags and not the size.

The stopgap retirement stands: the player walks on the road, which is real geometry that does
collide, rather than on an invisible ledge.
2026-11-09 18:13:29 +01:00
zwitschi bb8ae3eb4b test(visual): the vehicle shot gets a reference, and the character/car height fixes
The suite caught an incomplete target: demo-vehicle had an empty references list, which the
visual-loop test requires. It now points at Art/blender/vehicle/hatch-reference.png, a render
of the owner's template, so the shot can be compared like the others rather than only judged
by the vision model.
2026-11-09 16:01:34 +01:00
zwitschi b50afa4bcd feat(vehicle): the car renders, built from the owner's hot hatch template
The invisible-vehicle defect is closed. Shot 9 now shows the car instead of an empty road.

Tools/vehicle_from_template.py converts Art/blender/vehicle/hot hatch.blend into UE-ready
assets, following two skills:

  vehicle-artist - systems split (body 113k tris, trim 10.5k, glass 20.5k, lights 18.6k,
  wheels 47.8k), one UCX_ hull 1.45 x 3.94 x 0.76 m, two decimated LODs at 39.7k and 13.6k.
  unreal-export - scale normalised to a documented 4.05 m hot hatch before export (the
  template measures 7.26 m, factor 0.5579), then verified after import: body bounds
  1.51 x 4.02 x 1.22 m against the 4.05 m target.

Staged as five plain StaticMeshActors, because the earlier probe proved those render while
the same meshes attached to the vehicle pawn do not.

Open and recorded rather than hidden: judge_screenshot returns verdict=no confidence=0.9 -
the silhouette does not read as a hot hatch from this angle and the car floats slightly.
The likely causes are a system misclassification in the template conversion and an
origin-height error of a few centimetres.
2026-11-09 15:52:42 +01:00
zwitschi 2e0421480c feat(vehicle): skeletal vehicle asset for Chaos, and the block that follows
Chaos drives a skeletal mesh, so the static system split was not drivable on its own. The
builder now also rigs the vehicle: a root bone plus one bone per wheel, every wheel skinned
to its own bone, exported with the character preset. SK_Vehicle_Sedan imports with
SK_Vehicle_Sedan_Skeleton (508 triangles).

Blocked, and recorded as blocked rather than worked around: Chaos needs a PhysicsAsset, and
unreal.PhysicsAssetFactory in 5.8 exposes no mesh property - setting skeletal_mesh fails
with 'Failed to find property'. Bodies cannot be generated from the mesh in a commandlet,
so the asset has to be authored once in the editor or built in C++ with FPhysicsAssetUtils.

Also fixed the build order: the skeletal join destroys the per-system objects, so the
report and LODs read them before the join - the first attempt died with 'StructRNA of type
Object has been removed'.
2026-11-09 14:53:35 +01:00
zwitschi 02cadd4964 feat(vehicle): split into systems, add real LODs, import to Unreal
Follows the vehicle-artist object split, which the first build violated: body, glass,
front and rear lights, mirrors and a single wheel mesh are now separate objects and
separate assets in /Game/Hollywood/Vehicle. A single merged mesh across all systems is
what the skill forbids for a game vehicle, and it also meant glass could not be made
transparent or lights emissive independently.

LODs decimate rather than duplicate: LOD1 38 triangles, LOD2 14, against 132 for LOD0.
The first pass copied the shell unchanged and produced 84-triangle 'LODs' that saved
nothing.

Imports verified with bounds: body 4.76 x 1.76 x 1.28 m, wheel 0.22 x 0.66 x 0.66 m,
which match the modelled dimensions.

Open, recorded: LOD group wiring needs StaticMeshEditorSubsystem.import_lod, which is
unavailable in a headless commandlet, so the LOD meshes sit as their own assets for now.
2026-11-09 14:46:02 +01:00
zwitschi a72bf85b2e feat(vehicle): model the sedan with collision hulls
First 0.3.0 asset. Tools/vehicle_build.py builds a 4.72 x 1.80 x 1.44 m sedan - 2.78 m
wheelbase, 1.62 m track, 0.33 m wheels - as a three-tier silhouette: sill slab, bonnet
and boot shoulders, cabin greenhouse, plus glazing, lamps, bumpers and mirrors. 252
triangles.

Collision is four UCX_-prefixed convex hulls (body, cabin, two bumpers) per the
collision-proxy convention, hidden from render so the preview reads as the car rather
than the colliders.

Two corrections from the preview, both visible in the saved image: the first build was
one tall slab plus a floating cabin and read as a box truck, and the wheels were buried
inside a full-width sill - the sill is now 1.56 m so the wheels stand proud.

Reports every dimension to JSON so the Chaos vehicle config can read the wheelbase, track
and radius instead of restating them.
2026-11-09 14:43:20 +01:00
zwitschi c53386fcf4 docs(review): embed every screenshot inline so the review happens in the file
Each shot now has its own section with the image embedded, its view, expected
content and both verdicts, plus a status table at the top that links to them.
A path alone was not a review: the owner had to open five PNGs to judge a pass.

Also recorded in AGENTS.md (a path is not a review; approval ticks both the section
and the table row), the two retail assets and tools in the arc42 building block
view and the combined architecture.md, and prettier-formatted the changed files.
2026-11-09 09:33:23 +01:00
zwitschi 2614d259aa feat(facades): 0.1.6 retail frontage first pass — Lee Drug and 6806-6820
Models the two retail buildings beside the hero from their own photograph rather
than the generic kit: bronze cornice, mural row, lettering band, stepped Deco
parapet and rounded corner tower on 6800 Lee Drug; burgundy fascia, pilasters,
white-framed windows and banded parapet on 6806-6820. Reference analysis written
before any geometry (Art/blender/retail/ref-analysis.md), every unmeasured
proportion carried into the part report as an assumption.

6822 Budget is deferred, not guessed: no photograph of it exists and the 6.0 x 5.5 m
sliver cannot be read out of the neighbour shot.

Pipeline fixes found on the way:
- render_screenshots.sh called ./Engine/... for the capture step while every
  commandlet used the absolute path; set -e killed the script at 127 *after* the
  maps were rebuilt, so screenshots silently stayed a day old.
- the facade kit applies to every Frontage_*/North_* actor, so it pasted its own
  language over the new modelled facades; it now skips them, and the trim stages as
  RetailTrim_*.
- pure bay arithmetic moved to Tools/_common.py so it is unit-testable without
  Blender (4 new tests, 82 total pass).

Known gaps recorded in Screenshots/README.md, not hidden: the parapet reads as a
blank band, murals and lettering do not resolve at review distance, the tower is a
plain cylinder, and the review camera crops the east end.
2026-11-09 09:10:49 +01:00
zwitschi 73aca9db80 feat(facades): 0.1.5 — hero detail, scene audit, material orphaning fixed (#32)
Merged PR #32: 0.1.5 — hero detail, scene audit, material orphaning fixed

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 22:56:08 +01:00
zwitschi 5e2c5e29c0 feat(facades): 0.1.4 — module kit, textured materials, theatre trim (#30)
Merged PR #30: 0.1.4 — facade module kit, textured materials, theatre trim

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 20:29:11 +01:00
zwitschi ba68dcfdbc feat(hero): v0.1.2 — scale calibration, façade detail, export preset refactor (#28)
Merged PR #28: v0.1.2 — scale calibration, façade detail, export preset

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 18:48:33 +01:00
zwitschi 5ffe62f6aa feat(geo): adopt Mapbox building calibration (3.1 m per level) (#26)
Merged PR #26: adopt Mapbox building calibration

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:38:28 +01:00
zwitschi e75e5500a7 fix(street): 0.1.1 collision patch — flush sidewalk edge, graded crossing ramps (#21)
Merged PR #21: 0.1.1 collision patch (flush sidewalk edge, graded ramps)

Co-authored-by: Zwitschi <zwitschi82@gmail.com>
2026-11-08 16:19:11 +01:00
zwitschi 5aa883eaff docs: convert all architecture .adoc sections to markdown, prettier-format all .md
- Converted 13 individual .adoc section files to .md via
  asciidoctor→docbook5→pandoc(gfm) + post-processing, placed alongside
  the .adoc sources in Docs/Architecture/
- Rebuilt architecture.md from the individual section .md files
- Ran prettier --write on all .md in the repo (23 files changed):
  normalised table column alignment, trailing whitespace, line breaks
- Added .agents/ to .prettierignore (vendored third-party content)
- All .md now passes prettier --check
2026-11-08 13:33:44 +01:00
zwitschi accf3a5306 lighting: fixed daytime rig, real surface materials, dynamic lights
Completes "Fixed daytime lighting and basic ambience" for 0.1.0. All 14
screenshots approved by the owner.

Lighting rig (Tools/ue_build_demo_map.py):
- Sun pitch -50 / yaw 120, intensity 4, warm (255/245/230), atmosphere sun light
- Sky light 2.6 with real-time capture, sky atmosphere, height fog (density 0.008)
- Unbound post-process volume with fixed exposure (min == max brightness) so a
  shot cannot wash out or crush to black depending on how much sky it frames
- Dynamic-only: r.AllowStaticLighting=False plus MOVABLE lights and mesh
  components, so the editor's "LIGHTING NEEDS TO BE REBUILT" warning no longer
  fires and nothing needs baking

Bugs found and fixed on the way:
- Blender diffuse colours do not survive the FBX -> Unreal import: every site
  material arrived white and the terrain arrived with WorldGridMaterial, which
  is what actually made the scene look blown out. The demo builder now creates
  and assigns M_Asphalt, M_Concrete, M_Ground and M_RoadPaint.
- Only one of the three crossings reached the game: each zebra band was its own
  object, so Blender renamed the duplicates .001/.002 and exact-name site
  assembly skipped them. All bands are now one Crossing_Markings mesh.
- Demo cameras: street-level sat inside the facade body (rendering its unlit
  back face) and the crossing camera aimed at the s=36.6 crossing instead of
  the designated s=130.1 (UE X -14956).
- The terrain had no material and imported as a bright default grid.
- Cleared a committed Android File Server SecurityToken (plugin disabled and
  shipping-excluded; the value must stay empty).

Docs:
- Screenshots/README.md: one consolidated table for all 14 shots with map,
  camera view and description; all approved.
- MEMORY.md: records the material-import, exposure, light_color struct,
  band-naming, camera-bounds and lighting-build traps.
- TODO.md: lighting item marked done.

Squash of branch gameplay-lighting-0.1.0 (PR #3).
2026-11-07 18:22:19 +01:00
zwitschi bc2e421d33 gameplay: designated street crossing — zebra markings, corridor cut, up-facing street
- street_mesh.py: continental zebra markings (0.5 m bars, 0.5 m gaps, 3 m
  band) at all three signalised crossings, triangles not quads (FBX SDK
  refuses to triangulate the crown-twisted quads), stripes +6 cm above the
  DEM so they stay above the road surface; winding flipped so every street
  surface faces up (road/sidewalks/markings were 100% down-facing and
  invisible in engine once the terrain stopped covering them).
- terrain_from_dem.py: cut a 10.7 m corridor around the Hollywood Blvd
  centreline so road, kerbs and sidewalks are not buried under the terrain;
  sidewalks themselves cover the transition band, walk-check stays at 0 holes.
- Blender builders save with a writability guard + fatal failure: *.blend
  is LFS-lockable and checks out read-only; a failed save previously left
  downstream steps building from stale files.
- ue_import_site.py: Crossing_Markings imports with no collision (paint on
  the road); ue_build_demo_map.py stages it and chmods the lockable .umap
  before saving (save_map silently returned False on read-only maps).
- ue_build_review_map.py: demo-crossing camera moved to the real crossing
  (s=130.1 m -> UE -24291,-1340) and fixed the ensure_writable path (was
  missing the .umap suffix, so the chmod never fired).
2026-11-07 15:46:45 +01:00
zwitschi cef87b3d31 street: continental zebra markings at the three signalised crossings
0.5 m white bars with 0.5 m gaps across a 3 m band per crossing, laid
1 cm above the road surface. Makes the mid-block crossing (OSM way
385648319, s=130.1 m) a designated crossing in game. Metadata audit
passes (1 sidecar, 0 problems).
2026-11-07 15:24:21 +01:00
zwitschi c11cb4caa4 character: idle standing pose and warm skin, not the grey mannequin
- mpfb_character_export.py: new --pose idle folds the A/T-bind arms down to
  a relaxed standing rest pose (baked via pose.armature_apply before export);
  new make_diffuse synthesizes a warm skin-tone diffuse with a darker
  shorts-suggestive band, replacing the Cycles v2-skin bake that came out
  black and was slow at 1024^2.
- write_png now encodes from img.pixels via numpy (fast) because img.save()
  writes black for an in-memory image.
- Re-exported idle character, re-imported to /Game/Test/Character (138 bones,
  warm M_Character_Export/T_Character_Diffuse); removed stale T_Skin_Diffuse.
- Untracked the review PNGs again: the Screenshots/ gitignore rule had been
  dropped and the parallel session had committed the PNGs, against the
  AGENTS.md 'never commit the PNGs' rule. Restored the ignore rule; only
  Screenshots/README.md stays tracked.
- README/roadmap/memory updated; review-01-character re-pended for approval.
2026-11-07 14:11:06 +01:00
zwitschi cf392a02f7 gameplay: walkable demo map, reset and pause
Tools/ue_build_demo_map.py builds /Game/Hollywood/DemoMap: sun, sky, the hero
facade on the building line, six site meshes imported with collision, and a
player start on the north sidewalk. Tools/ue_import_site.py imports the site
FBXs; terrain, road and sidewalks use complex-as-simple so a simple hull cannot
ramp over the kerbs. Art/blender/site/export_site_fbx.py exports one FBX per
object so each can carry its own collision.

Reset (R) and pause (Escape) join the character and the input context: reset
restores the spawn transform and clears momentum, pause is a real game pause that
releases the cursor. DefaultInput.ini captures the mouse on launch.

Three placement defects found by measuring the imported actors rather than
trusting the plan: the spawn was on the wrong sidewalk because the FBX flip
inverts north, a mirror I added to the exporter double-corrected an already
correct sidewalk, and the street meshes only carry part of the north walk - the
rest is terrain.
2026-11-07 13:17:24 +01:00
zwitschi d4af4af90f hero: make the portals real recesses and land the facade on Z 0
The portal detail shot failed for a modelling reason, not a camera reason: the
doors were 0.1 m panels on a solid plinth, so they read as shading. The plinth is
now four piers plus a lintel band, with a 1.2 m reveal, stone jambs and lintels
standing 0.10 m proud and three mullions per opening. review-06 frames them from
8 m at eye height.

Two defects fixed while doing it: the jamb boxes hung 0.15 m below the pavement,
so the facade imported with min Z -15 cm; and a rebuild run against a read-only
.blend silently skipped the save, which is why screenshots kept showing the old
geometry.
2026-11-07 12:46:00 +01:00
zwitschi 386d4bee78 geo: one tool directory, one unit boundary, per-asset provenance
Geo/tools/ is now every geographic script, with Geo/README.md documenting each
one; the Blender-side builders moved beside their outputs in Art/blender/*/ and
import their helpers from Geo/tools/osm_common.py.

That module gains to_unreal_local() and to_blender_local(): the only place in the
repository that multiplies metres by 100, so a second silent scale cannot be
introduced. block_height_report.py adds height provenance for the block — 3 of
15 buildings have no height data and render at a placeholder 6.4 m.
asset_metadata.py writes and audits <asset>.geo.json sidecars; four now exist
(hero-facade, terrain, street, blockout) and the audit recomputes each centre from
the origin, failing anything that disagrees.
2026-11-07 12:28:26 +01:00
zwitschi c5e30d1c36 feat: hero facade blockout for 6840 Hollywood Blvd
Tools/hero_facade_build.py builds the north elevation parametrically and writes
a JSON report naming the evidence behind every part: the 25.75 m frontage and
14.5 m height are measured, the 4.96 m column bay comes from the sibling notes
and matches that frontage, the three portal flights follow the photographs (five
risers, overturning the inherited eight-step, 12 m wide claim), and the rest is
labelled assumption.

Tools/ue_hero_facade_import.py imports it with the ue_metric_v1 preset
(2604 x 1257 x 1450 cm, 1892 tris, 4 materials) and places it on the measured
building line of /Game/Hollywood/DemoMap.

Two new review shots at review-04 and review-05, both pending approval. Camera
placement cost four renders: the first two sat inside the facade body, and the
frontage faces Unreal -Y, so a camera has to stand at Y < -1450 cm, not north of
the building line in project terms.
2026-11-07 11:42:29 +01:00
zwitschi 101db3f2a4 chore: prepare repository for Gitea publication 2026-11-07 11:08:04 +01:00
zwitschi 1c6b956435 bootstrap: finish project bootstrap — scale test, MPFB character, build docs
- Static-mesh scale test passes: 1 m = 100.00 cm, origin preserved, axis
  mapping east→+X / north→−Y / up→+Z (named presets ue_metric_v1 both sides).
- MPFB 2.0.18 acceptance passes: generated human + standard 137-bone rig
  with weights + v2 skin baked to 1024² diffuse; imports as SkeletalMesh
  with Skeleton, Material, and Texture2D in /Game/Test/Character.
- MPFB headless requires XDG_CONFIG_HOME at the Flatpak config tree;
  documented in Art/blender/README.md, probe in Tools/addon_compat_probe.py.
- Docs/build.md completed: fresh-checkout build, packaging, headless checks.
- Docs/setup.md reconciled with reality; ROADMAP/bootstrap closed out.
2026-11-07 09:29:11 +01:00
zwitschi 327c906a7c chore: install blender-skills 1.3.0 for modelling work
94 MIT-licensed skill packs plus their shared references, copied unedited from
the local checkout of github.com/arjun988/blender-skills at commit 8f778d2
(1.2 MB, 269 Markdown files). They are agent instructions, not add-ons; the
pipeline still drives Blender headlessly through bpy, so the optional Blender MCP
server is not required. Provenance recorded in THIRD_PARTY_NOTICES.md and the
version, source, and update procedure in Docs/toolchain.md.
2026-11-06 23:49:19 +01:00
zwitschi cc209cde01 feat: assemble the site and pre-check walkability
site-2026-11-06.blend gathers terrain, street and blockout into categorised
collections: 281 objects, ~58k triangles. A downward raycast on a 1 m grid along
both sidewalks and the designated crossing finds no holes, so nothing drops the
player on either side. Two steps are flagged: 15.8 cm at the sidewalk's outer
terrain seam and 10-13 cm ramps at the crossing flush transitions, steeper than
a real 8% ramp.

Sampling is projected through PROJ: placing points with the OSM spherical
approximation put them metres off the UTM meshes and reported 91 phantom holes.
2026-11-06 23:48:02 +01:00
zwitschi d42e39faa9 feat: road, kerbs, and sidewalks on the DEM
Tools/street_mesh.py builds the corridor in the project frame from the measured
profile: carriageway -10.5..+10.5 m, kerbs 0.15 m (0.13 m as built), sidewalks
4.0 m south and 5.3 m north, all elevations sampled from the DEM. Kerbs flush at
the three signalised crossings. Crown at mid-block reproduces the cross-sections
to the centimetre.
2026-11-06 23:45:28 +01:00
zwitschi ece48649f5 feat: terrain mesh from the DEM, and correct the blockout frame
Tools/terrain_from_dem.py builds a 191x106 grid at 2 m over the block, Z being
NAVD88 minus the origin ground. Building footprints from the OSM extract are cut
out so rooftops never become terrain (72 footprints, 6310 cells). Spot checks
reproduce the measured profile: origin 0.00, Orange Dr +3.39, south gutter
+0.93, crown +1.38, north sidewalk +1.43.

Blosm projects with a sphere, not an ellipsoid, so its frame was ~0.4% oversized
and rotated by meridian convergence: about 2 m at the theatre. The blockout now
fits a six-parameter affine into EPSG:26911 on five control points (0.0 cm
residual) and transforms root objects only, since children inherit and double
transforming them read as a 2 m placement error.
2026-11-06 23:40:50 +01:00
zwitschi bf23a27939 feat: headless Blosm blockout of the demo block
Tools/blosm_blockout.py imports the committed extract in 3Dsimple mode and
reports per-building dimensions so a units failure is visible in the log. The
result (87 meshes) is committed as the blockout baseline: hero 25.68 x 54.88 x
14.50 m against 25.75 x 54.88 m of OSM geometry, so the import is 1:1 metres.

Two headless defects needed working around: the asset-directory check that
fires before mode is honoured, and an empty default-level table sending the
add-on down a broken relative import. Member fetching stays off so the import
does not depend on Overpass.
2026-11-06 23:33:41 +01:00