- 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>
209 lines
8.0 KiB
Rust
209 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
|
|
.chunks_exact(3)
|
|
.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(())
|
|
}
|