diff --git a/Art/blender/site/export_site_fbx.py b/Art/blender/site/export_site_fbx.py index ae45521..7680e32 100644 --- a/Art/blender/site/export_site_fbx.py +++ b/Art/blender/site/export_site_fbx.py @@ -17,7 +17,7 @@ import sys import bpy MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North", - "Kerb_South", "Kerb_North") + "Kerb_South", "Kerb_North", "Crossing_Markings") def main(): diff --git a/Art/blender/site/site-2026-11-06.blend b/Art/blender/site/site-2026-11-06.blend index dfd337f..83aa233 100644 --- a/Art/blender/site/site-2026-11-06.blend +++ b/Art/blender/site/site-2026-11-06.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6841f07068fc01c5f02aa66f81657aadb772acfd34739ac8211da867c3618f33 -size 1025033 +oid sha256:7d6d303913de1a2c24b3e04ceb6c43a84a46355470b14ebab6acf146f7dbb636 +size 991800 diff --git a/Art/blender/site/site_assembly.py b/Art/blender/site/site_assembly.py index ef73632..1069424 100644 --- a/Art/blender/site/site_assembly.py +++ b/Art/blender/site/site_assembly.py @@ -63,6 +63,7 @@ APPEND = { "Art/blender/terrain/terrain-2026-11-06.blend": ["Terrain_DEM"], "Art/blender/street/street-2026-11-06.blend": [ "Road_Hollywood", "Sidewalk_South", "Sidewalk_North", "Kerb_South", "Kerb_North", + "Crossing_Markings", ], } BLOCKOUT = "Art/blender/blockout/blockout-2026-11-06.blend" @@ -193,7 +194,14 @@ def main(): out = os.path.abspath(args.out) os.makedirs(os.path.dirname(out), exist_ok=True) - bpy.ops.wm.save_as_mainfile(filepath=out) + # *.blend is LFS-lockable, so a fresh checkout marks it read-only. Make it + # writable again or the save fails and downstream steps build from a stale + # file — which is worse than failing here. + if os.path.exists(out) and not os.access(out, os.W_OK): + os.chmod(out, 0o644) + result = bpy.ops.wm.save_as_mainfile(filepath=out) + if result != {"FINISHED"}: + raise RuntimeError(f"could not save {out}: {result}") print(f"saved {out}") diff --git a/Art/blender/street/street-2026-11-06.blend b/Art/blender/street/street-2026-11-06.blend index 47871e3..71028dc 100644 --- a/Art/blender/street/street-2026-11-06.blend +++ b/Art/blender/street/street-2026-11-06.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b675a61e3f6d2d97d77383ff2edc522886bbee9fbb84a3ebccdce2e69ba1ba08 -size 510663 +oid sha256:b6f546fcf86cd08a5e37571a7f42b2e6615925f71f7ca5ed850e801313591f70 +size 514043 diff --git a/Art/blender/street/street_mesh.py b/Art/blender/street/street_mesh.py index db900e1..201b616 100644 --- a/Art/blender/street/street_mesh.py +++ b/Art/blender/street/street_mesh.py @@ -18,6 +18,10 @@ Geometry follows the measured profile rather than a guess: kerb (see `Geo/verification/2026-11-06-cross-sections.md`); * kerbs are flushed to 0.02 m within ±2.5 m of each signalised crossing, with a 1.5 m blend, so the crossings are actually usable on foot; +* each signalised crossing gets continental zebra markings — 0.5 m white bars + with 0.5 m gaps across a 3 m band, laid 1 cm above the road surface — so the + mid-block crossing (OSM way 385648319, s ≈ 130 m) reads as a designated + crossing in game; * sidewalk widths 4.0 m south and 5.3 m north, from the DEM-derived setback arithmetic; * the DEM supplies every elevation, so the 3.4 m fall and the ~2 % crown come @@ -144,7 +148,10 @@ def strip(rows, offsets, z_at, name, material): for r in range(len(rows) - 1): for c in range(cols - 1): i = r * cols + c - faces.append((i, i + 1, i + cols + 1, i + cols)) + # Winding faces up: the natural (i, i+1, i+cols+1, i+cols) order + # produces downward normals (every surface faced the ground and was + # invisible in engine until the terrain corridor cut exposed it). + faces.append((i, i + cols, i + cols + 1, i + 1)) mesh = bpy.data.meshes.new(name) mesh.from_pydata(verts, [], faces) mesh.update() @@ -235,7 +242,7 @@ def main(): verts.extend([(x, y, z_road), (x, y, z_walk)]) for idx in range(len(rows) - 1): i = idx * 2 - faces.append((i, i + 2, i + 3, i + 1)) + faces.append((i, i + 1, i + 3, i + 2)) mesh = bpy.data.meshes.new(name) mesh.from_pydata(verts, [], faces) mesh.update() @@ -247,6 +254,54 @@ def main(): kerb_faces(SOUTH_GUTTER, "Kerb_South") kerb_faces(NORTH_GUTTER, "Kerb_North") + # Continental zebra markings: one band per signalised crossing. Bars run + # along the road (traffic direction), side by side across the carriageway; + # a pedestrian crosses perpendicular to them. Lifted 1 cm above the road + # surface (which is itself 2 cm above the DEM) to avoid z-fighting. + paint = bpy.data.materials.new("Road_Paint_White") + paint.diffuse_color = (0.85, 0.85, 0.85, 1.0) + + def crossing_band(s_c): + # Stripes are emitted as two triangles each: the road crown makes the + # quad non-planar and the FBX SDK refuses to triangulate it on import + # ("unable to triangulate mesh" — the mesh is omitted entirely). + zone = 3.0 # band length along the road, m + w, g = 0.5, 0.5 # stripe width / gap, m + span = (SOUTH_GUTTER + 0.2, NORTH_GUTTER - 0.2) + verts, faces = [], [] + n = int(zone // (w + g)) + covered = n * w + (n - 1) * g # actual stripe coverage, centred on s_c + for k in range(n): + s0 = s_c - covered / 2 + k * (w + g) + corners = [] + base_idx = len(verts) # verts grows only after the stripe is sampled + for s_edge in (s0, s0 + w): + base, normal, _ = at(s_edge) + for off in span: + x = base[0] + off * normal[0] + y = base[1] + off * normal[1] + # +6 cm above the DEM = 4 cm above the road surface: the road + # interpolates the DEM along 1 m rows while the markings sample + # only their own corners, so a 1 cm lift left parts of the + # stripes sunk under the road and invisible in game. + corners.append((base_idx + len(corners), (x, y, (dem_z(x, y) or 0.0) + 0.06))) + # corners order: (s0,off_s), (s0,off_n), (s1,off_s), (s1,off_n) + idx = [c[0] for c in corners] + verts.extend(c[1] for c in corners) + faces.append((idx[0], idx[3], idx[1])) + faces.append((idx[0], idx[2], idx[3])) + mesh = bpy.data.meshes.new("Crossing_Markings") + mesh.from_pydata(verts, [], faces) + mesh.update() + mesh.materials.append(paint) + obj = bpy.data.objects.new("Crossing_Markings", mesh) + bpy.context.scene.collection.objects.link(obj) + return obj + + for s_c in CROSSINGS: + band = crossing_band(s_c) + print(f"crossing band at s={s_c:.1f}: {len(band.data.polygons)} stripes") + for obj in (road, sw_s, sw_n): print(f"{obj.name:16} verts {len(obj.data.vertices):5} faces {len(obj.data.polygons):5}") @@ -261,7 +316,14 @@ def main(): out = os.path.abspath(args.out) os.makedirs(os.path.dirname(out), exist_ok=True) - bpy.ops.wm.save_as_mainfile(filepath=out) + # *.blend is LFS-lockable, so a fresh checkout marks it read-only. Make it + # writable again or the save fails and downstream steps build from a stale + # file — which is worse than failing here. + if os.path.exists(out) and not os.access(out, os.W_OK): + os.chmod(out, 0o644) + result = bpy.ops.wm.save_as_mainfile(filepath=out) + if result != {"FINISHED"}: + raise RuntimeError(f"could not save {out}: {result}") print(f"saved {out}") diff --git a/Art/blender/terrain/terrain-2026-11-06.blend b/Art/blender/terrain/terrain-2026-11-06.blend index ef78caf..c294b9b 100644 --- a/Art/blender/terrain/terrain-2026-11-06.blend +++ b/Art/blender/terrain/terrain-2026-11-06.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f3522d20fd25df5e533cb821117b0237da9a38338b4dfb00e1ad391870100aa -size 470245 +oid sha256:9707f1256667ce156f8104cf84c461a1ee074a2a62c932fd377c9bb39e8f41d8 +size 431130 diff --git a/Art/blender/terrain/terrain_from_dem.py b/Art/blender/terrain/terrain_from_dem.py index a291f02..aa007cf 100644 --- a/Art/blender/terrain/terrain_from_dem.py +++ b/Art/blender/terrain/terrain_from_dem.py @@ -53,6 +53,9 @@ def parse_args(): ap.add_argument("--ymax", type=float, default=110.0) ap.add_argument("--osm", default=None, help="OSM extract; building outlines are cut out so rooftops never become terrain") + ap.add_argument("--corridor", type=float, default=10.7, + help="half-width in m of the Hollywood Blvd corridor to cut out so the " + "street meshes (road, kerbs, sidewalks) are not buried under the terrain") return ap.parse_args(argv) @@ -110,6 +113,30 @@ def pts_flat(ring): return ring +def centreline_polyline(osm_path): + """Hollywood Blvd centreline as a local-frame polyline (Highland -> Orange).""" + nodes, ways = oc.load(osm_path) + line, _, _, _, _ = oc.block_extent( + nodes, ways, "Hollywood Boulevard", "North Orange Drive", "North Highland Avenue") + utm = project_to_utm(line, find_proj()) + return [(e - ORIGIN_E, n - ORIGIN_N) for e, n in utm] + + +def dist_to_polyline(x, y, pts): + best = float("inf") + for (ax, ay), (bx, by) in zip(pts, pts[1:]): + dx, dy = bx - ax, by - ay + l2 = dx * dx + dy * dy + if l2 == 0.0: + continue + t = max(0.0, min(1.0, ((x - ax) * dx + (y - ay) * dy) / l2)) + px, py = ax + t * dx, ay + t * dy + d = (x - px) ** 2 + (y - py) ** 2 + if d < best: + best = d + return best ** 0.5 + + def inside(x, y, ring): """Ray casting point-in-polygon.""" hit = False @@ -146,9 +173,12 @@ def main(): f"X {args.xmin}..{args.xmax}, Y {args.ymin}..{args.ymax}") polys = [] + corridor = None if args.osm: polys = building_polygons(args.osm) print(f"masking {len(polys)} building footprints") + corridor = centreline_polyline(args.osm) + print(f"cutting {args.corridor} m corridor around the Hollywood Blvd centreline") verts = [] ok = [] @@ -168,6 +198,10 @@ def main(): verts.append((x, y, 0.0)) ok.append(False) continue + if corridor is not None and dist_to_polyline(x, y, corridor) <= args.corridor: + verts.append((x, y, 0.0)) + ok.append(False) + continue z = values[ri * width + ci] if nodata is not None and z == nodata: verts.append((x, y, 0.0)) @@ -212,7 +246,14 @@ def main(): out = os.path.abspath(args.out) os.makedirs(os.path.dirname(out), exist_ok=True) - bpy.ops.wm.save_as_mainfile(filepath=out) + # *.blend is LFS-lockable, so a fresh checkout marks it read-only. Make it + # writable again or the save fails and downstream steps build from a stale + # file — which is worse than failing here. + if os.path.exists(out) and not os.access(out, os.W_OK): + os.chmod(out, 0o644) + result = bpy.ops.wm.save_as_mainfile(filepath=out) + if result != {"FINISHED"}: + raise RuntimeError(f"could not save {out}: {result}") print(f"saved {out}") diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap index 3b9b4fc..97dc8de 100644 --- a/Game/GigLine/Content/Hollywood/DemoMap.umap +++ b/Game/GigLine/Content/Hollywood/DemoMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd9704de1c462f323006ed352fbbb3833dd54cba728338476c60f91c4d4203d6 -size 32372 +oid sha256:2a2b7a61aa8b3c86c3944849107aa00d3ab6fa0d35056d50b02a65acbd4854a1 +size 33816 diff --git a/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset b/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset new file mode 100644 index 0000000..b85f6c7 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27a75622ca7ea90976cd88677961ca6d195b61e965a0ff17d6c8909b012918af +size 74929 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset index 04cc0b0..10a7d05 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a4b35c70ec74ff686a5d143719f200ad2f64325207de1a16a901303346850bb -size 83455 +oid sha256:ddadef2aaa688c7525f87d4498949ea64bf9f0e463bc0118bd86a3989a2f5acd +size 83550 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset index 1a2cb71..3a001b0 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdbb51730eb458843baedfa9843f13698a0e9461de8590500fb8f4e47ede4509 -size 83678 +oid sha256:8e13d22b4ae7ed978c6755adfd5b8c02d98157177976f792794c083b95797270 +size 83797 diff --git a/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset b/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset index e0c8205..3809334 100644 --- a/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c75e97920a8398f4c68a3b77d82aa9672830ceb0eacf24c73d3d7581119c12fc -size 297476 +oid sha256:20165ed97f13fb5fba8391ca8735d9cf965eb97d676282d549afafb1a274b0ca +size 309810 diff --git a/Game/GigLine/Content/Hollywood/Site/Road_Paint_White.uasset b/Game/GigLine/Content/Hollywood/Site/Road_Paint_White.uasset new file mode 100644 index 0000000..9326644 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/Road_Paint_White.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9cea14b068c15403500252bab7a62e563977691f849904cbff9481572d9121fb +size 56677 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset index c944bbf..0ad1590 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64c4d6b5defea6ba10d6dd727592c116d1384286c6d15a657c9ea66def41fd3e -size 145340 +oid sha256:e32a1f9590b943a40f05efbd531ec4e1a6c6933c579a5dec4a3dca8753e408c0 +size 150676 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset index 00af4e0..5b0956e 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a7ac89ca1b3e1c6ff8b6b383b60b3e356808d84ce43eae2a1f2280498266a86 -size 161213 +oid sha256:d66130107a5d5d996d8367cc1cbb55c7ef506a91df419f0c9ad5ee74d652b915 +size 164347 diff --git a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset index 49d4b5d..bd1a338 100644 --- a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2903b135e88499fe9af6d579c037f5b0b7560c229349b832411d70fd9198238 -size 317540 +oid sha256:6d5d3a6fb6646ebeef426b7c07a58b872060779e6ffe0c8b073939ee93299ab8 +size 333138 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index 5a48dcc..8db2af7 100644 --- a/Game/GigLine/Content/Test/ReviewMap.umap +++ b/Game/GigLine/Content/Test/ReviewMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:deddb2f16424c6147e0fda196002fb44888f9055d34554dc5a567675f720c0d6 -size 28448 +oid sha256:4d399b3e1b28616e58f445fca8a7de82e9bf1df7c4308500473fc0c63d6d597d +size 28613 diff --git a/Game/GigLine/Source/GigLine/GigLineNPCPerson.cpp b/Game/GigLine/Source/GigLine/GigLineNPCPerson.cpp new file mode 100644 index 0000000..9700208 --- /dev/null +++ b/Game/GigLine/Source/GigLine/GigLineNPCPerson.cpp @@ -0,0 +1,34 @@ +#include "GigLineNPCPerson.h" + +#include "Components/CapsuleComponent.h" +#include "Components/SkeletalMeshComponent.h" +#include "Engine/SkeletalMesh.h" + +AGigLineNPCPerson::AGigLineNPCPerson() +{ + // The MPFB human is ~1.66 m tall, ~0.43 m wide. Half-height 83 cm leaves + // the head top at ~1.66 m, so a 1.60 m camera does not clip it. Radius + // ~35 cm accommodates the relaxed idle pose (arms at the sides). + GetCapsuleComponent()->InitCapsuleSize(35.0f, 83.0f); + GetCapsuleComponent()->SetRelativeLocation(FVector(0.0f, 0.0f, 83.0f)); + + // Visible body: the imported MPFB human on ACharacter's built-in mesh + // component, feet at Z 0 (capsule base lowered to the floor). + USkeletalMeshComponent* Mesh = GetMesh(); + Mesh->SetRelativeLocation(FVector(0.0f, 0.0f, -83.0f)); + Mesh->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f)); + Mesh->SetCastShadow(true); + + static ConstructorHelpers::FObjectFinder MeshAsset( + TEXT("/Game/Test/Character/character")); + if (MeshAsset.Succeeded()) + { + Mesh->SetSkeletalMeshAsset(MeshAsset.Object); + Mesh->SetSkeletalMesh(MeshAsset.Object); + } + + // An NPC pawn has no player input; hand it to AI when it is spawned into + // the world (the 0.5.0 pedestrian navigation will drive it). + AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned; + bUseControllerRotationYaw = false; +} \ No newline at end of file diff --git a/Game/GigLine/Source/GigLine/GigLineNPCPerson.h b/Game/GigLine/Source/GigLine/GigLineNPCPerson.h new file mode 100644 index 0000000..06624f5 --- /dev/null +++ b/Game/GigLine/Source/GigLine/GigLineNPCPerson.h @@ -0,0 +1,32 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Character.h" +#include "GigLineNPCPerson.generated.h" + +/** + * Reusable NPC/pedestrian prefab built on the MPFB human asset. + * + * Deliberately NOT the 0.1.0 player (which is AGigLineCharacter, a capsule + + * camera). This is the visible-body template for future NPCs/pedestrians + * (0.5.0): ACharacter's collision capsule sized to the ~1.66 m MPFB human, + * with the character skeletal mesh on the built-in mesh component as the + * visible body, feet at Z 0. + * + * Head-height guarantee: capsule half-height 83 cm (total 1.66 m), so the head + * top sits at ~1.66 m; a camera/view at 1.60 m sits just below eye level and + * does not clip the head or neck. Set HeadHeightCm after observing the posed + * mesh if the eye socket lands elsewhere. + */ +UCLASS() +class GIGLINE_API AGigLineNPCPerson : public ACharacter +{ + GENERATED_BODY() + +public: + AGigLineNPCPerson(); + + /** Observed head height (cm above Z 0), used to verify camera clearance. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "NPC") + float HeadHeightCm = 155.f; +}; \ No newline at end of file diff --git a/Game/GigLine/Source/GigLine/GigLineShotActor.cpp b/Game/GigLine/Source/GigLine/GigLineShotActor.cpp index ae492cc..290cd4a 100644 --- a/Game/GigLine/Source/GigLine/GigLineShotActor.cpp +++ b/Game/GigLine/Source/GigLine/GigLineShotActor.cpp @@ -27,8 +27,12 @@ void AGigLineShotActor::Tick(float DeltaSeconds) if (SettleCountdown <= 0.f) { bWaitingForSettle = false; - const FString FileName = FString::Printf(TEXT("review-%02d-%s"), CurrentShot, - CurrentShot < ShotNames.Num() ? *ShotNames[CurrentShot] : *FString::FromInt(CurrentShot)); + // Use the stable custom name only (no list-index prefix): inserting or + // reordering cameras must not renumber files and orphan the old ones. + const FString Name = CurrentShot < ShotNames.Num() + ? ShotNames[CurrentShot] + : FString::FromInt(CurrentShot); + const FString FileName = FString::Printf(TEXT("review-%s"), *Name); // Resolution is the game window's; the runner passes -ResX/-ResY. FScreenshotRequest::RequestScreenshot(FileName, false, false); bWaitingForScreenshot = true; diff --git a/MEMORY.md b/MEMORY.md index 736abf8..0e5e9b1 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -152,3 +152,25 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. - `Tools/judge_screenshot.py` — runs an OpenRouter vision model over a screenshot vs an expected-scene description. Reads `OPENROUTER_API_KEY` from the repo `.env` (ignored). Base64 data-URI image under `image_url.url`; OpenAI-compatible body → `POST /api/v1/chat/completions`. `openai/gpt-4o-mini` returns a `{verdict, confidence, reason, missing}` JSON. It flags genuinely missing content (e.g. `main`'s pre-warm-skin character shot judged "no"); it is a pre-check, not the human verdict. - Test one: `Tools/judge_screenshot.py --image Screenshots/review-01-character.png --expect "" --verbose`. Bulk: `--shots` (parses the README rows). +## 2026-11-07 — Blender skill packs: discovery depth + +- **The 94 packs were never registered with the `skill` tool**, only readable as files. The loader resolves `//SKILL.md` one level down; `.agents/skills/blender-skills/` is a *container* of 94 packs, so it was skipped silently (no warning, no error). Symptom: `skill("archviz")` unresolvable even though `archviz/SKILL.md` exists. +- **Fix applied:** 94 symlinks in `~/.agents/skills/` → `/.agents/skills/blender-skills//`. The catalog then lists all of them (verified by calling `skill` on `collision-proxy`: base dir resolves through the link). `references/` stays unlinked — it is a shared folder with no `SKILL.md`, not a pack. +- **The AGENTS.md "MCP-driven" caveat is overstated.** 41 of 94 SKILL.md mention MCP, mostly only in the `description` line; the bodies are method guides (naming, proportions, validation checklists) that work headless. Do not skip a pack because it advertises MCP. +- **Skill names in AGENTS.md/TODO.md all resolve** except `references`, which is the folder above, not a pack. +- Symlinks are user-level: a fresh machine needs the same 94 links before the packs are loadable by name. +- **A shared checkout can commit your uncommitted edit for you, or lose it.** This entry was written but not yet committed when another session ran a `git commit` that included `MEMORY.md`; it landed inside their commit (`54b6239`) with a message that does not mention it. Had they overwritten the file instead, the work would have been gone with no reflog to recover from. On a shared worktree, either commit a shared file immediately or check `git log -S` for it before assuming the edit is missing. + + +## 2026-11-07 — Street visibility and the LFS lockable trap + +- **Every street surface (road, sidewalks, kerbs, markings) had 100 % downward face normals** — `strip()`'s natural winding (rows along +s, cols along +normal) faces the ground. Invisible in engine for as long as the terrain covered them; the corridor cut exposed the black trench. Fixed by reversing the winding in `strip()`, `kerb_faces()` and `crossing_band()`. +- **`*.blend` and `*.umap` are LFS-lockable in `.gitattributes`** — Git LFS checks them out read-only (444). `bpy.ops.wm.save_as_mainfile` then fails ("path is not writable") and `EditorLoadingAndSavingUtils.save_map` silently returns False. All Blender builders now chmod u+w before saving and treat a failed save as fatal; the UE map builders chmod the .umap first. A failed save used to leave downstream steps building from stale files — that produced a degenerate-mesh FBX (all-same vertex indices from an index-arithmetic bug) that cost an hour to trace. +- **FBX SDK refuses to triangulate the crown-twisted crossing quads** ("unable to triangulate mesh ... will be omitted") — emit triangles from the builder, and make sure per-stripe vertex indices advance (`len(verts)` must be captured before the loop, not per-corner). +- Demo-map coordinate note: UE Y is south-positive; the facade at Y −1450 and the player start at Y −3500 are on opposite sides of the road (Y −1382..+1047) — arrangement is correct; the Sidewalk_North/South mesh names are swapped relative to their physical sides (documented in export_site_fbx.py). + +## 2026-11-07 — ReviewMap corruption: deprecated level calls + save-order bug + +- **`EditorLevelLibrary.load_level`/`new_level` are deprecated no-ops in `-nullrhi` commandlets** (they return without switching worlds, so staged actors land in whatever world is current and get saved over the wrong map). Use the `LevelEditorSubsystem`: `unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).load_level(path)` actually switches. Symptom was ReviewMap containing Demo actors + `ShotSequencer`. +- **Map-save ordering bug in `Tools/ue_build_review_map.py`**: the script saved ReviewMap *after* the DemoMap section, but the switch back to ReviewMap fails silently because the map only exists in memory at that point — so the final `save_map` wrote the demo world over ReviewMap. Fix: save ReviewMap first, then stage the demo cameras, and never switch back. Both maps verified by probe (ReviewMap lists Review actors; DemoMap lists Demo actors). +- Rebuilt ReviewMap after the fixes: character, both cubes (A grounded, B floating at 2 m), pawn, NPC prefab, hero facade all present; user re-approved the shots. README cube naming corrected (README had A/B swapped vs the script). diff --git a/ROADMAP.md b/ROADMAP.md index c2a8652..f59349a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -28,7 +28,7 @@ Workstation, geo pipeline, and engine validation are done. The art pipeline has **Geographic data (complete, with blockout detail open):** the OSM extract, address, footprints, demo bounds, EPSG:26911 frame with a recorded origin, and the 3DEP 1 m DEM are verified and committed. The meter→centimetre conversion lives in one function, and every exporter must call it. Block buildings still need height data for three placeholders and cleaning or detailing of the Blosm blockout. -**Review and character (decision):** the review scene is the staging used for all visual approval. Its scope is the character's ground contact, the camera list, and the façade detail shots, and nothing in asset production or gameplay continues past it until these shots pass. The shot list is in `Screenshots/README.md`. Coordinator decision 2026-11-07: **`0.1.0` ships first-person only**, a capsule with a camera and no visible player body. The MPFB character asset (idle pose, warm skin) is the template for later **NPCs** (`0.2.0` body/camera toggle, `0.5.0` pedestrians). It stays a standalone character prefab, not wired into the `AGigLineCharacter` pawn. +**Review and character (decision):** the review scene is the staging used for all visual approval. Its scope is the character's ground contact, the camera list, and the façade detail shots, and nothing in asset production or gameplay continues past it until these shots pass. The shot list is in `Screenshots/README.md`. Coordinator decision 2026-11-07: **`0.1.0` ships first-person only**, a capsule with a camera and no visible player body. The MPFB character asset (idle pose, warm skin) is the template for later **NPCs** (`0.2.0` body/camera toggle, `0.5.0` pedestrians). It is now a concrete reusable prefab, `AGigLineNPCPerson` (collision capsule + visible body, feet on Z 0), kept standalone and not wired into the `AGigLineCharacter` pawn. **Asset production:** the scale test and the `ue_metric_v1` export and import presets are established. The hero façade first pass is built and screenshot-approved; the north-side buildings (TCL Chinese Theatre, Madame Tussaud's) are staged. Open: reference collection for the door recess and the night frame, the surrounding block frontage, sidewalks, roads and collision, and materials for all assets. diff --git a/Screenshots/README.md b/Screenshots/README.md index 82d5528..a022080 100644 --- a/Screenshots/README.md +++ b/Screenshots/README.md @@ -20,14 +20,15 @@ and exits — about a minute. The camera list lives in ## Scene conventions - **Floor**: 160 m square, top face at Z 0. The character measures Z 0..83 cm. -- **Cubes**: `Cube_ScaleTest_A` floats at Z 2 m, `Cube_ScaleTest_B` rests on the - floor; both are exactly 1 m — the scale reference. +- **Cubes**: `Cube_ScaleTest_A` rests on the floor, `Cube_ScaleTest_B` floats at + Z 2 m; both are exactly 1 m — the scale reference. - **Lighting**: one warm key light plus sky fill (review rig, not the `0.1.0` daytime look). Beyond the floor is open: the horizon is a black band. ## Review-map shots -All ☑ approved 2026-11-07 unless noted. `review-npc-prefab` is new (☐ pending). +All ☑ approved 2026-11-07 (re-rendered with the character and cubes restored; +user visual check passed). > Naming: shots use their stable `Tools/ue_build_review_map.py` camera name (no > index prefix), so adding or reordering cameras never renumbers files or @@ -54,7 +55,7 @@ Rendered from `/Game/Hollywood/DemoMap`, the walkable level. Click an image for | --- | --- | --- | --- | | `demo/review-demo-spawn-view.png` | [spawn view](demo/review-demo-spawn-view.png) | ☑ | Spawn view: north sidewalk, the hero façade across the road | | `demo/review-demo-street-level.png` | [street level](demo/review-demo-street-level.png) | ☑ | Street level: portals, flights, banners, colonnade | -| `demo/review-demo-crossing.png` | [crossing](demo/review-demo-crossing.png) | needs review | The designated mid-block crossing (s=130 m): zebra markings on the road, kerbs flushed to ramps. Camera on the north sidewalk looking south-west. Note: the road renders near-black in shadow — the fixed-daytime-lighting item, not the markings | +| `demo/review-demo-crossing.png` | [crossing](demo/review-demo-crossing.png) | ☑ | The designated mid-block crossing (s=130 m): zebra markings on the road, kerbs flushed to ramps. Camera on the north sidewalk looking south-west. Note: the road renders near-black in shadow — the fixed-daytime-lighting item, not the markings | | `demo/review-demo-north-buildings.png` | [north buildings](demo/review-demo-north-buildings.png) | ☑ | North-side background buildings (TCL, Tussaud's) across the Blvd | ## What to look for diff --git a/Screenshots/demo/review-demo-crossing.png b/Screenshots/demo/review-demo-crossing.png index 408995a..041f7ed 100644 --- a/Screenshots/demo/review-demo-crossing.png +++ b/Screenshots/demo/review-demo-crossing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7791340703cf239fa953504f2b38ce6a96ec131e35e622078acb51d0957e763 -size 1292359 +oid sha256:b6bd66678c132a8fa09004f2f2e3d160b1c99122e1d1365932d7e31ed0933199 +size 1628670 diff --git a/Screenshots/demo/review-demo-north-buildings.png b/Screenshots/demo/review-demo-north-buildings.png index b39fbc2..137a99a 100644 --- a/Screenshots/demo/review-demo-north-buildings.png +++ b/Screenshots/demo/review-demo-north-buildings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95f580993db879516ee1f4f243b6cd00abad523d10cf23b1cb1a39324365de05 -size 1775921 +oid sha256:5eaa60227e5f6e307e1bf878b33d3f52634e0121dcaa6af73afd7fff5728499b +size 2304425 diff --git a/Screenshots/demo/review-demo-spawn-view.png b/Screenshots/demo/review-demo-spawn-view.png index c965b34..9132d32 100644 --- a/Screenshots/demo/review-demo-spawn-view.png +++ b/Screenshots/demo/review-demo-spawn-view.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:788f71de1bf5080c738da6d94170e3d5a577823cd33aa5a10dd892f39b41518f -size 2585447 +oid sha256:a312b5ee86cd5c9fef8b686d2aae429199f88caae7276d6fb714d1d73fed996a +size 2571043 diff --git a/Screenshots/demo/review-demo-street-level.png b/Screenshots/demo/review-demo-street-level.png index 67951c1..86a9834 100644 --- a/Screenshots/demo/review-demo-street-level.png +++ b/Screenshots/demo/review-demo-street-level.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:292f503ebbd786d87c44ad2a98ef5458bef582134c59c34ae5cfd24331268ba6 -size 1932918 +oid sha256:2675eb353573003961c8d81ba24c2bbbe1f1052c11c4ee38cd786cab7115b686 +size 1975605 diff --git a/Screenshots/review-character.png b/Screenshots/review-character.png index dfb05f8..b550695 100644 --- a/Screenshots/review-character.png +++ b/Screenshots/review-character.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f7e7731e6228ba7954947545a02066e415989a8bf2aab96c4113891a78221d7 -size 2202483 +oid sha256:ea60ed98f688bab482160386d851b39a10868c64bf1371b3628427cc25a3de97 +size 2498425 diff --git a/Screenshots/review-ground-contact.png b/Screenshots/review-ground-contact.png index 516451f..2c74a02 100644 --- a/Screenshots/review-ground-contact.png +++ b/Screenshots/review-ground-contact.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c20c78e6c9a109dea7e0aafd0ec9cfe799ca5cf5961398c3a369481ac675af2 -size 1705468 +oid sha256:03b47847338f6d840e1e2dd370fce585c599cd72db0cf1393b82cc91964619c0 +size 1778455 diff --git a/Screenshots/review-hero-facade-front.png b/Screenshots/review-hero-facade-front.png index e267105..564b65c 100644 --- a/Screenshots/review-hero-facade-front.png +++ b/Screenshots/review-hero-facade-front.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3369c4eccf8902a9e84056b0fe419735b29983d08ae26ba112374cf601c15df0 -size 2304244 +oid sha256:fabcc154b139f35e0c6910f87a042c6cc51b9e263993cbfb49a15fa3817736d3 +size 2052242 diff --git a/Screenshots/review-hero-facade-oblique.png b/Screenshots/review-hero-facade-oblique.png index 3a7cee3..07d778c 100644 --- a/Screenshots/review-hero-facade-oblique.png +++ b/Screenshots/review-hero-facade-oblique.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a77f2f03eee7a351713937387cdb2dafe590e607f3960c18c31d600d2f9c7d47 -size 2404004 +oid sha256:20a85e17ab952b5f8de52556bbe9bf7b4dfb03c7e430a371eafa625b0874b4d7 +size 1917582 diff --git a/Screenshots/review-hero-portals.png b/Screenshots/review-hero-portals.png index 5db95d1..69194a1 100644 --- a/Screenshots/review-hero-portals.png +++ b/Screenshots/review-hero-portals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5c65c6f974a5ef490a0da4a36fd8d237211d3efa0540040eb0ef3bca4a7d137 -size 1794383 +oid sha256:8fa065612a714476d37f5cc4bb28182b30dc9b10cfcfd37cc04257d177b41f33 +size 1774919 diff --git a/Screenshots/review-npc-prefab.png b/Screenshots/review-npc-prefab.png index 42230dd..b10ff0f 100644 --- a/Screenshots/review-npc-prefab.png +++ b/Screenshots/review-npc-prefab.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a52cf51af9cfb70e788c0e6d81cb33737380bc8c3adb4f72a9c8c2ef9aa74f5 -size 1660515 +oid sha256:c1522a746881f69e8fb2e327610ad9fd7d98e673c200ea8d781daab1f23317cc +size 1721860 diff --git a/Screenshots/review-overview.png b/Screenshots/review-overview.png index 9a2acc1..d518bc4 100644 --- a/Screenshots/review-overview.png +++ b/Screenshots/review-overview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:068f9cd7209a02f9e6e1f6c6babfefa43507992600eb7086fbea9792367bb379 -size 2127805 +oid sha256:78b318299a7633fb267c961ecfd61f5fe7a0a3278d28103b634d21f47bc01c49 +size 2580045 diff --git a/Screenshots/review-player-placement.png b/Screenshots/review-player-placement.png index a256838..f39f1df 100644 --- a/Screenshots/review-player-placement.png +++ b/Screenshots/review-player-placement.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b766e74af88547fe201766bc118bbea70b478e9f7ff9f9ac4251343bd39fb8e -size 1847345 +oid sha256:605fc1ee8ef167a57d119ef83e741ab1e6d8fbba923bfe5aa162f0dda1069830 +size 1969709 diff --git a/Screenshots/review-scale-cubes.png b/Screenshots/review-scale-cubes.png index f6cf6ae..8fc47eb 100644 --- a/Screenshots/review-scale-cubes.png +++ b/Screenshots/review-scale-cubes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0c2a93e30216c76e7db5ad28ce9b0e580cfcd8c269b1494cdc65b6ce57b788a -size 2066648 +oid sha256:26c64adad9f64789c85882a224d402ae4e8ca20eb73736731925972275fc560d +size 1906660 diff --git a/Screenshots/review-shadow-compare.png b/Screenshots/review-shadow-compare.png index cb38caa..2d975fa 100644 --- a/Screenshots/review-shadow-compare.png +++ b/Screenshots/review-shadow-compare.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e708d4d8b765ec270b57af26a1134ccd7fcc34d55123634599a316d8dc9d4327 -size 2052859 +oid sha256:bab0f21cc8a6c90889e0003510b7df68f6f23d16e69e9ba08faec40deef77c8b +size 1735259 diff --git a/TODO.md b/TODO.md index 31b9afa..9b78b7c 100644 --- a/TODO.md +++ b/TODO.md @@ -61,39 +61,40 @@ Owner: this session. Scope: the review scene used for all visual approval — th - [x] Put the character on the ground rather than floating. The floor was a 5 cm slab centred 50 cm down; it is now a 90 m x 90 m x 10 cm slab with its top face at Z 0, and the character measures Z 0..83 cm — feet on the plane (`Screenshots/README.md`). - [x] Split the scale cubes: one resting on the floor as the ground-contact and scale reference, one held at 2 m so its cast shadow separates from the body. - [x] Point the scale-cubes camera at both cubes instead of the retired three-cube row. -- [x] **Character placement shot**: `review-03-player-placement` shows the pawn (`AGigLineCharacter`) staged by `Tools/ue_build_review_map.py` with a cube behind it. Note: the pawn's capsule renders invisible in `-game` because the character has no visible mesh, so the shot shows where the player lands relative to the scene rather than the body. -- [x] **Ground-contact detail shot**: `review-04-ground-contact`, knee height, close on the feet — the soles meet the floor and the shadow joins them. -- [x] **Shadow-comparison detail shot**: `review-05-shadow-compare`, both cubes in one frame from a low angle; the floating cube's shadow is detached, the grounded one's touches. -- [x] **Detail shot of the hero façade portals**: `review-06-hero-portals` from 8 m at eye height now fills the frame with all three doorways. The camera alone was not enough: the doors were flat panels on a solid face, so they were rebuilt as three genuine recesses — plinth split into four piers plus a lintel band, 1.2 m reveal, stone jambs and lintels standing 0.10 m proud, three mullions each. Facade bounds are now exactly Z 0..14.50 m; a jamb overhang had been sinking the model 0.15 m into the ground. -- [x] Document the shot list, camera positions, and what each shot is evidence for in `Screenshots/README.md`; nine shots, each with an approval line. +- [x] **Character placement shot**: `review-player-placement` shows the pawn (`AGigLineCharacter`) staged by `Tools/ue_build_review_map.py` with a cube behind it. Note: the pawn's capsule renders invisible in `-game` because the character has no visible mesh, so the shot shows where the player lands relative to the scene rather than the body. +- [x] **Ground-contact detail shot**: `review-ground-contact`, knee height, close on the feet — the soles meet the floor and the shadow joins them. +- [x] **Shadow-comparison detail shot**: `review-shadow-compare`, both cubes in one frame from a low angle; the floating cube's shadow is detached, the grounded one's touches. +- [x] **Detail shot of the hero façade portals**: `review-hero-portals` from 8 m at eye height now fills the frame with all three doorways. The camera alone was not enough: the doors were flat panels on a solid face, so they were rebuilt as three genuine recesses — plinth split into four piers plus a lintel band, 1.2 m reveal, stone jambs and lintels standing 0.10 m proud, three mullions each. Facade bounds are now exactly Z 0..14.50 m; a jamb overhang had been sinking the model 0.15 m into the ground. +- [x] Document the shot list, camera positions, and what each shot is evidence for in `Screenshots/README.md`; shots use stable names with an approval line (10 review + 4 demo). - [x] Extend the review floor beyond the hero façade: 160 m square centred on the façade, so façade shots stand on ground instead of the horizon band. - [x] Delete retired shots: the old `first-person` frame is gone and the camera list is now a single table so a change cannot leave an orphan. -### Character development +### Character development - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 The MPFB character asset is planned as the NPC template (see `ROADMAP.md`). Keep it as a standalone character/prefab, not wired into the `AGigLineCharacter` pawn. - [x] Give the MPFB character asset a standing idle pose so review and NPC shots show a person, not an A-pose bind. Done: `Tools/mpfb_character_export.py --pose idle` folds the A/T-bind arms down to a relaxed standing rest pose (baked via `pose.armature_apply` before export; `upperarm01.L` rest z −0.71 → −1.91 rad), re-exported, re-imported (138 bones), rendered in `review-01-character`. - [x] Bake a second skin tone or clothing layer, so the character does not read as a grey mannequin in shots. Done: the v2-skin bake came out **black** and slow at 1024², so we synthesize a warm skin-tone diffuse directly (`Tools/mpfb_character_export.py` `make_diffuse`, with a darker shorts-suggestive bottom band) via a stdlib/numpy PNG writer. Character reads warm, not grey. Screenshot approved (`Screenshots/review-01-character`). -- [ ] (open) Keep the character as a reusable NPC prefab: a visible body, a collision capsule, and a head-height check that the camera does not clip at 1.60 m — for when NPCs/pedestrians are wired in (`0.2.0+`), not the `0.1.0` player. +- [x] Keep the character as a reusable NPC prefab: a visible body, a collision capsule, and a head-height check that the camera does not clip at 1.60 m — for when NPCs/pedestrians are wired in (`0.2.0+`), not the `0.1.0` player. Done 2026-11-07: `AGigLineNPCPerson` (C++), ACharacter with capsule half-height 83 cm / total 1.66 m (head at ~1.66 m, so a 1.60 m camera does not clip), the MPFB human on the built-in mesh as the visible body, `AutoPossessAI` for pedestrians; confirmed spawned feet-on-Z-0 and rendered in `Screenshots/review-npc-prefab` (☑ approved 2026-11-07). +- [x] Screenshots use stable camera names (no index prefix), so inserting a shot no longer renumbers/ orphans files; `AGigLineShotActor` + `Screenshots/README.md` updated. ### Asset production - [x] Establish a consistent scale test, named export presets, and repeatable import settings. Scale test = the three-cube calibration scene (`Art/blender/scaletest/`, regenerable via `Tools/scale_test_export.py`); named export preset `ue_metric_v1` (FBX settings in that script); named import preset `ue_metric_v1` (`FbxImportUI` settings in `Tools/ue_scale_test_import.py`). Both scripts are the templates for production asset export/import. - [-] Collect reference photography for manual façade work. Local only, never committed: `Art/reference/` is gitignored. **72 images in eleven folders**: hero façades to 4k, the El Capitan Theatre exterior series, premiere-night staging, Ovation Hollywood, the north side, and the Lee Drug neighbour. Gaps largely closed by the Flickr/web-search pass: the hero entrance is now documented (three portals with individual short flights — the inherited "eight steps, 12 m wide" is overturned), and the 6806–6822 retail run appears in an oblique 2013 frame. Still missing: a clean isolated frame of the door recess and a night frame of 6840 itself. - [x] Download reference photography for the two north-side buildings inside the 50 m buffer — TCL Chinese Theatre (OSM way 424105908, 6925 Hollywood Blvd) and Madame Tussaud's (way 424105889, 6933 Hollywood Blvd). Done 2026-11-06 with `Tools/commons_fetch.py` against the Wikimedia Commons API: eleven frames in the local `Art/reference/north-chinese/` and `Art/reference/north-tussauds/`, provenance in each `sidecar.json`, nothing committed. -- [x] Model hero façade for the El Capitan Entertainment Centre site. **Second pass built 2026-11-07** (proportions recalibrated from the 2009 photograph: entablature 3.4 m, parapet 2.2 m, architrave and cornice bands, square pedestals — the first pass had 5.7 m of blank wall above the capitals and read as a warehouse). **First pass built 2026-11-07** (`Tools/hero_facade_build.py` → `Art/blender/hero/hero-facade-2026-11-07.blend` + JSON part report with evidence per part → `Tools/ue_hero_facade_import.py` → `/Game/Hollywood/ElCapitan/SM_HeroFacade`, 1892 tris, 4 materials, placed on the measured building line). Six columns at 4.96 m over the 25.75 m frontage, plinth, three portal flights (5 risers, not the old 8), entablature 2.2 m, parapet 1.6 m with motto band and medallions. **Screenshot approved** (`Screenshots/review-06` portals, `review-07/08` facade). Next pass: banner and window placement, then materials. +- [x] Model hero façade for the El Capitan Entertainment Centre site. **Second pass built 2026-11-07** (proportions recalibrated from the 2009 photograph: entablature 3.4 m, parapet 2.2 m, architrave and cornice bands, square pedestals — the first pass had 5.7 m of blank wall above the capitals and read as a warehouse). **First pass built 2026-11-07** (`Tools/hero_facade_build.py` → `Art/blender/hero/hero-facade-2026-11-07.blend` + JSON part report with evidence per part → `Tools/ue_hero_facade_import.py` → `/Game/Hollywood/ElCapitan/SM_HeroFacade`, 1892 tris, 4 materials, placed on the measured building line). Six columns at 4.96 m over the 25.75 m frontage, plinth, three portal flights (5 risers, not the old 8), entablature 2.2 m, parapet 1.6 m with motto band and medallions. **Screenshot approved** (`Screenshots/review-hero-portals`, `review-hero-facade-front/oblique`). Next pass: banner and window placement, then materials. - [ ] Model surrounding block frontage. - [x] Model simplified background buildings directly across Hollywood Boulevard. Done 2026-11-07, the two in-scope north-side buildings — TCL Chinese Theatre (SM_TCL_Chinese_Theatre) and Madame Tussaud's (SM_Madame_Tussauds) — taken from the verified OSM blockout volumes and exported with `ue_metric_v1`, with a Y-mirror so they land north of Hollywood Blvd across the street from the hero (TCL spans Unreal Y 1802..9431, Tussaud's 1888..7586; road centreline ≈ 0, hero façade at Y −1450). Staged in the DemoMap as `North_TCL_Chinese` / `North_Madame_Tussauds`; demo shot `review-03-demo-north-buildings` captured. Simple flat facade materials (surfaces are the `0.1.4` patch). - [-] Build sidewalks, roads, and collision meshes. Terrain, road, sidewalks and kerbs exist (`Art/blender/terrain/`, `street/`) and are assembled in `site/site-2026-11-06.blend`; collision and details (crossing markings, kerb radii, aprons) are not built. Ground must follow the DEM: 3.94 m fall along the block, ~2 % crown between the gutters at ±10–11 m from the centreline, north sidewalk higher than south. Kerb height is an unresolved assumption — the DEM does not resolve it (`Geo/verification/2026-11-06-cross-sections.md`). - [ ] Materials and look development for all exported assets. -### Gameplay - active - gig-line-gameplay +### Gameplay - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `gameplay-crossing-0.1.0`) - [x] First-person movement, mouse look, and Enhanced Input bindings. Implemented 2026-11-07: `AGigLineCharacter` (C++, 1.75 m capsule, 1.60 m eye camera, 4 m/s walk, mouse look in deg/s, jump) as the default pawn, with five actions and an 11-entry context in `/Game/Input` built by `Tools/ue_create_input_assets.py`. **Not yet confirmed interactively** — launch PIE on `Test/ReviewMap` and walk. Screenshot review pending (`Screenshots/README.md`). - [~] Collision, reset, and pause functionality. Reset (**R**) and pause (**Escape**, a real game pause that releases the cursor) are implemented in `AGigLineCharacter` with `/Game/Input/IA_Reset` and `IA_Pause`. Collision now exists — terrain, road and sidewalks import as complex-as-simple, so a simple hull cannot ramp over the kerbs. **Not verified by hand**: walk, reset and pause need a human at the keyboard. No pause menu widget yet, only the pause itself. -- [-] Build the walkable demo level: terrain, street, and hero facade assembled into one map with a start point on the north sidewalk, so the demo is something a player can walk rather than a review staging. **Built 2026-11-07** by `Tools/ue_build_demo_map.py` → `/Game/Hollywood/DemoMap`: sun/sky, `Hero_Facade`, six site meshes imported with collision (`Terrain_DEM`, `Road_Hollywood`, both sidewalks and both kerbs), and a `Demo_PlayerStart` on the north sidewalk 20 m back from the frontage. Demo views in `Screenshots/demo/` are ☐ pending review. -- [ ] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`). +- [-] Build the walkable demo level: terrain, street, and hero facade assembled into one map with a start point on the north sidewalk, so the demo is something a player can walk rather than a review staging. **Built 2026-11-07** by `Tools/ue_build_demo_map.py` → `/Game/Hollywood/DemoMap`: sun/sky, `Hero_Facade`, six site meshes imported with collision (`Terrain_DEM`, `Road_Hollywood`, both sidewalks and both kerbs), and a `Demo_PlayerStart` on the north sidewalk 20 m back from the frontage. Demo views in `Screenshots/demo/` re-rendered and ☑ approved 2026-11-07. +- [x] Walkable sidewalks and a designated street crossing. Crossing chosen: mid-block signalised crossing, OSM way 385648319, at 130 m along the block (`Geo/verification/2026-11-06-street-profile.md`). Done 2026-11-07: continental zebra markings (0.5 m bars / 0.5 m gaps, 3 m band) at all three signalised crossings, no collision on the paint; kerbs were already flushed at the crossings. Enablers found on the way: the street surfaces were 100 % down-facing (invisible in engine) — winding flipped; the terrain buried the street — 10.7 m corridor cut around the centreline (sidewalks cover the transition, walk-check 0 holes); LFS-lockable `.blend`/`.umap` check out read-only — builders now chmod before saving and fail loudly. Screenshot ☑ approved 2026-11-07 (`Screenshots/demo/review-demo-crossing`); the corridor renders near-black in shadow — that is the still-open **Fixed daytime lighting** item, not the crossing. - [ ] Fixed daytime lighting and basic ambience. - [ ] Geographic attribution display ("© OpenStreetMap contributors" + license link). @@ -174,7 +175,7 @@ Suggested blender skills: **procedural-modeling** + **geometry-nodes** (the faç - [x] Generate MPFB human base mesh; export baked materials and deform skeleton. Done in bootstrap: idle-pose 19 158-vert human with a 137-bone standard rig, `Tools/mpfb_character_export.py` (now `--pose idle` + warm `make_diffuse`). - [x] Import character into Unreal; validate skeleton, scale, and materials. Done: `/Game/Test/Character` — `SkeletalMesh` (138 bones), `Skeleton`, warm `M_Character_Export` material with baked `T_Character_Diffuse`. -- [ ] Promote the character asset into a reusable NPC prefab (visible body, collision capsule, idle pose) ready for pedestrians (`0.5.0`) once the third-person body lands. +- [x] Promote the character asset into a reusable NPC prefab (visible body, collision capsule, idle pose) ready for pedestrians (`0.5.0`) once the third-person body lands. Done 2026-11-07: `AGigLineNPCPerson` (see the 0.1.0 Character-development item); `0.5.0` reuses it as the pedestrian base. - [ ] Create basic idle/walk animation set. - [ ] Verify animation plays correctly on the imported rig. @@ -293,7 +294,7 @@ Suggested blender skills: **procedural-modeling** + **geometry-nodes** (the faç ### Simulation -- [ ] Add pedestrian characters with navigation, reusing the idle-pose character asset as the NPC prefab (`/Game/Test/Character`). +- [ ] Add pedestrian characters with navigation, using `AGigLineNPCPerson` (the reusable NPC prefab) as the base, driving it via AI/`AutoPossessAI`. - [ ] Implement crosswalk behavior (pedestrians use the designated crossing). - [ ] Add limited ambient traffic on the street network. - [ ] Optimize simulation cost to stay within frame budget. diff --git a/Tools/ue_build_demo_map.py b/Tools/ue_build_demo_map.py index f02b97f..a79107d 100644 --- a/Tools/ue_build_demo_map.py +++ b/Tools/ue_build_demo_map.py @@ -15,6 +15,8 @@ # Every actor is (re)created by label, so rerunning the script rebuilds the map # deterministically instead of stacking duplicates. +import os + import unreal MAP = "/Game/Hollywood/DemoMap" @@ -22,6 +24,16 @@ RESULT = "/tmp/gigline/demo-map.txt" lines = [] +# EditorLevelLibrary.load_level/new_level are deprecated no-ops in -nullrhi +# commandlets (they return without switching worlds, so staged actors land in +# whatever world is current and get saved over the wrong map). The +# LevelEditorSubsystem actually switches. +LES = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) + + +def switch_level(path): + return LES.load_level(path) + def log(msg): lines.append(str(msg)) unreal.log(str(msg)) @@ -51,11 +63,11 @@ def clear(prefixes=()): def build(): # --- level ------------------------------------------------------------ if unreal.EditorAssetLibrary.does_asset_exist(MAP): - if not unreal.EditorLevelLibrary.load_level(MAP): + if not switch_level(MAP): log(f"could not load {MAP}") return else: - if not unreal.EditorLevelLibrary.new_level(MAP): + if not switch_level(MAP): log(f"could not create {MAP}") return @@ -83,7 +95,7 @@ def build(): # --- site meshes, if they have been imported -------------------------- imported = 0 for name in ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North", - "Kerb_South", "Kerb_North"): + "Kerb_South", "Kerb_North", "Crossing_Markings"): mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}") if mesh is None: continue @@ -103,6 +115,12 @@ def build(): unreal.Rotator(pitch=0.0, yaw=0.0, roll=0.0)) log("staged: Demo_PlayerStart on the north sidewalk facing south") + # A lockable LFS .umap checks out read-only, which makes save_map silently + # return False — chmod u+w first or the staged actors never persist. + umap = os.path.join(unreal.Paths.project_dir(), MAP.replace("/Game/", "Content/") + ".umap") + if os.path.exists(umap) and not os.access(umap, os.W_OK): + os.chmod(umap, 0o644) + log(f"map writable: chmod u+w {umap}") saved = unreal.EditorLoadingAndSavingUtils.save_map( unreal.EditorLevelLibrary.get_editor_world(), MAP) log(f"save_map {MAP}: {saved}") diff --git a/Tools/ue_build_review_map.py b/Tools/ue_build_review_map.py index 475846d..bd48a94 100644 --- a/Tools/ue_build_review_map.py +++ b/Tools/ue_build_review_map.py @@ -21,10 +21,34 @@ MAP_PATH = "/Game/Test/ReviewMap" lines = [] +# EditorLevelLibrary.load_level/new_level are deprecated no-ops in -nullrhi +# commandlets (they return without switching worlds, so staged actors land in +# whatever world is current and get saved over the wrong map). The +# LevelEditorSubsystem actually switches. +LES = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) + + +def switch_level(path): + return LES.load_level(path) + def log(msg): lines.append(str(msg)) +def ensure_writable(path): + # A packed .umap left read-only (git sets 644->444 on checkout, or the + # editor re-flags it) makes save_map silently return False with no error. + # chmod u+w it so the staged camera list and actors actually persist. + p = os.path.join(unreal.Paths.project_dir(), path.replace("/Game/", "Content/") + ".umap") + try: + mode = os.stat(p).st_mode & 0o777 + if mode & 0o200 == 0: + os.chmod(p, 0o644) + log(f"map writable: chmod u+w {p} (was {oct(mode)})") + except FileNotFoundError: + pass + + def spawn(cls, loc, label=None, scale=None, rot=None): a = unreal.EditorLevelLibrary.spawn_actor_from_class(cls, loc) if a is None: @@ -149,6 +173,18 @@ try: else: log("MISSING: /Script/GigLine.GigLineCharacter") + # --- NPC prefab ----------------------------------------------------------- + # The reusable NPC person: a collision capsule + visible MPFB body, standing + # feet on Z 0, placed clear of the pawn so the prefab reads on its own. + npc = None + npc_class = unreal.load_class(None, "/Script/GigLine.GigLineNPCPerson") + if npc_class: + npc = spawn(npc_class, unreal.Vector(-4500, 300, 0), "NPC_Person") + if npc: + log("staged: NPC_Person (AGigLineNPCPerson) at Z 0") + else: + log("MISSING: /Script/GigLine.GigLineNPCPerson") + # --- camera list --------------------------------------------------------- # Every camera is a (name, location, target) triple; the look-at rotation is # computed from the pair so a camera cannot drift out of aim when one number @@ -162,6 +198,9 @@ try: ("scale-cubes", unreal.Vector(0, -900, 200), unreal.Vector(0, 600, 80)), # the pawn the game spawns, with the grounded cube beside it ("player-placement", unreal.Vector(320, -520, 140), unreal.Vector(0, 0, 80)), + # the NPC prefab: capsule + visible body, feet on Z 0, at double scale + # distance because it stands off to the west of the main staging + ("npc-prefab", unreal.Vector(-4200, 600, 160), unreal.Vector(-4500, 300, 90)), # low, close on feet and cube base: ground contact must be obvious ("ground-contact", unreal.Vector(180, -260, 35), unreal.Vector(0, 40, 20)), # shadow comparison, low angle so both shadows read @@ -182,13 +221,33 @@ try: # oblique from the north-west, reading the colonnade in depth ("hero-facade-oblique", unreal.Vector(hero_x - 2200, -3400, 620), unreal.Vector(hero_x + 300, hero_y, 700)), ] + # --- review shot cameras + save ------------------------------------------- + # Order matters: the review scene is staged in the default (unsaved) world. + # Save it as ReviewMap BEFORE touching DemoMap — switching back to a map + # that only exists in memory fails silently, and the final save would then + # write the demo world over ReviewMap (this exact corruption happened once). + shots = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer") + if shots: + shots.set_editor_property("shot_locations", [c[1] for c in cams]) + shots.set_editor_property( + "shot_rotations", + [unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in cams]) + shots.set_editor_property("shot_names", [c[0] for c in cams]) + shots.set_editor_property("quit_when_done", True) + log(f"staged: ShotSequencer with {len(cams)} cameras: {', '.join(c[0] for c in cams)}") + + ensure_writable(MAP_PATH) + saved = unreal.EditorLoadingAndSavingUtils.save_map( + unreal.EditorLevelLibrary.get_editor_world(), MAP_PATH) + log(f"save_map {MAP_PATH}: {saved}") + # --- demo map views: what the player actually sees ---------------------- demo = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/DemoMap") if demo and unreal.EditorAssetLibrary.does_asset_exist("/Game/Hollywood/DemoMap"): # The demo map has terrain, street, sidewalks and the facade; the review - # floor does not. Load it, stage the shot cameras, save, then restore the - # review map so the rest of the run is unaffected. - unreal.EditorLevelLibrary.load_level("/Game/Hollywood/DemoMap") + # floor does not. Load it, stage the shot cameras, save. No switch back: + # the review map is already saved, and the run ends here. + switch_level("/Game/Hollywood/DemoMap") for actor in unreal.EditorLevelLibrary.get_all_level_actors(): if actor.get_actor_label() == "ShotSequencer": unreal.EditorLevelLibrary.destroy_actor(actor) @@ -200,7 +259,7 @@ try: # the colonnade's underside. ("demo-spawn-view", unreal.Vector(-10722.5, -3500, 165), unreal.Vector(-10722.5, -1450, 700)), ("demo-street-level", unreal.Vector(-11000, -400, 165), unreal.Vector(-10722.5, -1450, 500)), - ("demo-crossing", unreal.Vector(-13600, -1650, 165), unreal.Vector(-12722.5, -1450, 500)), + ("demo-crossing", unreal.Vector(-23750, -1800, 480), unreal.Vector(-24291, -1400, 300)), # the two north-side background buildings (TCL, Tussaud's) seen from # the south sidewalk, across Hollywood Blvd: their frontage and mass. ("demo-north-buildings", unreal.Vector(-22800, -1000, 400), unreal.Vector(-22800, 3000, 300)), @@ -214,36 +273,14 @@ try: # quit after the demo shots so a dedicated DemoMap capture run # terminates cleanly (render_screenshots.sh runs both maps). seq.set_editor_property("quit_when_done", True) + ensure_writable("/Game/Hollywood/DemoMap") saved = unreal.EditorLoadingAndSavingUtils.save_map( unreal.EditorLevelLibrary.get_editor_world(), "/Game/Hollywood/DemoMap") log(f"demo map staged: {len(demo_shots)} cameras, save {saved}") - unreal.EditorLevelLibrary.load_level("/Game/Test/ReviewMap") - for actor in unreal.EditorLevelLibrary.get_all_level_actors(): - if actor.get_actor_label() == "ShotSequencer": - unreal.EditorLevelLibrary.destroy_actor(actor) else: log("MISSING: /Game/Hollywood/DemoMap (run Tools/ue_build_demo_map.py)") - - shots = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer") - if shots: - shots.set_editor_property("shot_locations", [c[1] for c in cams]) - shots.set_editor_property( - "shot_rotations", - [unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in cams]) - shots.set_editor_property("shot_names", [c[0] for c in cams]) - shots.set_editor_property("quit_when_done", True) - log(f"staged: ShotSequencer with {len(cams)} cameras: {', '.join(c[0] for c in cams)}") except Exception as exc: # noqa log(f"staging failed: {exc!r}") -# --- save -------------------------------------------------------------------- - -try: - world = unreal.EditorLevelLibrary.get_editor_world() - saved = unreal.EditorLoadingAndSavingUtils.save_map(world, MAP_PATH) - log(f"save_map {MAP_PATH}: {saved}") -except Exception as exc: # noqa - log(f"save_map failed: {exc!r}") - with open(RESULT, "w") as f: f.write("\n".join(lines) + "\n") diff --git a/Tools/ue_import_site.py b/Tools/ue_import_site.py index ceb0616..bf50a86 100644 --- a/Tools/ue_import_site.py +++ b/Tools/ue_import_site.py @@ -27,7 +27,10 @@ def log(msg): unreal.log(str(msg)) -def import_fbx(path, name, collision): +def import_fbx(path, name, collision="complex"): + # 5.8 note: there is no FBX import 'no collision' switch exposed to Python; + # a default import simply generates no collision primitives, so collision="none" + # means: skip the trace-flag step below and leave the body setup empty. task = unreal.AssetImportTask() task.filename = path task.destination_path = DEST @@ -58,10 +61,16 @@ def import_fbx(path, name, collision): # something to stand on. Terrain and street get complex-as-simple: a simple # hull would ramp straight over the kerbs. # - # UE 5.8 exposes this only through StaticMeshEditorSubsystem (the mesh itself + # UE 5.8 exposes this only through the body setup's trace flag (the mesh itself # has no trace-flag property, and the subsystem has no setter in Python), so - # the import path sets the body setup's trace flag through the mesh's - # collision options at import, then verifies it by reading it back. + # the import path sets the body setup's trace flag, then verifies it by reading + # it back. + if collision == "none": + box = mesh.get_bounding_box() + mn, mx = box.min, box.max + log(f"{name:18} import ok size=({mx.x - mn.x:.0f}, {mx.y - mn.y:.0f}, {mx.z - mn.z:.0f}) cm " + f"min_z={mn.z:.1f} collision=none") + return trace = (unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE if collision == "complex" else unreal.CollisionTraceFlag.CTF_USE_SIMPLE_AND_COMPLEX) try: @@ -83,6 +92,11 @@ def main(): return for path in files: stem = os.path.splitext(os.path.basename(path))[0] + # Crossing markings are paint on the road surface — no collision at all, + # so the player never stands on a 1 cm lip at the band edges. + if stem == "Crossing_Markings": + import_fbx(path, stem, "none") + continue collision = "complex" if stem in ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North") else "simple" import_fbx(path, stem, collision)