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 f186e53..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:8ae571a174d7c07f01ebd90d60ef4c3ffaacc11d0fc8476c6d85eb00acb8fbbd -size 512324 +oid sha256:b6f546fcf86cd08a5e37571a7f42b2e6615925f71f7ca5ed850e801313591f70 +size 514043 diff --git a/Art/blender/street/street_mesh.py b/Art/blender/street/street_mesh.py index cdd99e9..201b616 100644 --- a/Art/blender/street/street_mesh.py +++ b/Art/blender/street/street_mesh.py @@ -148,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() @@ -239,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() @@ -259,24 +262,34 @@ def main(): 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 - zone / 2 + k * (w + g) + 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] - corners.append((len(verts), (x, y, (dem_z(x, y) or 0.0) + 0.03))) + # +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[1], idx[3], idx[2])) + 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() @@ -303,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 edb6e47..5230027 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:d6eb86a071caaa75dd40d5f39e0746a1905844c3e479eb86c25081536f1acab9 +oid sha256:68526b7095cfca222b500ff6968eb687ad1771cb31d0a58cbb7dace518968210 size 32372 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 461473e..816d9a2 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:4d2d5e8d826561523a076f0b6e11ab53d46d744b024311281b39a454f96f8f0c +oid sha256:208a1670a337ad19c75ee4b58bfd997dc6018f770ea9edaf85680f6bf723f10c size 32736 diff --git a/Tools/ue_build_demo_map.py b/Tools/ue_build_demo_map.py index f02b97f..8e6a6a7 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" @@ -83,7 +85,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 +105,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 168fc5b..950175f 100644 --- a/Tools/ue_build_review_map.py +++ b/Tools/ue_build_review_map.py @@ -29,7 +29,7 @@ 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/")) + 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: @@ -229,7 +229,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)), 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)