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:
1 parent
985dfcbac7
commit
bc2e421d33
20 files changed
+133
-36
No files matched your search
@@ -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():
|
||||
|
||||
Binary file not shown.
@@ -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}")
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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}")
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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}")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user