feat(vehicle): skeletal vehicle asset for Chaos, and the block that follows
Chaos drives a skeletal mesh, so the static system split was not drivable on its own. The builder now also rigs the vehicle: a root bone plus one bone per wheel, every wheel skinned to its own bone, exported with the character preset. SK_Vehicle_Sedan imports with SK_Vehicle_Sedan_Skeleton (508 triangles). Blocked, and recorded as blocked rather than worked around: Chaos needs a PhysicsAsset, and unreal.PhysicsAssetFactory in 5.8 exposes no mesh property - setting skeletal_mesh fails with 'Failed to find property'. Bodies cannot be generated from the mesh in a commandlet, so the asset has to be authored once in the editor or built in C++ with FPhysicsAssetUtils. Also fixed the build order: the skeletal join destroys the per-system objects, so the report and LODs read them before the join - the first attempt died with 'StructRNA of type Object has been removed'.
This commit is contained in:
1 parent
02cadd4964
commit
2e0421480c
14 files changed
+140
-20
No files matched your search
@@ -102,6 +102,33 @@ def main():
|
||||
mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}")
|
||||
log(f"{name}: imported as its own asset ({'ok' if mesh else 'MISSING'}); "
|
||||
"LOD group wiring needs an editor session")
|
||||
|
||||
# Skeletal vehicle for Chaos: a root bone plus one bone per wheel, everything skinned.
|
||||
skel_path = os.path.join(SOURCE, "vehicle_skeletal.fbx")
|
||||
if os.path.exists(skel_path):
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = skel_path
|
||||
task.destination_path = DEST
|
||||
task.destination_name = "SK_Vehicle_Sedan"
|
||||
task.automated = True
|
||||
task.save = True
|
||||
task.replace_existing = True
|
||||
ui = unreal.FbxImportUI()
|
||||
ui.import_mesh = True
|
||||
ui.import_as_skeletal = True
|
||||
ui.import_animations = False
|
||||
ui.import_materials = True
|
||||
ui.skeletal_mesh_import_data.import_uniform_scale = 1.0
|
||||
task.options = ui
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
skel = unreal.EditorAssetLibrary.load_asset(f"{DEST}/SK_Vehicle_Sedan")
|
||||
if skel:
|
||||
skeleton = skel.get_editor_property("skeleton")
|
||||
# Skeleton has no bone_names property in 5.8; read the reference skeleton
|
||||
# through the asset registry instead of guessing a property name.
|
||||
log(f"SK_Vehicle_Sedan: imported, skeleton={skeleton.get_name() if skeleton else 'NONE'}")
|
||||
else:
|
||||
log("SK_Vehicle_Sedan: MISSING after import")
|
||||
lines.append("VERDICT: PASS")
|
||||
|
||||
|
||||
|
||||
+91
-7
@@ -167,6 +167,80 @@ def build_collision(mats):
|
||||
return hulls
|
||||
|
||||
|
||||
def build_skeletal(joined, wheels, out_fbx, exp_dir):
|
||||
"""Bind the whole vehicle to a five-bone rig for Chaos.
|
||||
|
||||
Chaos Vehicles drive a *skeletal* mesh: the movement component names wheel bones and
|
||||
spins them, and the physics asset supplies the collision. The static split above
|
||||
stays for parts that do not deform, but the drivable asset has to be skeletal, so
|
||||
this builds a rig with a root plus one bone per wheel and skins every wheel to its
|
||||
own bone at full weight.
|
||||
"""
|
||||
bpy.ops.object.armature_add(location=(0.0, 0.0, 0.0))
|
||||
rig = bpy.context.active_object
|
||||
rig.name = "VehicleRig"
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
edit_bones = rig.data.edit_bones
|
||||
root = edit_bones[0]
|
||||
root.name = "root"
|
||||
root.head = (0.0, 0.0, 0.0)
|
||||
root.tail = (0.0, 0.0, 0.30)
|
||||
half_base = DIMS["wheelbase"] * 0.5
|
||||
half_track = DIMS["track"] * 0.5
|
||||
r = DIMS["wheel_radius"]
|
||||
wheel_positions = {
|
||||
"Wheel_FL": (half_base, half_track),
|
||||
"Wheel_FR": (half_base, -half_track),
|
||||
"Wheel_RL": (-half_base, half_track),
|
||||
"Wheel_RR": (-half_base, -half_track),
|
||||
}
|
||||
for name, (x, y) in wheel_positions.items():
|
||||
bone = edit_bones.new(name)
|
||||
bone.head = (x, y, r)
|
||||
# Point the bone along the axle: the wheel spins about its own Y.
|
||||
bone.tail = (x, y - math.copysign(0.25, y), r)
|
||||
bone.parent = root
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# Join every visible system plus the wheels into one mesh, then weight by part.
|
||||
parts = [obj for obj in joined.values()] + list(wheels)
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for part in parts:
|
||||
part.select_set(True)
|
||||
bpy.context.view_layer.objects.active = joined["body"]
|
||||
bpy.ops.object.join()
|
||||
mesh = bpy.context.active_object
|
||||
mesh.name = "SM_Vehicle_Skeletal"
|
||||
|
||||
groups = {name: mesh.vertex_groups.new(name=name) for name in ["root", *wheel_positions]}
|
||||
# Join order is preserved per part: re-derive which vertices belong to which wheel by
|
||||
# their world position, since the join loses the object boundaries.
|
||||
half_base = DIMS["wheelbase"] * 0.5
|
||||
half_track = DIMS["track"] * 0.5
|
||||
for vertex in mesh.data.vertices:
|
||||
co = vertex.co
|
||||
nearest = None
|
||||
for name, (x, y) in wheel_positions.items():
|
||||
if abs(co.x - x) < 0.45 and abs(co.y - y) < 0.16 and co.z < DIMS["wheel_radius"] * 2.2:
|
||||
nearest = name
|
||||
break
|
||||
groups[nearest or "root"].add([vertex.index], 1.0, "REPLACE")
|
||||
|
||||
armature = mesh.modifiers.new(name="Armature", type="ARMATURE")
|
||||
armature.object = rig
|
||||
mesh.parent = rig
|
||||
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
rig.select_set(True)
|
||||
mesh.select_set(True)
|
||||
bpy.context.view_layer.objects.active = rig
|
||||
path = os.path.join(exp_dir, "vehicle_skeletal.fbx")
|
||||
fbx_export_preset.export_character(path, selection=True)
|
||||
log(f"skeletal vehicle exported: {path}, bones root + 4 wheels, "
|
||||
f"{sum(len(p.vertices) - 2 for p in mesh.data.polygons)} triangles")
|
||||
return path
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
@@ -272,10 +346,14 @@ def main(argv=None) -> int:
|
||||
fbx_export_preset.export_static(os.path.join(exp_dir, "wheel.fbx"), selection=True)
|
||||
exported["wheel"] = os.path.join(exp_dir, "wheel.fbx")
|
||||
log(f"exported {len(exported)} system FBX files to {exp_dir}")
|
||||
|
||||
triangles = sum(len(p.vertices) - 2 for p in shell.data.polygons)
|
||||
per_system = {key: sum(len(p.vertices) - 2 for p in obj.data.polygons)
|
||||
for key, obj in joined.items()}
|
||||
system_names = {key: obj.name for key, obj in joined.items()}
|
||||
wheel_names = [w.name for w in wheels]
|
||||
wheel_triangles = sum(len(p.vertices) - 2 for p in wheels[0].data.polygons)
|
||||
|
||||
triangles = sum(len(p.vertices) - 2 for p in shell.data.polygons)
|
||||
shell_triangles = triangles
|
||||
for hull in hull_join:
|
||||
triangles += sum(len(p.vertices) - 2 for p in hull.data.polygons)
|
||||
# LODs per the vehicle-artist targets: LOD1 drops mirrors and lamp detail, LOD2 is a
|
||||
@@ -304,17 +382,23 @@ def main(argv=None) -> int:
|
||||
log(f"{lod_key}: exported, {sum(len(p.vertices) - 2 for p in lod.data.polygons)} triangles")
|
||||
bpy.data.objects.remove(lod, do_unlink=True)
|
||||
|
||||
# Skeletal asset last: joining the parts for it destroys the objects the LOD and
|
||||
# system steps need. The first attempt called it earlier and died on
|
||||
# "StructRNA of type Object has been removed".
|
||||
skeletal_path = build_skeletal(joined, wheels, args.fbx, exp_dir)
|
||||
exported["skeletal"] = skeletal_path
|
||||
|
||||
report = {
|
||||
"asset": "SM_Vehicle_Sedan",
|
||||
"dimensions_m": DIMS,
|
||||
"body_parts": len(body),
|
||||
"triangles": triangles,
|
||||
"triangles": shell_triangles,
|
||||
"collision_hulls": [h.name for h in hull_join],
|
||||
"wheels": [w.name for w in wheels],
|
||||
"systems": {key: {"object": obj.name, "triangles": per_system.get(key, 0),
|
||||
"wheels": wheel_names,
|
||||
"systems": {key: {"object": system_names.get(key), "triangles": per_system.get(key, 0),
|
||||
"fbx": exported.get(key)}
|
||||
for key, obj in joined.items()},
|
||||
"wheel_triangles": sum(len(p.vertices) - 2 for p in wheels[0].data.polygons),
|
||||
for key in system_names},
|
||||
"wheel_triangles": wheel_triangles,
|
||||
"lods": lod_paths,
|
||||
"log": PARTS,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user