aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/fparkan-game/src/main.rs7
-rw-r--r--crates/fparkan-terrain-format/src/lib.rs79
-rw-r--r--crates/fparkan-terrain/src/lib.rs6
-rw-r--r--docs/tomes/05-render.md6
4 files changed, 90 insertions, 8 deletions
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs
index 9a30c60..b16cec1 100644
--- a/apps/fparkan-game/src/main.rs
+++ b/apps/fparkan-game/src/main.rs
@@ -45,6 +45,7 @@ use fparkan_runtime::{
loaded_mission_assets, loaded_mission_object_drafts, loaded_terrain, EngineConfig, EngineMode,
EngineServices, MissionAssets, MissionLoadPhase, MissionObjectDraft, MissionRequest,
};
+use fparkan_terrain::TerrainMaterialLayers;
use fparkan_terrain::TerrainWorld;
use fparkan_vfs::DirectoryVfs;
#[cfg(test)]
@@ -477,9 +478,11 @@ fn static_preview_terrain_base_materials(
packed_tags
.into_iter()
.map(|packed_tag| {
- let selector = packed_tag & 0x00ff;
+ let selection = TerrainMaterialLayers::from_packed_tag(packed_tag).land2_selection();
let selected = load_standalone_wear_material_texture_mip0_rgba8_from_root(
- root, &wear_path, selector,
+ root,
+ &wear_path,
+ selection.material_index,
)
.map_err(|err| {
format!(
diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs
index ddb439d..6e018c0 100644
--- a/crates/fparkan-terrain-format/src/lib.rs
+++ b/crates/fparkan-terrain-format/src/lib.rs
@@ -74,6 +74,57 @@ pub struct TerrainMaterialLayers {
pub land2_selector: u8,
}
+/// One `World3D` material-manager lookup key.
+///
+/// The original manager selects a WEAR table in the upper 16 bits and its
+/// positional material row in the lower 16 bits.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct TerrainMaterialSelection {
+ /// Zero-based material-manager table index.
+ pub table_index: u16,
+ /// Positional material row in that WEAR table.
+ pub material_index: u16,
+}
+
+impl TerrainMaterialSelection {
+ /// Encodes the original material-manager phase lookup key.
+ #[must_use]
+ pub fn phase_key(self) -> u32 {
+ (u32::from(self.table_index) << 16) | u32::from(self.material_index)
+ }
+}
+
+impl TerrainMaterialLayers {
+ /// Constructs the decoded pair from its on-disk packed tag.
+ #[must_use]
+ pub const fn from_packed_tag(material_tag: u16) -> Self {
+ let [land2_selector, land1] = material_tag.to_le_bytes();
+ Self {
+ land1_selector: if land1 == u8::MAX { None } else { Some(land1) },
+ land2_selector,
+ }
+ }
+
+ /// Returns the table-zero `Land1.wea` selection when it exists.
+ #[must_use]
+ pub fn land1_selection(self) -> Option<TerrainMaterialSelection> {
+ self.land1_selector
+ .map(|material_index| TerrainMaterialSelection {
+ table_index: 0,
+ material_index: u16::from(material_index),
+ })
+ }
+
+ /// Returns the table-one `Land2.wea` selection.
+ #[must_use]
+ pub fn land2_selection(self) -> TerrainMaterialSelection {
+ TerrainMaterialSelection {
+ table_index: 1,
+ material_index: u16::from(self.land2_selector),
+ }
+ }
+}
+
/// Terrain face with 28-byte source layout.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TerrainFace28 {
@@ -97,11 +148,7 @@ impl TerrainFace28 {
/// Decodes the proven two-table selector layout of [`Self::material_tag`].
#[must_use]
pub const fn material_layers(&self) -> TerrainMaterialLayers {
- let [land2_selector, land1] = self.material_tag.to_le_bytes();
- TerrainMaterialLayers {
- land1_selector: if land1 == u8::MAX { None } else { Some(land1) },
- land2_selector,
- }
+ TerrainMaterialLayers::from_packed_tag(self.material_tag)
}
}
@@ -1427,6 +1474,20 @@ mod tests {
land2_selector: 2,
}
);
+ assert_eq!(
+ document.faces[0].material_layers().land1_selection(),
+ Some(TerrainMaterialSelection {
+ table_index: 0,
+ material_index: 1,
+ })
+ );
+ assert_eq!(
+ document.faces[0]
+ .material_layers()
+ .land2_selection()
+ .phase_key(),
+ 0x0001_0002
+ );
raw_face[4..6].copy_from_slice(&0xff03_u16.to_le_bytes());
let nres = decode_nres(&minimal_land_msh(&raw_face)).expect("nres");
@@ -1438,6 +1499,14 @@ mod tests {
land2_selector: 3,
}
);
+ assert_eq!(document.faces[0].material_layers().land1_selection(), None);
+ assert_eq!(
+ document.faces[0].material_layers().land2_selection(),
+ TerrainMaterialSelection {
+ table_index: 1,
+ material_index: 3,
+ }
+ );
}
#[test]
diff --git a/crates/fparkan-terrain/src/lib.rs b/crates/fparkan-terrain/src/lib.rs
index 65146b9..4eef871 100644
--- a/crates/fparkan-terrain/src/lib.rs
+++ b/crates/fparkan-terrain/src/lib.rs
@@ -23,6 +23,12 @@
use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument};
use std::collections::VecDeque;
+/// Terrain material-selector contract preserved from the decoded map mesh.
+///
+/// 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};
+
/// Terrain world.
#[derive(Clone, Debug, Default)]
pub struct TerrainWorld {
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index e75712c..469fbe1 100644
--- a/docs/tomes/05-render.md
+++ b/docs/tomes/05-render.md
@@ -1559,7 +1559,11 @@ two WEAR selectors must not be collapsed into an invented one-layer material,
and the type-18 `aux18` stream is now documented as microtexture mapping data.
`TerrainFace28::material_layers` exposes the evidenced high-byte `Land1`
selector (with `0xff` absent sentinel) and low-byte `Land2` selector. The
-manager's actual blend/pass operation remains unrecovered.
+manager's actual blend/pass operation remains unrecovered. `World3D.dll`
+supplies the manager ABI: its load slot appends `Land1` as table 0 and `Land2`
+as table 1, while its material-phase lookup receives the exact 32-bit selector
+`(table_index << 16) | material_index`. `TerrainMaterialSelection` preserves
+that key for the renderer without assuming how the two phases are blended.
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