fix(render): select native Land2 multitexture variants
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
This commit is contained in:
3 files changed
+132
-15
No files matched your search
@@ -8639,14 +8639,38 @@ fn static_preview_terrain_base_materials(
|
|||||||
}
|
}
|
||||||
let initial_phase_index =
|
let initial_phase_index =
|
||||||
usize::from(static_material_phase.unwrap_or(0)).min(phase_count.saturating_sub(1));
|
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))
|
let initial_detail_phase_index = if let Some(phase) = static_material_phase {
|
||||||
.min(detail_phases.len().saturating_sub(1));
|
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| {
|
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))
|
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| {
|
let initial_overlay_detail_phase_index = overlay_detail_phases
|
||||||
usize::from(static_material_phase.unwrap_or(0)).min(phases.len().saturating_sub(1))
|
.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);
|
let mut phase_material_indices = Vec::with_capacity(phase_count);
|
||||||
for phase_index in 0..phase_count {
|
for phase_index in 0..phase_count {
|
||||||
let (base, base_phase) = &base_phases[phase_index];
|
let (base, base_phase) = &base_phases[phase_index];
|
||||||
@@ -8776,7 +8800,7 @@ fn static_preview_terrain_base_materials(
|
|||||||
},
|
},
|
||||||
detail: Some(MaterialPhaseBinding {
|
detail: Some(MaterialPhaseBinding {
|
||||||
document: detail_document,
|
document: detail_document,
|
||||||
animation_block_index: 0,
|
animation_block_index: layers.detail_base_selection().animation_block_index,
|
||||||
wear_row_start_ms: 0,
|
wear_row_start_ms: 0,
|
||||||
random_state: packed_tag as u32 ^ 0x5A5A_A5A5,
|
random_state: packed_tag as u32 ^ 0x5A5A_A5A5,
|
||||||
frozen_phase_index: static_material_phase.map(usize::from),
|
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 {
|
overlay_detail: overlay_detail_document.map(|document| MaterialPhaseBinding {
|
||||||
document,
|
document,
|
||||||
animation_block_index: 0,
|
animation_block_index: overlay_detail_selection
|
||||||
|
.expect("overlay detail selection")
|
||||||
|
.animation_block_index,
|
||||||
wear_row_start_ms: 0,
|
wear_row_start_ms: 0,
|
||||||
random_state: packed_tag as u32 ^ 0xC3C3_3C3C,
|
random_state: packed_tag as u32 ^ 0xC3C3_3C3C,
|
||||||
frozen_phase_index: static_material_phase.map(usize::from),
|
frozen_phase_index: static_material_phase.map(usize::from),
|
||||||
@@ -13135,6 +13161,53 @@ mod tests {
|
|||||||
assert_eq!(environment_missions, expected_missions);
|
assert_eq!(environment_missions, expected_missions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
fn draw_range_reorder_remaps_all_material_animation_bindings() {
|
fn draw_range_reorder_remaps_all_material_animation_bindings() {
|
||||||
let range = |first_index, material_index| VulkanStaticDrawRange {
|
let range = |first_index, material_index| VulkanStaticDrawRange {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ pub struct TerrainMaterialLayers {
|
|||||||
pub overlay_selector: Option<u8>,
|
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
|
/// The original manager selects a WEAR table in the upper 16 bits and its
|
||||||
/// positional material row in the lower 16 bits.
|
/// positional material row in the lower 16 bits.
|
||||||
@@ -68,6 +68,9 @@ pub struct TerrainMaterialSelection {
|
|||||||
pub table_index: u16,
|
pub table_index: u16,
|
||||||
/// Positional material row in that WEAR table.
|
/// Positional material row in that WEAR table.
|
||||||
pub material_index: u16,
|
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 {
|
impl TerrainMaterialSelection {
|
||||||
@@ -99,6 +102,7 @@ impl TerrainMaterialLayers {
|
|||||||
TerrainMaterialSelection {
|
TerrainMaterialSelection {
|
||||||
table_index: 0,
|
table_index: 0,
|
||||||
material_index: u16::from(self.base_selector),
|
material_index: u16::from(self.base_selector),
|
||||||
|
animation_block_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,27 +113,30 @@ impl TerrainMaterialLayers {
|
|||||||
.map(|material_index| TerrainMaterialSelection {
|
.map(|material_index| TerrainMaterialSelection {
|
||||||
table_index: 0,
|
table_index: 0,
|
||||||
material_index: u16::from(material_index),
|
material_index: u16::from(material_index),
|
||||||
|
animation_block_index: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the detail selection corresponding to the base row in
|
/// Returns the multitexture detail selection corresponding to the base
|
||||||
/// `Land2.wea`.
|
/// row in `Land2.wea`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn detail_base_selection(self) -> TerrainMaterialSelection {
|
pub fn detail_base_selection(self) -> TerrainMaterialSelection {
|
||||||
TerrainMaterialSelection {
|
TerrainMaterialSelection {
|
||||||
table_index: 1,
|
table_index: 1,
|
||||||
material_index: u16::from(self.base_selector),
|
material_index: u16::from(self.base_selector),
|
||||||
|
animation_block_index: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the optional detail selection corresponding to the overlay row
|
/// Returns the optional multitexture detail selection corresponding to
|
||||||
/// in `Land2.wea`.
|
/// the overlay row in `Land2.wea`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn detail_overlay_selection(self) -> Option<TerrainMaterialSelection> {
|
pub fn detail_overlay_selection(self) -> Option<TerrainMaterialSelection> {
|
||||||
self.overlay_selector
|
self.overlay_selector
|
||||||
.map(|material_index| TerrainMaterialSelection {
|
.map(|material_index| TerrainMaterialSelection {
|
||||||
table_index: 1,
|
table_index: 1,
|
||||||
material_index: u16::from(material_index),
|
material_index: u16::from(material_index),
|
||||||
|
animation_block_index: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1948,6 +1955,7 @@ mod tests {
|
|||||||
TerrainMaterialSelection {
|
TerrainMaterialSelection {
|
||||||
table_index: 0,
|
table_index: 0,
|
||||||
material_index: 2,
|
material_index: 2,
|
||||||
|
animation_block_index: 0,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1965,6 +1973,13 @@ mod tests {
|
|||||||
.phase_key(),
|
.phase_key(),
|
||||||
0x0001_0002
|
0x0001_0002
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
document.faces[0]
|
||||||
|
.material_layers()
|
||||||
|
.detail_base_selection()
|
||||||
|
.animation_block_index,
|
||||||
|
1
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
document.faces[0]
|
document.faces[0]
|
||||||
.material_layers()
|
.material_layers()
|
||||||
@@ -1973,6 +1988,14 @@ mod tests {
|
|||||||
.phase_key(),
|
.phase_key(),
|
||||||
0x0001_0001
|
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());
|
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");
|
||||||
@@ -1989,6 +2012,7 @@ mod tests {
|
|||||||
TerrainMaterialSelection {
|
TerrainMaterialSelection {
|
||||||
table_index: 0,
|
table_index: 0,
|
||||||
material_index: 3,
|
material_index: 3,
|
||||||
|
animation_block_index: 0,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+23
-3
@@ -1059,13 +1059,33 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) {
|
|||||||
|
|
||||||
## Материалы ландшафта
|
## Материалы ландшафта
|
||||||
|
|
||||||
Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Старший байт
|
Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Младший байт
|
||||||
`TerrainFace28.material_tag` выбирает материал первой таблицы, младший —
|
`TerrainFace28.material_tag` выбирает базовый материал `Land1.wea`, старший —
|
||||||
второй. `0xff` в старшем байте означает отсутствие первого слоя.
|
дополнительный материал той же таблицы. `0xff` в старшем байте означает
|
||||||
|
отсутствие дополнительного слоя. `Land2.wea` содержит соответствующие этим
|
||||||
|
двум строкам detail-материалы.
|
||||||
В AutoDemo встречаются теги `0102`, `0203`, `ff01`, `ff02`, `ff03`.
|
В AutoDemo встречаются теги `0102`, `0203`, `ff01`, `ff02`, `ff03`.
|
||||||
Менеджер World3D получает селектор `(table_index << 16) | material_index`
|
Менеджер World3D получает селектор `(table_index << 16) | material_index`
|
||||||
и возвращает выбранную фазу MAT0 размером 76 байт.
|
и возвращает выбранную фазу 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 18 в `Land.msh` оригинал называет microtexture mapping chunk;
|
||||||
он содержит четырёхбайтовые записи. Type 11 хранит пары `u16`, выбранные
|
он содержит четырёхбайтовые записи. Type 11 хранит пары `u16`, выбранные
|
||||||
таблицей terrain slots. Их первое поле — ключ shade cache, а не номер строки
|
таблицей terrain slots. Их первое поле — ключ shade cache, а не номер строки
|
||||||
|
|||||||
Reference in new issue
Block a user