gameplay: designated street crossing — zebra markings, corridor cut, up-facing street

- street_mesh.py: continental zebra markings (0.5 m bars, 0.5 m gaps, 3 m
  band) at all three signalised crossings, triangles not quads (FBX SDK
  refuses to triangulate the crown-twisted quads), stripes +6 cm above the
  DEM so they stay above the road surface; winding flipped so every street
  surface faces up (road/sidewalks/markings were 100% down-facing and
  invisible in engine once the terrain stopped covering them).
- terrain_from_dem.py: cut a 10.7 m corridor around the Hollywood Blvd
  centreline so road, kerbs and sidewalks are not buried under the terrain;
  sidewalks themselves cover the transition band, walk-check stays at 0 holes.
- Blender builders save with a writability guard + fatal failure: *.blend
  is LFS-lockable and checks out read-only; a failed save previously left
  downstream steps building from stale files.
- ue_import_site.py: Crossing_Markings imports with no collision (paint on
  the road); ue_build_demo_map.py stages it and chmods the lockable .umap
  before saving (save_map silently returned False on read-only maps).
- ue_build_review_map.py: demo-crossing camera moved to the real crossing
  (s=130.1 m -> UE -24291,-1340) and fixed the ensure_writable path (was
  missing the .umap suffix, so the chmod never fired).
This commit is contained in:
zwitschi committed 2026-11-07 15:46:45 +01:00
1 parent 985dfcbac7
commit bc2e421d33
20 files changed
+133 -36

No files matched your search

+26 -6
View File
@@ -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}")