Files
gig-line/Tools/ue_import_site.py
T
zwitschi 1b4b46e957 fix(site): slice the road too - the 2 m lid was a convex hull, not geometry
The road was never mis-modelled: a 279 m strip imports with ONE auto-generated convex hull, and a
convex hull's top is the strip's maximum height. A downward trace therefore landed at 366 cm - a flat
lid about 2 m above the visible road, which sits at roughly 152 cm there. That single fact explains
every failed sidewalk fix, the 'road above its own sidewalk' reading, and why the player always ended
up on Road_Hollywood.

Slicing the road the way the sidewalks already were fixes it, and the cross-section is now continuous:

  Terrain_DEM       154
  Sidewalk_North_07 158
  Road_Hollywood_07 158   (was 366)

Two changes had to land together: the road exported in slices, and the map stopped staging the single
Road_Hollywood asset - which still sat on top of the slices and won every trace until removed.

All 23 captures regenerated; the player and the car both stand on a continuous ground plane.
2026-11-09 19:15:10 +01:00

128 lines
5.3 KiB
Python

# Import the Blender site meshes (terrain, road, sidewalks, kerbs) into Unreal.
#
# blender ... --python Art/blender/site/export_site_fbx.py -- --out /tmp/gigline/site
# <engine>/.../UnrealEditor-Cmd <uproject> -run=pythonscript \
# -script=<repo>/Tools/ue_import_site.py -nullrhi -unattended -nopause -stdout
#
# Each mesh is a separate FBX because they carry different materials and want
# different collision settings (terrain and street are walkable surfaces).
# Destination: /Game/Hollywood/Site/<name>.
#
# Y-axis note: the FBX preset maps Blender +Y (north) to Unreal -Y, so the site
# arrives in the same frame as the hero facade. Nothing is mirrored here.
import glob
import os
import unreal
SRC = "/tmp/gigline/site"
DEST = "/Game/Hollywood/Site"
RESULT = "/tmp/gigline/site-import.txt"
lines = []
def log(msg):
lines.append(str(msg))
unreal.log(str(msg))
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
task.destination_name = name
task.automated = True
task.save = True
task.replace_existing = True
ui = unreal.FbxImportUI()
ui.import_mesh = True
ui.import_materials = True
ui.import_textures = False
ui.import_as_skeletal = False
smd = ui.static_mesh_import_data
smd.import_uniform_scale = 1.0
smd.combine_meshes = True
smd.convert_scene_unit = False
smd.force_front_x_axis = False
smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS
# Generate collision at import. A default FBX static-mesh import creates NO
# collision primitives, so CTF_USE_COMPLEX_AS_SIMPLE later has nothing to use
# and the player falls through the walkable surfaces (observed at runtime:
# ground trace under the spawn hits nothing). auto_generate_collision bakes
# collision geometry into the mesh so the walkable plane actually blocks.
smd.auto_generate_collision = True
task.options = ui
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}")
if mesh is None:
log(f"MISSING after import: {DEST}/{name}")
return
# Collision is requested at import time so the 0.1.0 walkability check has
# 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 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, 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
# Walkable surfaces block with SIMPLE + COMPLEX collision so the player's
# capsule always has ground: CFG_USE_SIMPLE_AND_COMPLEX uses the auto-
# generated simple collision AND the mesh triangles. (Complex-as-simple
# alone only worked if the mesh carried complex collision, which a default
# FBX static-mesh import does not.)
trace = unreal.CollisionTraceFlag.CTF_USE_SIMPLE_AND_COMPLEX
try:
body = mesh.get_editor_property("body_setup")
body.set_editor_property("collision_trace_flag", trace)
mesh.set_editor_property("body_setup", body)
except Exception as exc: # noqa: BLE001 - report, do not fail the whole import
log(f"{name}: could not set trace flag ({exc}); leaving the default")
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={collision}")
def main():
files = sorted(glob.glob(os.path.join(SRC, "*.fbx")))
if not files:
log(f"no FBX in {SRC}; run Art/blender/site/export_site_fbx.py first")
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
# 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", "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)
try:
main()
except Exception as exc: # noqa: BLE001
import traceback
log(f"FAILED: {type(exc).__name__}: {exc}")
log(traceback.format_exc())
with open(RESULT, "w") as f:
f.write("\n".join(lines) + "\n")