Files
benilla-octo/crates/benilla-formats/examples/wmo_coplanar.rs
T
SamandClaude Fable 5.1 60138f5d5d every objective line shows, the detail follows the row, and 1.98 lints
- a quest objective that carries both a creature and an item is two lines in the quest log, as the reference's four passes enumerate it: Graverobbers shows its Embalming Ichor (2249)
- the quest log's detail follows the live selection inside a frame, so a quest addon's log walk reads the right quest; Questie no longer files a chain quest under its shortest sibling (2247)
- a UI tint change is not a material change: a run's colour rides the batch's instance tag, so the PlayerFrame glow no longer re-prepares a material every frame (2245)
- the workspace lints clean on rustc 1.98: constant-size chunks become as_chunks, one slice fill, two unused globs
- WOW_HOVER_PROBE aims the pick in a headless run, and the inspect card names the tooltip's ladder stage (2248, 2250)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 17:01:27 +07:00

211 lines
8.0 KiB
Rust

//! Which batches of a WMO are **coplanar with each other**, and by how much:
//! `cargo run -p benilla-formats --example wmo_coplanar -- <wmo-path-or-substring> [gap_yd]`
//! e.g. `wmo_coplanar orctower 0.05`.
//!
//! The falsifier for a z-fighting report. Two surfaces that swap which one is in front do it for one
//! of two reasons, and they take different fixes: a **true tie** (the author put both faces on the
//! same plane, so the depth test has nothing to decide with and the winner is whoever drew last) or
//! **depth precision** (a real but tiny separation that quantises away). The distinction is a
//! property of the *file*, not of the frame — so read it here rather than inferring it from pixels.
//!
//! Prints, for every pair of batches whose geometry overlaps in space, the smallest plane gap between
//! near-parallel faces that actually overlap. A `0.000` gap is a tie; anything else is a separation
//! to compare against the depth buffer's resolution at that range.
//!
//! Output is Blizzard data — pipe it to the scratchpad, never into the repo.
use benilla_formats::RenderSubmesh;
/// Faces this close to parallel are treated as the same plane orientation (≈2.6°).
const PARALLEL_DOT: f32 = 0.999;
/// One triangle reduced to its plane plus the box it occupies — everything the gap test needs.
struct Face {
normal: [f32; 3],
/// Plane offset: `normal · vertex`.
d: f32,
min: [f32; 3],
max: [f32; 3],
/// The triangle's own corners, kept so a batch can be compared against **itself**: two faces of
/// one flat quad share an edge and are trivially coplanar, which is not a defect. Real overlapping
/// sheets share no vertex.
verts: [[f32; 3]; 3],
}
fn faces(sub: &RenderSubmesh) -> Vec<Face> {
sub.indices
.as_chunks::<3>()
.0
.iter()
.filter_map(|t| {
let p: Vec<[f32; 3]> = t.iter().map(|&i| sub.positions[i as usize]).collect();
let (u, v) = (sub3(p[1], p[0]), sub3(p[2], p[0]));
let n = [
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
];
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
if len < 1e-9 {
return None; // degenerate triangle: no plane to speak of
}
let normal = [n[0] / len, n[1] / len, n[2] / len];
let mut min = p[0];
let mut max = p[0];
for q in &p[1..] {
for c in 0..3 {
min[c] = min[c].min(q[c]);
max[c] = max[c].max(q[c]);
}
}
Some(Face {
d: dot(normal, p[0]),
normal,
min,
max,
verts: [p[0], p[1], p[2]],
})
})
.collect()
}
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
/// Do the two boxes overlap, allowing `slack` of separation on every axis?
fn boxes_touch(a: &Face, b: &Face, slack: f32) -> bool {
(0..3).all(|c| a.min[c] - slack <= b.max[c] && b.min[c] - slack <= a.max[c])
}
/// Do the triangles share a corner? Two halves of a quad do; two stacked sheets do not.
fn shares_a_vertex(a: &Face, b: &Face) -> bool {
a.verts.iter().any(|p| {
b.verts
.iter()
.any(|q| (0..3).all(|c| (p[c] - q[c]).abs() < 1e-5))
})
}
/// The smallest plane gap between near-parallel, spatially-overlapping faces of `a` and `b`, or
/// `None` if no such pair sits within `gap`. When `a` and `b` are the same batch, edge-sharing
/// neighbours are skipped — they are how a flat surface is built, not a defect.
fn closest_coplanar(a: &[Face], b: &[Face], gap: f32, same_batch: bool) -> Option<f32> {
let mut best: Option<f32> = None;
for (i, fa) in a.iter().enumerate() {
// Within one batch every unordered pair is visited once, and a face never fights itself.
let rest = if same_batch { &b[i + 1..] } else { b };
for fb in rest {
if !boxes_touch(fa, fb, gap) || (same_batch && shares_a_vertex(fa, fb)) {
continue;
}
let d = dot(fa.normal, fb.normal);
if d.abs() < PARALLEL_DOT {
continue;
}
// Flip B's plane to A's orientation so the offsets are comparable.
let db = if d < 0.0 { -fb.d } else { fb.d };
let g = (fa.d - db).abs();
if g <= gap && best.is_none_or(|x| g < x) {
best = Some(g);
}
}
}
best
}
fn main() -> anyhow::Result<()> {
let mut args = std::env::args().skip(1);
let pat = args
.next()
.ok_or_else(|| anyhow::anyhow!("usage: wmo_coplanar <wmo-path-or-substring> [gap_yd]"))?;
// Report pairs whose faces sit within this gap. The default is generous — a real z-fight needs a
// gap far under a centimetre, but seeing the near misses tells you where the margin actually is.
let gap: f32 = args.next().map_or(Ok(0.05), |g| g.parse())?;
let data = benilla_formats::wow_data().expect("no WoW install found (set $WOW_DATA)");
let mut chain = benilla_formats::open_chain(&data)?;
let path = if pat.to_lowercase().ends_with(".wmo") {
pat.clone()
} else {
let lower = pat.to_lowercase();
chain
.list()?
.into_iter()
.map(|e| e.name)
.find(|n| {
let l = n.to_lowercase();
l.ends_with(".wmo") && l.contains(&lower) && !l.contains("_00")
})
.ok_or_else(|| anyhow::anyhow!("no root .wmo matching {pat:?}"))?
};
println!("{path}\n");
let subs = benilla_formats::load_wmo(&mut chain, &path)?;
println!(
"=== {} batches (spawn order = depth-bias order) ===",
subs.len()
);
for (i, s) in subs.iter().enumerate() {
let n = s.positions.len();
let (mut min, mut max) = ([f32::MAX; 3], [f32::MIN; 3]);
for p in &s.positions {
for c in 0..3 {
min[c] = min[c].min(p[c]);
max[c] = max[c].max(p[c]);
}
}
println!(
"[{i:3}] bias {:3} {:5} tri {:?}{}{} span ({:.1},{:.1},{:.1})..({:.1},{:.1},{:.1}) {}",
i + 1,
s.indices.len() / 3,
s.blend,
if s.interior { " INT" } else { "" },
if s.emissive { " UNLIT" } else { "" },
min[0], min[1], min[2], max[0], max[1], max[2],
s.texture.as_deref().unwrap_or("(none)"),
);
let _ = n;
}
let all: Vec<Vec<Face>> = subs.iter().map(faces).collect();
// Facing is ignored throughout: two coplanar faces z-fight whether or not their normals agree.
println!("\n=== batches coplanar WITH THEMSELVES (gap <= {gap}) ===");
println!("(no per-batch depth bias can separate these — the pipeline draws them as one call)");
let mut self_found = 0;
for (i, f) in all.iter().enumerate() {
if let Some(g) = closest_coplanar(f, f, gap, true) {
self_found += 1;
println!(
"[{i:3}] gap {g:.5} yd {}",
subs[i].texture.as_deref().unwrap_or("(none)")
);
}
}
if self_found == 0 {
println!("(none)");
}
println!("\n=== coplanar batch pairs (gap <= {gap}) ===");
let mut found = 0;
for a in 0..all.len() {
for b in (a + 1)..all.len() {
if let Some(g) = closest_coplanar(&all[a], &all[b], gap, false) {
found += 1;
println!(
"[{a:3}] x [{b:3}] gap {g:.5} yd {} | {}",
subs[a].texture.as_deref().unwrap_or("(none)"),
subs[b].texture.as_deref().unwrap_or("(none)"),
);
}
}
}
if found == 0 {
println!("(none — no two batches share a plane within {gap} yd)");
}
Ok(())
}