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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -16,7 +16,10 @@ Actionable task list for this project. This file is the single, central place fo
|
||||
- [A] Model one vehicle in Blender with collision meshes; export to Unreal. **Model done** (`Tools/vehicle_build.py` → `Art/blender/vehicle/gigline-sedan.blend`, preview in `Art/blender/vehicle/preview.png`): 4.72 x 1.80 x 1.44 m, 2.78 m wheelbase, 1.62 m track, 0.33 m wheels, 252 triangles, four `UCX_` convex hulls. Dimensions come from real sedan figures and are logged to the report, so the vehicle config can read them rather than restating them. - **Imported into Unreal** (`Tools/ue_vehicle_import.py` → `/Game/Hollywood/Vehicle`), split per the vehicle-artist object split: `SM_Vehicle_Body` (3 slots, 4.76 x 1.76 x 1.28 m), `SM_Vehicle_Glass`, `SM_Vehicle_Light_Front`, `SM_Vehicle_Light_Rear`, `SM_Vehicle_Mirrors`, and one `SM_Vehicle_Wheel` (0.22 x 0.66 x 0.66 m) reused four times. The body FBX carries the `UCX_` hulls as simple collision.
|
||||
- **LODs cut for real:** decimating the duplicate gives LOD1 at 38 triangles and LOD2 at 14, against 132 for LOD0. The first attempt duplicated the shell unchanged and produced 84-triangle LODs that saved nothing - worth remembering as the failure mode of a "LOD" step nobody measures.
|
||||
- **LOD group wiring is open:** `StaticMeshEditorSubsystem.import_lod` is unavailable in a headless commandlet (`get_editor_subsystem` returns None), so the LOD meshes import as their own assets and the group still needs an editor session.
|
||||
- [ ] Set up Chaos Vehicle with wheel and physics configuration.
|
||||
- [A] Set up Chaos Vehicle with wheel and physics configuration. **Blocked on the physics asset, which cannot be generated headless.**
|
||||
- **Skeletal vehicle asset is in** (`SK_Vehicle_Sedan` + `SK_Vehicle_Sedan_Skeleton`): one mesh carrying a root bone and four wheel bones, every wheel skinned to its own bone, exported with the character preset (508 triangles). Chaos drives a skeletal mesh, so the static split alone was not drivable.
|
||||
- **Blocked:** Chaos needs a `PhysicsAsset`, and `unreal.PhysicsAssetFactory` in 5.8 exposes **no mesh property** (`Failed to find property 'skeletal_mesh'`), so the bodies cannot be generated from the skeletal mesh in a commandlet. Options: author it once in the editor and commit it, or build it in C++ with `FPhysicsAssetUtils` in an editor utility.
|
||||
- **Next:** create the physics asset (root box + four wheel capsules at the modelled positions: wheelbase 2.78 m, track 1.62 m, radius 0.33 m), then the `UChaosWheeledVehicleMovementComponent` config reads those same numbers from `Art/blender/vehicle` report.
|
||||
- [ ] Implement enter/exit interaction.
|
||||
- [ ] Build a closed driving route with boundaries.
|
||||
- [ ] Tune basic handling to a drivable state.
|
||||
|
||||
@@ -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