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.
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.
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.
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.
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.
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'.
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.
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.
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.
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.
- 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
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).
- 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).
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).
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.