Taxi demo update with sedan
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python
|
||||
"""Generate the demo-sokoban .blend assets (forklift, crate, pad, wall).
|
||||
|
||||
Run once with the project Blender:
|
||||
blender -b -Y -P assets/blender/scripts/gen_sokoban_assets.py
|
||||
|
||||
Writes forklift.blend, crate.blend, pad.blend and wall.blend into
|
||||
assets/blender/vehicles/, from where the import_vehicles CMake target
|
||||
exports them through export_vehicles.py like the other vehicles.
|
||||
|
||||
Axis convention: blender2ogre EX_SWAP_AXIS='xz-y' maps Blender
|
||||
(x, y, z) -> Ogre (x, z, -y), so Ogre +Z (vehicle forward, Jolt
|
||||
chassis +Z) is Blender -Y and Ogre +Y (up) is Blender +Z. All meshes
|
||||
are modelled around the origin: the forklift body sits in the chassis
|
||||
collider space (collider half extents 0.7 x 0.3 x 1.2, wheels at
|
||||
(+/-0.65, -0.2, +/-0.85) r=0.3), the crate fills the 1 m box collider,
|
||||
the pad is a flat 1.2 x 1.2 marker slab.
|
||||
|
||||
NOTE: join_into() makes the first part the join target, whose object
|
||||
location stays baked into the exported mesh as an origin offset (the
|
||||
exporter does not apply it). For the forklift that is body-front at
|
||||
blender (0, -0.45, 0.55), so a blender point (X, Y, Z) renders at
|
||||
Ogre (X, Z - 0.55, -(Y + 0.45)). Keep that offset in mind when
|
||||
matching mesh geometry against VehicleComponent offsets.
|
||||
"""
|
||||
|
||||
import os
|
||||
import bpy
|
||||
from math import radians
|
||||
|
||||
OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "vehicles")
|
||||
|
||||
|
||||
def reset_scene():
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
|
||||
|
||||
def make_material(name, color, roughness=0.8, metallic=0.0):
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
||||
bsdf.inputs["Base Color"].default_value = (*color, 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = roughness
|
||||
bsdf.inputs["Metallic"].default_value = metallic
|
||||
return mat
|
||||
|
||||
|
||||
def add_box(name, dims, loc, mat=None):
|
||||
bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc)
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
obj.dimensions = dims
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False,
|
||||
scale=True)
|
||||
if mat:
|
||||
obj.data.materials.append(mat)
|
||||
return obj
|
||||
|
||||
|
||||
def add_cylinder(name, radius, depth, loc, mat=None, rotation=None,
|
||||
vertices=24):
|
||||
bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius,
|
||||
depth=depth, location=loc)
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
if rotation:
|
||||
obj.rotation_euler = rotation
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True,
|
||||
scale=True)
|
||||
if mat:
|
||||
obj.data.materials.append(mat)
|
||||
return obj
|
||||
|
||||
|
||||
def join_into(name, objects):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in objects:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = objects[0]
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
obj.data.name = name # the exporter names .mesh files after mesh data
|
||||
return obj
|
||||
|
||||
|
||||
def save_blend(filename):
|
||||
path = os.path.join(OUT_DIR, filename)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=path)
|
||||
print("wrote " + path)
|
||||
|
||||
|
||||
def gen_forklift():
|
||||
"""forklift.mesh (body+mast), forklift-forks.mesh (carriage+tines,
|
||||
lifted by VehicleSystem) and forklift-wheel.mesh.
|
||||
|
||||
Chassis origin is the Jolt body origin; the visible body sits on
|
||||
top of the collider box (which spans y -0.3..0.3 in Ogre space).
|
||||
Front (mast, forks) is Blender -Y == Ogre +Z.
|
||||
"""
|
||||
reset_scene()
|
||||
yellow = make_material("forklift", (0.85, 0.6, 0.05))
|
||||
steel = make_material("forklift-steel", (0.35, 0.35, 0.38),
|
||||
roughness=0.5, metallic=0.6)
|
||||
dark = make_material("wheel", (0.08, 0.08, 0.08), roughness=0.95)
|
||||
seat_mat = make_material("forklift-seat", (0.1, 0.1, 0.12))
|
||||
|
||||
parts = []
|
||||
# Main chassis / counterweight body, split for a driver footwell:
|
||||
# the front block (hood/dash) stops at blender y 0.0 so the seated
|
||||
# driver's legs dangle into the recess in front of the seat.
|
||||
parts.append(add_box("body-front", (1.3, 0.9, 0.5), (0, -0.45, 0.55),
|
||||
yellow))
|
||||
# Nose extension: continues the hood forward over the front wheels
|
||||
# (blender y -1.35..-0.9 -> Ogre z 0.45..0.90); the wheels tuck
|
||||
# under it with only their fronts/sides poking out. A separate
|
||||
# part so body-front keeps its location, which join_into() bakes
|
||||
# in as the mesh origin offset (see the file docstring).
|
||||
parts.append(add_box("body-nose", (1.3, 0.45, 0.5),
|
||||
(0, -1.125, 0.55), yellow))
|
||||
# Under-seat block (the seat itself sits on top, see below).
|
||||
parts.append(add_box("body-seat", (0.7, 0.5, 0.42), (0, 0.45, 0.59),
|
||||
yellow))
|
||||
# Footwell floor.
|
||||
parts.append(add_box("footwell", (0.7, 0.5, 0.06), (0, 0.05, 0.36),
|
||||
steel))
|
||||
# Rear counterweight bulge (back is blender +Y). Slid forward to
|
||||
# touch/embed 1 cm into the under-seat block (back face y 0.70) so
|
||||
# there is no gap; its rear is covered by the chassis collider
|
||||
# (half z 1.3 with offset z -0.3 -> Ogre z -1.6..1.0).
|
||||
parts.append(add_box("counterweight", (1.2, 0.45, 0.62),
|
||||
(0, 0.915, 0.6), yellow))
|
||||
# Steering column: runs from the hood top up to just under the
|
||||
# wheel hub. NOTE the mesh coordinates: join_into() bakes the
|
||||
# first part's (body-front) location (0, -0.45, 0.55) in as an
|
||||
# origin offset, so a blender point (X, Y, Z) lands at Ogre
|
||||
# (X, Z - 0.55, -(Y + 0.45)). VehicleComponent::steeringWheelOffset
|
||||
# (0, 0.75, -0.3) is therefore blender (0, -0.15, 1.30); the column
|
||||
# leans 25 deg toward the driver (+Y), matching the wheel tilt.
|
||||
parts.append(add_cylinder("steering-column", 0.03, 0.65,
|
||||
(0, -0.30, 0.98), steel,
|
||||
rotation=(radians(-25.0), 0, 0)))
|
||||
# Driver seat (shifted 0.1 back from the driver pin so the legs
|
||||
# dangle into the footwell opening and the gap behind the seat
|
||||
# closes).
|
||||
parts.append(add_box("seat-base", (0.55, 0.5, 0.12),
|
||||
(0, 0.45, 0.86), seat_mat))
|
||||
parts.append(add_box("seat-back", (0.55, 0.12, 0.5),
|
||||
(0, 0.68, 1.1), seat_mat))
|
||||
# Overhead guard: 4 posts + roof (raised so the seated driver's
|
||||
# head stays below the roof).
|
||||
for sx in (-0.58, 0.58):
|
||||
for sy in (-0.15, 0.95):
|
||||
parts.append(add_box("post", (0.08, 0.08, 1.20),
|
||||
(sx, sy, 1.395), steel))
|
||||
parts.append(add_box("roof", (1.28, 1.3, 0.06), (0, 0.4, 2.02),
|
||||
steel))
|
||||
# Mast rails at the very front, ahead of the front wheels (wheel
|
||||
# front edge is Ogre z 1.15; the mast front face lands at Ogre z
|
||||
# 1.14) so the fork carriage rides in front of them.
|
||||
for sx in (-0.45, 0.45):
|
||||
parts.append(add_box("mast", (0.14, 0.12, 1.5),
|
||||
(sx, -1.53, 0.95), steel))
|
||||
# Low cross beam at axle height linking the nose to the mast;
|
||||
# passes between the front wheels (beam x half 0.5 clears the
|
||||
# wheel inner edges at x +/-0.55).
|
||||
parts.append(add_box("mast-cross", (1.0, 0.75, 0.12),
|
||||
(0, -1.275, 0.35), steel))
|
||||
join_into("forklift", parts)
|
||||
|
||||
# Fork carriage + tines as a separate mesh: VehicleSystem attaches
|
||||
# it as a child node at VehicleComponent::forkOffset (chassis
|
||||
# space, (0,0,1.17) for this model — that shifts the assembly in
|
||||
# front of the front wheels) and raises it by the fork height, so
|
||||
# the geometry is modelled at the fork-down position relative to
|
||||
# the carriage. join_into() bakes the carriage location
|
||||
# (0, -1.12, 0.15) in as the origin offset, so a blender point
|
||||
# (X, Y, Z) of this mesh renders at Ogre (X, Z - 0.15, -(Y + 1.12))
|
||||
# plus forkOffset.
|
||||
fork_parts = []
|
||||
# Carriage plate sliding on the mast.
|
||||
fork_parts.append(add_box("carriage", (0.85, 0.06, 0.55),
|
||||
(0, -1.12, 0.15), steel))
|
||||
# Fork tines reaching forward at floor level: the chassis rides
|
||||
# ~0.73 above the floor, so the tines are dropped to Ogre y -0.70
|
||||
# (blender z -0.55) — just above the floor and the resting crate's
|
||||
# bottom edge. They have no collider; the chassis front face stops
|
||||
# at the crate.
|
||||
for sx in (-0.35, 0.35):
|
||||
fork_parts.append(add_box("fork", (0.16, 0.95, 0.05),
|
||||
(sx, -1.55, -0.55), steel))
|
||||
fork_parts.append(add_box("fork-heel", (0.16, 0.06, 0.45),
|
||||
(sx, -1.12, -0.325), steel))
|
||||
join_into("forklift-forks", fork_parts)
|
||||
|
||||
# Wheel: the wrapper's getWheelWorldTransform assumes a Y-axis
|
||||
# cylinder (Jolt GetWheelWorldTransform(inWheelRight=Y,
|
||||
# inWheelUp=X)), so keep the default Blender cylinder axis (Z),
|
||||
# which exports to Ogre Y.
|
||||
wheel = add_cylinder("forklift-wheel", 0.3, 0.2, (0, 0, 0), dark)
|
||||
wheel.data.name = "forklift-wheel"
|
||||
|
||||
# Steering wheel: modelled at the origin with the spin axis on
|
||||
# Blender Z (exports to Ogre Y); VehicleSystem places it at
|
||||
# VehicleComponent::steeringWheelOffset with a tilt and spins it
|
||||
# around that local axis with the steering. Rim + hub + 3 spokes.
|
||||
sw_parts = []
|
||||
bpy.ops.mesh.primitive_torus_add(major_segments=24,
|
||||
minor_segments=8,
|
||||
major_radius=0.19,
|
||||
minor_radius=0.028,
|
||||
location=(0, 0, 0))
|
||||
rim = bpy.context.active_object
|
||||
rim.name = "sw-rim"
|
||||
rim.data.materials.append(dark)
|
||||
sw_parts.append(rim)
|
||||
sw_parts.append(add_cylinder("sw-hub", 0.04, 0.06, (0, 0, 0), dark))
|
||||
for ang in (0, 60, 120):
|
||||
spoke = add_box("sw-spoke", (0.34, 0.03, 0.02), (0, 0, 0), dark)
|
||||
spoke.rotation_euler = (0, 0, radians(ang))
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True,
|
||||
scale=True)
|
||||
sw_parts.append(spoke)
|
||||
join_into("forklift-steering-wheel", sw_parts)
|
||||
|
||||
save_blend("forklift.blend")
|
||||
|
||||
|
||||
def gen_crate():
|
||||
"""crate.mesh: 1 m box centred at the origin (matches a 0.5 half
|
||||
extents box collider), with proud edge slats for a wooden-crate
|
||||
look."""
|
||||
reset_scene()
|
||||
wood = make_material("crate", (0.45, 0.3, 0.12))
|
||||
wood_dark = make_material("crate-frame", (0.32, 0.2, 0.08))
|
||||
|
||||
parts = []
|
||||
parts.append(add_box("core", (0.96, 0.96, 0.96), (0, 0, 0), wood))
|
||||
# 12 edge beams.
|
||||
e = 0.5
|
||||
b = 0.07
|
||||
for sx in (-e, e):
|
||||
for sy in (-e, e):
|
||||
parts.append(add_box("edge", (b, b, 1.0), (sx, sy, 0),
|
||||
wood_dark))
|
||||
for sx in (-e, e):
|
||||
for sz in (-e, e):
|
||||
parts.append(add_box("edge", (b, 1.0, b), (sx, 0, sz),
|
||||
wood_dark))
|
||||
for sy in (-e, e):
|
||||
for sz in (-e, e):
|
||||
parts.append(add_box("edge", (1.0, b, b), (0, sy, sz),
|
||||
wood_dark))
|
||||
join_into("crate", parts)
|
||||
save_blend("crate.blend")
|
||||
|
||||
|
||||
def gen_pad():
|
||||
"""pad.mesh: flat 1.2 x 1.2 marker slab, 4 cm thick, centred at the
|
||||
origin. The zone system swaps its material for covered/uncovered
|
||||
feedback, so keep a single named material."""
|
||||
reset_scene()
|
||||
pad_mat = make_material("pad", (0.9, 0.75, 0.1))
|
||||
parts = []
|
||||
parts.append(add_box("slab", (1.2, 1.2, 0.04), (0, 0, 0), pad_mat))
|
||||
join_into("pad", parts)
|
||||
save_blend("pad.blend")
|
||||
|
||||
|
||||
def gen_wall():
|
||||
"""wall.mesh: unit 1 x 1 x 1 cube centred at the origin, scaled
|
||||
per-instance in the scene for yard boundary walls (the buildings
|
||||
Cube.mesh is not 1-unit, so the demo gets its own)."""
|
||||
reset_scene()
|
||||
concrete = make_material("sokoban-wall", (0.55, 0.55, 0.52))
|
||||
parts = []
|
||||
parts.append(add_box("wall", (1.0, 1.0, 1.0), (0, 0, 0), concrete))
|
||||
join_into("wall", parts)
|
||||
save_blend("wall.blend")
|
||||
|
||||
|
||||
gen_forklift()
|
||||
gen_crate()
|
||||
gen_pad()
|
||||
gen_wall()
|
||||
@@ -0,0 +1,451 @@
|
||||
#!/usr/bin/env python
|
||||
"""Generate the demo-taxi .blend assets (sedan-a, sedan-taxi).
|
||||
|
||||
Run once with the project Blender:
|
||||
blender -b -Y -P assets/blender/scripts/gen_taxi_assets.py
|
||||
|
||||
Writes sedan-a.blend and sedan-taxi.blend into assets/blender/vehicles/,
|
||||
from where the import_vehicles CMake target exports them through
|
||||
export_vehicles.py like the other vehicles. After editing this script,
|
||||
re-run the command above and then:
|
||||
cmake --build build-vscode --target import_vehicles -j4
|
||||
|
||||
Axis convention: blender2ogre EX_SWAP_AXIS='xz-y' maps Blender
|
||||
(x, y, z) -> Ogre (x, z, -y), so Ogre +Z (vehicle forward, Jolt
|
||||
chassis +Z) is Blender -Y and Ogre +Y (up) is Blender +Z. The cars are
|
||||
modelled with the chassis origin at the axle-box centre, 0.58 m above
|
||||
the ground contact plane (Blender z -0.58), matching a physics box
|
||||
collider of half extents (0.9, 0.35, 2.4) centred at the origin.
|
||||
|
||||
Dimensions (Blender coords; front = -Y):
|
||||
overall length y -2.45..+2.45 (Ogre z +/-2.45, incl. bumpers)
|
||||
overall width x +/-0.95 (fender flares; doors/sills at +/-0.88)
|
||||
body bottom z -0.30, except the side skirts (sills and the
|
||||
front/rear side panels) which reach z -0.49 (9 cm
|
||||
ground clearance) so the seated character's feet --
|
||||
the sit pose dangles them below the floor pan --
|
||||
stay hidden behind the skirt from side views
|
||||
beltline z +0.32 (0.90 m above ground)
|
||||
roof z +1.02 (1.60 m above ground -- a chair-sitting
|
||||
character with feet on the floor pan (butt ~0.45 m
|
||||
above the feet, head ~+0.85 above the butt) clears
|
||||
the headliner)
|
||||
wheels (x +/-0.80, y -/+1.50, z -0.25), r 0.33, w 0.22
|
||||
|
||||
Origin baking: unlike gen_sokoban_assets.py (which lets join_into()
|
||||
bake the first part's location in as an origin offset), here every
|
||||
primitive gets its location applied to the mesh data right away, so
|
||||
joined meshes start out in chassis space with a zero origin -- the
|
||||
body shells (sedan-a-blue/-red/-grey, sedan-taxi) therefore have NO
|
||||
baked origin offset: mesh vertex (x, y, z) renders at Ogre
|
||||
(x, z, -y) relative to the chassis origin.
|
||||
|
||||
Doors/hood/trunk are modelled at the CLOSED position and then
|
||||
re-origined with set_origin() onto their hinge line (the object moves
|
||||
to the hinge, the mesh data becomes hinge-local), so a later phase can
|
||||
rotate them around the hinge:
|
||||
<body>-door-fl/-fr origin at the front-bottom hinge edge,
|
||||
panel spans backward (+Y) and up (+Z)
|
||||
<body>-door-rl/-rr same, hinge at the door's front-bottom edge
|
||||
<body>-hood origin at the cowl (rear edge of the hood)
|
||||
<body>-trunk origin at the cabin-side edge of the trunk lid
|
||||
The shell meshes carry the door/hood/trunk openings (sills, cowl,
|
||||
bulkhead, fenders), so shell + parts fit together flush (parts embed
|
||||
~2 cm into the shell to avoid coincident-face z-fighting).
|
||||
|
||||
Part list per body variant (exported mesh data names, referenced by
|
||||
the scene JSON):
|
||||
sedan-a.blend: sedan-a-blue, sedan-a-red, sedan-a-grey
|
||||
+ <name>-door-fl/-fr/-rl/-rr, <name>-hood,
|
||||
<name>-trunk for each
|
||||
+ sedan-wheel, sedan-steering-wheel (shared)
|
||||
sedan-taxi.blend: sedan-taxi
|
||||
+ sedan-taxi-door-fl/-fr/-rl/-rr,
|
||||
sedan-taxi-hood, sedan-taxi-trunk
|
||||
(reuses sedan-wheel/-steering-wheel from
|
||||
sedan-a.blend -- do NOT duplicate them here)
|
||||
|
||||
The sedan-wheel cylinder axis is Blender Z (exports to Ogre Y), like
|
||||
the forklift's: the wrapper's getWheelWorldTransform assumes a Y-axis
|
||||
cylinder. The steering wheel (rim + hub + 3 spokes at the origin)
|
||||
also spins around Blender Z. Doors on Blender +X are the Ogre-left
|
||||
(-fl/-rl) side (facing Ogre +Z, +X is left).
|
||||
|
||||
Materials are plain named Principled-BSDF materials; blender2ogre
|
||||
writes one .material file per material (EX_SEPARATE_MATERIALS).
|
||||
sedan-taxi.blend re-defines the shared ones (sedan-glass, sedan-trim,
|
||||
...) with identical values, which simply re-writes the same files.
|
||||
"""
|
||||
|
||||
import os
|
||||
import bpy
|
||||
from math import radians
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"..", "vehicles")
|
||||
|
||||
# Shared hardpoints (see the file docstring).
|
||||
Z_BOT = -0.30 # body bottom edge (ground contact at -0.58)
|
||||
BELT = 0.32 # beltline: solid body below, glasshouse above
|
||||
ROOF = 1.02 # roof exterior (1.60 m above ground -- a chair-sitting
|
||||
# character (feet on the floor pan, head ~0.85 above
|
||||
# the cushion) fits under the headliner)
|
||||
WHEEL_X = 0.80
|
||||
WHEEL_Y = 1.50 # front axle at Blender y -1.50 (Ogre z +1.50)
|
||||
WHEEL_Z = -0.25
|
||||
WHEEL_R = 0.33
|
||||
WHEEL_W = 0.22
|
||||
SIDE_X = 0.88 # door/sill outer face
|
||||
FENDER_X = 0.95 # fender flare outer face
|
||||
|
||||
# Door openings (y extents) and hinge positions.
|
||||
DOOR_F = (-0.55, 0.44) # front door, hinge at y -0.55 (front-bottom)
|
||||
DOOR_R = (0.46, 1.12) # rear door, hinge at y 0.46 (front-bottom)
|
||||
DOOR_Z = (-0.10, BELT) # panel z extent (bottom embeds into the sill)
|
||||
|
||||
# Silhouette parameters per body type.
|
||||
TYPE_A = dict(hood_top=0.20, nose_top=0.14, tail_top=0.30, trunk_top=0.30,
|
||||
glass=(0.78, -0.70, 1.30, 0.60, -0.42, 1.02),
|
||||
stripe=False, sign=False)
|
||||
TYPE_B = dict(hood_top=0.22, nose_top=0.16, tail_top=0.34, trunk_top=0.34,
|
||||
glass=(0.82, -0.78, 1.32, 0.70, -0.55, 1.10),
|
||||
stripe=True, sign=True)
|
||||
|
||||
|
||||
def reset_scene():
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
|
||||
|
||||
def make_material(name, color, roughness=0.8, metallic=0.0):
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
bsdf = mat.node_tree.nodes["Principled BSDF"]
|
||||
bsdf.inputs["Base Color"].default_value = (*color, 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = roughness
|
||||
bsdf.inputs["Metallic"].default_value = metallic
|
||||
return mat
|
||||
|
||||
|
||||
def add_box(name, dims, loc, mat=None):
|
||||
"""Add a box at a world-space location; the location is applied to
|
||||
the mesh data immediately so the object location stays zero (no
|
||||
hidden origin offset for join_into()/the exporter)."""
|
||||
bpy.ops.mesh.primitive_cube_add(size=1.0, location=loc)
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
obj.dimensions = dims
|
||||
bpy.ops.object.transform_apply(location=True, rotation=False,
|
||||
scale=True)
|
||||
if mat:
|
||||
obj.data.materials.append(mat)
|
||||
return obj
|
||||
|
||||
|
||||
def add_cylinder(name, radius, depth, loc, mat=None, rotation=None,
|
||||
vertices=24):
|
||||
bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius,
|
||||
depth=depth, location=loc)
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
if rotation:
|
||||
obj.rotation_euler = rotation
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True,
|
||||
scale=True)
|
||||
if mat:
|
||||
obj.data.materials.append(mat)
|
||||
return obj
|
||||
|
||||
|
||||
def add_frustum(name, bx, by0, by1, tx, ty0, ty1, z0, z1, mat=None):
|
||||
"""Tapered box (truncated pyramid) between z0 (bottom rect half
|
||||
width bx, y by0..by1) and z1 (top rect tx, ty0..ty1) -- used for
|
||||
the glasshouse. Mesh data is in world space, object at origin."""
|
||||
verts = [(-bx, by0, z0), (bx, by0, z0), (bx, by1, z0), (-bx, by1, z0),
|
||||
(-tx, ty0, z1), (tx, ty0, z1), (tx, ty1, z1), (-tx, ty1, z1)]
|
||||
# Winding gives outward normals (bottom down, top up, sides out).
|
||||
faces = [(0, 3, 2, 1), (4, 5, 6, 7),
|
||||
(0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)]
|
||||
mesh = bpy.data.meshes.new(name)
|
||||
mesh.from_pydata(verts, [], faces)
|
||||
mesh.update()
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
if mat:
|
||||
mesh.materials.append(mat)
|
||||
return obj
|
||||
|
||||
|
||||
def join_into(name, objects):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
for obj in objects:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = objects[0]
|
||||
bpy.ops.object.join()
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
obj.data.name = name # the exporter names .mesh files after mesh data
|
||||
return obj
|
||||
|
||||
|
||||
def set_origin(obj, point):
|
||||
"""Move the mesh data origin to a world-space hinge point: verts
|
||||
become hinge-local, the object moves to the hinge (closed pose
|
||||
unchanged). Exported .mesh vertices are hinge-local because the
|
||||
exporter does not apply object locations."""
|
||||
obj.data.transform(Matrix.Translation(Vector((-point[0], -point[1],
|
||||
-point[2]))))
|
||||
obj.location = point
|
||||
|
||||
|
||||
def save_blend(filename):
|
||||
path = os.path.join(OUT_DIR, filename)
|
||||
bpy.ops.wm.save_as_mainfile(filepath=path)
|
||||
print("wrote " + path)
|
||||
|
||||
|
||||
def make_shared_materials():
|
||||
"""Materials shared by both .blends (identical definitions)."""
|
||||
return {
|
||||
'glass': make_material("sedan-glass", (0.04, 0.06, 0.08),
|
||||
roughness=0.15, metallic=0.3),
|
||||
'trim': make_material("sedan-trim", (0.10, 0.10, 0.11)),
|
||||
'interior': make_material("sedan-interior", (0.16, 0.14, 0.12)),
|
||||
'light': make_material("sedan-light", (0.95, 0.92, 0.75),
|
||||
roughness=0.3),
|
||||
'taillight': make_material("sedan-taillight", (0.55, 0.05, 0.05),
|
||||
roughness=0.3),
|
||||
}
|
||||
|
||||
|
||||
def build_shell(prefix, paint, mats, cfg):
|
||||
"""Body shell with the door/hood/trunk openings: floor pan, sills,
|
||||
cowl, engine bay, nose, fenders, bulkhead, trunk well, tail,
|
||||
bumpers, lights, glasshouse + roof cap (plus taxi sign/stripes for
|
||||
type B). Exported with a zero origin (chassis space)."""
|
||||
glass, trim, interior = mats['glass'], mats['trim'], mats['interior']
|
||||
ht, nt = cfg['hood_top'], cfg['nose_top']
|
||||
tt, kt = cfg['tail_top'], cfg['trunk_top']
|
||||
parts = []
|
||||
|
||||
# Floor pan and rockers (sills carry the door bottoms). The sill
|
||||
# reaches down to z -0.49 (side skirt, see the file docstring).
|
||||
parts.append(add_box("floor", (1.30, 2.10, 0.18), (0, 0.30, -0.21),
|
||||
trim))
|
||||
for sx in (-0.765, 0.765):
|
||||
parts.append(add_box("sill", (0.23, 1.80, 0.41),
|
||||
(sx, 0.35, -0.285), paint))
|
||||
# Cowl/dash panel between hood and glasshouse (front door hinges
|
||||
# sit on its lower corners).
|
||||
parts.append(add_box("cowl", (1.76, 0.22, 0.62), (0, -0.64, 0.01),
|
||||
paint))
|
||||
# Engine bay block under the hood (dark, seen through panel gaps).
|
||||
parts.append(add_box("bay", (1.30, 1.42, 0.40), (0, -1.44, -0.10),
|
||||
trim))
|
||||
# Nose: grille block + bumper + headlights.
|
||||
parts.append(add_box("nose", (1.70, 0.19, nt + 0.30),
|
||||
(0, -2.225, nt / 2 - 0.15), paint))
|
||||
parts.append(add_box("bumper-f", (1.80, 0.13, 0.24),
|
||||
(0, -2.385, -0.17), trim))
|
||||
parts.append(add_box("grille", (0.90, 0.06, 0.14),
|
||||
(0, -2.30, nt - 0.16), trim))
|
||||
for sx in (-0.58, 0.58):
|
||||
parts.append(add_box("headlight", (0.35, 0.06, 0.12),
|
||||
(sx, -2.30, nt - 0.10), mats['light']))
|
||||
# Front side panels ahead of and behind the front wheel arch
|
||||
# (arch opening y -1.88..-1.10, fender over the top); both reach
|
||||
# down to z -0.49 (side skirt, see the file docstring).
|
||||
for sx in (-0.765, 0.765):
|
||||
parts.append(add_box("fside-f", (0.23, 0.27, nt + 0.49),
|
||||
(sx, -2.015, nt / 2 - 0.245), paint))
|
||||
parts.append(add_box("fside-r", (0.23, 0.37, 0.81),
|
||||
(sx, -0.915, -0.085), paint))
|
||||
for sx in (-0.80, 0.80):
|
||||
parts.append(add_box("fender-f", (0.30, 0.78, ht - 0.05),
|
||||
(sx, -1.49, ht / 2 + 0.035), paint))
|
||||
# Interior: dashboard, front seats, rear bench (seen through the
|
||||
# glasshouse).
|
||||
parts.append(add_box("dash", (1.30, 0.20, 0.18), (0, -0.45, 0.20),
|
||||
interior))
|
||||
for sx in (-0.35, 0.35):
|
||||
parts.append(add_box("seat-f", (0.50, 0.45, 0.30),
|
||||
(sx, 0.0, 0.03), interior))
|
||||
parts.append(add_box("seat-f-back", (0.50, 0.14, 0.55),
|
||||
(sx, 0.26, 0.20), interior))
|
||||
parts.append(add_box("bench", (1.30, 0.45, 0.28), (0, 0.85, 0.02),
|
||||
interior))
|
||||
parts.append(add_box("bench-back", (1.30, 0.16, 0.50),
|
||||
(0, 1.10, 0.20), interior))
|
||||
# Rear bulkhead (trunk hinges on its top), trunk well, tail,
|
||||
# rear fenders, bumper, taillights.
|
||||
parts.append(add_box("bulkhead", (1.76, 0.22, 0.62), (0, 1.25, 0.01),
|
||||
paint))
|
||||
parts.append(add_box("well", (1.30, 0.80, 0.40), (0, 1.75, -0.10),
|
||||
trim))
|
||||
for sx in (-0.80, 0.80):
|
||||
parts.append(add_box("fender-r", (0.30, 0.78, tt - 0.05),
|
||||
(sx, 1.49, tt / 2 + 0.035), paint))
|
||||
for sx in (-0.765, 0.765):
|
||||
parts.append(add_box("rside", (0.23, 0.27, tt + 0.51),
|
||||
(sx, 2.015, tt / 2 - 0.235), paint))
|
||||
parts.append(add_box("tail", (1.70, 0.19, tt + 0.30),
|
||||
(0, 2.205, tt / 2 - 0.15), paint))
|
||||
parts.append(add_box("bumper-r", (1.80, 0.15, 0.24),
|
||||
(0, 2.375, -0.17), trim))
|
||||
for sx in (-0.58, 0.58):
|
||||
parts.append(add_box("taillight", (0.35, 0.06, 0.12),
|
||||
(sx, 2.30, tt - 0.18), mats['taillight']))
|
||||
# Glasshouse: tapered glass box from the beltline to just under
|
||||
# the roof, body-colour roof cap on top.
|
||||
bx, by0, by1, tx, ty0, ty1 = cfg['glass']
|
||||
parts.append(add_frustum("glasshouse", bx, by0, by1, tx, ty0, ty1,
|
||||
BELT, ROOF - 0.05, glass))
|
||||
parts.append(add_box("roof", (2 * tx + 0.08, ty1 - ty0 + 0.08, 0.05),
|
||||
(0, (ty0 + ty1) / 2, ROOF - 0.025), paint))
|
||||
if cfg['stripe']:
|
||||
stripe = mats['stripe']
|
||||
# Colour-blocked dark stripe band (z 0.02..0.14) along the
|
||||
# shell sides; the door segments live in the door meshes and
|
||||
# the band breaks over the wheel arches.
|
||||
for sx in (-0.895, 0.895):
|
||||
parts.append(add_box("stripe-f", (0.03, 0.25, 0.12),
|
||||
(sx, -2.015, 0.08), stripe))
|
||||
parts.append(add_box("stripe-b", (0.03, 0.20, 0.12),
|
||||
(sx, 1.25, 0.08), stripe))
|
||||
parts.append(add_box("stripe-r", (0.03, 0.25, 0.12),
|
||||
(sx, 2.015, 0.08), stripe))
|
||||
if cfg['sign']:
|
||||
parts.append(add_box("sign-base", (0.60, 0.34, 0.04),
|
||||
(0, 0.30, ROOF + 0.02), trim))
|
||||
parts.append(add_box("sign", (0.55, 0.28, 0.16),
|
||||
(0, 0.30, ROOF + 0.11), mats['sign']))
|
||||
return join_into(prefix, parts)
|
||||
|
||||
|
||||
def build_door(name, paint, mats, cfg, front, left):
|
||||
"""One door: panel + handle (+ taxi stripe), painted to match the
|
||||
body, origin on the front-bottom hinge edge."""
|
||||
(y0, y1) = DOOR_F if front else DOOR_R
|
||||
(z0, z1) = DOOR_Z
|
||||
sx = SIDE_X - 0.025 if left else -(SIDE_X - 0.025)
|
||||
length = y1 - y0
|
||||
cy = (y0 + y1) / 2
|
||||
out = SIDE_X + 0.015 if left else -(SIDE_X + 0.015)
|
||||
parts = [add_box("panel", (0.05, length, z1 - z0),
|
||||
(sx, cy, (z0 + z1) / 2), paint)]
|
||||
parts.append(add_box("handle", (0.03, 0.15, 0.03),
|
||||
(out, y1 - 0.18, BELT - 0.08), mats['trim']))
|
||||
if cfg['stripe']:
|
||||
parts.append(add_box("stripe", (0.03, length - 0.04, 0.12),
|
||||
(out, cy, 0.08), mats['stripe']))
|
||||
obj = join_into(name, parts)
|
||||
set_origin(obj, (sx, y0, z0))
|
||||
return obj
|
||||
|
||||
|
||||
def build_hood(name, paint, cfg):
|
||||
"""Hood slab, origin at the cowl (rear edge of the hood)."""
|
||||
ht = cfg['hood_top']
|
||||
parts = [add_box("hood", (1.60, 1.44, 0.06), (0, -1.44, ht - 0.03),
|
||||
paint)]
|
||||
obj = join_into(name, parts)
|
||||
set_origin(obj, (0, -0.75, ht - 0.03))
|
||||
return obj
|
||||
|
||||
|
||||
def build_trunk(name, paint, cfg):
|
||||
"""Trunk lid, origin at its cabin-side edge (near the back
|
||||
glass)."""
|
||||
kt = cfg['trunk_top']
|
||||
parts = [add_box("trunk", (1.60, 0.84, 0.06), (0, 1.74, kt - 0.03),
|
||||
paint)]
|
||||
obj = join_into(name, parts)
|
||||
set_origin(obj, (0, 1.35, kt - 0.03))
|
||||
return obj
|
||||
|
||||
|
||||
def build_body(prefix, paint, mats, cfg):
|
||||
"""One full car: shell + 4 doors + hood + trunk (7 meshes)."""
|
||||
build_shell(prefix, paint, mats, cfg)
|
||||
for front, tag_f in ((True, 'f'), (False, 'r')):
|
||||
for left, tag_s in ((True, 'l'), (False, 'r')):
|
||||
build_door("%s-door-%s%s" % (prefix, tag_f, tag_s),
|
||||
paint, mats, cfg, front, left)
|
||||
build_hood(prefix + "-hood", paint, cfg)
|
||||
build_trunk(prefix + "-trunk", paint, cfg)
|
||||
|
||||
|
||||
def build_wheel():
|
||||
"""sedan-wheel: dark tire + lighter hub as one mesh (two
|
||||
submeshes). Cylinder axis stays on Blender Z so it exports to
|
||||
Ogre Y, like the forklift wheel."""
|
||||
tire = make_material("sedan-tire", (0.07, 0.07, 0.07), roughness=0.95)
|
||||
hub = make_material("sedan-hub", (0.55, 0.55, 0.58), roughness=0.4,
|
||||
metallic=0.6)
|
||||
parts = [add_cylinder("tire", WHEEL_R, WHEEL_W, (0, 0, 0), tire,
|
||||
vertices=16),
|
||||
add_cylinder("hub", 0.17, WHEEL_W + 0.02, (0, 0, 0), hub,
|
||||
vertices=12)]
|
||||
join_into("sedan-wheel", parts)
|
||||
|
||||
|
||||
def build_steering_wheel():
|
||||
"""sedan-steering-wheel: rim + hub + 3 spokes at the origin, spin
|
||||
axis Blender Z (exports to Ogre Y), like the forklift's."""
|
||||
dark = make_material("sedan-steering", (0.08, 0.08, 0.09))
|
||||
parts = []
|
||||
bpy.ops.mesh.primitive_torus_add(major_segments=24, minor_segments=8,
|
||||
major_radius=0.17,
|
||||
minor_radius=0.025,
|
||||
location=(0, 0, 0))
|
||||
rim = bpy.context.active_object
|
||||
rim.name = "sw-rim"
|
||||
rim.data.materials.append(dark)
|
||||
parts.append(rim)
|
||||
parts.append(add_cylinder("sw-hub", 0.035, 0.06, (0, 0, 0), dark))
|
||||
for ang in (0, 60, 120):
|
||||
spoke = add_box("sw-spoke", (0.30, 0.025, 0.02), (0, 0, 0), dark)
|
||||
spoke.rotation_euler = (0, 0, radians(ang))
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True,
|
||||
scale=True)
|
||||
parts.append(spoke)
|
||||
join_into("sedan-steering-wheel", parts)
|
||||
|
||||
|
||||
def gen_sedan_a():
|
||||
"""sedan-a.blend: type-A body (three-box sedan after Car2.blend)
|
||||
in three colours, plus the shared wheel and steering wheel."""
|
||||
reset_scene()
|
||||
mats = make_shared_materials()
|
||||
blue = make_material("sedan-a-blue", (0.08, 0.18, 0.65),
|
||||
roughness=0.35, metallic=0.2)
|
||||
red = make_material("sedan-a-red", (0.62, 0.07, 0.07),
|
||||
roughness=0.35, metallic=0.2)
|
||||
grey = make_material("sedan-a-grey", (0.45, 0.46, 0.48),
|
||||
roughness=0.35, metallic=0.2)
|
||||
build_body("sedan-a-blue", blue, mats, TYPE_A)
|
||||
build_body("sedan-a-red", red, mats, TYPE_A)
|
||||
build_body("sedan-a-grey", grey, mats, TYPE_A)
|
||||
build_wheel()
|
||||
build_steering_wheel()
|
||||
save_blend("sedan-a.blend")
|
||||
|
||||
|
||||
def gen_sedan_taxi():
|
||||
"""sedan-taxi.blend: type-B body (boxier, upright glasshouse after
|
||||
Car5_Taxi.blend) in taxi yellow with a roof sign and a dark
|
||||
checker-stripe band. Reuses sedan-wheel/-steering-wheel from
|
||||
sedan-a.blend."""
|
||||
reset_scene()
|
||||
mats = make_shared_materials()
|
||||
mats['stripe'] = make_material("sedan-taxi-dark", (0.08, 0.08, 0.08))
|
||||
mats['sign'] = make_material("taxi-sign", (0.95, 0.85, 0.40),
|
||||
roughness=0.4)
|
||||
yellow = make_material("sedan-taxi-yellow", (0.85, 0.60, 0.05),
|
||||
roughness=0.35, metallic=0.2)
|
||||
build_body("sedan-taxi", yellow, mats, TYPE_B)
|
||||
save_blend("sedan-taxi.blend")
|
||||
|
||||
|
||||
gen_sedan_a()
|
||||
gen_sedan_taxi()
|
||||
@@ -0,0 +1,484 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Vehicle pipeline generator for convention-built vehicle blends.
|
||||
#
|
||||
# Does everything export_vehicles.py does (blender2ogre .scene/.mesh/
|
||||
# .material export + a .glb preview) and additionally derives a
|
||||
# ready-to-merge scene-entity JSON fragment (<name>.vehicle.json) from
|
||||
# the blend by naming convention:
|
||||
#
|
||||
# root the single parentless MESH object; its mesh DATA frame
|
||||
# is the chassis frame (object location is NOT baked into
|
||||
# the .mesh, so author the mesh data relative to the
|
||||
# intended chassis origin).
|
||||
# * Tire wheel objects; mesh data is rotated so the axle lands
|
||||
# on Blender Z (Ogre Y, the convention
|
||||
# getWheelWorldTransform assumes); the objects are
|
||||
# counter-rotated so the scene look is unchanged.
|
||||
# * Door door child entities, hinge-local mesh data (origin on
|
||||
# the hinge line); each gets a seat actuator for the
|
||||
# nearest seat.
|
||||
# * hood hood child entity.
|
||||
# * trunk trunk child entity.
|
||||
# * Steering steering wheel; mesh data is rotated so the spin axis
|
||||
# lands on Blender Z (Ogre Y, the convention VehicleSystem
|
||||
# assumes for steeringWheelMeshName).
|
||||
# seat_* EMPTY objects marking character seat pins (character
|
||||
# root with the sitting-vehicle animation; no offsets
|
||||
# applied). The one with "driver" in the name becomes
|
||||
# the driver seat.
|
||||
#
|
||||
# The emitted JSON has Ogre coordinates ((x, z, -y) of the Blender
|
||||
# chassis frame). See assets/blender/vehicles/CAR_MODELING.md.
|
||||
#
|
||||
# Usage:
|
||||
# blender <vehicle>.blend -b -Y -P gen_vehicle.py -- <out>.glb <out>.vehicle.json
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
import bpy
|
||||
from math import radians
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
argv = sys.argv
|
||||
argv = argv[argv.index("--") + 1:]
|
||||
|
||||
incpath = os.path.dirname(__file__)
|
||||
|
||||
sys.path.insert(0, incpath)
|
||||
sys.path.insert(1, incpath + "/blender2ogre")
|
||||
|
||||
import io_ogre
|
||||
try:
|
||||
io_ogre.register()
|
||||
except:
|
||||
pass
|
||||
|
||||
glb_file = argv[0]
|
||||
json_file = argv[1]
|
||||
outdir = os.path.dirname(glb_file)
|
||||
base = os.path.splitext(os.path.basename(bpy.data.filepath))[0]
|
||||
|
||||
print("gen_vehicle: %s -> %s, %s" % (bpy.data.filepath, glb_file,
|
||||
json_file))
|
||||
|
||||
for obj in list(bpy.data.objects):
|
||||
if obj.name.endswith("-col"):
|
||||
bpy.data.objects.remove(obj)
|
||||
|
||||
# GLB preview of the untouched scene (as modeled).
|
||||
bpy.ops.export_scene.gltf(filepath=glb_file,
|
||||
use_selection=False,
|
||||
check_existing=False,
|
||||
export_format='GLB',
|
||||
export_texture_dir='textures', export_texcoords=True,
|
||||
export_normals=True,
|
||||
export_tangents=True,
|
||||
export_materials='EXPORT',
|
||||
export_colors=True,
|
||||
use_mesh_edges=False,
|
||||
use_mesh_vertices=False,
|
||||
export_cameras=False,
|
||||
use_visible=False,
|
||||
use_renderable=False,
|
||||
export_yup=True,
|
||||
export_apply=False,
|
||||
export_animations=False,
|
||||
export_morph=True,
|
||||
export_morph_animation=False,
|
||||
export_morph_normal=True,
|
||||
export_morph_tangent=True,
|
||||
export_lights=False,
|
||||
export_skins=True)
|
||||
|
||||
|
||||
def chassis_pos(obj, root_inv):
|
||||
"""Object origin in the chassis frame (root mesh-data frame)."""
|
||||
return root_inv @ obj.matrix_world.translation
|
||||
|
||||
|
||||
def mesh_bbox(me):
|
||||
vs = [v.co for v in me.vertices]
|
||||
mn = Vector((min(v.x for v in vs), min(v.y for v in vs),
|
||||
min(v.z for v in vs)))
|
||||
mx = Vector((max(v.x for v in vs), max(v.y for v in vs),
|
||||
max(v.z for v in vs)))
|
||||
return mn, mx
|
||||
|
||||
|
||||
def ogre(v):
|
||||
"""Blender chassis frame -> Ogre (x, z, -y)."""
|
||||
return (round(v.x, 4), round(v.z, 4), round(-v.y, 4))
|
||||
|
||||
|
||||
def ogre_vec3(v):
|
||||
x, y, z = ogre(v)
|
||||
return {"x": x, "y": y, "z": z}
|
||||
|
||||
|
||||
# --- find the root (the single parentless MESH object) ---
|
||||
roots = [o for o in bpy.data.objects
|
||||
if o.type == 'MESH' and o.parent is None]
|
||||
if len(roots) != 1:
|
||||
raise RuntimeError("gen_vehicle: expected exactly one parentless "
|
||||
"MESH object (the body shell), found %d: %s" %
|
||||
(len(roots), [o.name for o in roots]))
|
||||
root = roots[0]
|
||||
root_inv = root.matrix_world.inverted()
|
||||
print("gen_vehicle: root '%s' (mesh data '%s')" %
|
||||
(root.name, root.data.name))
|
||||
|
||||
# --- classify parts by naming convention ---
|
||||
wheels = []
|
||||
doors = []
|
||||
seats = []
|
||||
hood = None
|
||||
trunk = None
|
||||
steering = None
|
||||
extras = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if obj == root:
|
||||
continue
|
||||
lname = obj.name.lower()
|
||||
if obj.type == 'EMPTY':
|
||||
if lname.startswith("seat"):
|
||||
seats.append(obj)
|
||||
continue
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
if "tire" in lname or "wheel" in lname:
|
||||
wheels.append(obj)
|
||||
elif "door" in lname:
|
||||
doors.append(obj)
|
||||
elif "hood" in lname or "bonnet" in lname:
|
||||
hood = obj
|
||||
elif "trunk" in lname:
|
||||
trunk = obj
|
||||
elif "steering" in lname:
|
||||
steering = obj
|
||||
else:
|
||||
extras.append(obj)
|
||||
|
||||
if len(wheels) < 2:
|
||||
raise RuntimeError("gen_vehicle: no wheel objects found "
|
||||
"(name them '* Tire')")
|
||||
if not seats:
|
||||
raise RuntimeError("gen_vehicle: no seat empties found "
|
||||
"(name them 'seat_*')")
|
||||
print("gen_vehicle: %d wheels, %d doors, %d seats, hood=%s, trunk=%s, "
|
||||
"steering=%s, extras=%s" %
|
||||
(len(wheels), len(doors), len(seats),
|
||||
hood.name if hood else "-", trunk.name if trunk else "-",
|
||||
steering.name if steering else "-",
|
||||
[o.name for o in extras]))
|
||||
|
||||
# --- wheel geometry (from the shared tire mesh data, pre-rotation) ---
|
||||
tyme = wheels[0].data
|
||||
wmn, wmx = mesh_bbox(tyme)
|
||||
wheel_radius = max(wmx.y - wmn.y, wmx.z - wmn.z) / 2.0
|
||||
wheel_width = wmx.x - wmn.x
|
||||
|
||||
wheel_info = []
|
||||
for obj in wheels:
|
||||
p = chassis_pos(obj, root_inv)
|
||||
wheel_info.append({"obj": obj, "pos": p})
|
||||
axle_mid_y = (min(w["pos"].y for w in wheel_info) +
|
||||
max(w["pos"].y for w in wheel_info)) / 2.0
|
||||
|
||||
# --- seats ---
|
||||
seat_info = []
|
||||
for obj in seats:
|
||||
p = chassis_pos(obj, root_inv)
|
||||
seat_info.append({"obj": obj, "pos": p,
|
||||
"front": p.y < axle_mid_y,
|
||||
"left": p.x > 0.0,
|
||||
"driver": "driver" in obj.name.lower()})
|
||||
driver_seats = [s for s in seat_info if s["driver"]]
|
||||
if len(driver_seats) != 1:
|
||||
raise RuntimeError("gen_vehicle: expected exactly one seat empty "
|
||||
"with 'driver' in the name, found %d" %
|
||||
len(driver_seats))
|
||||
for s in seat_info:
|
||||
if s["driver"]:
|
||||
s["name"] = "driver"
|
||||
elif s["front"]:
|
||||
s["name"] = "passenger"
|
||||
else:
|
||||
s["name"] = "rl" if s["left"] else "rr"
|
||||
|
||||
# --- rename mesh data blocks to the <base>-<role> scheme ---
|
||||
def part_side_row(p):
|
||||
return ("f" if p.y < axle_mid_y else "r") + \
|
||||
("l" if p.x > 0.0 else "r")
|
||||
|
||||
|
||||
def sanitize(name):
|
||||
return name.lower().replace(" ", "-").replace("_", "-")
|
||||
|
||||
|
||||
root.data.name = base
|
||||
for w in wheel_info:
|
||||
w["obj"].data.name = base + "-wheel"
|
||||
for d in doors:
|
||||
p = chassis_pos(d, root_inv)
|
||||
d.data.name = base + "-door-" + part_side_row(p)
|
||||
if hood:
|
||||
hood.data.name = base + "-hood"
|
||||
if trunk:
|
||||
trunk.data.name = base + "-trunk"
|
||||
if steering:
|
||||
steering.data.name = base + "-steering-wheel"
|
||||
for e in extras:
|
||||
e.data.name = base + "-" + sanitize(e.data.name)
|
||||
|
||||
# --- rename materials (<base>-<material>) and unpack textures ---
|
||||
used_mats = set()
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH':
|
||||
for m in obj.data.materials:
|
||||
if m:
|
||||
used_mats.add(m)
|
||||
for m in used_mats:
|
||||
if not m.name.lower().startswith(base.lower()):
|
||||
m.name = base + "-" + sanitize(m.name)
|
||||
|
||||
tex_index = 0
|
||||
saved_images = set()
|
||||
for m in used_mats:
|
||||
if not m.node_tree:
|
||||
continue
|
||||
for n in m.node_tree.nodes:
|
||||
if n.type != 'TEX_IMAGE' or not n.image:
|
||||
continue
|
||||
img = n.image
|
||||
if img in saved_images or not img.packed_file:
|
||||
continue
|
||||
saved_images.add(img)
|
||||
tex_name = base + ("-atlas.png" if tex_index == 0 else
|
||||
"-tex%d.png" % tex_index)
|
||||
tex_index += 1
|
||||
img.filepath_raw = os.path.join(outdir, tex_name)
|
||||
img.file_format = 'PNG'
|
||||
img.save()
|
||||
print("gen_vehicle: unpacked texture '%s' -> %s" %
|
||||
(img.name, tex_name))
|
||||
|
||||
# --- JSON fragment (Ogre coordinates) ---
|
||||
smn, smx = mesh_bbox(root.data)
|
||||
collider = {
|
||||
"halfHeight": 1.0,
|
||||
"meshName": "",
|
||||
"offset": ogre_vec3(Vector(((smx.x + smn.x) / 2.0,
|
||||
(smx.y + smn.y) / 2.0,
|
||||
(smx.z + smn.z) / 2.0))),
|
||||
"parameters": {
|
||||
"x": round((smx.x - smn.x) / 2.0, 4),
|
||||
"y": round((smx.z - smn.z) / 2.0, 4),
|
||||
"z": round((smx.y - smn.y) / 2.0, 4),
|
||||
},
|
||||
"radius": 0.5,
|
||||
"rotationOffset": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0},
|
||||
"shapeType": "box",
|
||||
}
|
||||
|
||||
json_wheels = []
|
||||
# Jolt's suspension spring settles at L_rest = maxLength - g/(2*pi*f)^2
|
||||
# below the attach point. The modeled wheel centers are the intended
|
||||
# REST position, so place the attach points L_rest above them — the car
|
||||
# then rides exactly at the modeled stance.
|
||||
SUSP_MIN = 0.1
|
||||
SUSP_MAX = 0.35
|
||||
# Softer than the 1.8/0.7 starting point: gives the body some slack
|
||||
# over bumps instead of pogo-stiff wheels (which also cost grip).
|
||||
SUSP_FREQ = 1.3
|
||||
SUSP_DAMP = 0.6
|
||||
SUSP_REST = SUSP_MAX - 9.81 / (2.0 * math.pi * SUSP_FREQ) ** 2
|
||||
for w in sorted(wheel_info, key=lambda w: w["pos"].y):
|
||||
front = w["pos"].y < axle_mid_y
|
||||
attach = Vector((w["pos"].x, w["pos"].y, w["pos"].z + SUSP_REST))
|
||||
json_wheels.append({
|
||||
"driven": not front,
|
||||
"maxHandBrakeTorque": 0.0 if front else 300.0,
|
||||
"maxSteerAngleDeg": 35.0 if front else 0.0,
|
||||
"position": ogre_vec3(attach),
|
||||
"radius": round(wheel_radius, 4),
|
||||
"suspensionDamping": SUSP_DAMP,
|
||||
"suspensionFrequency": SUSP_FREQ,
|
||||
"suspensionMaxLength": SUSP_MAX,
|
||||
"suspensionMinLength": SUSP_MIN,
|
||||
"width": round(wheel_width, 4),
|
||||
})
|
||||
|
||||
json_seats = []
|
||||
for s in seat_info:
|
||||
json_seats.append({
|
||||
"name": s["name"],
|
||||
"offset": ogre_vec3(s["pos"]),
|
||||
"driver": s["driver"],
|
||||
})
|
||||
|
||||
driver_offset = ogre_vec3(driver_seats[0]["pos"])
|
||||
|
||||
children = []
|
||||
|
||||
|
||||
def make_child(name, mesh, pos, actuator_action=None):
|
||||
child = {
|
||||
"children": [],
|
||||
"name": {"name": name},
|
||||
"renderable": {"meshName": mesh + ".mesh", "visible": True},
|
||||
"transform": {
|
||||
"position": ogre_vec3(pos),
|
||||
"rotation": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0},
|
||||
"scale": {"x": 1.0, "y": 1.0, "z": 1.0},
|
||||
},
|
||||
}
|
||||
if actuator_action:
|
||||
child["actuator"] = {
|
||||
"actionNames": [actuator_action],
|
||||
"height": 2.0,
|
||||
"radius": 1.2,
|
||||
}
|
||||
return child
|
||||
|
||||
|
||||
def nearest_seat_name(p):
|
||||
best = None
|
||||
best_dist = 1e9
|
||||
for s in seat_info:
|
||||
d = (s["pos"] - p).length
|
||||
if d < best_dist:
|
||||
best_dist = d
|
||||
best = s["name"]
|
||||
return best
|
||||
|
||||
|
||||
def seat_for_door(p):
|
||||
"""The seat behind a door: same side and row (front/rear)."""
|
||||
front = p.y < axle_mid_y
|
||||
left = p.x > 0.0
|
||||
for s in seat_info:
|
||||
if s["front"] == front and s["left"] == left:
|
||||
return s["name"]
|
||||
return nearest_seat_name(p)
|
||||
|
||||
|
||||
for d in doors:
|
||||
p = chassis_pos(d, root_inv)
|
||||
seat_name = seat_for_door(p)
|
||||
children.append(make_child(
|
||||
base + "_door_" + part_side_row(p), d.data.name, p,
|
||||
"vehicle_seat_" + seat_name))
|
||||
if hood:
|
||||
children.append(make_child(base + "_hood", hood.data.name,
|
||||
chassis_pos(hood, root_inv)))
|
||||
if trunk:
|
||||
children.append(make_child(base + "_trunk", trunk.data.name,
|
||||
chassis_pos(trunk, root_inv)))
|
||||
|
||||
vehicle = {
|
||||
"cameraDistance": 6.0,
|
||||
"cameraHeight": 1.6,
|
||||
"engineBrake": 0.5,
|
||||
"maxPitchRollAngleDeg": 60.0,
|
||||
"maxTorque": 300.0,
|
||||
"rearSteer": False,
|
||||
"seatOffset": driver_offset,
|
||||
"steeringRatio": 4.0,
|
||||
"steeringWheelMeshName": "",
|
||||
"steeringWheelOffset": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||
"steeringWheelTiltDeg": 25.0,
|
||||
"wheelMeshName": base + "-wheel.mesh",
|
||||
"wheels": json_wheels,
|
||||
"seats": json_seats,
|
||||
"seatCharacterScale": 1.0,
|
||||
# Arcade-friendly grip: multipliers over the Jolt default tire
|
||||
# curves (peak 1.2); 1.0 feels like wet ice at speed (full lock at
|
||||
# ~15 m/s turns only ~44 deg in 2.5 s; 3.0 lateral turns ~88 deg).
|
||||
# Note longitudinal grip adds understeer on a RWD car (1.5 costs
|
||||
# ~16 deg on the same turn), so keep it near stock.
|
||||
"tireFrictionLongitudinal": 1.3,
|
||||
"tireFrictionLateral": 3.0,
|
||||
# Anti-roll bars per axle pair (Jolt VehicleAntiRollBar); with the
|
||||
# soft 1.3 Hz springs a full-lock turn otherwise rolls the car past
|
||||
# 45 deg onto two wheels.
|
||||
"antiRollBarStiffness": 2000.0,
|
||||
}
|
||||
if steering:
|
||||
vehicle["steeringWheelMeshName"] = base + "-steering-wheel.mesh"
|
||||
vehicle["steeringWheelOffset"] = ogre_vec3(
|
||||
chassis_pos(steering, root_inv))
|
||||
|
||||
entity = {
|
||||
"children": children,
|
||||
"collider": collider,
|
||||
"name": {"name": base},
|
||||
"renderable": {"meshName": base + ".mesh", "visible": True},
|
||||
"rigidBody": {
|
||||
"bodyType": "dynamic",
|
||||
"enabled": True,
|
||||
"friction": 0.8,
|
||||
"isSensor": False,
|
||||
"mass": 1200.0,
|
||||
"restitution": 0.0,
|
||||
},
|
||||
"transform": {
|
||||
"position": {"x": 0.0, "y": 0.0, "z": 0.0},
|
||||
"rotation": {"w": 1.0, "x": 0.0, "y": 0.0, "z": 0.0},
|
||||
"scale": {"x": 1.0, "y": 1.0, "z": 1.0},
|
||||
},
|
||||
"vehicle": vehicle,
|
||||
}
|
||||
|
||||
with open(json_file, "w") as f:
|
||||
json.dump({"entities": [entity]}, f, indent=2)
|
||||
print("gen_vehicle: wrote %s" % json_file)
|
||||
|
||||
# --- axis normalization (mesh data rotated, objects counter-rotated,
|
||||
# so the .mesh files follow the engine conventions while the
|
||||
# exported scene keeps the modeled look) ---
|
||||
def rotate_data_shared(data, rot):
|
||||
data.transform(rot)
|
||||
inv = rot.inverted()
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH' and obj.data == data:
|
||||
obj.matrix_world = obj.matrix_world @ inv
|
||||
|
||||
|
||||
for data in set(w["obj"].data for w in wheel_info):
|
||||
# Axle from Blender X onto Blender Z (-> Ogre Y).
|
||||
rotate_data_shared(data, Matrix.Rotation(radians(90.0), 4, 'Y'))
|
||||
if steering:
|
||||
# Disc normal from Blender Y onto Blender Z (-> Ogre Y spin axis).
|
||||
rotate_data_shared(steering.data,
|
||||
Matrix.Rotation(radians(90.0), 4, 'X'))
|
||||
|
||||
# --- OGRE export (same parameters as export_vehicles.py) ---
|
||||
scene_file = glb_file.replace(".glb", "").replace(".gltf", "") + ".scene"
|
||||
|
||||
bpy.ops.ogre.export(filepath=scene_file,
|
||||
EX_SWAP_AXIS='xz-y',
|
||||
EX_V2_MESH_TOOL_VERSION='v2',
|
||||
EX_EXPORT_XML_DELETE=True,
|
||||
EX_SCENE=True,
|
||||
EX_SELECTED_ONLY=False,
|
||||
EX_EXPORT_HIDDEN=False,
|
||||
EX_FORCE_CAMERA=False,
|
||||
EX_FORCE_LIGHTS=False,
|
||||
EX_NODE_ANIMATION=True,
|
||||
EX_MATERIALS=True,
|
||||
EX_SEPARATE_MATERIALS=True,
|
||||
EX_COPY_SHADER_PROGRAMS=True,
|
||||
EX_MESH=True,
|
||||
EX_LOD_LEVELS=3,
|
||||
EX_LOD_DISTANCE=100,
|
||||
EX_LOD_PERCENT=40
|
||||
)
|
||||
|
||||
bpy.ops.wm.read_homefile(use_empty=True)
|
||||
import time
|
||||
time.sleep(2)
|
||||
bpy.ops.wm.quit_blender()
|
||||
Reference in New Issue
Block a user