test(vehicle): the terrain alone reproduces it, and two collision fixes did not help

Built the map-content bisect the task asked for: Tools/ue_vehicle_probe_map.py duplicates ReviewMap,
optionally adds one demo mesh and the pawn, and saves it as VehicleProbe.

  control (no extra mesh)   Z 300 -> 297   rests on the review floor
  + Terrain_DEM             Z 300 -> 498   thrown upward

So Terrain_DEM alone reproduces the fault in a map that works. Two fixes were tried:

- Tools/ue_clear_simple_collision.py cleared the auto-generated convex hulls from all 76 site/street
  meshes - the terrain had ONE convex element over 279 m - and set them to complex-as-simple. Still rises.
- The terrain component set to QueryOnly, so traces hit it and physics ignores it. Still rises.

Neither changed the behaviour, so the ejection is neither the convex hull nor the collision channel. The
next probe is the complex triangles themselves: give Terrain_DEM no collision at all in the probe map and
see whether the pawn falls. If it falls, the triangle winding is pushing bodies out of the ground mesh -
the same inverted-winding family that made the sidewalk unstandable earlier.

Both tools have README rows; the pawn is parked again.
This commit is contained in:
zwitschi committed 2026-11-09 22:20:28 +01:00
1 parent 410d924403
commit a1771cb55f
103 files changed
+393 -174

No files matched your search

+2
View File
@@ -55,6 +55,8 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
| `ue_import_site.py` | Imports the six site FBXs into `/Game/Hollywood/Site`, setting collision complexity per mesh |
| `ue_hero_facade_import.py` | Imports the façade and places it on the measured building line |
| `ue_build_demo_map.py` | Builds `/Game/Hollywood/DemoMap`: lighting, façade, site meshes, player start |
| `ue_clear_simple_collision.py` | Removes the FBX import's auto-generated convex hulls from the site and street meshes and sets them to complex-as-simple, so physics bodies are not pushed out of a 279 m hull. |
| `ue_vehicle_probe_map.py` | Copies `ReviewMap` to `VehicleProbe`, optionally adds one demo mesh and the Chaos pawn, and saves it, to bisect which map content throws a physics body into the air. Set `GIGLINE_PROBE_ASSET` to stage one mesh; unset for the control. |
| `ue_build_review_map.py` | Rebuilds `Test/ReviewMap`, stages the shot camera list, and stages the demo-map views |
| `ue_create_input_assets.py` | Creates `/Game/Input` actions and the mapping context headlessly |
| `ue_character_anim_import.py` | Imports the animated FBX into `/Game/Test/Character` and **verifies an AnimSequence exists** — an FBX exported without `bake_anim` imports a mesh and no clips, with no error. Writes `VERDICT: PASS/FAIL` to `/tmp/gigline/character-anim-import.txt`. |
+8
View File
@@ -549,6 +549,14 @@ def build():
log(f"terrain collision: {unreal.GigLineAssetTools.get_collision_complexity(asset)} "
f"(saved asset: {saved_asset})")
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"Demo_{name}")
if actor and "Terrain" in name:
# Terrain answers traces (so the player walks on it) but is invisible to the physics
# solver: a body inside its triangle soup is pushed out every frame, which is what threw
# the Chaos vehicle into the air. Vehicles drive on the street slices, which carry their
# own UCX boxes.
comp_t = actor.get_component_by_class(unreal.StaticMeshComponent)
if comp_t:
comp_t.set_collision_enabled(unreal.CollisionEnabled.QUERY_ONLY)
if actor:
comp = actor.static_mesh_component
comp.set_static_mesh(mesh)
+66
View File
@@ -0,0 +1,66 @@
"""Remove the auto-generated simple collision from the site meshes, keeping their triangles.
<engine>/UnrealEditor-Cmd <uproject> -run=pythonscript \
-script=<repo>/Tools/ue_clear_simple_collision.py -nullrhi -unattended -nopause -stdout
Why: the FBX import runs with `auto_generate_collision`, which adds ONE convex hull per mesh. For a
279 m terrain or road that hull is a giant slab, and any *physics body* inside it - the Chaos vehicle -
is pushed out of the hull every frame. Walked characters never notice, because character movement
sweeps instead of resolving penetration, which is why the player was fine while the car was thrown
into the air.
Clearing the hulls leaves `CTF_USE_COMPLEX_AS_SIMPLE`, so traces and physics use the real triangles.
Result: /tmp/gigline/clear-simple-collision.txt
"""
import unreal
FOLDERS = ("/Game/Hollywood/Site", "/Game/Hollywood/Streets")
RESULT = "/tmp/gigline/clear-simple-collision.txt"
lines = []
def log(message):
lines.append(str(message))
unreal.log(str(message))
def main():
cleared = 0
for folder in FOLDERS:
for path in unreal.EditorAssetLibrary.list_assets(folder, recursive=False):
mesh = unreal.EditorAssetLibrary.load_asset(path)
if not isinstance(mesh, unreal.StaticMesh):
continue
body = mesh.get_editor_property("body_setup")
if body is None:
continue
agg = body.get_editor_property("agg_geom")
convex = len(agg.get_editor_property("convex_elems"))
boxes = len(agg.get_editor_property("box_elems"))
if not convex and not boxes:
continue
agg.set_editor_property("convex_elems", [])
agg.set_editor_property("box_elems", [])
body.set_editor_property("agg_geom", agg)
body.set_editor_property("collision_trace_flag",
unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE)
mesh.set_editor_property("body_setup", body)
unreal.EditorAssetLibrary.save_loaded_asset(mesh)
log(f"{mesh.get_name()}: cleared {convex} convex, {boxes} box, complex-as-simple")
cleared += 1
lines.append(f"VERDICT: {'PASS' if cleared else 'NOTHING-TO-DO'}")
log(f"{cleared} meshes cleared")
try:
main()
except Exception as exc: # noqa: BLE001
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
lines.append("VERDICT: FAIL")
with open(RESULT, "w") as f:
f.write("\n".join(lines) + "\n")
+105
View File
@@ -0,0 +1,105 @@
"""Add one piece of demo content to ReviewMap and save it as a vehicle probe level.
GIGLINE_PROBE_ASSET=Terrain_DEM <engine>/UnrealEditor-Cmd <uproject> \
-run=pythonscript -script=<repo>/Tools/ue_vehicle_probe_map.py -nullrhi -unattended -nopause
Why: the Chaos pawn falls correctly on ReviewMap's empty floor and is thrown upward everywhere in
DemoMap. Rather than guess which of DemoMap's 593 staged meshes is responsible, add them one group
at a time to the map that works and watch for the moment the pawn stops falling. With no
GIGLINE_PROBE_ASSET set, the level is a plain copy of ReviewMap - the control.
Assets are looked up as /Game/Hollywood/Site/<name> first, then /Game/Hollywood/Buildings/<name>.
Result: /tmp/gigline/vehicle-probe.txt
"""
import os
import unreal
RESULT = "/tmp/gigline/vehicle-probe.txt"
TARGET = "/Game/Test/VehicleProbe"
SOURCE = "/Game/Test/ReviewMap"
SEARCH = ("/Game/Hollywood/Site", "/Game/Hollywood/Buildings",
"/Game/Hollywood/Streets", "/Game/Hollywood/Props")
lines = []
def log(message):
lines.append(str(message))
unreal.log(str(message))
def find_asset(name):
for folder in SEARCH:
path = f"{folder}/{name}"
if unreal.EditorAssetLibrary.does_asset_exist(path):
return path
return None
def main():
wanted = os.environ.get("GIGLINE_PROBE_ASSET", "").strip()
if not unreal.EditorAssetLibrary.does_asset_exist(SOURCE):
log(f"missing source level {SOURCE}")
lines.append("VERDICT: FAIL")
return
target = TARGET
# A level that the previous run left behind cannot always be deleted while the editor holds it,
# so fall back to a suffixed name rather than failing the probe.
for suffix in ("", "_b", "_c", "_d"):
target = TARGET + suffix
if not unreal.EditorAssetLibrary.does_asset_exist(target):
break
unreal.EditorAssetLibrary.delete_asset(target)
if not unreal.EditorAssetLibrary.does_asset_exist(target):
break
if not unreal.EditorAssetLibrary.duplicate_asset(SOURCE, target):
log(f"could not duplicate {SOURCE} -> {TARGET}")
lines.append("VERDICT: FAIL")
return
log(f"duplicated {SOURCE} -> {target}")
unreal.EditorLevelLibrary.load_level(target)
if wanted:
path = find_asset(wanted)
if path is None:
log(f"MISSING asset {wanted} in {SEARCH}")
lines.append("VERDICT: FAIL")
return
mesh = unreal.EditorAssetLibrary.load_asset(path)
actor = unreal.EditorLevelLibrary.spawn_actor_from_class(
unreal.StaticMeshActor, unreal.Vector(0.0, 0.0, 0.0))
actor.get_component_by_class(unreal.StaticMeshComponent).set_static_mesh(mesh)
actor.set_actor_label(f"Probe_{wanted}")
log(f"staged probe mesh {path} at the origin")
else:
log("control run: no extra content")
vehicle_class = unreal.load_class(None, "/Script/GigLine.GigLineVehicle")
if vehicle_class:
pawn = unreal.EditorLevelLibrary.spawn_actor_from_class(
vehicle_class, unreal.Vector(600.0, 0.0, 300.0))
pawn.set_actor_label("Probe_Vehicle")
log("staged the Chaos pawn at (600, 0, 300)")
else:
log("vehicle class not found")
if unreal.EditorLevelLibrary.save_current_level():
log(f"saved {target}")
lines.append(f"LEVEL: {target}")
lines.append("VERDICT: PASS")
else:
log(f"save failed for {target}")
lines.append("VERDICT: FAIL")
try:
main()
except Exception as exc: # noqa: BLE001
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
lines.append("VERDICT: FAIL")
with open(RESULT, "w") as f:
f.write("\n".join(lines) + "\n")