//! **Which M2 batches share their triangles with another batch** — the coplanar-sibling census: //! `cargo run -p benilla-formats --example m2_shared_section -- [path-substring]`. //! //! An M2 skin *section* is a run of triangles; a *batch* is one draw of one section. When two //! batches name the SAME section they rasterize the SAME triangles — a base layer and a shine / //! reflect layer authored on top of it (`ARMORREFLECT3`, `BALISTASHINE02`). The reference draws //! both from one vertex array under depth-write + LEQUAL, so the second always wins the tie //! exactly (wow-re `m2-depth-blend-state`). //! //! We only match that if both batches take the SAME vertex-transform path. They do not when a //! consolidator (`static_gx`, `terrain_stream::merge`) admits one and refuses the other: the //! retained lane bakes world positions on the CPU and the entity lane rotates on the GPU, and the //! two agree only to the last few ULPs — enough, on coplanar triangles, to turn the depth test //! into a per-pixel coin flip that re-rolls whenever the camera moves. That is the ballista's //! flickering bolt heads and shields. //! //! So this prints, per model, every multi-batch section and whether its batches DISAGREE on the //! facts the consolidators exclude on (env-mapped UVs, render flags `0x10` no-depth-write / `0x08` //! no-depth-test, additive blend 3/4). A `SPLIT` line is a model that can z-fight against itself. //! //! Output is Blizzard data — pipe it to the scratchpad, never into the repo. use std::io::Cursor; /// The facts every consolidator excludes on — a batch answering `true` can never ride the /// retained/merged lanes, so it stays on the entity path whatever its siblings do. fn refusable(model: &benilla_m2::M2Model, batch: &benilla_m2::SkinBatch) -> bool { let mat = model.materials.get(batch.material_index as usize); let flags = mat.map_or(0, |m| m.flags.bits()); let blend = mat.map_or(0, |m| m.blend_mode.bits()); model.stage_is_env_mapped(batch, 0) || flags & 0x10 != 0 || flags & 0x08 != 0 || matches!(blend, 3 | 4) } fn main() -> anyhow::Result<()> { let pat = std::env::args().nth(1).unwrap_or_default().to_lowercase(); let data = benilla_formats::wow_data().expect("no WoW install found (set $WOW_DATA)"); let mut chain = benilla_formats::open_chain(&data)?; let names: Vec = chain .list()? .into_iter() .map(|e| e.name) .filter(|n| { let l = n.to_lowercase(); l.ends_with(".m2") && l.contains(&pat) }) .collect(); let (mut models, mut with_shared, mut with_split) = (0usize, 0usize, 0usize); let (mut batches_total, mut batches_shared, mut batches_split) = (0usize, 0usize, 0usize); for name in &names { let Ok(bytes) = chain.read_file(name) else { continue; }; let Ok(m2) = benilla_m2::parse_m2(&mut Cursor::new(bytes.as_slice())) else { continue; }; let model = m2.model(); let Ok(skin) = model.parse_embedded_skin(&bytes, 0) else { continue; }; models += 1; batches_total += skin.batches().len(); // section index → (batch count, refusable count) let mut per_section: std::collections::BTreeMap = Default::default(); for b in skin.batches() { let e = per_section.entry(b.skin_section_index).or_default(); e.0 += 1; e.1 += usize::from(refusable(model, b)); } let shared: Vec<_> = per_section.iter().filter(|(_, v)| v.0 > 1).collect(); if shared.is_empty() { continue; } with_shared += 1; let mut split = false; for (sec, (n, r)) in &shared { batches_shared += n; // Disagreement is the defect: some of this section's batches divert, some cannot. if *r > 0 && *r < *n { split = true; batches_split += n; println!("SPLIT {name} section {sec}: {n} batches, {r} refusable"); } } if split { with_split += 1; } } println!( "\n{models} models parsed, {batches_total} batches\n\ {with_shared} models have a multi-batch section ({batches_shared} batches in one)\n\ {with_split} models SPLIT one ({batches_split} batches) — coplanar siblings that land in \ different lanes" ); Ok(()) }