Fixed vehicle rendering
This commit is contained in:
@@ -275,14 +275,17 @@ for m in used_mats:
|
||||
print("gen_vehicle: unpacked texture '%s' -> %s" %
|
||||
(img.name, tex_name))
|
||||
|
||||
# --- merged single material + ID map (CAR_MODELING.md) ---
|
||||
# Every mesh part is re-materialized onto a single material named
|
||||
# <base> so blender2ogre emits ONE submesh per part, and the exporter's
|
||||
# fixed-function .material is replaced with the VehicleIdMap shader
|
||||
# materials after the export. The ID map texture shares the color
|
||||
# atlas UV space (all parts MUST use that one UV space with no texel
|
||||
# shared between materials; fix the blend if not) and marks each
|
||||
# fragment: red = paint (atlas color), green = bloom (lights),
|
||||
# --- merged paint material + separate glass material (CAR_MODELING.md) ---
|
||||
# Every mesh part is re-materialized onto TWO materials: <base> for
|
||||
# paint/bloom faces and <base>-glass for glass faces, so blender2ogre
|
||||
# emits one submesh per part (glass-less parts) or two (glass last).
|
||||
# A single merged transparent submesh forced the WHOLE part (interior
|
||||
# included) into the transparent render queue, where the depth-writing
|
||||
# window glass culled interior parts that sorted behind it ("holes" in
|
||||
# the cabin with the road showing through). The ID map texture shares
|
||||
# the color atlas UV space (all parts MUST use that one UV space with
|
||||
# no texel shared between materials; fix the blend if not) and marks
|
||||
# each fragment: red = paint (atlas color), green = bloom (lights),
|
||||
# blue = glass.
|
||||
IDMAP_SIZE = 512
|
||||
|
||||
@@ -367,10 +370,10 @@ def bake_idmap(mesh_datas, path):
|
||||
|
||||
|
||||
def move_glass_faces_last():
|
||||
"""Re-order each mesh so glass faces export last: with the merged
|
||||
transparent material (depth_write on) the cabin stays visible
|
||||
through the windows only if glass triangles draw after the
|
||||
interior."""
|
||||
"""Re-order each mesh so glass faces export last. With glass on
|
||||
its own material slot (see the consolidation below) blender2ogre
|
||||
groups faces into submeshes by material anyway; the sort just
|
||||
keeps the glass submesh's triangles contiguous at the end."""
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'MESH':
|
||||
continue
|
||||
@@ -410,13 +413,35 @@ for obj in bpy.data.objects:
|
||||
move_glass_faces_last()
|
||||
bake_idmap(all_mesh_datas, os.path.join(outdir, base + "-idmap.png"))
|
||||
|
||||
merged_mat = bpy.data.materials.new(base)
|
||||
merged_mat = bpy.data.materials.get(base)
|
||||
if merged_mat is None:
|
||||
merged_mat = bpy.data.materials.new(base)
|
||||
merged_mat.use_nodes = True
|
||||
# The blend already HAS a "<base>-glass" material (source materials are
|
||||
# renamed to <base>-<material>); reuse it — materials.new() would
|
||||
# silently create "<base>-glass.001", and the exported submesh material
|
||||
# name must match the .material template exactly.
|
||||
glass_mat = bpy.data.materials.get(base + "-glass")
|
||||
if glass_mat is None:
|
||||
glass_mat = bpy.data.materials.new(base + "-glass")
|
||||
glass_mat.use_nodes = True
|
||||
for me in all_mesh_datas:
|
||||
# materials.clear() zeroes every polygon's material_index, so
|
||||
# classify faces against the original slots first.
|
||||
glass_face = [
|
||||
p.material_index < len(me.materials) and
|
||||
me.materials[p.material_index] is not None and
|
||||
"glass" in me.materials[p.material_index].name.lower()
|
||||
for p in me.polygons
|
||||
]
|
||||
me.materials.clear()
|
||||
me.materials.append(merged_mat)
|
||||
print("gen_vehicle: consolidated %d mesh data blocks onto material "
|
||||
"'%s'" % (len(all_mesh_datas), base))
|
||||
if any(glass_face):
|
||||
me.materials.append(glass_mat)
|
||||
for p, g in zip(me.polygons, glass_face):
|
||||
p.material_index = 1 if g else 0
|
||||
print("gen_vehicle: consolidated %d mesh data blocks onto materials "
|
||||
"'%s'/'%s-glass'" % (len(all_mesh_datas), base, base))
|
||||
|
||||
# --- JSON fragment (Ogre coordinates) ---
|
||||
smn, smx = mesh_bbox(root.data)
|
||||
@@ -664,6 +689,18 @@ bpy.ops.ogre.export(filepath=scene_file,
|
||||
IDMAP_MATERIAL_TEMPLATE = """// written by gen_vehicle.py — merged ID-map vehicle material
|
||||
// (CAR_MODELING.md). Not a blender2ogre product; hand edits are
|
||||
// overwritten by every export.
|
||||
//
|
||||
// Three materials per car:
|
||||
// @BASE@ paint/bloom submeshes, fully opaque.
|
||||
// @BASE@-glass near glass submesh (<10 m): alpha-blended,
|
||||
// depth_write OFF so it lands in OGRE's
|
||||
// transparent collection and can never
|
||||
// depth-block the interior/characters behind it
|
||||
// (they render first, the glass blends over).
|
||||
// @BASE@-glass-opaque far glass submesh (>=10 m): solid.
|
||||
// The demo's SedanGlassLodListener swaps only the -glass submeshes
|
||||
// between the near and far variants; paint submeshes always keep
|
||||
// @BASE@.
|
||||
material @BASE@
|
||||
{
|
||||
receive_shadows on
|
||||
@@ -672,7 +709,42 @@ material @BASE@
|
||||
pass
|
||||
{
|
||||
cull_hardware clockwise
|
||||
depth_write on
|
||||
|
||||
vertex_program_ref VehicleIdMapVP
|
||||
{
|
||||
}
|
||||
|
||||
fragment_program_ref VehicleIdMapFP
|
||||
{
|
||||
param_named glassAlpha float 1.0
|
||||
}
|
||||
|
||||
texture_unit
|
||||
{
|
||||
texture @BASE@-atlas.png
|
||||
tex_address_mode wrap
|
||||
filtering linear linear none
|
||||
}
|
||||
|
||||
texture_unit
|
||||
{
|
||||
texture @BASE@-idmap.png
|
||||
tex_address_mode clamp
|
||||
filtering point point none
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
material @BASE@-glass
|
||||
{
|
||||
receive_shadows on
|
||||
technique
|
||||
{
|
||||
pass
|
||||
{
|
||||
cull_hardware clockwise
|
||||
depth_write off
|
||||
scene_blend alpha_blend
|
||||
|
||||
vertex_program_ref VehicleIdMapVP
|
||||
@@ -701,7 +773,7 @@ material @BASE@
|
||||
}
|
||||
}
|
||||
|
||||
material @BASE@-opaque
|
||||
material @BASE@-glass-opaque
|
||||
{
|
||||
receive_shadows on
|
||||
technique
|
||||
@@ -742,8 +814,11 @@ with open(mat_path, "w") as f:
|
||||
f.write(IDMAP_MATERIAL_TEMPLATE.replace("@BASE@", base))
|
||||
print("gen_vehicle: wrote merged material %s" % mat_path)
|
||||
|
||||
# Drop the stale split-material files from pre-ID-map exports so the
|
||||
# resource dir does not accumulate them.
|
||||
# blender2ogre also writes a fixed-function <base>-glass.material for
|
||||
# the glass material slot; drop it (and the stale pre-ID-map split
|
||||
# files) — the template above defines every <base>* material and a
|
||||
# duplicate definition from another script would shadow it depending
|
||||
# on resource parse order.
|
||||
for suffix in ("-color", "-color-bloom", "-glass"):
|
||||
stale = os.path.join(outdir, base + suffix + ".material")
|
||||
if os.path.exists(stale):
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
# Creating a Car Model for the Game (Blender -> Engine)
|
||||
|
||||
This is the contract a car model must satisfy to work with the vehicle
|
||||
pipeline: drivable via `VehicleComponent`/`VehicleSystem`, enterable
|
||||
seats via `VehicleControllerSystem`, openable doors/hood/trunk, and a
|
||||
seated character that actually fits the cabin.
|
||||
|
||||
The reference implementation is `sedan-v2.blend` in this directory — a
|
||||
hand-modeled car processed by the convention-based generator
|
||||
`assets/blender/scripts/gen_vehicle.py`. This is the recommended
|
||||
route: model the car once following the naming conventions below and
|
||||
the generator produces the meshes, materials, textures AND a
|
||||
ready-to-merge scene-entity JSON fragment.
|
||||
|
||||
(The older fully-procedural route, `assets/blender/scripts/
|
||||
gen_taxi_assets.py` -> `sedan-a.blend` / `sedan-taxi.blend`, still
|
||||
works and follows the same engine-side conventions, but its
|
||||
`sitting-chair`-at-0.85-scale cabin numbers are superseded by the
|
||||
`sitting-vehicle` animation below.)
|
||||
|
||||
## 1. Export pipeline (gen_vehicle.py)
|
||||
|
||||
- Put the `.blend` in `assets/blender/vehicles/` and add it to
|
||||
`VEHICLES_GEN_SRC` in `assets/blender/vehicles/CMakeLists.txt`.
|
||||
- Build target `import_vehicles` runs:
|
||||
|
||||
```
|
||||
blender <name>.blend -b -Y -P gen_vehicle.py -- <out>.glb <out>.vehicle.json
|
||||
```
|
||||
|
||||
which does everything `export_vehicles.py` does (blender2ogre
|
||||
`.scene` + one `.mesh` per mesh data block + one `.material` per
|
||||
material, 3 LOD levels, plus a `.glb` preview) and additionally:
|
||||
- classifies parts by naming convention (see §2),
|
||||
- renames mesh data blocks to `<base>-<role>` (`sedan-v2`,
|
||||
`sedan-v2-door-fl`, `sedan-v2-wheel`, `sedan-v2-steering-wheel`,
|
||||
...),
|
||||
- renames materials to `<base>-<material>` and unpacks packed
|
||||
textures to `<base>-atlas.png` next to the output,
|
||||
- normalizes wheel/steering mesh axes (see §3),
|
||||
- emits `<name>.vehicle.json`: a complete scene entity (collider
|
||||
from the shell bounding box, wheels, seats, steering, door/hood/
|
||||
trunk children with seat actuators).
|
||||
- Objects named `*-col` are deleted before export (collision helpers).
|
||||
- After (re)export, artifacts land in `build-vscode/resources/vehicles/`.
|
||||
One target re-exports changed blends, re-stages the editScene runtime
|
||||
resources and re-merges the generated vehicle JSONs into the demo-taxi
|
||||
scenes (this edits the source scene JSONs — merge_vehicle.py preserves
|
||||
entity id/name/transform, so placement edits survive):
|
||||
|
||||
```bash
|
||||
cmake --build build-vscode --target update_vehicles
|
||||
```
|
||||
|
||||
## 2. Naming conventions (what the generator picks up)
|
||||
|
||||
- **root**: the single parentless MESH object is the body shell; its
|
||||
mesh DATA frame is the chassis frame (object locations are NOT baked
|
||||
into the `.mesh`, so author shell vertex data relative to the
|
||||
intended chassis origin).
|
||||
- **`* Tire`** (the word "tire" or "wheel" in the name): wheel objects.
|
||||
All wheels may share one mesh data block. Wheel radius/width are
|
||||
measured from the mesh bounds. No mirrored copy is needed for the
|
||||
right side: Jolt reports every wheel with the same axle direction, so
|
||||
`VehicleSystem` flips right-side (chassis -X) wheel visuals 180 deg
|
||||
about the local forward axis at runtime — model one wheel and reuse it
|
||||
for all four corners.
|
||||
- **`* Door`**: door child entities; origin on the hinge line (model
|
||||
the door CLOSED, then Object -> Set Origin -> Origin to 3D Cursor at
|
||||
the hinge). Each door gets a seat actuator for the seat behind it
|
||||
(same side + row).
|
||||
- **`* hood` / `* bonnet`**, **`* trunk`**: hood/trunk child entities,
|
||||
origin on the hinge line.
|
||||
- **`* Steering`**: the steering wheel.
|
||||
- **`seat_*`**: EMPTY objects marking character seat pins. Exactly one
|
||||
must have "driver" in its name (e.g. `seat_driver`); the front
|
||||
non-driver seat becomes `passenger`, rear seats become `rl`/`rr` by
|
||||
side. The empty's position is where the character's ROOT (base) is
|
||||
pinned — see §5.
|
||||
- Everything else is exported as an extra mesh named
|
||||
`<base>-<sanitized mesh name>` but gets no child entity.
|
||||
|
||||
## 3. Axis and origin conventions
|
||||
|
||||
The exporter swaps axes `EX_SWAP_AXIS='xz-y'`: Blender `(x, y, z)`
|
||||
becomes Ogre `(x, z, -y)`.
|
||||
|
||||
| Blender | Ogre / engine | Meaning |
|
||||
|---------|---------------|---------|
|
||||
| `-Y` | `+Z` | vehicle forward (Jolt chassis +Z) |
|
||||
| `+Z` | `+Y` | up |
|
||||
| `+X` | `+X` | vehicle LEFT side |
|
||||
|
||||
- **Chassis origin**: pick it once (sedan-v2: axle-box center, ~0.6 m
|
||||
above ground contact) and model the shell data around it. The
|
||||
collider, wheels, seats and part pivots in the generated JSON are all
|
||||
relative to it automatically.
|
||||
- **Wheels**: model them as they look (axle along Blender X). The
|
||||
generator rotates the wheel mesh DATA +90 deg about Blender Y so the
|
||||
axle lands on Blender Z (Ogre Y, which `getWheelWorldTransform`
|
||||
assumes) and counter-rotates the objects, so the exported scene keeps
|
||||
the modeled look. Don't pre-rotate.
|
||||
- **Steering wheel**: model it tilted as installed; the generator
|
||||
rotates the mesh DATA +90 deg about Blender X so the disc normal
|
||||
lands on Blender Z (Ogre Y, the spin axis `VehicleSystem` uses) and
|
||||
counter-rotates the object.
|
||||
- **blender2ogre names `.mesh` files after the mesh DATA name** — the
|
||||
generator handles the renaming, but never redefine another blend's
|
||||
mesh data names (all blends export into the same
|
||||
`resources/vehicles/` directory and silently overwrite).
|
||||
|
||||
## 4. Materials and textures (merged ID-map material)
|
||||
|
||||
- The generator consolidates EVERY mesh part (shell, doors, hood,
|
||||
trunk, wheels, steering, extras) onto **two materials**: `<base>`
|
||||
for paint/bloom faces and `<base>-glass` for glass faces — so
|
||||
blender2ogre emits **one submesh per part**, or two (paint first,
|
||||
glass last) for glass-bearing parts. The blender2ogre `.material`
|
||||
output is replaced by a hand-written one referencing the shared GLSL
|
||||
programs `VehicleIdMapVP`/`VehicleIdMapFP` (`vehicle_idmap.vert/.frag/.program`,
|
||||
staged into `resources/vehicles/` by CMake; OpenGL ES 2.0 compatible
|
||||
via `OgreUnifiedShader.h`).
|
||||
**Why not one merged submesh:** a single transparent submesh forces
|
||||
the WHOLE part (interior included) into OGRE's transparent render
|
||||
queue, where renderables sort back-to-front by node distance — the
|
||||
depth-writing window glass then culls interior parts whose nodes
|
||||
sort closer to the camera ("holes" punched in the cabin with the
|
||||
road showing through, 2026-10-21/22). Opaque paint + separate
|
||||
depth-write-off glass is the only arrangement that stays correct
|
||||
under per-object transparency sorting.
|
||||
- Per-fragment behavior comes from an **auto-generated ID map**
|
||||
(`<base>-idmap.png`, 512x512) that shares the color-atlas UV space:
|
||||
red = paint (atlas color, multiplied by the shader's `vehicleColor`
|
||||
uniform — the future per-car registry color hook), green = bloom
|
||||
(lights; optional `bloomEmissive` term, 0 by default), blue = glass
|
||||
(`glassColor` tint, alpha `glassAlpha`).
|
||||
- The car UVs are degenerate by design: every face collapses onto a
|
||||
single color-atlas texel (the atlas is a palette). The ID map simply
|
||||
paints the same texels with the ID color, so **no UV texel may be
|
||||
shared between two different materials** — the generator warns when
|
||||
that happens; fix the blend (give one of them its own texel) rather
|
||||
than working around it.
|
||||
- Source-material roles are recognized by name: `*glass*` -> glass,
|
||||
`*bloom*` -> lights, everything else -> paint.
|
||||
- **Filtering**: because the atlas is a palette, mipmaps are poison —
|
||||
minification averages neighboring palette texels and distant cars
|
||||
bleed wrong colors (found 2026-10-16: blue headlights at range). The
|
||||
generated `.material` therefore samples the atlas with
|
||||
`filtering linear linear none` (no mips) and the ID map with
|
||||
`filtering point point none`. Never "upgrade" these to mipmapped
|
||||
filtering.
|
||||
- **Sampler binding**: `vehicle_idmap.program` binds the samplers
|
||||
explicitly (`param_named uAtlas int 0` / `param_named uIdMap int 1`).
|
||||
Without this the two samplers are auto-assigned in driver
|
||||
uniform-enumeration order, which is not stable — `uIdMap` can end up
|
||||
reading the ATLAS (unit 0), and then every paint texel's blue channel
|
||||
leaks into `glassAlpha` and the whole car goes semi-transparent
|
||||
(found 2026-10-16: see-through doors on llvmpipe while another
|
||||
machine rendered fine). Keep the explicit `int` params in sync with
|
||||
the texture_unit order in the generated `.material`.
|
||||
- **Near/far glass**: the generated `<base>.material` defines `<base>`
|
||||
(opaque paint, `glassAlpha 1.0`), `<base>-glass` (near glass:
|
||||
alpha-blended, `depth_write off`, `glassAlpha 0.25`) and
|
||||
`<base>-glass-opaque` (far glass: solid). The demo's
|
||||
`SedanGlassLodListener` swaps ONLY the glass submeshes between
|
||||
`<base>-glass` and `<base>-glass-opaque` at 10 m from the camera
|
||||
(Ogre 14 ignores `Material::setLodLevels` for entities); paint
|
||||
submeshes keep `<base>` at all distances. `depth_write off` is what
|
||||
routes the near glass into the transparent collection — do not add
|
||||
depth write back to it (that is what depth-blocked the driver's
|
||||
face/hair behind the window, 2026-10-21). No per-car setup is needed
|
||||
for new families beyond adding the base name to
|
||||
`kMergedCarMaterials` in the demo's `demo_main.cpp`.
|
||||
- Glass faces are sorted to the END of each mesh during generation so
|
||||
the glass submesh's triangles stay contiguous (cosmetic; the submesh
|
||||
split is what carries the rendering correctness).
|
||||
- For color variants of the same geometry, duplicate the mesh data and
|
||||
move the paint faces to a different atlas texel — or (later) drive
|
||||
`vehicleColor` per entity.
|
||||
|
||||
## 5. Matching the seated/driving character
|
||||
|
||||
The character is pinned to the seat point with its root node
|
||||
(`VehicleControllerSystem::pinCharacterToSeat`) and plays the
|
||||
locomotion state machine's `driving` state, mapped to the
|
||||
`sitting-vehicle` animation in the demo's `animation_tree.json`.
|
||||
|
||||
**With `sitting-vehicle`, the seat pin IS the character's base
|
||||
position** — the pose is authored so the butt, hands and feet land
|
||||
correctly relative to the root, so no offsets and no scaling are
|
||||
applied (`seatCharacterScale 1.0` in the generated JSON). Place each
|
||||
`seat_*` empty exactly where the character's feet-between point should
|
||||
sit: roughly on the floor pan at the cushion front edge.
|
||||
|
||||
- sedan-v2 seat pins (chassis space): driver/passenger
|
||||
(±0.48, -0.28, 0.201), rl/rr (±0.48, -0.28, -0.603); driver is
|
||||
front-LEFT (+X).
|
||||
- The generator takes the wheel centers as the intended REST position
|
||||
and lifts the suspension attach points by
|
||||
`L_rest = maxLength - g/(2*pi*freq)^2` (0.35 - 0.147 = 0.203 at the
|
||||
defaults min/max/freq/damping 0.1/0.35/1.3/0.6), so the car rides at
|
||||
the modeled stance. The suspension is deliberately soft-ish —
|
||||
stiffer springs make the wheels pogo and cost grip.
|
||||
- Tire friction: the JSON carries `tireFrictionLongitudinal` /
|
||||
`tireFrictionLateral` (1.3 / 3.0 for sedans), multipliers over the
|
||||
Jolt default tire curves (peak 1.2) which are then combined with the
|
||||
ground friction via `sqrt(tire * ground)` — at the stock 1.0 the car
|
||||
drives like it is on wet ice (full lock at ~15 m/s turns only ~44
|
||||
deg in 2.5 s; 3.0 lateral turns ~88 deg). More longitudinal grip
|
||||
adds understeer on a RWD car, so keep it near stock.
|
||||
- Anti-roll bars: `antiRollBarStiffness` (2000 for sedans) adds a Jolt
|
||||
`VehicleAntiRollBar` per axle pair to limit body roll. The engine
|
||||
also rightens a car stuck past ~35 deg of tilt automatically
|
||||
(VehicleSystem), so two-wheel balancing acts recover on their own.
|
||||
- Center of mass: `centerOfMassOffset` shifts the physics COM in
|
||||
chassis space (Jolt `OffsetCenterOfMassShape` — only the COM and the
|
||||
inertia about it move; the collider, wheel attachments and the
|
||||
render node stay put). A box collider puts the COM at the box
|
||||
center — for a sedan ~1 m above the tire contact patches, twice the
|
||||
real value — and the car trips onto its side in hard corners (the
|
||||
taxi's taller box makes it worse). The generator places the COM
|
||||
just above wheel-hub height; tweak the chassis-space offset in the
|
||||
editor Vehicle panel (applied live) if a new model still feels
|
||||
tippy.
|
||||
- Downforce: `downforceCoefficient` (35 plain sedan, 45 taxi — the
|
||||
CMake invocation passes it per model) applies a straight down force
|
||||
of coefficient * speed^2 at the COM while moving — less bounce and,
|
||||
because the tire normal load (and with it the lateral impulse
|
||||
budget) grows with speed^2, the turn radius stops growing with
|
||||
speed: this is the high-speed cornering grip lever. Raise it when a
|
||||
model understeers at speed, lower it if the car feels glued.
|
||||
- Default chassis tuning in the JSON: mass 1200, maxTorque 300,
|
||||
engineBrake 0 (cars coast when the accelerator is released — braking
|
||||
is the driver's job: S while rolling forward brakes via the
|
||||
brake-before-reverse rule, Space is the handbrake; slow yard vehicles
|
||||
like the forklift use ~0.5 so they do not roll forever),
|
||||
front steer 35 deg, rear driven + handbrake 300,
|
||||
steeringRatio 4 — tweak per vehicle after merging.
|
||||
|
||||
## 6. Scene integration
|
||||
|
||||
The generated `<name>.vehicle.json` is a complete entity fragment.
|
||||
Merge it into a scene JSON with the helper
|
||||
`src/features/editScene/demos/demo-taxi/merge_vehicle.py`:
|
||||
|
||||
```bash
|
||||
python3 merge_vehicle.py <scene.json> \
|
||||
<builddir>/resources/vehicles/<name>.vehicle.json <entityName>
|
||||
```
|
||||
|
||||
It replaces the named scene entity (see `sedan1` in
|
||||
`demo_scene_vehicletest.json`) with the generated one, renaming the
|
||||
entity and children and preserving the scene entity's id and
|
||||
transform. **The scene copy does NOT track the blend** — after every
|
||||
blend edit re-run the `update_vehicles` target (see §1), which
|
||||
re-exports, re-stages resources and re-merges both demo-taxi scenes in
|
||||
one step. The door
|
||||
children carry `actuator`s with the generic per-seat actions
|
||||
`vehicle_seat_driver` / `_passenger` / `_rl` / `_rr` (defined in
|
||||
`lua-scripts/data2.lua`), which send the `vehicle_enter` event; the
|
||||
vehicle is resolved by proximity, so the same actions work for every
|
||||
car.
|
||||
|
||||
## 7. Minimal-effort multi-look strategy
|
||||
|
||||
- **Color variants**: duplicate shell + door mesh data, swap the paint
|
||||
material (atlas UVs make this cheap).
|
||||
- **Silhouette variants** (e.g. the taxi): keep the hardpoints fixed —
|
||||
wheel positions, `seat_*` empties, hinge lines, floor pan — and vary
|
||||
the outer panels (roof sign, livery, bumpers). The generated JSON
|
||||
then carries over nearly unchanged.
|
||||
- **Kitbashing**: keep a parts-library blend with the wheel, steering
|
||||
wheel, seats and dash; append into new car blends. Never reuse
|
||||
another blend's mesh DATA names (export clash, §3).
|
||||
|
||||
## 8. Verifying a new car
|
||||
|
||||
From the demo-taxi build directory
|
||||
(`build-vscode/src/features/editScene/demos/demo-taxi`):
|
||||
|
||||
```bash
|
||||
# Sit the player in a seat and dump a framed screenshot:
|
||||
xvfb-run -a -s "-screen 0 1920x1080x24" ./demoTaxi \
|
||||
demo_scene_vehicletest.json --force-pos 4.5 1.5 0.3 90 -15 \
|
||||
--debug-enter-vehicle <vehicleEntityName> \
|
||||
--debug-enter-seat driver --screenshot 700 /tmp/seat.png
|
||||
```
|
||||
|
||||
Check: butt on the cushion, head under the headliner, feet inside the
|
||||
body, glass transparent near / opaque past ~10 m, steering wheel in
|
||||
front of the driver. The `--debug-enter-*` run also prints a bone
|
||||
probe (head bone y should match the exported animation, ~0.75 for
|
||||
sitting-vehicle). Then run the headless contract:
|
||||
|
||||
```bash
|
||||
./demoTaxi --headless --test-vehicle-drive demo_scene_vehicletest.json
|
||||
```
|
||||
|
||||
(always with `--headless`: without it the frame delta is wall time and
|
||||
the fixed-length W-hold drives the car off the test floor, failing the
|
||||
exit-spot raycast for timing reasons, not because the car is broken).
|
||||
Binary file not shown.
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Merged vehicle material — fragment stage.
|
||||
*
|
||||
* Per fragment the ID map (same UV space as the color atlas) selects
|
||||
* which parameter set applies, by multiplying each candidate value by
|
||||
* its ID channel weight:
|
||||
*
|
||||
* idmap.r = paint — atlas color, tinted by vehicleColor (the future
|
||||
* per-car registry color hook; white for now)
|
||||
* idmap.g = bloom — lights; shaded like paint today, plus an
|
||||
* optional emissive term (bloomEmissive, off by
|
||||
* default to keep the current look)
|
||||
* idmap.b = glass — glassColor tint, alpha = glassAlpha; glass
|
||||
* faces live on their own submesh whose material
|
||||
* the demo swaps by camera distance: "<base>-glass"
|
||||
* (near, alpha 0.25, depth_write off) /
|
||||
* "<base>-glass-opaque" (far, alpha 1.0) —
|
||||
* SedanGlassLodListener
|
||||
*
|
||||
* Lighting is a single directional light (the sun) + scene ambient,
|
||||
* Blinn specular on paint and glass. OpenGL ES 2.0 compatible via
|
||||
* OgreUnifiedShader.h.
|
||||
*/
|
||||
OGRE_NATIVE_GLSL_VERSION_DIRECTIVE
|
||||
#include <OgreUnifiedShader.h>
|
||||
|
||||
OGRE_UNIFORMS(
|
||||
uniform vec4 lightDirView;
|
||||
uniform vec4 lightDiffuse;
|
||||
uniform vec4 ambient;
|
||||
uniform vec4 vehicleColor;
|
||||
uniform vec4 glassColor;
|
||||
uniform float glassAlpha;
|
||||
uniform float bloomEmissive;
|
||||
uniform float specularStrength;
|
||||
uniform float shininess;
|
||||
)
|
||||
|
||||
SAMPLER2D(uAtlas, 0);
|
||||
SAMPLER2D(uIdMap, 1);
|
||||
|
||||
MAIN_PARAMETERS
|
||||
IN(vec2 vUV, TEXCOORD0)
|
||||
IN(vec3 vNormal, TEXCOORD1)
|
||||
IN(vec3 vPosView, TEXCOORD2)
|
||||
MAIN_DECLARATION
|
||||
{
|
||||
vec4 atlas = texture2D(uAtlas, vUV);
|
||||
vec3 idm = texture2D(uIdMap, vUV).rgb;
|
||||
float wPaint = idm.r;
|
||||
float wBloom = idm.g;
|
||||
float wGlass = idm.b;
|
||||
|
||||
vec3 base = atlas.rgb * vehicleColor.rgb * wPaint +
|
||||
atlas.rgb * wBloom +
|
||||
glassColor.rgb * wGlass;
|
||||
|
||||
vec3 n = normalize(vNormal);
|
||||
vec3 l = normalize(-lightDirView.xyz);
|
||||
vec3 v = normalize(-vPosView);
|
||||
vec3 h = normalize(l + v);
|
||||
float diff = max(dot(n, l), 0.0);
|
||||
float spec = pow(max(dot(n, h), 0.0), shininess) *
|
||||
specularStrength * (wPaint + wGlass);
|
||||
|
||||
vec3 lit = base * (ambient.rgb + diff * lightDiffuse.rgb) +
|
||||
spec * lightDiffuse.rgb;
|
||||
lit += atlas.rgb * bloomEmissive * wBloom;
|
||||
|
||||
float alpha = mix(1.0, glassAlpha, wGlass);
|
||||
gl_FragColor = vec4(lit, alpha);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Merged single-material vehicle shader (paint / bloom / glass selected
|
||||
* per fragment from an ID map texture sharing the color atlas UV space).
|
||||
* Used by the <car>.material files written by gen_vehicle.py; see
|
||||
* assets/blender/vehicles/CAR_MODELING.md. OpenGL ES 2.0 compatible
|
||||
* (OgreUnifiedShader.h).
|
||||
*/
|
||||
vertex_program VehicleIdMapVP glsl glsles glslang hlsl
|
||||
{
|
||||
source vehicle_idmap.vert
|
||||
default_params
|
||||
{
|
||||
param_named_auto worldViewProj worldviewproj_matrix
|
||||
param_named_auto worldView worldview_matrix
|
||||
}
|
||||
}
|
||||
|
||||
fragment_program VehicleIdMapFP glsl glsles glslang hlsl
|
||||
{
|
||||
source vehicle_idmap.frag
|
||||
default_params
|
||||
{
|
||||
param_named_auto lightDirView light_direction_view_space 0
|
||||
param_named_auto lightDiffuse light_diffuse_colour 0
|
||||
param_named_auto ambient ambient_light_colour
|
||||
param_named vehicleColor float4 1.0 1.0 1.0 1.0
|
||||
param_named glassColor float4 0.8 0.8 0.8 1.0
|
||||
param_named glassAlpha float 0.25
|
||||
param_named bloomEmissive float 0.0
|
||||
param_named specularStrength float 0.5
|
||||
param_named shininess float 32.0
|
||||
// explicit texture-unit bindings: without these the samplers
|
||||
// are auto-assigned in driver uniform-enumeration order and
|
||||
// uIdMap can end up reading the atlas (unit 0) — every paint
|
||||
// texel's blue channel then leaks into glassAlpha and the
|
||||
// whole car goes semi-transparent (2026-10-16)
|
||||
param_named uAtlas int 0
|
||||
param_named uIdMap int 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Merged vehicle material — vertex stage.
|
||||
*
|
||||
* One material for the whole car (see assets/blender/vehicles/
|
||||
* CAR_MODELING.md): the fragment stage selects paint / bloom (lights) /
|
||||
* glass behaviour per fragment from an ID map texture that shares the
|
||||
* color atlas UV space. OpenGL ES 2.0 compatible via
|
||||
* OgreUnifiedShader.h.
|
||||
*/
|
||||
OGRE_NATIVE_GLSL_VERSION_DIRECTIVE
|
||||
#include <OgreUnifiedShader.h>
|
||||
|
||||
OGRE_UNIFORMS(
|
||||
uniform mat4 worldViewProj;
|
||||
uniform mat4 worldView;
|
||||
)
|
||||
|
||||
OUT(vec2 vUV, TEXCOORD0)
|
||||
OUT(vec3 vNormal, TEXCOORD1)
|
||||
OUT(vec3 vPosView, TEXCOORD2)
|
||||
|
||||
MAIN_PARAMETERS
|
||||
IN(vec4 vertex, POSITION)
|
||||
IN(vec3 normal, NORMAL)
|
||||
IN(vec2 uv0, TEXCOORD0)
|
||||
MAIN_DECLARATION
|
||||
{
|
||||
gl_Position = mul(worldViewProj, vertex);
|
||||
vUV = uv0;
|
||||
/* Vehicles are never non-uniformly scaled, so the plain
|
||||
* world-view 3x3 is a fine normal transform. */
|
||||
vNormal = normalize(mul(mat3(worldView), normal));
|
||||
vPosView = mul(worldView, vertex).xyz;
|
||||
}
|
||||
@@ -394,28 +394,30 @@ EDITSCENE_PROFILE=1 ./demoTaxi --headless --benchmark
|
||||
# assets/blender/vehicles/CAR_MODELING.md. After editing a vehicle
|
||||
# blend run `cmake --build build-vscode --target update_vehicles` (one
|
||||
# step: re-export, re-stage resources, re-merge both demo scenes).
|
||||
# Rendering (phase 5.5): every part (body, doors, hood, trunk, wheels,
|
||||
# steering) exports with exactly ONE submesh on ONE shared material
|
||||
# (`sedan-v2` / `sedan-v2-taxi`) using the VehicleIdMap shader
|
||||
# (resources/vehicles/vehicle_idmap.*, GLES2-compatible,
|
||||
# OgreUnifiedShader.h) — a baked ID-map texture (red = paint, green =
|
||||
# bloom/lights, blue = glass, sharing the cars' palette-degenerate UV
|
||||
# space) selects paint/bloom/glass behavior per fragment; the
|
||||
# `vehicleColor` shader param is the hook for future per-car colors.
|
||||
# Two variants swap by camera distance (`<base>` alpha-blended glass
|
||||
# within 10 m, `<base>-opaque` beyond). The near variant carries
|
||||
# `transparent_sorting force`: `alpha_blend` + `depth_write on`
|
||||
# otherwise lands in OGRE's SOLID pass-group collection (ordered by
|
||||
# Pass* address, arbitrary), and the window glass depth-blocked the
|
||||
# driver's face/hair behind it (2026-10-21); forcing transparent
|
||||
# sorting keeps `depth_write on` for car self-occlusion while
|
||||
# rendering the car after opaque characters. gen_vehicle.py bakes the ID
|
||||
# Rendering (phase 5.5, glass split 2026-10-22): every part (body,
|
||||
# doors, hood, trunk, wheels, steering) exports with ONE paint submesh
|
||||
# on the opaque `<base>` material plus, for glass-bearing parts, a
|
||||
# trailing glass submesh on `<base>-glass` — both using the
|
||||
# VehicleIdMap shader (resources/vehicles/vehicle_idmap.*,
|
||||
# GLES2-compatible, OgreUnifiedShader.h) — a baked ID-map texture
|
||||
# (red = paint, green = bloom/lights, blue = glass, sharing the cars'
|
||||
# palette-degenerate UV space) selects paint/bloom/glass behavior per
|
||||
# fragment; the `vehicleColor` shader param is the hook for future
|
||||
# per-car colors. Do NOT merge glass back into the paint submesh: a
|
||||
# single transparent submesh puts the whole part into OGRE's
|
||||
# transparent queue, where per-renderable distance sorting lets the
|
||||
# depth-writing windows cull interior parts behind them (holes in the
|
||||
# cabin) and earlier depth-blocked the driver's face/hair. The near
|
||||
# glass material (`<base>-glass`, within 10 m) is alpha-blended with
|
||||
# `depth_write off` — that is what routes it to the transparent
|
||||
# collection after the opaque interior; the far variant
|
||||
# (`<base>-glass-opaque`) is solid. SedanGlassLodListener swaps only
|
||||
# the glass submeshes between the two. gen_vehicle.py bakes the ID
|
||||
# map (warning on palette texels shared between materials — fix the
|
||||
# blend), sorts glass faces last per mesh and writes the `.material`
|
||||
# itself (the palette atlas is sampled with mipmaps OFF — `linear
|
||||
# linear none` — because minification averages neighboring palette
|
||||
# texels and distant cars bled wrong colors); the demo's
|
||||
# SedanGlassLodListener does the near/far swap. The shader's samplers
|
||||
# texels and distant cars bled wrong colors). The shader's samplers
|
||||
# are bound explicitly (`param_named uAtlas int 0` / `param_named
|
||||
# uIdMap int 1` in vehicle_idmap.program) — OGRE's sampler
|
||||
# auto-assignment follows driver uniform-enumeration order and let
|
||||
|
||||
@@ -301,10 +301,12 @@ cabin completely). `fixupSedanGlassMaterial()` (demo_main.cpp) makes
|
||||
`sedan-glass-opaque`; `SedanGlassLodListener` swaps every vehicle's
|
||||
glass submeshes between the two by camera distance (10 m threshold) —
|
||||
Ogre 14 no longer applies `Material::setLodLevels` to entities, so a
|
||||
real LOD material never switches. (Superseded in phase 5.5: the
|
||||
listener now swaps the single merged VehicleIdMap material
|
||||
`<base>` <-> `<base>-opaque`; `fixupSedanGlassMaterial()` only remains
|
||||
for the legacy `sedan-glass`.)
|
||||
real LOD material never switches. (Superseded in phase 5.5, then
|
||||
again on 2026-10-22: convention-built cars now export an opaque
|
||||
`<base>` paint submesh plus a separate `<base>-glass` submesh per
|
||||
glass-bearing part, and the listener swaps only the glass submeshes
|
||||
`<base>-glass` <-> `<base>-glass-opaque`; `fixupSedanGlassMaterial()`
|
||||
only remains for the legacy `sedan-glass`.)
|
||||
|
||||
**sedan-v2 (hand-modeled base sedan, current reference).** The
|
||||
hand-modeled `assets/blender/vehicles/sedan-v2.blend` replaced the
|
||||
@@ -328,9 +330,10 @@ offsets, `seatCharacterScale` 1.0). `sedan-v2-glass` was added to
|
||||
`kGlassMaterialNames` for the near/far glass swap. The full contract
|
||||
is `assets/blender/vehicles/CAR_MODELING.md`; the taxi variant of
|
||||
sedan-v2 is next. (Superseded in phase 5.5: the three
|
||||
color/color-bloom/glass materials are now ONE merged VehicleIdMap
|
||||
material per car with the near/far swap done on it — see the 5.5
|
||||
results.) One engine fix this exposed:
|
||||
color/color-bloom/glass materials became ONE merged VehicleIdMap
|
||||
material per car — then on 2026-10-22 glass split back out into its
|
||||
own submesh/material per glass-bearing part because the merged
|
||||
transparent submesh broke cabin occlusion; see the 5.5 follow-up.) One engine fix this exposed:
|
||||
`AnimationTreeSystem` now disables animations that fall out of the
|
||||
evaluated set in a single frame (a large-deltaTime fade-out step used
|
||||
to leave the old animation enabled at its last weight forever — the
|
||||
@@ -2961,14 +2964,32 @@ taxi's lights should look distinct.
|
||||
pass happened to sort before the face/hair RTSS passes, the window
|
||||
glass wrote closer depth first, and the character fragments behind
|
||||
it failed the depth test (HW vs SW skinning differed only because
|
||||
the generated passes' addresses shifted). Fix: `transparent_sorting
|
||||
force` in the near variant's pass in `gen_vehicle.py`'s
|
||||
IDMAP_MATERIAL_TEMPLATE — routes the car to the transparent
|
||||
collection (after opaque characters, back-to-front) while keeping
|
||||
`depth_write on` for per-pixel car self-occlusion. The `-opaque`
|
||||
far variant is unchanged (stays solid by design). A proper
|
||||
glass-submesh split remains the structural alternative if this ever
|
||||
regresses.
|
||||
the generated passes' addresses shifted). First fix:
|
||||
`transparent_sorting force` in the near variant's pass — routes the
|
||||
car to the transparent collection (after opaque characters,
|
||||
back-to-front) while keeping `depth_write on`.
|
||||
- **Holes punched in the cabin through the windows (fixed 2026-10-22,
|
||||
supersedes `transparent_sorting force`).** With the whole part in
|
||||
the transparent collection, per-renderable back-to-front sorting by
|
||||
node distance let the depth-writing window glass cull OTHER PARTS of
|
||||
the same car whose nodes sorted closer to the camera — door cards /
|
||||
seats / dashboard went missing with the road and its markings
|
||||
showing through, always reproducible looking through the front/rear
|
||||
windows with the car over the centerline. Root fix: glass is its
|
||||
own submesh again — `gen_vehicle.py` consolidates each part onto
|
||||
`<base>` (paint/bloom faces, fully opaque) plus `<base>-glass`
|
||||
(glass faces, alpha-blended with `depth_write OFF`, so it auto-lands
|
||||
in the transparent collection and blends over the already-rendered
|
||||
interior without ever depth-blocking it); `<base>-glass-opaque` is
|
||||
the far variant, and `SedanGlassLodListener` now swaps only glass
|
||||
submeshes (paint keeps `<base>` at all distances). The whole-car
|
||||
`<base>-opaque` material is gone. Batch cost: +1 batch per
|
||||
glass-bearing part for NEAR cars only (sedans: body, 4 doors, hood,
|
||||
trunk — the latter two carry small glass faces). Verified with
|
||||
`--debug-enter-vehicle` + `--free-camera` screenshots: driver fully
|
||||
visible through side and front glass, cabin intact from every angle.
|
||||
Post-split benchmark-drive: PASS, wall avg 13.4 ms, batches avg 53
|
||||
(+3 vs the merged-submesh build — the near-car glass submeshes).
|
||||
Post-fix benchmark-drive (llvmpipe): PASS, find split avg objs
|
||||
0.31 ms (was 13.7), physics split avg step 0.96 ms with zero
|
||||
physics spikes >8 ms. Remaining spikes for phase 11.5: `water`
|
||||
|
||||
@@ -2013,14 +2013,17 @@ static void createFloorMesh(const Ogre::String &meshName,
|
||||
}
|
||||
|
||||
/*
|
||||
* The convention-built cars (gen_vehicle.py, CAR_MODELING.md) export a
|
||||
* single merged ID-map material per car family: "<base>" is the
|
||||
* transparent near variant, "<base>-opaque" the far variant (glass
|
||||
* renders solid), both shipped in the generated <base>.material.
|
||||
* SedanGlassLodListener swaps every part's submeshes between the two
|
||||
* depending on camera distance (Ogre 14 no longer applies
|
||||
* Material::setLodLevels to entities, so a real LOD material never
|
||||
* switches technique).
|
||||
* The convention-built cars (gen_vehicle.py, CAR_MODELING.md) export
|
||||
* one paint submesh per part (material "<base>", always opaque) plus,
|
||||
* for glass-bearing parts, a trailing glass submesh (material
|
||||
* "<base>-glass", alpha-blended with depth_write OFF — it lands in
|
||||
* OGRE's transparent collection and blends over the already-rendered
|
||||
* interior; keeping glass in the paint submesh made the depth-writing
|
||||
* windows cull interior parts that sorted behind them). The far
|
||||
* variant "<base>-glass-opaque" renders the glass solid past 10 m;
|
||||
* SedanGlassLodListener swaps only the glass submeshes between the two
|
||||
* (Ogre 14 no longer applies Material::setLodLevels to entities, so a
|
||||
* real LOD material never switches technique).
|
||||
*
|
||||
* The legacy export_vehicles.py cars still split glass into its own
|
||||
* fixed-function material; for those the same swap is faked here:
|
||||
@@ -2058,11 +2061,12 @@ static void fixupSedanGlassMaterial()
|
||||
}
|
||||
|
||||
/*
|
||||
* Per-vehicle near/far material swap: the plain material (transparent
|
||||
* glass) within 10 m of the camera, its "-opaque" variant beyond — see
|
||||
* Per-vehicle near/far glass swap: "<base>-glass" (transparent) within
|
||||
* 10 m of the camera, "<base>-glass-opaque" beyond — see
|
||||
* fixupSedanGlassMaterial. Walks the whole node subtree of each
|
||||
* vehicle, so child entities (doors, wheels, steering) are covered
|
||||
* too; with the merged materials this switches every part at once.
|
||||
* vehicle so child entities (doors, wheels, steering) are covered too.
|
||||
* Paint submeshes ("<base>") are opaque at every distance and are left
|
||||
* alone.
|
||||
*/
|
||||
struct SedanGlassLodListener : public Ogre::FrameListener {
|
||||
EditorApp *app;
|
||||
@@ -2113,17 +2117,21 @@ struct SedanGlassLodListener : public Ogre::FrameListener {
|
||||
for (const char *base :
|
||||
kMergedCarMaterials) {
|
||||
const Ogre::String
|
||||
opaque =
|
||||
glassNear =
|
||||
Ogre::String(base) +
|
||||
"-glass";
|
||||
const Ogre::String
|
||||
glassFar =
|
||||
glassNear +
|
||||
"-opaque";
|
||||
if (cur2 != base &&
|
||||
cur2 != opaque)
|
||||
if (cur2 != glassNear &&
|
||||
cur2 != glassFar)
|
||||
continue;
|
||||
const Ogre::String
|
||||
want =
|
||||
near ?
|
||||
base :
|
||||
opaque;
|
||||
glassNear :
|
||||
glassFar;
|
||||
if (cur2 != want)
|
||||
sub->setMaterialName(
|
||||
want);
|
||||
|
||||
Reference in New Issue
Block a user