Files
benilla-octo/crates/benilla-formats/examples/eye_quad_pass.rs
T
SamandClaude Fable 5 6b980b9efb the player build: one binary that carries its content
- a player build ships beside the dev one — dev chords and probe surfaces go dark, and a gate enforces what a player can still reach, by medium not by module
- the binary carries its own shaders and its shipped UI, finds the WoW folder by itself, and keeps its local state in one benilla-config folder
- the glue screens get a capture shutter, and the engine stops depending on the protocol crate

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:18:02 +07:00

202 lines
8.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! **The flush-emitter depth contest, computed offline** — what fraction of a particle emitter's
//! constant-depth billboard survives `LEQUAL` against its own model's z-writing batches, from a
//! given camera direction. No client, no camera, no capture: pure geometry over *our* parse.
//!
//! This exists to hold benilla's asset side against wow-re's measured table
//! (`system/models/scratch/part-flush-emitter-depth.md` §4 — Voidwalker bone 60, half 0.0833:
//! front 43.4 %, 30° above 38.8 %, 60° left 43.3 %). Agreement means our mesh, our bone pivots and
//! our quad model are faithful and any missing glow is the *renderer's*; disagreement localises the
//! bug to the asset side, where it can be fixed without ever launching the game.
//!
//! `cargo run -p benilla-formats --example eye_quad_pass -- 'Creature\Voidwalker\Voidwalker.m2'`
//!
//! Model space, rest pose, WoW axes (X forward, Y left, Z up) — the emitter record `position` is
//! the quad centre (it is bit-identical to its bone's pivot, and the palette is identity at rest).
//! The occluder is every batch that is **not** `no_depth_write`, exactly the set the depth buffer
//! holds when the particle pass runs.
use benilla_formats::RenderSubmesh;
/// Möller–Trumbore. Returns the ray parameter `t` of the hit, if the ray meets the triangle ahead
/// of its origin.
fn ray_tri(orig: [f32; 3], dir: [f32; 3], a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> Option<f32> {
let sub = |p: [f32; 3], q: [f32; 3]| [p[0] - q[0], p[1] - q[1], p[2] - q[2]];
let cross = |p: [f32; 3], q: [f32; 3]| {
[
p[1] * q[2] - p[2] * q[1],
p[2] * q[0] - p[0] * q[2],
p[0] * q[1] - p[1] * q[0],
]
};
let dot = |p: [f32; 3], q: [f32; 3]| p[0] * q[0] + p[1] * q[1] + p[2] * q[2];
let (e1, e2) = (sub(b, a), sub(c, a));
let h = cross(dir, e2);
let det = dot(e1, h);
if det.abs() < 1e-9 {
return None; // ray parallel to the triangle plane
}
// Two-sided on purpose: the shell's inward-facing triangles occlude too — the depth buffer
// holds whatever rasterized, and these batches are authored two-sided or seen from inside.
let inv = 1.0 / det;
let s = sub(orig, a);
let u = inv * dot(s, h);
if !(0.0..=1.0).contains(&u) {
return None;
}
let q = cross(s, e1);
let v = inv * dot(dir, q);
if v < 0.0 || u + v > 1.0 {
return None;
}
let t = inv * dot(e2, q);
(t > 1e-6).then_some(t)
}
/// The camera-facing orthonormal pair for a view direction (the two screen axes the quad's corners
/// are offset along — the third axis carries the shared depth).
fn screen_axes(view: [f32; 3]) -> ([f32; 3], [f32; 3]) {
// `view` points from the camera toward the model. Any up-ish reference not parallel to it.
let up = if view[2].abs() > 0.9 {
[1.0, 0.0, 0.0]
} else {
[0.0, 0.0, 1.0]
};
let cross = |p: [f32; 3], q: [f32; 3]| {
[
p[1] * q[2] - p[2] * q[1],
p[2] * q[0] - p[0] * q[2],
p[0] * q[1] - p[1] * q[0],
]
};
let norm = |v: [f32; 3]| {
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
[v[0] / l, v[1] / l, v[2] / l]
};
let right = norm(cross(view, up));
let real_up = norm(cross(right, view));
(right, real_up)
}
/// Fraction of the quad's area that passes `LEQUAL` — i.e. whose sample point is not occluded by
/// any z-writing triangle between it and the camera. Constant depth: every sample sits on the plane
/// through `center` perpendicular to `view`, so "occluded" is simply "a triangle lies in front".
fn pass_fraction(
tris: &[([f32; 3], [f32; 3], [f32; 3])],
center: [f32; 3],
half: f32,
view: [f32; 3],
n: usize,
) -> f32 {
let (right, up) = screen_axes(view);
// Cast *toward* the camera: anything hit is in front of the sample point.
let back = [-view[0], -view[1], -view[2]];
let mut passed = 0usize;
let mut total = 0usize;
for iy in 0..n {
for ix in 0..n {
// Sample cell centres so the grid is unbiased at the quad's edges.
let fx = (ix as f32 + 0.5) / n as f32 * 2.0 - 1.0;
let fy = (iy as f32 + 0.5) / n as f32 * 2.0 - 1.0;
let p = [
center[0] + right[0] * fx * half + up[0] * fy * half,
center[1] + right[1] * fx * half + up[1] * fy * half,
center[2] + right[2] * fx * half + up[2] * fy * half,
];
total += 1;
if !tris
.iter()
.any(|&(a, b, c)| ray_tri(p, back, a, b, c).is_some())
{
passed += 1;
}
}
}
passed as f32 / total as f32
}
/// Every triangle of every batch that writes depth — the occluder set, in model space.
///
/// `all` ignores the per-material `no_depth_write` bit, i.e. models a renderer that writes depth for
/// the whole model. That is the counterfactual worth pricing: it is what benilla's distance-fade
/// blend twin does (it forces depth-write ON regardless of the flag), so the two columns bracket
/// "did we leak the model's own transparent cards into the depth buffer?".
fn occluders(subs: &[RenderSubmesh], all: bool) -> Vec<([f32; 3], [f32; 3], [f32; 3])> {
let mut tris = Vec::new();
for s in subs {
if !all && (s.no_depth_write || s.no_depth_test) {
continue;
}
for t in s.indices.chunks_exact(3) {
tris.push((
s.positions[t[0] as usize],
s.positions[t[1] as usize],
s.positions[t[2] as usize],
));
}
}
tris
}
fn main() -> anyhow::Result<()> {
let virt = std::env::args()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("usage: eye_quad_pass <m2 path>"))?;
let data = benilla_formats::wow_data().expect("no WoW install found (set $WOW_DATA)");
let mut chain = benilla_formats::open_chain(&data)?;
let bytes = chain.read_file(&virt)?;
let subs = benilla_formats::parse_m2_render_submeshes(&bytes, "", &[])?;
let tris = occluders(&subs, false);
let tris_all = occluders(&subs, true);
let writing: Vec<usize> = subs
.iter()
.enumerate()
.filter(|(_, s)| !s.no_depth_write && !s.no_depth_test)
.map(|(i, _)| i)
.collect();
println!(
"{} batches, {} z-writing ({:?}), {} occluding triangles",
subs.len(),
writing.len(),
writing,
tris.len()
);
// Camera directions, as (label, azimuth° about +Z from +X, elevation° above the horizontal).
// `view` points camera → model, so the camera sits at -view.
let views: &[(&str, f32, f32)] = &[
("front (+X)", 0.0, 0.0),
("15 above", 0.0, 15.0),
("30 above", 0.0, 30.0),
("45 above", 0.0, 45.0),
("60 above", 0.0, 60.0),
("30 left", 30.0, 0.0),
("60 left", 60.0, 0.0),
("30 left + 15 above", 30.0, 15.0),
("30 left + 30 above", 30.0, 30.0),
];
for e in benilla_formats::parse_m2_particle_emitters(&bytes)?.iter() {
// Only the plane/quad emitters we can model this way, and only those actually mounted in
// the mesh — the point of the exercise.
println!(
"\nemitter bone {} pos ({:.4}, {:.4}, {:.4}) blend {:?}",
e.bone, e.position[0], e.position[1], e.position[2], e.blend
);
for (set, occ) in [("flagged", &tris), ("ALL-write", &tris_all)] {
let half = 0.0833f32;
print!(" {set:9} half {half:.4}:");
for &(label, az, el) in views {
let (a, l) = (az.to_radians(), el.to_radians());
// Camera at azimuth `az`, elevation `el`, looking at the model: view = -(camera dir)
let view = [-(a.cos() * l.cos()), -(a.sin() * l.cos()), -(l.sin())];
let f = pass_fraction(occ, e.position, half, view, 64);
print!(" {label} {:.1}%", f * 100.0);
}
println!();
}
}
Ok(())
}