feat(terrain): encode material manager selectors

This commit is contained in:
2026-07-18 17:01:17 +04:00
parent 83caf4d6ab
commit a858910853
4 changed files with 90 additions and 8 deletions
+5 -2
View File
@@ -45,6 +45,7 @@ use fparkan_runtime::{
loaded_mission_assets, loaded_mission_object_drafts, loaded_terrain, EngineConfig, EngineMode, loaded_mission_assets, loaded_mission_object_drafts, loaded_terrain, EngineConfig, EngineMode,
EngineServices, MissionAssets, MissionLoadPhase, MissionObjectDraft, MissionRequest, EngineServices, MissionAssets, MissionLoadPhase, MissionObjectDraft, MissionRequest,
}; };
use fparkan_terrain::TerrainMaterialLayers;
use fparkan_terrain::TerrainWorld; use fparkan_terrain::TerrainWorld;
use fparkan_vfs::DirectoryVfs; use fparkan_vfs::DirectoryVfs;
#[cfg(test)] #[cfg(test)]
@@ -477,9 +478,11 @@ fn static_preview_terrain_base_materials(
packed_tags packed_tags
.into_iter() .into_iter()
.map(|packed_tag| { .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( let selected = load_standalone_wear_material_texture_mip0_rgba8_from_root(
root, &wear_path, selector, root,
&wear_path,
selection.material_index,
) )
.map_err(|err| { .map_err(|err| {
format!( format!(
+74 -5
View File
@@ -74,6 +74,57 @@ pub struct TerrainMaterialLayers {
pub land2_selector: u8, 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. /// Terrain face with 28-byte source layout.
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct TerrainFace28 { pub struct TerrainFace28 {
@@ -97,11 +148,7 @@ impl TerrainFace28 {
/// Decodes the proven two-table selector layout of [`Self::material_tag`]. /// Decodes the proven two-table selector layout of [`Self::material_tag`].
#[must_use] #[must_use]
pub const fn material_layers(&self) -> TerrainMaterialLayers { pub const fn material_layers(&self) -> TerrainMaterialLayers {
let [land2_selector, land1] = self.material_tag.to_le_bytes(); TerrainMaterialLayers::from_packed_tag(self.material_tag)
TerrainMaterialLayers {
land1_selector: if land1 == u8::MAX { None } else { Some(land1) },
land2_selector,
}
} }
} }
@@ -1427,6 +1474,20 @@ mod tests {
land2_selector: 2, 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()); raw_face[4..6].copy_from_slice(&0xff03_u16.to_le_bytes());
let nres = decode_nres(&minimal_land_msh(&raw_face)).expect("nres"); let nres = decode_nres(&minimal_land_msh(&raw_face)).expect("nres");
@@ -1438,6 +1499,14 @@ mod tests {
land2_selector: 3, 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] #[test]
+6
View File
@@ -23,6 +23,12 @@
use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument}; use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument};
use std::collections::VecDeque; 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. /// Terrain world.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct TerrainWorld { pub struct TerrainWorld {
+5 -1
View File
@@ -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. and the type-18 `aux18` stream is now documented as microtexture mapping data.
`TerrainFace28::material_layers` exposes the evidenced high-byte `Land1` `TerrainFace28::material_layers` exposes the evidenced high-byte `Land1`
selector (with `0xff` absent sentinel) and low-byte `Land2` selector. The 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 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 draw range keyed by the original packed `material_tag`; it neither reorders