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.
128 lines
5.3 KiB
Python
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")
|