AO — ZERO EXTRA BYTES. It lives in the alpha byte of the packed colour lane,
which was already dead weight: model.rs wrote the glTF baseColorFactor alpha
there, the skinned shader multiplied it into v_tint.w, and the pixel shader
threw it away by returning a hardcoded 1.0. We were paying for the channel and
never reading it. 24 bytes/vertex before and after.
The multiply scales AMBIENT ONLY — `albedo * (ambient * ao + direct)`. Folding
AO into direct as well would darken a sunlit wall twice, and direct light is
already zero where a surface faces away, which is precisely where occlusion is
the ambient term's job.
Static-only holds BY CONSTRUCTION without touching skin.rs: that file writes
pack_unorm8x4(1,1,1,1), so characters get ambient * 1.0 — an exact no-op
through the shared shader.
Cost over the real catalogue (4,442 models, 2.5M verts): 2.64 ms/model average,
102 ms worst case — down from 409 ms. Dense interior kits get a reduced ray
budget, and the hemisphere distributes over the ACTUAL ray count rather than
the nominal one; without that fix a reduced budget samples only near the normal
and reads as uniformly unoccluded. The 4x speedup moved the crevice share
14.7% -> 15.0%, i.e. cost nothing visually. Nothing in the library falls below
0.40 occlusion — the floor clamp is what keeps low-poly art out of the mud.
Contact AO needed one fix found by rendering it: an ellipse inscribed in a
square footprint pulls away from the corners, so a castle piece read as
standing in a spotlight rather than touching the ground. It is a squircle now
(|x|^4+|z|^4=1) with segments landing on the corners and edge midpoints.
STEERING FIXED ONCE, AT THE SOURCE. New libs/game/sim/heading.rs states the
convention in one place — forward is -Z, right is +X, POSITIVE YAW TURNS LEFT —
with heading_to_forward/right, forward_to_heading, steer_to_yaw_rate,
heading_delta. Seven tests read as statements of intent ("steering right
decreases heading") so a future sign flip fails loudly. The car's torque and
its autodrive route-follower both route through it and the inline atan2 calls
are gone. The inversion was exactly the trap the module now documents: positive
steer produced positive yaw, which turns left.
DOUBLE BRAINS, found by wiring: spawn_blocks ran unconditionally after
build_world, so every villager got a SECOND Npc block — two brains steering one
body — plus a second car. 28 NPCs for 14 entities; now 14.
The car is a real mesh (toy-car-kit/vehicle-truck) found by description and
scaled from its own bounds onto the chassis, box hidden. The rigid body stays
the physics.
BIG WORLD RENDERS: ARCADE_WORLD=big, street demo still default. 596 props, 217
colliders, 64 draw items (per-pack atlas batching working), 611 shadow casters,
14 NPCs, 63 of 64 models loaded, 15 ms to plan. 506,962 TRIANGLES — that will
not fit a Quest, and roads are 382 of 596 placements, so road decoration and
distant woods scatter are the first cuts a governor should make.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
87 lines
3 KiB
Rust
87 lines
3 KiB
Rust
//! Bake cost and AO distribution over the real Kenney catalogue.
|
|
//!
|
|
//! cargo run -p makepad-game-render --release --example ao_probe
|
|
//!
|
|
//! Reports parse+bake time per model and library-wide, and how much of each
|
|
//! model actually darkens — a bake that leaves everything at 1.0 is doing
|
|
//! nothing, and one that drives everything to the floor is mud.
|
|
|
|
use std::time::Instant;
|
|
|
|
use makepad_game_render::model::{StaticModel, MODEL_VERTEX_FLOATS};
|
|
|
|
fn main() {
|
|
let root = std::env::args()
|
|
.nth(1)
|
|
.unwrap_or_else(|| "apps/arcade/resources/models/kenney".to_string());
|
|
let mut files: Vec<std::path::PathBuf> = Vec::new();
|
|
collect(std::path::Path::new(&root), &mut files);
|
|
files.sort();
|
|
if files.is_empty() {
|
|
eprintln!("no .glb under {root} — run apps/arcade/download_assets.sh");
|
|
return;
|
|
}
|
|
|
|
let limit: usize = std::env::var("AO_LIMIT")
|
|
.ok()
|
|
.and_then(|v| v.parse().ok())
|
|
.unwrap_or(usize::MAX);
|
|
|
|
let mut total_ms = 0.0f64;
|
|
let mut total_verts = 0usize;
|
|
let mut total_tris = 0usize;
|
|
let mut done = 0usize;
|
|
let mut worst: Vec<(f64, String, usize)> = Vec::new();
|
|
// Distribution of the baked AO term across every vertex in the library.
|
|
let mut buckets = [0usize; 5];
|
|
|
|
for f in files.iter().take(limit) {
|
|
let Ok(bytes) = std::fs::read(f) else { continue };
|
|
let t = Instant::now();
|
|
let Ok(m) = StaticModel::parse_glb(&bytes) else { continue };
|
|
let ms = t.elapsed().as_secs_f64() * 1000.0;
|
|
total_ms += ms;
|
|
total_verts += m.vertex_count();
|
|
total_tris += m.triangle_count();
|
|
done += 1;
|
|
worst.push((ms, f.file_name().unwrap().to_string_lossy().into(), m.vertex_count()));
|
|
|
|
for i in 0..m.vertex_count() {
|
|
let packed = m.vertices[i * MODEL_VERTEX_FLOATS + 5].to_bits();
|
|
let ao = ((packed >> 24) & 0xff) as f32 / 255.0;
|
|
let b = ((1.0 - ao) * 5.0).min(4.0) as usize;
|
|
buckets[b] += 1;
|
|
}
|
|
}
|
|
|
|
worst.sort_by(|a, b| b.0.total_cmp(&a.0));
|
|
println!("models {done}, verts {total_verts}, tris {total_tris}");
|
|
println!(
|
|
"parse+bake {:.1} ms total, {:.3} ms/model avg",
|
|
total_ms,
|
|
total_ms / done.max(1) as f64
|
|
);
|
|
println!("slowest:");
|
|
for (ms, name, v) in worst.iter().take(5) {
|
|
println!(" {ms:7.2} ms {v:6} verts {name}");
|
|
}
|
|
let tv = total_verts.max(1) as f32;
|
|
println!("AO spread (share of vertices):");
|
|
for (i, c) in buckets.iter().enumerate() {
|
|
let lo = 1.0 - (i as f32 + 1.0) * 0.2;
|
|
let hi = 1.0 - i as f32 * 0.2;
|
|
println!(" {lo:.2}-{hi:.2} {:5.1}%", *c as f32 / tv * 100.0);
|
|
}
|
|
}
|
|
|
|
fn collect(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
|
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
|
for e in rd.flatten() {
|
|
let p = e.path();
|
|
if p.is_dir() {
|
|
collect(&p, out);
|
|
} else if p.extension().map(|x| x == "glb").unwrap_or(false) {
|
|
out.push(p);
|
|
}
|
|
}
|
|
}
|