From 048ad71f5c5c5a21aabb42dad1562b40b7a54ea2 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Fri, 30 Oct 2026 23:16:31 +0400 Subject: fix(render): hide native zero-alpha MSH batches --- adapters/fparkan-render-vulkan/src/asset_mesh.rs | 6 ++ adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 73 +++++++++++++++--- .../fparkan-render-vulkan/src/ffi/smoke_types.rs | 3 + apps/fparkan-game/src/main.rs | 90 +++++++++++++++++++++- crates/fparkan-msh/src/lib.rs | 31 ++++++++ crates/fparkan-runtime/src/lib.rs | 1 + docs/appendices/knowledge-boundaries.md | 21 +++++ docs/reference/msh.md | 4 + docs/reference/render-frame.md | 17 ++++ 9 files changed, 234 insertions(+), 12 deletions(-) diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index e2a8395..f691b34 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -1379,6 +1379,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices, batches, triangle_descriptors: None, @@ -1439,6 +1440,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: Vec::new(), batches: vec![ batch(0, 0, 0), @@ -1686,6 +1688,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: vec![0, 1, 2, 0, 1, 2], batches: vec![batch(0, 3, 0), batch(3, 3, 0)], triangle_descriptors: None, @@ -1746,6 +1749,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: Vec::new(), batches: Vec::new(), triangle_descriptors: None, @@ -1853,6 +1857,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: vec![0, 1, 2, 0, 1, 2], batches: vec![batch(0, 3, 0), batch(3, 3, 0)], triangle_descriptors: None, @@ -1945,6 +1950,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: vec![0, 1, 2], batches: vec![batch(0, 3, 0)], triangle_descriptors: None, diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index 90bce14..522f95f 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -423,6 +423,7 @@ impl VulkanSmokeRenderer { draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()], draw_order_dirty: false, draw_range_world_queue: vec![None; create_info.mesh.draw_ranges.len()], + draw_range_diffuse_alpha_overrides: vec![None; create_info.mesh.draw_ranges.len()], world_transparent_background_ranges: Vec::new(), draw_range_base_pipeline_states: create_info .mesh @@ -901,6 +902,27 @@ impl VulkanSmokeRenderer { Ok(()) } + /// Overrides one draw's diffuse alpha without changing its shared material. + pub fn set_draw_range_diffuse_alpha_override( + &mut self, + range_index: usize, + diffuse_alpha: Option, + ) -> Result<(), VulkanSmokeRendererError> { + if diffuse_alpha.is_some_and(|alpha| !alpha.is_finite() || !(0.0..=1.0).contains(&alpha)) { + return Err(VulkanSmokeRendererError::InvalidStaticMesh { + context: "dynamic draw-range diffuse alpha must be finite and 0..1", + }); + } + let alpha_override = self + .draw_range_diffuse_alpha_overrides + .get_mut(range_index) + .ok_or(VulkanSmokeRendererError::InvalidStaticMesh { + context: "dynamic draw-range alpha index is out of bounds", + })?; + *alpha_override = diffuse_alpha; + Ok(()) + } + /// Sets the native per-range alpha reference in legacy 0..=255 units. /// /// Zero remains an explicit value; use the native-default constant when @@ -1874,11 +1896,16 @@ impl VulkanSmokeRenderer { context: "static material alpha", }, )?; - let material_alphas = [ - material_alpha[0].to_ne_bytes(), - material_alpha[1].to_ne_bytes(), - ] - .concat(); + let diffuse_alpha_override = self + .draw_range_diffuse_alpha_overrides + .get(range_index) + .ok_or(VulkanSmokeRendererError::InvariantViolation { + context: "draw-range diffuse-alpha override", + })?; + let material_alphas = + draw_material_alphas(*material_alpha, *diffuse_alpha_override) + .map(f32::to_ne_bytes) + .concat(); device.device().cmd_push_constants( command_buffer, resources.pipeline_layout, @@ -2368,6 +2395,16 @@ fn apply_world_transparency( } } +fn draw_material_alphas( + material_alphas: [f32; 2], + diffuse_alpha_override: Option, +) -> [f32; 2] { + [ + diffuse_alpha_override.unwrap_or(material_alphas[0]), + material_alphas[1], + ] +} + fn world_material_pipeline_states( original: LegacyPipelineState, category: u8, @@ -2535,10 +2572,10 @@ mod tests { use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh}; use super::{ apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index, - mark_draw_order_dirty_on_activity_change, set_and_mark_dirty_if_changed, - sorted_world_draw_order, take_runtime_children_with_validation_snapshot, - take_runtime_owners_in_dependency_order, validate_geometry_upload, - world_material_pipeline_states, RollbackOnDrop, + draw_material_alphas, mark_draw_order_dirty_on_activity_change, + set_and_mark_dirty_if_changed, sorted_world_draw_order, + take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order, + validate_geometry_upload, world_material_pipeline_states, RollbackOnDrop, }; use fparkan_render::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState}; use std::cell::RefCell; @@ -3141,4 +3178,22 @@ mod tests { assert!(world_material_pipeline_states(original, 5, Some(false)).is_none()); assert!(world_material_pipeline_states(fixed_state, 5, None).is_none()); } + + #[test] + fn draw_alpha_override_is_per_range_and_none_restores_material_alpha() { + let shared_material = [0.25, 0.5]; + + assert_eq!(draw_material_alphas(shared_material, Some(0.0)), [0.0, 0.5]); + assert_eq!(draw_material_alphas(shared_material, None), shared_material); + + let updated_material = [0.75, 0.125]; + assert_eq!( + draw_material_alphas(updated_material, None), + updated_material + ); + assert_eq!( + draw_material_alphas(updated_material, Some(0.0)), + [0.0, 0.125] + ); + } } diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index 51f8384..f004cb2 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -1456,6 +1456,9 @@ pub struct VulkanSmokeRenderer { /// a fixed screen/sky/weather pass; `Some(false)` is world opaque and /// `Some(true)` is world transparent. pub(super) draw_range_world_queue: Vec>, + /// Optional diffuse-alpha override for each draw range, independent of + /// the shared material record. + pub(super) draw_range_diffuse_alpha_overrides: Vec>, /// Fixed sky background ranges placed between world opaque and transparent. pub(super) world_transparent_background_ranges: Vec, /// Pipeline state before transparent queue depth policy is applied. diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 1da3312..61c2d1e 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -1053,6 +1053,13 @@ fn native_world_transparent(range: &VulkanStaticDrawRange, diffuse_alpha: f32) - diffuse_alpha < 1.0 || (range.batch_flags & 0x108) != 0 } +fn native_msh_diffuse_alpha_override(batch_flags: u16, type16_present: bool) -> Option { + if type16_present || batch_flags & 0x0008 == 0 { + return None; + } + (batch_flags & 0x0040 != 0 && batch_flags & 0x0010 == 0).then_some(0.0) +} + fn native_msh_handedness(transform: LegacyIron3dEulerTransform, scale: [f32; 3]) -> i8 { let Some(origin) = transform.try_transform_scaled_point([0.0; 3], scale) else { return 0; @@ -2273,6 +2280,7 @@ impl EnvironmentGpuScene { fn new( mesh: &mut VulkanStaticMesh, native_handedness_by_range: &mut HashMap, + type16_present_ranges: &mut HashSet, materials: &mut Vec, preview_material_animations: &mut Vec, sun_occlusion_range_indices: &mut Vec, @@ -2700,6 +2708,15 @@ impl EnvironmentGpuScene { remapped_handedness.insert(new_range_index, handedness); } *native_handedness_by_range = remapped_handedness; + let mut remapped_type16_present_ranges = + HashSet::with_capacity(type16_present_ranges.len()); + for old_range_index in std::mem::take(type16_present_ranges) { + let new_range_index = remap.get(old_range_index).copied().ok_or_else(|| { + format!("type-16 MSH draw range remap is missing range {old_range_index}") + })?; + remapped_type16_present_ranges.insert(new_range_index); + } + *type16_present_ranges = remapped_type16_present_ranges; for layer in &mut scene.sky_layers { layer.range.range_index = *remap .get(layer.range.range_index) @@ -3965,6 +3982,7 @@ fn multiply_row_major(left: [f32; 16], right: [f32; 16]) -> [f32; 16] { struct StaticPreviewScene { mesh: VulkanStaticMesh, native_handedness_by_range: HashMap, + type16_present_ranges: HashSet, materials: Vec, material_animations: Vec, sun_occlusion_range_indices: Vec, @@ -4401,6 +4419,7 @@ fn static_preview_mesh_and_materials( draw_ranges: Vec::new(), }; let mut native_handedness_by_range = HashMap::new(); + let mut type16_present_ranges = HashSet::new(); let mut materials = Vec::new(); let mut material_animations = Vec::new(); let mut sun_occlusion_range_indices = Vec::new(); @@ -4768,6 +4787,9 @@ fn static_preview_mesh_and_materials( )?; for range_index in range_start..mesh.draw_ranges.len() { native_handedness_by_range.insert(range_index, component_handedness); + if model.validated.type16_present { + type16_present_ranges.insert(range_index); + } } if root_sun_eligible { sun_occlusion_range_indices.extend(range_start..mesh.draw_ranges.len()); @@ -4842,6 +4864,7 @@ fn static_preview_mesh_and_materials( Ok(StaticPreviewScene { mesh, native_handedness_by_range, + type16_present_ranges, materials, material_animations, sun_occlusion_range_indices, @@ -5562,6 +5585,7 @@ fn run_static_vulkan_mode( Some(EnvironmentGpuScene::new( &mut preview.mesh, &mut preview.native_handedness_by_range, + &mut preview.type16_present_ranges, &mut preview.materials, &mut preview.material_animations, &mut preview.sun_occlusion_range_indices, @@ -5677,6 +5701,7 @@ fn replay_environment_delta_seconds(last_applied_ms: u32, current_ms: u32) -> f3 struct StaticVulkanApp { mesh: VulkanStaticMesh, native_handedness_by_range: HashMap, + type16_present_ranges: HashSet, materials: Vec, material_animations: Vec, sun_occlusion_range_indices: Vec, @@ -5765,6 +5790,7 @@ impl StaticVulkanApp { Self { mesh: preview.mesh, native_handedness_by_range: preview.native_handedness_by_range, + type16_present_ranges: preview.type16_present_ranges, materials: preview.materials, material_animations: preview.material_animations, sun_occlusion_range_indices: preview.sun_occlusion_range_indices, @@ -5832,7 +5858,17 @@ impl StaticVulkanApp { .materials .get(usize::from(range.material_index)) .map_or(1.0, |material| material.diffuse_alpha); - let transparent = native_world_transparent(range, alpha); + let alpha_override = native_msh_diffuse_alpha_override( + range.batch_flags, + self.type16_present_ranges.contains(&range_index), + ); + let effective_alpha = alpha_override.unwrap_or(alpha); + renderer + .set_draw_range_diffuse_alpha_override(range_index, alpha_override) + .map_err(|error| { + format!("configure world draw range {range_index} alpha: {error}") + })?; + let transparent = native_world_transparent(range, effective_alpha); renderer .set_draw_range_transparency(range_index, transparent) .map_err(|error| format!("configure world draw range {range_index}: {error}"))?; @@ -5853,7 +5889,7 @@ impl StaticVulkanApp { &self.mesh, range, range.index_count, - alpha, + effective_alpha, self.view_camera.position, self.native_handedness_by_range.get(&range_index).copied(), ), @@ -5869,6 +5905,7 @@ impl StaticVulkanApp { mesh: &VulkanStaticMesh, materials: &[VulkanStaticMaterial], world_range_indices: &[usize], + type16_present_ranges: &HashSet, world_range_index_counts: &HashMap, native_handedness_by_range: &HashMap, eye_world: [f32; 3], @@ -5881,7 +5918,7 @@ impl StaticVulkanApp { .get(range_index) .ok_or_else(|| "world draw range index is out of bounds".to_string())?; let state = active_materials.get(&range_index); - let alpha = state.map_or_else( + let material_alpha = state.map_or_else( || { materials .get(usize::from(range.material_index)) @@ -5889,6 +5926,14 @@ impl StaticVulkanApp { }, |state| state.diffuse_alpha, ); + let alpha_override = native_msh_diffuse_alpha_override( + range.batch_flags, + type16_present_ranges.contains(&range_index), + ); + let alpha = alpha_override.unwrap_or(material_alpha); + renderer + .set_draw_range_diffuse_alpha_override(range_index, alpha_override) + .map_err(|error| format!("update world draw range {range_index} alpha: {error}"))?; renderer .set_draw_range_transparency(range_index, native_world_transparent(range, alpha)) .map_err(|error| format!("update world draw range {range_index}: {error}"))?; @@ -6322,6 +6367,7 @@ impl StaticVulkanApp { &self.mesh, &self.materials, &self.world_range_indices, + &self.type16_present_ranges, &self.world_range_index_counts, &self.native_handedness_by_range, frame.camera.position, @@ -7342,6 +7388,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: vec![], batches: vec![], triangle_descriptors: None, @@ -7671,6 +7718,7 @@ mod tests { let preview = StaticPreviewScene { mesh: initial_mesh.clone(), native_handedness_by_range: HashMap::new(), + type16_present_ranges: HashSet::new(), materials: Vec::new(), material_animations: Vec::new(), sun_occlusion_range_indices: Vec::new(), @@ -8241,6 +8289,17 @@ mod tests { )); } + #[test] + fn native_cpu_shaded_msh_alpha_override_uses_only_the_constant_batch_rule() { + assert_eq!(native_msh_diffuse_alpha_override(0x0148, false), Some(0.0)); + assert_eq!(native_msh_diffuse_alpha_override(0x0108, false), None); + assert_eq!(native_msh_diffuse_alpha_override(0x0118, false), None); + assert_eq!(native_msh_diffuse_alpha_override(0x0058, false), None); + assert_eq!(native_msh_diffuse_alpha_override(0x0148, true), None); + assert_eq!(native_msh_diffuse_alpha_override(0x0058, true), None); + assert_eq!(native_msh_diffuse_alpha_override(0x0040, false), None); + } + #[test] fn material_animation_selects_pipeline_category_with_active_phase() { let variant = MaterialVariantSet { @@ -10009,4 +10068,29 @@ mod tests { original_count ); } + #[test] + fn world_draw_range_count_clears_and_reapplies_constant_alpha_override() { + let triangle = native_front_triangle(); + let (mesh, mut range) = facing_mesh(0x0148, &triangle); + let original_count = range.index_count; + let mut alpha_override = None; + let update = |range: &VulkanStaticDrawRange, alpha_override: &mut Option| { + *alpha_override = native_msh_diffuse_alpha_override(range.batch_flags, false); + world_draw_range_index_count( + &mesh, + range, + original_count, + alpha_override.unwrap_or(1.0), + [0.0; 3], + None, + ) + }; + + assert_eq!(update(&range, &mut alpha_override), 0); + range.batch_flags = 0x0108; + assert_eq!(update(&range, &mut alpha_override), original_count); + range.batch_flags = 0x0148; + assert_eq!(update(&range, &mut alpha_override), 0); + assert_eq!(original_count, 3); + } } diff --git a/crates/fparkan-msh/src/lib.rs b/crates/fparkan-msh/src/lib.rs index c35a3dc..3bbe17e 100644 --- a/crates/fparkan-msh/src/lib.rs +++ b/crates/fparkan-msh/src/lib.rs @@ -21,6 +21,8 @@ pub const STREAM_UV0: u32 = 5; /// Optional secondary texture coordinate stream used by native lightmap/detail /// stages. MSH stores it as the type-18 four-byte packed UV stream. pub const STREAM_UV1: u32 = 18; +/// Optional type-16 stream. Its presence marks MSH vertex-colour data. +pub const STREAM_TYPE_16: u32 = 16; /// Triangle index stream. pub const STREAM_INDICES: u32 = 6; /// Optional triangle-descriptor stream. @@ -101,6 +103,10 @@ pub struct ModelAsset { pub uv0: Option>, /// Optional secondary packed texture coordinates from type 18. pub uv1: Option>, + /// Whether the source MSH contains a type-16 vertex-colour stream. + /// + /// The stream payload is preserved separately and is not decoded here. + pub type16_present: bool, /// Triangle indices. pub indices: Vec, /// Draw batches. @@ -1116,6 +1122,7 @@ fn parse_model_document(document: &NresDocument) -> Result let uv1 = read_optional_stream(document, STREAM_UV1)? .map(|raw| parse_secondary_uv_stream(raw, positions.len())) .transpose()?; + let type16_present = read_optional_stream(document, STREAM_TYPE_16)?.is_some(); let triangle_descriptors = read_optional_triangle_descriptors(document)? .map(|raw| parse_triangle_descriptors(&raw.bytes)) .transpose()?; @@ -1135,6 +1142,7 @@ fn parse_model_document(document: &NresDocument) -> Result normals, uv0, uv1, + type16_present, indices, batches, triangle_descriptors, @@ -1641,6 +1649,25 @@ mod tests { assert_eq!(model.node_count, 0); assert!(model.slots.is_empty()); assert_eq!(model.triangle_descriptors, None); + assert!(!model.type16_present); + } + + #[test] + fn type16_presence_is_preserved_without_decoding_its_payload() { + let payload = [0xDE, 0xAD, 0xBE, 0xEF]; + let document = decode_nested(&build_nres(&[ + stream(STREAM_NODE_TABLE, 38, b"Res1", &[]), + stream(STREAM_SLOTS, 0, b"Res2", &slots_payload(&[])), + stream(STREAM_POSITIONS, 0, b"Res3", &[]), + stream(STREAM_INDICES, 0, b"Res6", &[]), + stream(STREAM_BATCHES, 0, b"Res13", &[]), + stream(STREAM_TYPE_16, 0, b"Res16", &payload), + ])) + .expect("nested NRes"); + let model = + validate_msh(&decode_msh(&document).expect("MSH document")).expect("model asset"); + + assert!(model.type16_present); } #[test] @@ -2262,6 +2289,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: Vec::new(), batches: Vec::new(), triangle_descriptors: None, @@ -2324,6 +2352,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: Vec::new(), batches: Vec::new(), triangle_descriptors: None, @@ -2389,6 +2418,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: Vec::new(), batches: Vec::new(), triangle_descriptors: None, @@ -3282,6 +3312,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices, batches, node_names: None, diff --git a/crates/fparkan-runtime/src/lib.rs b/crates/fparkan-runtime/src/lib.rs index 1bacc54..16c5b02 100644 --- a/crates/fparkan-runtime/src/lib.rs +++ b/crates/fparkan-runtime/src/lib.rs @@ -6808,6 +6808,7 @@ mod tests { normals: None, uv0: None, uv1: None, + type16_present: false, indices: Vec::new(), batches: Vec::new(), triangle_descriptors: None, diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 68a01d0..580d545 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -18,6 +18,27 @@ state transitions и окончательный порядок части transp стене и дверном проёме. Мировое преобразование, фильтры категории и подключение к свободной камере остаются открытыми ([MSH](../reference/msh.md)). +Для MSH без потока цветов type 16 подтверждён только один постоянный alpha +override для диапазонов с CPU-цветом и raw-флагом `Batch20` `0x0008`: GOG +`Terrain.dll`, SHA-256 +`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`, RVA +`0x1B0F0`, задаёт alpha `0`, если установлен bit `0x0040` и сброшен `0x0010`. +Это покрывает `0x0148`; `0x0108`, `0x0118` и `0x0058` сохраняют alpha MAT0, +причём в `0x0058` bit `0x0010` имеет приоритет. Поток type 16 или отсутствие +`0x0008` исключают override. Проверка заводской сцены с фиксированной нативной +камерой подтвердила на одном кадре входа восстановление X-образного рисунка +двери, который виден и в оригинале; прежний Vulkan-readback показывал на этом +месте сплошной чёрный прямоугольник. Сравнение выполнено при времени сцены +2000 мс и трёх кадрах, запуск с Vulkan validation завершился без предупреждений +и ошибок. Это подтверждает только структуру проёма в этой сцене: правильность +освещения и полный попиксельный паритет не установлены. + +Общее fade-поведение по расстоянию в preview не реализовано. Открыты точная +метрика расстояния, выбор первого CPU-принятого треугольника диапазона и +эквивалентность frustum/culling-отбора оригинала; эти вопросы нельзя вывести из +захвата заводской двери. Данные потока type 16 ещё не декодированы, поэтому +правило выше относится только к MSH-диапазонам без этого потока цветов. + Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state log на минимальных сценах с одним типом материала. diff --git a/docs/reference/msh.md b/docs/reference/msh.md index 7c6eca7..e2716d2 100644 --- a/docs/reference/msh.md +++ b/docs/reference/msh.md @@ -18,6 +18,7 @@ type 9 service stream type 10 strings and node names type 13 Batch20 records type 15 auxiliary stream +type 16 optional vertex-colour data type 17 auxiliary data type 18 packed UV1 (optional secondary coordinates) type 19 animation frame map @@ -25,6 +26,9 @@ type 20 rare auxiliary table ``` Reader ищет entries по type, но сохраняет исходный порядок для roundtrip. +Поле `ModelAsset.type16_present` отмечает только наличие entry типа 16. Данные +payload сохраняются, но этот путь рендера пока не преобразует их в цвета +вершин. ## Node and slot selection diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md index 8cba630..3269c67 100644 --- a/docs/reference/render-frame.md +++ b/docs/reference/render-frame.md @@ -68,6 +68,23 @@ Pending frame хранит пакет MAT0 для мира и, при налич `batch` `0x108`. Такой диапазон при включённой проверке глубины не записывает глубину. За фиксированными диапазонами окружения сохраняются их явные состояния. +Для MSH без потока цветов type 16 диапазон, цвет которого вычисляет CPU, при +raw-флаге `Batch20` `0x0008` получает переопределение alpha только в одном +подтверждённом случае. В GOG `Terrain.dll` с SHA-256 +`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` проверка +по RVA `0x1B0F0` задаёт alpha `0`, когда установлен bit `0x0040` и сброшен +`0x0010`; поэтому для `0x0148` alpha равна нулю. Для `0x0108` и `0x0118` +превью сохраняет alpha MAT0; для `0x0058` действует приоритет bit `0x0010`, +также оставляя alpha MAT0. При наличии type 16 или отсутствии `0x0008` +переопределения нет. Оно относится к одному draw range и не меняет общий +материал; alpha `0` временно задаёт число индексов `0`, а при очистке +переопределения исходное число индексов восстанавливается. + +Общий fade по расстоянию пока не реализован. Открыты точная метрика расстояния, +выбор первого CPU-принятого треугольника диапазона и эквивалентность frustum- и +culling-отбору оригинала. Поэтому состояния `0x0108` и `0x0118` оставляют alpha +MAT0, пока эти входные данные не установлены. + В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на своих местах; прозрачная очередь мира сортируется от дальних элементов к -- cgit v1.2.3