feat(vehicle): build the skeletal mesh from the hot hatch
vehicle_from_template.py now authors the physics mesh from the real hot hatch: apply the mirror/nodes modifiers, derive the 4 wheel centres, add a root + 4 wheel bones, weight and export vehicle_skeletal.fbx. A rotation orients the car so it imports unrolled (length +X, width +Y, height +Z); wheel bones land at front +X, rear -X, left/right +/-Y. The vehicle still floats (rises to 33 m) - recorded in MEMORY.
This commit is contained in:
1 parent
d8cbb0c95b
commit
63ba9d91ce
3 files changed
+140
No files matched your search
@@ -230,6 +230,90 @@ def join_system(objects, name):
|
||||
return joined
|
||||
|
||||
|
||||
def build_skeletal(joined, args):
|
||||
"""Bind the body + wheels to a root + 4 wheel bones for the Chaos vehicle.
|
||||
|
||||
The hot hatch's nose is along +Y, so wheel front/rear is the Y sign and left/right is the
|
||||
X sign (the wheels are mirrored in the template, so the joined mesh has all four). Each
|
||||
wheel is weighted to its own bone at full weight; the body goes to the root.
|
||||
"""
|
||||
import math
|
||||
|
||||
body = joined["body"]
|
||||
wheels = joined["wheels"]
|
||||
|
||||
# Derive the four wheel centres from the wheel geometry: group vertices by quadrant.
|
||||
# After the Unreal-frame rotation, front/rear is the X sign and left/right is the Z sign.
|
||||
quadrants = {"FL": [], "FR": [], "RL": [], "RR": []}
|
||||
for vertex in wheels.data.vertices:
|
||||
world = wheels.matrix_world @ vertex.co
|
||||
key = ("F" if world.x > 0 else "R") + ("L" if world.z > 0 else "R")
|
||||
quadrants[key].append(world)
|
||||
centres = {}
|
||||
for key, verts in quadrants.items():
|
||||
if not verts:
|
||||
continue
|
||||
centres[f"Wheel_{key}"] = (
|
||||
sum(v.x for v in verts) / len(verts),
|
||||
sum(v.y for v in verts) / len(verts),
|
||||
sum(v.z for v in verts) / len(verts),
|
||||
)
|
||||
if len(centres) != 4:
|
||||
raise SystemExit(f"expected 4 wheel quadrants, got {len(centres)}: {list(centres)}")
|
||||
log(f"wheel centres: " + ", ".join(f"{k}=({v[0]:.2f},{v[1]:.2f},{v[2]:.2f})"
|
||||
for k, v in sorted(centres.items())))
|
||||
|
||||
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)
|
||||
for name, (x, y, z) in sorted(centres.items()):
|
||||
bone = edit_bones.new(name)
|
||||
bone.head = (x, y, z)
|
||||
# Axle direction is irrelevant to the Chaos solver (it drives the wheel about the
|
||||
# vehicle's Y), so point the tail straight down for a stable reference frame.
|
||||
bone.tail = (x, y, z - 0.25)
|
||||
bone.parent = root
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# Join the body and wheels into one skinned mesh.
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
body.select_set(True)
|
||||
wheels.select_set(True)
|
||||
bpy.context.view_layer.objects.active = 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", *centres]}
|
||||
for vertex in mesh.data.vertices:
|
||||
world = mesh.matrix_world @ vertex.co
|
||||
nearest, best = "root", 0.6 ** 2
|
||||
for name, (x, y, z) in centres.items():
|
||||
d = (world.x - x) ** 2 + (world.y - y) ** 2 + (world.z - z) ** 2
|
||||
if d < best:
|
||||
best, nearest = d, name
|
||||
groups[nearest].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(args.out, "vehicle_skeletal.fbx")
|
||||
fbx_export_preset.export_character(path, selection=True)
|
||||
log(f"skeletal vehicle exported: {path}, bones root + 4 wheels")
|
||||
return path
|
||||
|
||||
|
||||
def add_collision(body):
|
||||
"""One convex box per major volume, `UCX_`-prefixed for Unreal."""
|
||||
hulls = []
|
||||
@@ -282,6 +366,38 @@ def main(argv=None) -> int:
|
||||
|
||||
normalise_scale(meshes)
|
||||
|
||||
# Apply the MIRROR modifier so the left wheels exist in the mesh data BEFORE the nose
|
||||
# rotation; the mirror is in the object's local space, so rotating first would mirror
|
||||
# about a rotated axis.
|
||||
for obj in meshes:
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
obj.select_set(True)
|
||||
for modifier in list(obj.modifiers):
|
||||
try:
|
||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||
except RuntimeError:
|
||||
pass
|
||||
obj.select_set(False)
|
||||
|
||||
# Rotate so the FBX axis conversion (Blender Y->Unreal -Z, Z->-Y) lands the car unrolled:
|
||||
# length along +X (forward), width along Y, height along Z. The mapping is X->-Z, Y->X,
|
||||
# Z->-Y, i.e. the rotation matrix [[0,1,0],[0,0,-1],[-1,0,0]].
|
||||
from mathutils import Matrix
|
||||
rot = Matrix((
|
||||
(0.0, 1.0, 0.0),
|
||||
(0.0, 0.0, -1.0),
|
||||
(-1.0, 0.0, 0.0),
|
||||
))
|
||||
for obj in meshes:
|
||||
obj.rotation_euler = rot.to_euler()
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
|
||||
obj.select_set(False)
|
||||
bpy.context.view_layer.update()
|
||||
log("rotated to the Unreal frame (length +X, width +Y, height +Z)")
|
||||
|
||||
systems = {}
|
||||
for obj in meshes:
|
||||
systems.setdefault(classify(obj), []).append(obj)
|
||||
@@ -363,6 +479,12 @@ def main(argv=None) -> int:
|
||||
with open(args.report, "w") as handle:
|
||||
json.dump(report, handle, indent=2)
|
||||
print(f"[hatch] report -> {args.report}")
|
||||
|
||||
# Build the skeletal (physics) mesh last, after the report has recorded the separate
|
||||
# systems: build_skeletal joins the body and wheels, so it must not run before the
|
||||
# static exports and report are written.
|
||||
build_skeletal(joined, args)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user