diff --git a/apps/fparkan-cli/src/main.rs b/apps/fparkan-cli/src/main.rs index c59ca1f..e469509 100644 --- a/apps/fparkan-cli/src/main.rs +++ b/apps/fparkan-cli/src/main.rs @@ -128,6 +128,12 @@ struct TerrainInspectOutput { faces: usize, slots: usize, material_tags: Vec, + shade_material_pairs: usize, + #[serde(skip_serializing_if = "Option::is_none")] + shade_material_lookup_min: Option, + #[serde(skip_serializing_if = "Option::is_none")] + shade_material_lookup_max: Option, + shade_batch_boundaries: usize, } #[derive(Serialize)] @@ -473,6 +479,22 @@ fn inspect_terrain(args: &[String]) -> Result<(), String> { .map(|(tag, faces)| TerrainMaterialTagCount { tag, faces }) .collect::>(); material_tags.sort_unstable_by_key(|entry| entry.tag); + let shade_material_pairs = (0..terrain.slots.slots_raw.len()) + .filter_map(|slot_index| terrain.slot_material_pairs(slot_index)) + .flatten() + .collect::>(); + let shade_material_lookup_min = shade_material_pairs + .iter() + .map(|pair| pair.shade_selection().material_index) + .min(); + let shade_material_lookup_max = shade_material_pairs + .iter() + .map(|pair| pair.shade_selection().material_index) + .max(); + let shade_batch_boundaries = shade_material_pairs + .iter() + .filter(|pair| pair.flags & 0x0010 != 0) + .count(); println!( "{}", serialize_json(&TerrainInspectOutput { @@ -484,6 +506,10 @@ fn inspect_terrain(args: &[String]) -> Result<(), String> { faces: terrain.faces.len(), slots: terrain.slots.slots_raw.len(), material_tags, + shade_material_pairs: shade_material_pairs.len(), + shade_material_lookup_min, + shade_material_lookup_max, + shade_batch_boundaries, })? ); Ok(()) @@ -733,12 +759,16 @@ mod tests { TerrainMaterialTagCount { tag: 0, faces: 1 }, TerrainMaterialTagCount { tag: 3, faces: 1 }, ], + shade_material_pairs: 2, + shade_material_lookup_min: Some(1), + shade_material_lookup_max: Some(2), + shade_batch_boundaries: 1, }) .expect("serialize terrain inspection"); assert_eq!( json, - "{\"schema_version\":\"fparkan-terrain-inspect-v1\",\"path\":\"DATA/MAPS/AutoMAP/Land.msh\",\"positions\":3,\"min\":[-1.0,-2.0,-3.0],\"max\":[4.0,5.0,6.0],\"faces\":2,\"slots\":4,\"material_tags\":[{\"tag\":0,\"faces\":1},{\"tag\":3,\"faces\":1}]}" + "{\"schema_version\":\"fparkan-terrain-inspect-v1\",\"path\":\"DATA/MAPS/AutoMAP/Land.msh\",\"positions\":3,\"min\":[-1.0,-2.0,-3.0],\"max\":[4.0,5.0,6.0],\"faces\":2,\"slots\":4,\"material_tags\":[{\"tag\":0,\"faces\":1},{\"tag\":3,\"faces\":1}],\"shade_material_pairs\":2,\"shade_material_lookup_min\":1,\"shade_material_lookup_max\":2,\"shade_batch_boundaries\":1}" ); } } diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs index ce97d62..764de09 100644 --- a/crates/fparkan-terrain-format/src/lib.rs +++ b/crates/fparkan-terrain-format/src/lib.rs @@ -211,6 +211,22 @@ pub struct TerrainMaterialPair { pub flags: u16, } +impl TerrainMaterialPair { + /// Returns the material-manager selection used by `GetShade`. + /// + /// `GetShade` owns a manager loaded only with `Shade.wea`, so its + /// 16-bit lookup occupies row bits of that manager's table zero. It is + /// deliberately separate from the `Land1.wea`/`Land2.wea` selections + /// carried by [`TerrainMaterialLayers`]. + #[must_use] + pub const fn shade_selection(self) -> TerrainMaterialSelection { + TerrainMaterialSelection { + table_index: 0, + material_index: self.material_lookup, + } + } +} + impl TerrainSlotTable { /// Returns the render-dispatch header for one decoded slot record. #[must_use] @@ -1569,6 +1585,13 @@ mod tests { }, ]) ); + assert_eq!( + document.slot_material_pairs(0).expect("material pairs")[0].shade_selection(), + TerrainMaterialSelection { + table_index: 0, + material_index: 0x0102, + } + ); assert_eq!(document.slot_material_pairs(1), None); document.slots.slots_raw[0][2..4].copy_from_slice(&3_u16.to_le_bytes()); diff --git a/crates/fparkan-terrain/src/lib.rs b/crates/fparkan-terrain/src/lib.rs index 4eef871..a69df2a 100644 --- a/crates/fparkan-terrain/src/lib.rs +++ b/crates/fparkan-terrain/src/lib.rs @@ -27,7 +27,9 @@ use std::collections::VecDeque; /// /// Applications access this semantic terrain API through this terrain crate /// rather than taking a direct dependency on the binary-format parser. -pub use fparkan_terrain_format::{TerrainMaterialLayers, TerrainMaterialSelection}; +pub use fparkan_terrain_format::{ + TerrainMaterialLayers, TerrainMaterialPair, TerrainMaterialSelection, +}; /// Terrain world. #[derive(Clone, Debug, Default)] diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 5dd2e0c..f6f7832 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1584,9 +1584,14 @@ index, then groups the pairs by material lookup before constructing draw work. `TerrainSlotTable::render_dispatch` exposes exactly those two proven disk fields, while `LandMeshDocument::slot_material_pairs` decodes the selected type-11 entries as `{ material_lookup: u16, flags: u16 }`. Flag `0x0010` is a -proven batch boundary in `GetShade`; the lookup's mapping to the two WEAR -tables, the meanings of its remaining flags, and the final blend operation -remain unassigned. +proven batch boundary in `GetShade`. Crucially, this lookup does **not** select +either map-local Land table: the `GetShade` constructor loads its own one-table +manager from `system.rlb` resource `Shade.wea`, so the 16-bit value is that +manager's table-zero row. `TerrainMaterialPair::shade_selection` records this +separate selector instead of conflating it with `Land1.wea`/`Land2.wea`. The +AutoDemo inspector now reports 128 slots, 3,174 addressed shade pairs, selector +range `0..3173`, and 392 `0x0010` batch boundaries. The meanings of remaining +flags and the final blend operation remain unassigned. The static Vulkan bridge now carries each contiguous face run as a separate draw range keyed by the original packed `material_tag`; it neither reorders