- 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>
202 lines
8.0 KiB
Rust
202 lines
8.0 KiB
Rust
//! **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(())
|
||
}
|