fix(render): hide native zero-alpha MSH batches
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m43s

This commit is contained in:
Valentin Popov committed 2026-10-30 23:16:31 +04:00
1 parent eaf5df3ad0
commit 048ad71f5c
9 files changed
+233 -11

No files matched your search

@@ -1379,6 +1379,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices, indices,
batches, batches,
triangle_descriptors: None, triangle_descriptors: None,
@@ -1439,6 +1440,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: Vec::new(), indices: Vec::new(),
batches: vec![ batches: vec![
batch(0, 0, 0), batch(0, 0, 0),
@@ -1686,6 +1688,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: vec![0, 1, 2, 0, 1, 2], indices: vec![0, 1, 2, 0, 1, 2],
batches: vec![batch(0, 3, 0), batch(3, 3, 0)], batches: vec![batch(0, 3, 0), batch(3, 3, 0)],
triangle_descriptors: None, triangle_descriptors: None,
@@ -1746,6 +1749,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: Vec::new(), indices: Vec::new(),
batches: Vec::new(), batches: Vec::new(),
triangle_descriptors: None, triangle_descriptors: None,
@@ -1853,6 +1857,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: vec![0, 1, 2, 0, 1, 2], indices: vec![0, 1, 2, 0, 1, 2],
batches: vec![batch(0, 3, 0), batch(3, 3, 0)], batches: vec![batch(0, 3, 0), batch(3, 3, 0)],
triangle_descriptors: None, triangle_descriptors: None,
@@ -1945,6 +1950,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: vec![0, 1, 2], indices: vec![0, 1, 2],
batches: vec![batch(0, 3, 0)], batches: vec![batch(0, 3, 0)],
triangle_descriptors: None, triangle_descriptors: None,
@@ -423,6 +423,7 @@ impl VulkanSmokeRenderer {
draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()], draw_range_sort_keys: vec![None; create_info.mesh.draw_ranges.len()],
draw_order_dirty: false, draw_order_dirty: false,
draw_range_world_queue: vec![None; create_info.mesh.draw_ranges.len()], 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(), world_transparent_background_ranges: Vec::new(),
draw_range_base_pipeline_states: create_info draw_range_base_pipeline_states: create_info
.mesh .mesh
@@ -901,6 +902,27 @@ impl VulkanSmokeRenderer {
Ok(()) 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<f32>,
) -> 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. /// Sets the native per-range alpha reference in legacy 0..=255 units.
/// ///
/// Zero remains an explicit value; use the native-default constant when /// Zero remains an explicit value; use the native-default constant when
@@ -1874,10 +1896,15 @@ impl VulkanSmokeRenderer {
context: "static material alpha", context: "static material alpha",
}, },
)?; )?;
let material_alphas = [ let diffuse_alpha_override = self
material_alpha[0].to_ne_bytes(), .draw_range_diffuse_alpha_overrides
material_alpha[1].to_ne_bytes(), .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(); .concat();
device.device().cmd_push_constants( device.device().cmd_push_constants(
command_buffer, command_buffer,
@@ -2368,6 +2395,16 @@ fn apply_world_transparency(
} }
} }
fn draw_material_alphas(
material_alphas: [f32; 2],
diffuse_alpha_override: Option<f32>,
) -> [f32; 2] {
[
diffuse_alpha_override.unwrap_or(material_alphas[0]),
material_alphas[1],
]
}
fn world_material_pipeline_states( fn world_material_pipeline_states(
original: LegacyPipelineState, original: LegacyPipelineState,
category: u8, category: u8,
@@ -2535,10 +2572,10 @@ mod tests {
use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh}; use super::super::{VulkanDynamicDrawRange, VulkanStaticDrawRange, VulkanStaticMesh};
use super::{ use super::{
apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index, apply_geometry_range_updates, apply_world_transparency, completed_readback_buffer_index,
mark_draw_order_dirty_on_activity_change, set_and_mark_dirty_if_changed, draw_material_alphas, mark_draw_order_dirty_on_activity_change,
sorted_world_draw_order, take_runtime_children_with_validation_snapshot, set_and_mark_dirty_if_changed, sorted_world_draw_order,
take_runtime_owners_in_dependency_order, validate_geometry_upload, take_runtime_children_with_validation_snapshot, take_runtime_owners_in_dependency_order,
world_material_pipeline_states, RollbackOnDrop, validate_geometry_upload, world_material_pipeline_states, RollbackOnDrop,
}; };
use fparkan_render::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState}; use fparkan_render::{LegacyBlendMode, LegacyCullMode, LegacyDepthMode, LegacyPipelineState};
use std::cell::RefCell; 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(original, 5, Some(false)).is_none());
assert!(world_material_pipeline_states(fixed_state, 5, None).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]
);
}
} }
@@ -1456,6 +1456,9 @@ pub struct VulkanSmokeRenderer {
/// a fixed screen/sky/weather pass; `Some(false)` is world opaque and /// a fixed screen/sky/weather pass; `Some(false)` is world opaque and
/// `Some(true)` is world transparent. /// `Some(true)` is world transparent.
pub(super) draw_range_world_queue: Vec<Option<bool>>, pub(super) draw_range_world_queue: Vec<Option<bool>>,
/// Optional diffuse-alpha override for each draw range, independent of
/// the shared material record.
pub(super) draw_range_diffuse_alpha_overrides: Vec<Option<f32>>,
/// Fixed sky background ranges placed between world opaque and transparent. /// Fixed sky background ranges placed between world opaque and transparent.
pub(super) world_transparent_background_ranges: Vec<usize>, pub(super) world_transparent_background_ranges: Vec<usize>,
/// Pipeline state before transparent queue depth policy is applied. /// Pipeline state before transparent queue depth policy is applied.
+87 -3
View File
@@ -1053,6 +1053,13 @@ fn native_world_transparent(range: &VulkanStaticDrawRange, diffuse_alpha: f32) -
diffuse_alpha < 1.0 || (range.batch_flags & 0x108) != 0 diffuse_alpha < 1.0 || (range.batch_flags & 0x108) != 0
} }
fn native_msh_diffuse_alpha_override(batch_flags: u16, type16_present: bool) -> Option<f32> {
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 { fn native_msh_handedness(transform: LegacyIron3dEulerTransform, scale: [f32; 3]) -> i8 {
let Some(origin) = transform.try_transform_scaled_point([0.0; 3], scale) else { let Some(origin) = transform.try_transform_scaled_point([0.0; 3], scale) else {
return 0; return 0;
@@ -2273,6 +2280,7 @@ impl EnvironmentGpuScene {
fn new( fn new(
mesh: &mut VulkanStaticMesh, mesh: &mut VulkanStaticMesh,
native_handedness_by_range: &mut HashMap<usize, i8>, native_handedness_by_range: &mut HashMap<usize, i8>,
type16_present_ranges: &mut HashSet<usize>,
materials: &mut Vec<VulkanStaticMaterial>, materials: &mut Vec<VulkanStaticMaterial>,
preview_material_animations: &mut Vec<MaterialAnimationBinding>, preview_material_animations: &mut Vec<MaterialAnimationBinding>,
sun_occlusion_range_indices: &mut Vec<usize>, sun_occlusion_range_indices: &mut Vec<usize>,
@@ -2700,6 +2708,15 @@ impl EnvironmentGpuScene {
remapped_handedness.insert(new_range_index, handedness); remapped_handedness.insert(new_range_index, handedness);
} }
*native_handedness_by_range = remapped_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 { for layer in &mut scene.sky_layers {
layer.range.range_index = *remap layer.range.range_index = *remap
.get(layer.range.range_index) .get(layer.range.range_index)
@@ -3965,6 +3982,7 @@ fn multiply_row_major(left: [f32; 16], right: [f32; 16]) -> [f32; 16] {
struct StaticPreviewScene { struct StaticPreviewScene {
mesh: VulkanStaticMesh, mesh: VulkanStaticMesh,
native_handedness_by_range: HashMap<usize, i8>, native_handedness_by_range: HashMap<usize, i8>,
type16_present_ranges: HashSet<usize>,
materials: Vec<VulkanStaticMaterial>, materials: Vec<VulkanStaticMaterial>,
material_animations: Vec<MaterialAnimationBinding>, material_animations: Vec<MaterialAnimationBinding>,
sun_occlusion_range_indices: Vec<usize>, sun_occlusion_range_indices: Vec<usize>,
@@ -4401,6 +4419,7 @@ fn static_preview_mesh_and_materials(
draw_ranges: Vec::new(), draw_ranges: Vec::new(),
}; };
let mut native_handedness_by_range = HashMap::new(); let mut native_handedness_by_range = HashMap::new();
let mut type16_present_ranges = HashSet::new();
let mut materials = Vec::new(); let mut materials = Vec::new();
let mut material_animations = Vec::new(); let mut material_animations = Vec::new();
let mut sun_occlusion_range_indices = 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() { for range_index in range_start..mesh.draw_ranges.len() {
native_handedness_by_range.insert(range_index, component_handedness); native_handedness_by_range.insert(range_index, component_handedness);
if model.validated.type16_present {
type16_present_ranges.insert(range_index);
}
} }
if root_sun_eligible { if root_sun_eligible {
sun_occlusion_range_indices.extend(range_start..mesh.draw_ranges.len()); sun_occlusion_range_indices.extend(range_start..mesh.draw_ranges.len());
@@ -4842,6 +4864,7 @@ fn static_preview_mesh_and_materials(
Ok(StaticPreviewScene { Ok(StaticPreviewScene {
mesh, mesh,
native_handedness_by_range, native_handedness_by_range,
type16_present_ranges,
materials, materials,
material_animations, material_animations,
sun_occlusion_range_indices, sun_occlusion_range_indices,
@@ -5562,6 +5585,7 @@ fn run_static_vulkan_mode(
Some(EnvironmentGpuScene::new( Some(EnvironmentGpuScene::new(
&mut preview.mesh, &mut preview.mesh,
&mut preview.native_handedness_by_range, &mut preview.native_handedness_by_range,
&mut preview.type16_present_ranges,
&mut preview.materials, &mut preview.materials,
&mut preview.material_animations, &mut preview.material_animations,
&mut preview.sun_occlusion_range_indices, &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 { struct StaticVulkanApp {
mesh: VulkanStaticMesh, mesh: VulkanStaticMesh,
native_handedness_by_range: HashMap<usize, i8>, native_handedness_by_range: HashMap<usize, i8>,
type16_present_ranges: HashSet<usize>,
materials: Vec<VulkanStaticMaterial>, materials: Vec<VulkanStaticMaterial>,
material_animations: Vec<MaterialAnimationBinding>, material_animations: Vec<MaterialAnimationBinding>,
sun_occlusion_range_indices: Vec<usize>, sun_occlusion_range_indices: Vec<usize>,
@@ -5765,6 +5790,7 @@ impl StaticVulkanApp {
Self { Self {
mesh: preview.mesh, mesh: preview.mesh,
native_handedness_by_range: preview.native_handedness_by_range, native_handedness_by_range: preview.native_handedness_by_range,
type16_present_ranges: preview.type16_present_ranges,
materials: preview.materials, materials: preview.materials,
material_animations: preview.material_animations, material_animations: preview.material_animations,
sun_occlusion_range_indices: preview.sun_occlusion_range_indices, sun_occlusion_range_indices: preview.sun_occlusion_range_indices,
@@ -5832,7 +5858,17 @@ impl StaticVulkanApp {
.materials .materials
.get(usize::from(range.material_index)) .get(usize::from(range.material_index))
.map_or(1.0, |material| material.diffuse_alpha); .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 renderer
.set_draw_range_transparency(range_index, transparent) .set_draw_range_transparency(range_index, transparent)
.map_err(|error| format!("configure world draw range {range_index}: {error}"))?; .map_err(|error| format!("configure world draw range {range_index}: {error}"))?;
@@ -5853,7 +5889,7 @@ impl StaticVulkanApp {
&self.mesh, &self.mesh,
range, range,
range.index_count, range.index_count,
alpha, effective_alpha,
self.view_camera.position, self.view_camera.position,
self.native_handedness_by_range.get(&range_index).copied(), self.native_handedness_by_range.get(&range_index).copied(),
), ),
@@ -5869,6 +5905,7 @@ impl StaticVulkanApp {
mesh: &VulkanStaticMesh, mesh: &VulkanStaticMesh,
materials: &[VulkanStaticMaterial], materials: &[VulkanStaticMaterial],
world_range_indices: &[usize], world_range_indices: &[usize],
type16_present_ranges: &HashSet<usize>,
world_range_index_counts: &HashMap<usize, u32>, world_range_index_counts: &HashMap<usize, u32>,
native_handedness_by_range: &HashMap<usize, i8>, native_handedness_by_range: &HashMap<usize, i8>,
eye_world: [f32; 3], eye_world: [f32; 3],
@@ -5881,7 +5918,7 @@ impl StaticVulkanApp {
.get(range_index) .get(range_index)
.ok_or_else(|| "world draw range index is out of bounds".to_string())?; .ok_or_else(|| "world draw range index is out of bounds".to_string())?;
let state = active_materials.get(&range_index); let state = active_materials.get(&range_index);
let alpha = state.map_or_else( let material_alpha = state.map_or_else(
|| { || {
materials materials
.get(usize::from(range.material_index)) .get(usize::from(range.material_index))
@@ -5889,6 +5926,14 @@ impl StaticVulkanApp {
}, },
|state| state.diffuse_alpha, |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 renderer
.set_draw_range_transparency(range_index, native_world_transparent(range, alpha)) .set_draw_range_transparency(range_index, native_world_transparent(range, alpha))
.map_err(|error| format!("update world draw range {range_index}: {error}"))?; .map_err(|error| format!("update world draw range {range_index}: {error}"))?;
@@ -6322,6 +6367,7 @@ impl StaticVulkanApp {
&self.mesh, &self.mesh,
&self.materials, &self.materials,
&self.world_range_indices, &self.world_range_indices,
&self.type16_present_ranges,
&self.world_range_index_counts, &self.world_range_index_counts,
&self.native_handedness_by_range, &self.native_handedness_by_range,
frame.camera.position, frame.camera.position,
@@ -7342,6 +7388,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: vec![], indices: vec![],
batches: vec![], batches: vec![],
triangle_descriptors: None, triangle_descriptors: None,
@@ -7671,6 +7718,7 @@ mod tests {
let preview = StaticPreviewScene { let preview = StaticPreviewScene {
mesh: initial_mesh.clone(), mesh: initial_mesh.clone(),
native_handedness_by_range: HashMap::new(), native_handedness_by_range: HashMap::new(),
type16_present_ranges: HashSet::new(),
materials: Vec::new(), materials: Vec::new(),
material_animations: Vec::new(), material_animations: Vec::new(),
sun_occlusion_range_indices: 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] #[test]
fn material_animation_selects_pipeline_category_with_active_phase() { fn material_animation_selects_pipeline_category_with_active_phase() {
let variant = MaterialVariantSet { let variant = MaterialVariantSet {
@@ -10009,4 +10068,29 @@ mod tests {
original_count 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<f32>| {
*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);
}
} }
+31
View File
@@ -21,6 +21,8 @@ pub const STREAM_UV0: u32 = 5;
/// Optional secondary texture coordinate stream used by native lightmap/detail /// Optional secondary texture coordinate stream used by native lightmap/detail
/// stages. MSH stores it as the type-18 four-byte packed UV stream. /// stages. MSH stores it as the type-18 four-byte packed UV stream.
pub const STREAM_UV1: u32 = 18; 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. /// Triangle index stream.
pub const STREAM_INDICES: u32 = 6; pub const STREAM_INDICES: u32 = 6;
/// Optional triangle-descriptor stream. /// Optional triangle-descriptor stream.
@@ -101,6 +103,10 @@ pub struct ModelAsset {
pub uv0: Option<Vec<[u16; 2]>>, pub uv0: Option<Vec<[u16; 2]>>,
/// Optional secondary packed texture coordinates from type 18. /// Optional secondary packed texture coordinates from type 18.
pub uv1: Option<Vec<[u16; 2]>>, pub uv1: Option<Vec<[u16; 2]>>,
/// 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. /// Triangle indices.
pub indices: Vec<u16>, pub indices: Vec<u16>,
/// Draw batches. /// Draw batches.
@@ -1116,6 +1122,7 @@ fn parse_model_document(document: &NresDocument) -> Result<ModelAsset, MshError>
let uv1 = read_optional_stream(document, STREAM_UV1)? let uv1 = read_optional_stream(document, STREAM_UV1)?
.map(|raw| parse_secondary_uv_stream(raw, positions.len())) .map(|raw| parse_secondary_uv_stream(raw, positions.len()))
.transpose()?; .transpose()?;
let type16_present = read_optional_stream(document, STREAM_TYPE_16)?.is_some();
let triangle_descriptors = read_optional_triangle_descriptors(document)? let triangle_descriptors = read_optional_triangle_descriptors(document)?
.map(|raw| parse_triangle_descriptors(&raw.bytes)) .map(|raw| parse_triangle_descriptors(&raw.bytes))
.transpose()?; .transpose()?;
@@ -1135,6 +1142,7 @@ fn parse_model_document(document: &NresDocument) -> Result<ModelAsset, MshError>
normals, normals,
uv0, uv0,
uv1, uv1,
type16_present,
indices, indices,
batches, batches,
triangle_descriptors, triangle_descriptors,
@@ -1641,6 +1649,25 @@ mod tests {
assert_eq!(model.node_count, 0); assert_eq!(model.node_count, 0);
assert!(model.slots.is_empty()); assert!(model.slots.is_empty());
assert_eq!(model.triangle_descriptors, None); 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] #[test]
@@ -2262,6 +2289,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: Vec::new(), indices: Vec::new(),
batches: Vec::new(), batches: Vec::new(),
triangle_descriptors: None, triangle_descriptors: None,
@@ -2324,6 +2352,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: Vec::new(), indices: Vec::new(),
batches: Vec::new(), batches: Vec::new(),
triangle_descriptors: None, triangle_descriptors: None,
@@ -2389,6 +2418,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: Vec::new(), indices: Vec::new(),
batches: Vec::new(), batches: Vec::new(),
triangle_descriptors: None, triangle_descriptors: None,
@@ -3282,6 +3312,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices, indices,
batches, batches,
node_names: None, node_names: None,
+1
View File
@@ -6808,6 +6808,7 @@ mod tests {
normals: None, normals: None,
uv0: None, uv0: None,
uv1: None, uv1: None,
type16_present: false,
indices: Vec::new(), indices: Vec::new(),
batches: Vec::new(), batches: Vec::new(),
triangle_descriptors: None, triangle_descriptors: None,
+21
View File
@@ -18,6 +18,27 @@ state transitions и окончательный порядок части transp
стене и дверном проёме. Мировое преобразование, фильтры категории и стене и дверном проёме. Мировое преобразование, фильтры категории и
подключение к свободной камере остаются открытыми ([MSH](../reference/msh.md)). подключение к свободной камере остаются открытыми ([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 Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
log на минимальных сценах с одним типом материала. log на минимальных сценах с одним типом материала.
+4
View File
@@ -18,6 +18,7 @@ type 9 service stream
type 10 strings and node names type 10 strings and node names
type 13 Batch20 records type 13 Batch20 records
type 15 auxiliary stream type 15 auxiliary stream
type 16 optional vertex-colour data
type 17 auxiliary data type 17 auxiliary data
type 18 packed UV1 (optional secondary coordinates) type 18 packed UV1 (optional secondary coordinates)
type 19 animation frame map type 19 animation frame map
@@ -25,6 +26,9 @@ type 20 rare auxiliary table
``` ```
Reader ищет entries по type, но сохраняет исходный порядок для roundtrip. Reader ищет entries по type, но сохраняет исходный порядок для roundtrip.
Поле `ModelAsset.type16_present` отмечает только наличие entry типа 16. Данные
payload сохраняются, но этот путь рендера пока не преобразует их в цвета
вершин.
## Node and slot selection ## Node and slot selection
+17
View File
@@ -68,6 +68,23 @@ Pending frame хранит пакет MAT0 для мира и, при налич
`batch` `0x108`. Такой диапазон при включённой проверке глубины не записывает `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, пока эти входные данные не установлены.
В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков
явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на
своих местах; прозрачная очередь мира сортируется от дальних элементов к своих местах; прозрачная очередь мира сортируется от дальних элементов к