feat(site): slice the ground into 20 m segments - and the slice hypothesis dies with them

The site exporter now splits sidewalks and kerbs into ~20 m pieces
(SEGMENTED / SEGMENT_M in Art/blender/site/export_site_fbx.py), 56 slices in all, so each has a
bounding box that means something where it stands instead of reporting the highest point on a
279 m slope. They import and stage (63 site meshes in the demo map).

The hypothesis behind it is dead, and that is the useful part: with the invisible sidewalk
stopgap retired, the player now falls to Road_Hollywood at 366 - the slices do not block either.
So the 279 m aspect ratio was never the cause; a 20 m slice of the same mesh fails identically.
The sidewalk geometry itself is what does not collide, and the next place to look is its shape -
an open surface with no volume, inverted winding or zero-thickness triangles - not the import
flags and not the size.

The stopgap retirement stands: the player walks on the road, which is real geometry that does
collide, rather than on an invisible ledge.
This commit is contained in:
zwitschi committed 2026-11-09 18:13:29 +01:00
1 parent 1ee2a35d53
commit 1916321761
86 files changed
+275 -57

No files matched your search

+16 -23
View File
@@ -500,6 +500,13 @@ def build():
roughness=0.72, specular=0.22, noise_scale=0.02,
roughness_variation=0.12),
}
# Segmented ground: the site export slices sidewalks and kerbs into 20 m pieces, so the
# map stages every slice. A single 279 m slab could not report an honest local height and
# its collision was degenerate; slices fix both.
segments = [f"Sidewalk_North_{i:02d}" for i in range(14)] + \
[f"Sidewalk_South_{i:02d}" for i in range(14)] + \
[f"Kerb_North_{i:02d}" for i in range(14)] + \
[f"Kerb_South_{i:02d}" for i in range(14)]
SURFACE_MATERIALS = {
"Terrain_DEM": "M_Ground",
"Road_Hollywood": "M_Asphalt",
@@ -514,6 +521,11 @@ def build():
for mat_name, spec in MATERIAL_SPECS.items():
built[mat_name] = ensure_material(mat_name, spec, textures)
for segment in segments:
if unreal.EditorAssetLibrary.does_asset_exist(f"/Game/Hollywood/Site/{segment}"):
SURFACE_MATERIALS[segment] = ("M_Concrete" if "Sidewalk" in segment
else "M_KerbConcrete")
imported = 0
for name, mat_name in SURFACE_MATERIALS.items():
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
@@ -681,29 +693,10 @@ def build():
# pavement top, so the capsule started inside the sidewalk block and the downward trace
# exited through it to Road_Hollywood at 366 - which is what made the character look like
# it floated while standing on a surface 14 cm above the road.
player_z = 469.0
# Stopgap collision for the north sidewalk. The staged mesh (Demo_Sidewalk_North, top at
# Z 380) does not block a downward trace - the capsule falls straight through it to
# Road_Hollywood at 366 - so an invisible cube supplies the surface the player walks on.
# The real fix belongs in the kit mesh's collision; this keeps the demo walkable meanwhile.
sidewalk_collision = spawn(unreal.StaticMeshActor,
unreal.Vector(-14000.0, -1482.5, 376.0),
"Demo_SidewalkCollision")
if sidewalk_collision:
cube = unreal.EditorAssetLibrary.load_asset("/Engine/BasicShapes/Cube.Cube")
comp = sidewalk_collision.get_component_by_class(unreal.StaticMeshComponent)
if cube and comp:
comp.set_static_mesh(cube)
comp.set_mobility(unreal.ComponentMobility.STATIC)
# A 100 cm engine cube: 279 m long, 8.6 m wide, 8 cm thick -> top at Z 380.
sidewalk_collision.set_actor_scale3d(unreal.Vector(279.0, 8.6, 0.08))
comp.set_collision_profile_name("BlockAll")
sidewalk_collision.set_actor_hidden_in_game(True)
loc = sidewalk_collision.get_actor_location()
scale = sidewalk_collision.get_actor_scale3d()
log(f"staged: Demo_SidewalkCollision at Z {loc.z:.0f}, scale Z {scale.z:.3f} "
f"(top {loc.z + 50.0 * scale.z:.0f} cm)")
player_z = 400.0
# The invisible sidewalk collision slab is retired: the site export now slices the
# sidewalks into 20 m pieces, so the walkable surface is real geometry with an honest
# local height instead of a 279 m slab whose bounding box was the highest point anywhere.
spawn(unreal.PlayerStart, unreal.Vector(-13336.0, -1150.0, player_z), "Demo_PlayerStart",
unreal.Rotator(pitch=0.0, yaw=90.0, roll=0.0))
log("staged: Demo_PlayerStart on the cut north sidewalk, facing south (+Y)")
+5 -2
View File
@@ -107,8 +107,11 @@ def main():
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"
# Segmented ground surfaces keep their names with a numeric suffix; both the base
# names and the slices want the same walkable collision.
walkable = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North",
"Kerb_South", "Kerb_North")
collision = "complex" if any(stem.startswith(w) for w in walkable) else "simple"
import_fbx(path, stem, collision)
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)