diff options
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 87 | ||||
| -rw-r--r-- | crates/fparkan-terrain-format/src/lib.rs | 34 | ||||
| -rw-r--r-- | docs/tomes/05-render.md | 26 |
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, а не номер строки |
