aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apps/fparkan-game/src/main.rs87
-rw-r--r--crates/fparkan-terrain-format/src/lib.rs34
-rw-r--r--docs/tomes/05-render.md26
3 files changed, 132 insertions, 15 deletions
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs
index 6101c94..2c63a29 100644
--- a/apps/fparkan-game/src/main.rs
+++ b/apps/fparkan-game/src/main.rs
@@ -8639,14 +8639,38 @@ fn static_preview_terrain_base_materials(
}
let initial_phase_index =
usize::from(static_material_phase.unwrap_or(0)).min(phase_count.saturating_sub(1));
- let initial_detail_phase_index = usize::from(static_material_phase.unwrap_or(0))
- .min(detail_phases.len().saturating_sub(1));
+ let initial_detail_phase_index = if let Some(phase) = static_material_phase {
+ usize::from(phase).min(detail_phases.len().saturating_sub(1))
+ } else {
+ sample_material_phase(
+ &detail_document,
+ layers.detail_base_selection().animation_block_index,
+ 0,
+ 0,
+ 0,
+ )
+ .map_err(|error| format!("sample terrain detail material: {error}"))?
+ .phase_index
+ };
let initial_overlay_phase_index = overlay_phases.as_ref().map(|phases| {
usize::from(static_material_phase.unwrap_or(0)).min(phases.len().saturating_sub(1))
});
- let initial_overlay_detail_phase_index = overlay_detail_phases.as_ref().map(|phases| {
- usize::from(static_material_phase.unwrap_or(0)).min(phases.len().saturating_sub(1))
- });
+ let initial_overlay_detail_phase_index = overlay_detail_phases
+ .as_ref()
+ .zip(overlay_detail_document.as_ref())
+ .zip(overlay_detail_selection)
+ .map(|((phases, document), selection)| {
+ if let Some(phase) = static_material_phase {
+ Ok(usize::from(phase).min(phases.len().saturating_sub(1)))
+ } else {
+ sample_material_phase(document, selection.animation_block_index, 0, 0, 0)
+ .map(|sample| sample.phase_index)
+ .map_err(|error| {
+ format!("sample terrain overlay detail material: {error}")
+ })
+ }
+ })
+ .transpose()?;
let mut phase_material_indices = Vec::with_capacity(phase_count);
for phase_index in 0..phase_count {
let (base, base_phase) = &base_phases[phase_index];
@@ -8776,7 +8800,7 @@ fn static_preview_terrain_base_materials(
},
detail: Some(MaterialPhaseBinding {
document: detail_document,
- animation_block_index: 0,
+ animation_block_index: layers.detail_base_selection().animation_block_index,
wear_row_start_ms: 0,
random_state: packed_tag as u32 ^ 0x5A5A_A5A5,
frozen_phase_index: static_material_phase.map(usize::from),
@@ -8792,7 +8816,9 @@ fn static_preview_terrain_base_materials(
}),
overlay_detail: overlay_detail_document.map(|document| MaterialPhaseBinding {
document,
- animation_block_index: 0,
+ animation_block_index: overlay_detail_selection
+ .expect("overlay detail selection")
+ .animation_block_index,
wear_row_start_ms: 0,
random_state: packed_tag as u32 ^ 0xC3C3_3C3C,
frozen_phase_index: static_material_phase.map(usize::from),
@@ -13136,6 +13162,53 @@ mod tests {
}
#[test]
+ #[ignore = "requires installed GOG resources; set FPARKAN_GAME_ROOT"]
+ fn gog_terrain_detail_uses_multitexture_animation_block() {
+ let root = std::env::var_os("FPARKAN_GAME_ROOT")
+ .map(PathBuf::from)
+ .unwrap_or_else(|| PathBuf::from(r"C:\GOG Games\Parkan - Iron Strategy"));
+ let mut loader = StandaloneWearMaterialLoader::new(&root);
+ let mut mesh = VulkanStaticMesh::smoke_triangle();
+ mesh.draw_ranges[0].material_index = 0x0102;
+ let mut materials = Vec::new();
+ let mut animations = Vec::new();
+ let selections = static_preview_terrain_base_materials(
+ &mut loader,
+ &root,
+ "DATA/MAPS/Tut_2/Land.msh",
+ &mesh,
+ &mut materials,
+ &mut animations,
+ None,
+ )
+ .expect("prepare real terrain material layers");
+ let binding = &mut animations[0];
+ let sample = binding.detail.as_mut().unwrap().sample(0, 2000).unwrap();
+ assert_eq!(
+ sample.texture_raw.split(|&byte| byte == 0).next().unwrap(),
+ b"L09M.0"
+ );
+ let selected = &materials[usize::from(selections[0].1)];
+ let path = root.join("DATA/MAPS/Tut_2/Land2.wea");
+ let (detail, _) = load_standalone_vulkan_texture(&mut loader, &path, 2, 1).unwrap();
+ let (overlay_detail, _) = load_standalone_vulkan_texture(&mut loader, &path, 1, 1).unwrap();
+ assert!(
+ selected.detail_texture.as_ref() == Some(&detail),
+ "initial detail descriptor must use the multitexture phase"
+ );
+ assert!(
+ selected.overlay_detail_texture.as_ref() == Some(&overlay_detail),
+ "initial overlay-detail descriptor must use the multitexture phase"
+ );
+ assert!(
+ sample_material_animations(&mut animations, &materials, 2000)
+ .unwrap()
+ .stage_phases
+ .is_empty()
+ );
+ }
+
+ #[test]
fn draw_range_reorder_remaps_all_material_animation_bindings() {
let range = |first_index, material_index| VulkanStaticDrawRange {
first_index,
diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs
index e4028f9..595f4d8 100644
--- a/crates/fparkan-terrain-format/src/lib.rs
+++ b/crates/fparkan-terrain-format/src/lib.rs
@@ -58,7 +58,7 @@ pub struct TerrainMaterialLayers {
pub overlay_selector: Option<u8>,
}
-/// One `World3D` material-manager lookup key.
+/// One `World3D` material-manager phase query.
///
/// The original manager selects a WEAR table in the upper 16 bits and its
/// positional material row in the lower 16 bits.
@@ -68,6 +68,9 @@ pub struct TerrainMaterialSelection {
pub table_index: u16,
/// Positional material row in that WEAR table.
pub material_index: u16,
+ /// Animation block passed separately to GetMaterialPhase. The multitexture
+ /// path uses block 1 for Land2, with the material sampler's block-0 fallback.
+ pub animation_block_index: usize,
}
impl TerrainMaterialSelection {
@@ -99,6 +102,7 @@ impl TerrainMaterialLayers {
TerrainMaterialSelection {
table_index: 0,
material_index: u16::from(self.base_selector),
+ animation_block_index: 0,
}
}
@@ -109,27 +113,30 @@ impl TerrainMaterialLayers {
.map(|material_index| TerrainMaterialSelection {
table_index: 0,
material_index: u16::from(material_index),
+ animation_block_index: 0,
})
}
- /// Returns the detail selection corresponding to the base row in
- /// `Land2.wea`.
+ /// Returns the multitexture detail selection corresponding to the base
+ /// row in `Land2.wea`.
#[must_use]
pub fn detail_base_selection(self) -> TerrainMaterialSelection {
TerrainMaterialSelection {
table_index: 1,
material_index: u16::from(self.base_selector),
+ animation_block_index: 1,
}
}
- /// Returns the optional detail selection corresponding to the overlay row
- /// in `Land2.wea`.
+ /// Returns the optional multitexture detail selection corresponding to
+ /// the overlay row in `Land2.wea`.
#[must_use]
pub fn detail_overlay_selection(self) -> Option<TerrainMaterialSelection> {
self.overlay_selector
.map(|material_index| TerrainMaterialSelection {
table_index: 1,
material_index: u16::from(material_index),
+ animation_block_index: 1,
})
}
}
@@ -1948,6 +1955,7 @@ mod tests {
TerrainMaterialSelection {
table_index: 0,
material_index: 2,
+ animation_block_index: 0,
}
);
assert_eq!(
@@ -1968,11 +1976,26 @@ mod tests {
assert_eq!(
document.faces[0]
.material_layers()
+ .detail_base_selection()
+ .animation_block_index,
+ 1
+ );
+ assert_eq!(
+ document.faces[0]
+ .material_layers()
.detail_overlay_selection()
.expect("detail overlay")
.phase_key(),
0x0001_0001
);
+ assert_eq!(
+ document.faces[0]
+ .material_layers()
+ .detail_overlay_selection()
+ .unwrap()
+ .animation_block_index,
+ 1
+ );
raw_face[4..6].copy_from_slice(&0xff03_u16.to_le_bytes());
let nres = decode_nres(&minimal_land_msh(&raw_face)).expect("nres");
@@ -1989,6 +2012,7 @@ mod tests {
TerrainMaterialSelection {
table_index: 0,
material_index: 3,
+ animation_block_index: 0,
}
);
assert_eq!(
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index 3f98ac4..55d3a09 100644
--- a/docs/tomes/05-render.md
+++ b/docs/tomes/05-render.md
@@ -1059,13 +1059,33 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) {
## Материалы ландшафта
-Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Старший байт
-`TerrainFace28.material_tag` выбирает материал первой таблицы, младший —
-второй. `0xff` в старшем байте означает отсутствие первого слоя.
+Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Младший байт
+`TerrainFace28.material_tag` выбирает базовый материал `Land1.wea`, старший —
+дополнительный материал той же таблицы. `0xff` в старшем байте означает
+отсутствие дополнительного слоя. `Land2.wea` содержит соответствующие этим
+двум строкам detail-материалы.
В AutoDemo встречаются теги `0102`, `0203`, `ff01`, `ff02`, `ff03`.
Менеджер World3D получает селектор `(table_index << 16) | material_index`
и возвращает выбранную фазу MAT0 размером 76 байт.
+Таблица, строка и блок анимации — разные части запроса. В быстром проходе
+с двумя текстурами `CShade` запрашивает для Land2 блок анимации **1**. Он может
+выбирать другую текстуру, а не только другие коэффициенты освещения:
+например, в «Конструкторе» это `L09M.0` вместо `L09.0`. Подмена блока первым
+приводит к умножению базового изображения на неподходящую detail-текстуру и
+меняет яркость и контраст земли. `TerrainMaterialSelection` сохраняет номер
+блока отдельно от packed material key; приложение использует его при загрузке
+дескриптора и в последующих кадрах. Если второго блока нет, общий MAT0 sampler
+возвращается к первому, как оригинальный `GetMaterialPhase`.
+
+В GOG `Terrain.dll` (сборка из [таблицы бинарников](../reference/original-binaries.md))
+быстрая ветвь находится в RVA `0x1A7AC–0x1A7FB`. Живое исполнение передаёт
+handle `0x00010002`, block `1` и устанавливает режим текстур `9`; первая
+текстура умножается на diffuse, вторая — на результат с множителем два.
+Ближний дополнительный проход с порогом `260 / FOV` относится к fallback без
+этого режима. На проверенном устройстве он не выбирается, поэтому переносить
+его дистанционный порог в быстрый проход нельзя.
+
Type 18 в `Land.msh` оригинал называет microtexture mapping chunk;
он содержит четырёхбайтовые записи. Type 11 хранит пары `u16`, выбранные
таблицей terrain slots. Их первое поле — ключ shade cache, а не номер строки