Files
benilla-octo/crates/benilla-formats/examples/wmo_doubled.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

222 lines
8.7 KiB
Rust

//! Which faces of a WMO are **authored twice** — two triangles on the same three positions:
//! `cargo run -p benilla-formats --example wmo_doubled -- <wmo-path-or-substring>`
//! e.g. `wmo_doubled orctower`.
//!
//! The falsifier `wmo_coplanar` structurally cannot be. That tool skips same-batch face pairs that
//! share a vertex ("two halves of a quad") — but a thin sheet whose two *sides* are authored by
//! reusing the same vertices with **reversed winding** shares all three, so a doubled canvas awning
//! is invisible to it by construction (it reported the B38 tent's nearest partner at 1.91 yd while
//! the tent's own two sides sat at 0.000). This tool asks the opposite question: which triangles
//! occupy the SAME three positions?
//!
//! Doubled-with-opposite-winding is not a defect — it is how 1.12 authors visible-from-both-sides
//! cloth when the material is single-sided: the real client backface-culls each copy from the wrong
//! side, so exactly one covers any pixel. It *becomes* a defect the moment the renderer draws WMO
//! batches two-sided — ours did until `3af8854f` honoured MOMT `0x04` (B38, decision 0680): both
//! copies then rasterize at depths equal to the ulp, and the winner per pixel is floating-point
//! noise — latched while the camera is bit-still, flickering while anything creeps. That is the
//! defect this tool was built to name, and it is now closed; it stays as the census. Each pair
//! prints with its
//! batches' MOMT flags: `0x04` (UNCULLED) set means the file *asked* for two-sided and the double
//! draw is authored intent; clear means the renderer's shortcut is manufacturing a z-tie the file
//! never contained.
//!
//! Output is Blizzard data — pipe it to the scratchpad, never into the repo.
use std::collections::HashMap;
use benilla_wmo::{parse_wmo, ParsedWmo};
/// Positions are matched to a tenth of a millimetre — far under authoring precision, far over f32
/// noise at building scale.
const QUANT: f32 = 10_000.0;
fn key(p: [f32; 3]) -> [i64; 3] {
p.map(|c| (c * QUANT).round() as i64)
}
/// A triangle's identity (order-free) plus the data needed to classify its partner.
struct Tri {
batch: usize,
/// Geometric normal in original winding order — two copies with `dot < 0` are opposite-wound.
normal: [f32; 3],
/// Mean authored (MONR) normal, to name which side of the sheet this copy is.
authored: [f32; 3],
}
fn main() -> anyhow::Result<()> {
let pat = std::env::args()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("usage: wmo_doubled <wmo-path-or-substring>"))?;
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");
// The root's MOMT table, raw — the submeshes don't carry material flags, and the flags decide
// whether a doubled pair is authored intent (0x04 UNCULLED) or a renderer-manufactured tie.
let root_bytes = chain.read_file(&path.to_ascii_lowercase())?;
let (materials, textures, tex_map) = match parse_wmo(&mut std::io::Cursor::new(&root_bytes))? {
ParsedWmo::Root(r) => (r.materials, r.textures, r.texture_offset_index_map),
ParsedWmo::Group(_) => anyhow::bail!("{path} is a group file, want the root"),
};
println!("=== MOMT ({} materials) ===", materials.len());
for (i, m) in materials.iter().enumerate() {
let tex = textures
.get(m.get_texture1_index(&tex_map) as usize)
.map(String::as_str)
.unwrap_or("(none)");
println!(
"[{i:3}] flags {:#06x}{} blend {} {tex}",
m.flags,
if m.flags & 0x04 != 0 { " UNCULLED" } else { "" },
m.blend_mode,
);
}
let subs = benilla_formats::load_wmo(&mut chain, &path)?;
// One global bucket per geometric triangle, across every batch: any bucket holding two entries
// is a doubled face, wherever its copies live.
let mut buckets: HashMap<[[i64; 3]; 3], Vec<Tri>> = HashMap::new();
for (bi, s) in subs.iter().enumerate() {
for t in s.indices.chunks_exact(3) {
let p: Vec<[f32; 3]> = t.iter().map(|&i| s.positions[i as usize]).collect();
let (u, v) = (sub3(p[1], p[0]), sub3(p[2], p[0]));
let n = cross(u, v);
if len(n) < 1e-9 {
continue; // degenerate: no facing to double
}
let mut authored = [0.0f32; 3];
for &i in t {
let a = s.normals.get(i as usize).copied().unwrap_or([0.0; 3]);
for c in 0..3 {
authored[c] += a[c];
}
}
let mut k = [key(p[0]), key(p[1]), key(p[2])];
k.sort_unstable();
buckets.entry(k).or_default().push(Tri {
batch: bi,
normal: n,
authored,
});
}
}
// Doubled-face census per batch pair, split by winding — opposite is the two-sided-sheet
// authoring; same-winding true duplicates would be a different (and stranger) finding.
/// Per batch pair: doubled-face count + each side's summed authored normal.
type OppositeCensus = HashMap<(usize, usize), (usize, [f32; 3], [f32; 3])>;
let mut opposite: OppositeCensus = HashMap::new();
let mut same: HashMap<(usize, usize), usize> = HashMap::new();
for tris in buckets.values() {
for i in 0..tris.len() {
for j in (i + 1)..tris.len() {
let (a, b) = (&tris[i], &tris[j]);
let pair = (a.batch.min(b.batch), a.batch.max(b.batch));
if dot(a.normal, b.normal) < 0.0 {
let e = opposite.entry(pair).or_insert((0, [0.0; 3], [0.0; 3]));
e.0 += 1;
// Accumulate each side's authored normal by batch order, so the report can say
// "batch A faces up, batch B faces down" rather than just "they differ".
let (first, second) = if a.batch <= b.batch { (a, b) } else { (b, a) };
for c in 0..3 {
e.1[c] += first.authored[c];
e.2[c] += second.authored[c];
}
} else {
*same.entry(pair).or_default() += 1;
}
}
}
}
let describe = |bi: usize| {
let s = &subs[bi];
format!(
"[{bi:3}] bias {:3} {:?} {}",
bi + 1,
s.blend,
s.texture.as_deref().unwrap_or("(none)"),
)
};
println!("\n=== doubled faces, OPPOSITE winding (the two-sided-sheet authoring) ===");
let mut pairs: Vec<_> = opposite.iter().collect();
pairs.sort_by_key(|(k, _)| **k);
if pairs.is_empty() {
println!("(none)");
}
for (&(a, b), &(count, na, nb)) in pairs {
println!(
"{count:5} tri {} <-> {}",
describe(a),
if a == b {
"ITSELF".to_string()
} else {
describe(b)
}
);
println!(
" side A authored normal ~[{:+.2},{:+.2},{:+.2}] side B ~[{:+.2},{:+.2},{:+.2}]",
na[0] / count as f32 / 3.0,
na[1] / count as f32 / 3.0,
na[2] / count as f32 / 3.0,
nb[0] / count as f32 / 3.0,
nb[1] / count as f32 / 3.0,
nb[2] / count as f32 / 3.0,
);
}
println!(
"\n=== doubled faces, SAME winding (true duplicates — would double-draw even culled) ==="
);
if same.is_empty() {
println!("(none)");
}
let mut pairs: Vec<_> = same.iter().collect();
pairs.sort_by_key(|(k, _)| **k);
for (&(a, b), &count) in pairs {
println!(
"{count:5} tri {} <-> {}",
describe(a),
if a == b {
"ITSELF".to_string()
} else {
describe(b)
}
);
}
Ok(())
}
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn cross(u: [f32; 3], v: [f32; 3]) -> [f32; 3] {
[
u[1] * v[2] - u[2] * v[1],
u[2] * v[0] - u[0] * v[2],
u[0] * v[1] - u[1] * v[0],
]
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn len(a: [f32; 3]) -> f32 {
dot(a, a).sqrt()
}