- The main menu bar's two empty recesses fill. The micro menu — the eight arched buttons: character, spellbook, talents, quest log, socials, world map, main menu, help — sits in the dark panel between the page arrows and the bags, transcribed from the reference's own layout: the character button wears your portrait, a button holds itself down while its panel is up, and the talent button appears at level 10. The latency meter fills the one slot the bar art leaves transparent for exactly it, tinted green/yellow/red on the reference's 300/600 ms thresholds and drawn UNDER the bar art, seen through its window, the way 1.12 actually does it. The socials and main-menu buttons find their panels one snapshot later; help still waits. - The UI explains itself on hover: 1.12 ships its detailed two-line tooltips ON — a FrameXML default, not the opt-in extra it is often remembered as — so the micro buttons, your own portrait, the XP bar, the five bag buttons and the ping meter all answer a hover with the reference's own explanations. - The PvP flag, end to end: crossed swords on the player, target and party frames, driven by the two descriptor bits the wire actually carries, and `/pvp` speaks — the yellow toast plus the system chat line, the OFF sentence being the one that warns the flag lingers for five minutes. It really does linger: turning it off waits out the server's timer, which looks like a bug and is the reference. - Quest markers stopped freezing. The grey `!` over a questgiver above your level goes gold the moment you ding, an NPC re-entering view is asked afresh instead of wearing a session-old answer, and the marker drops when the questgiver bit goes off — the re-ask triggers mirror the reference's own set, plus one named addition (your own level changing). - Water and heights: swimming downhill no longer bounces between swim and fall ten times a second — the surface line is a two-sided constraint, so the swimmer settles with the river instead of being left behind by it. And collision height is per-unit now, read from the unit's own model: a gnome used to swim with a third of a yard of water over her head because five depth lines were quoting a human's number. - A round of pixel honesty in text and chat: the newest chat line no longer wears a wider drop shadow than every line above it (shadows move in whole pixels now), keeps its descenders, and hover boxes stop painting over the chat body. The chat input's history browse survives programmatic edits, a failed script chunk warns instead of vanishing, and recall deepens to 64 lines. Idle animation got honest too: a model's idle is Stand resolved through its own lookup table — never file order, which had the duel flag bobbing on its spawn clip — and an armed idle is culled on the box its animation authors, not the bind pose, so the flag stops vanishing at most camera angles. 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 = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../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(())
|
|
}
|