aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 16:31:13 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 16:31:13 +0300
commit575124830b678d0c4d5b37a708bba9692a4e47ef (patch)
treee36ae21ca5eb4cbac35523809991ffdbe6099e84
parent7a7578d0db2a0f6bd0a440f5ef1f72432f8b38ae (diff)
downloadfparkan-575124830b678d0c4d5b37a708bba9692a4e47ef.tar.xz
fparkan-575124830b678d0c4d5b37a708bba9692a4e47ef.zip
feat(terrain): separate shade material selections
-rw-r--r--apps/fparkan-cli/src/main.rs32
-rw-r--r--crates/fparkan-terrain-format/src/lib.rs23
-rw-r--r--crates/fparkan-terrain/src/lib.rs4
-rw-r--r--docs/tomes/05-render.md11
4 files changed, 65 insertions, 5 deletions
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<TerrainMaterialTagCount>,
+ shade_material_pairs: usize,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ shade_material_lookup_min: Option<u16>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ shade_material_lookup_max: Option<u16>,
+ shade_batch_boundaries: usize,
}
#[derive(Serialize)]
@@ -473,6 +479,22 @@ fn inspect_terrain(args: &[String]) -> Result<(), String> {
.map(|(tag, faces)| TerrainMaterialTagCount { tag, faces })
.collect::<Vec<_>>();
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::<Vec<_>>();
+ 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