Taxi demo update with sedan

This commit is contained in:
2026-09-17 02:55:53 +03:00
parent c748a7dd1e
commit 3c7f50a4ef
21 changed files with 2007 additions and 105 deletions
Binary file not shown.
Binary file not shown.
@@ -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()
+451
View File
@@ -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()
+484
View File
@@ -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()
+45 -1
View File
@@ -2,7 +2,9 @@ project(vehicles)
set(VEHICLES_SRC boat-big.blend boat.blend
boat-gobbot.blend boat-sails.blend
raft.blend tiny-boat.blend)
raft.blend tiny-boat.blend
forklift.blend crate.blend pad.blend
wall.blend sedan-a.blend sedan-taxi.blend)
set(VEHICLE_OUTPUT_FILES)
foreach(VEHICLE_FILE ${VEHICLES_SRC})
@@ -22,4 +24,46 @@ foreach(VEHICLE_FILE ${VEHICLES_SRC})
list(APPEND VEHICLE_OUTPUT_FILES ${VEHICLE_OUTPUT_FILE} ${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.scene)
endforeach()
# Convention-built drivable vehicles: same mesh/material export plus a
# generated scene-entity JSON fragment (<name>.vehicle.json, see
# assets/blender/scripts/gen_vehicle.py and vehicles/CAR_MODELING.md).
set(VEHICLES_GEN_SRC sedan-v2.blend)
foreach(VEHICLE_FILE ${VEHICLES_GEN_SRC})
get_filename_component(FILE_NAME ${VEHICLE_FILE} NAME_WE)
set(VEHICLE_OUTPUT_FILE ${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.glb)
set(VEHICLE_JSON_FILE ${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.vehicle.json)
add_custom_command(
OUTPUT ${VEHICLE_OUTPUT_FILE} ${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.scene ${VEHICLE_JSON_FILE}
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/resources/vehicles
COMMAND ${BLENDER} ${CMAKE_CURRENT_SOURCE_DIR}/${VEHICLE_FILE}
-b -Y -P
${CMAKE_SOURCE_DIR}/assets/blender/scripts/gen_vehicle.py
-- ${VEHICLE_OUTPUT_FILE} ${VEHICLE_JSON_FILE}
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.glb -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.scene -P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
COMMAND touch ${VEHICLE_OUTPUT_FILE}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${VEHICLE_FILE} ${CMAKE_SOURCE_DIR}/assets/blender/scripts/gen_vehicle.py)
list(APPEND VEHICLE_OUTPUT_FILES ${VEHICLE_OUTPUT_FILE} ${CMAKE_BINARY_DIR}/resources/vehicles/${FILE_NAME}.scene ${VEHICLE_JSON_FILE})
endforeach()
add_custom_target(import_vehicles ALL DEPENDS ${VEHICLE_OUTPUT_FILES})
# One-step vehicle refresh: re-export changed .blend files (import_vehicles),
# re-stage the resources for the editScene runtime and re-merge the generated
# vehicle entity JSONs into the demo-taxi scenes. NOTE: the merge step EDITS
# the source scene JSONs under src/features/editScene/demos/demo-taxi (that is
# the point - merge_vehicle.py preserves entity id/name/transform, so hand
# edits to placement survive). Run after editing a vehicle .blend:
# cmake --build <build-dir> --target update_vehicles
set(DEMO_TAXI_DIR ${CMAKE_SOURCE_DIR}/src/features/editScene/demos/demo-taxi)
add_custom_target(update_vehicles
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_BINARY_DIR}/resources
${CMAKE_BINARY_DIR}/src/features/editScene/resources
COMMAND python3 merge_vehicle.py demo_scene_vehicletest.json
${CMAKE_BINARY_DIR}/resources/vehicles/sedan-v2.vehicle.json sedan1
COMMAND python3 merge_vehicle.py demo_scene_exterior.json
${CMAKE_BINARY_DIR}/resources/vehicles/sedan-v2.vehicle.json sedan_plain
WORKING_DIRECTORY ${DEMO_TAXI_DIR}
DEPENDS import_vehicles)