Fixed vehicle rendering

This commit is contained in:
2026-10-22 10:14:44 +03:00
parent ff81a9a657
commit c561fad79e
9 changed files with 625 additions and 69 deletions
+94 -19
View File
@@ -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):
+301
View File
@@ -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;
}
+20 -18
View File
@@ -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
+36 -15
View File
@@ -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);