diff options
Diffstat (limited to 'adapters/fparkan-render-vulkan/src')
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/asset_mesh.rs | 179 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke.rs | 23 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs | 72 | ||||
| -rw-r--r-- | adapters/fparkan-render-vulkan/src/ffi/tests.rs | 2 |
4 files changed, 209 insertions, 67 deletions
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index 9d084ae..0de1381 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -7,7 +7,7 @@ use fparkan_msh::{ ModelAsset, NodeId, }; use fparkan_render::{LegacyDepthMode, LegacyIron3dEulerTransform, LegacyPipelineState}; -use fparkan_terrain_format::LandMeshDocument; +use fparkan_terrain_format::{LandMeshDocument, TerrainFaceLod, TerrainLod, TerrainLodError}; fn world_pipeline_state() -> LegacyPipelineState { LegacyPipelineState { @@ -34,6 +34,8 @@ pub enum VulkanAssetMeshError { /// Number of supplied node poses. actual: usize, }, + /// Terrain Node38/Slot68 records do not assign every face to one LOD. + InvalidTerrainLodMapping(TerrainLodError), } /// Evaluated world pose of one standard Node38 node in model-local space. @@ -108,6 +110,9 @@ impl std::fmt::Display for VulkanAssetMeshError { f, "node pose buffer has {actual} poses, but the MSH has {expected} nodes" ), + Self::InvalidTerrainLodMapping(error) => { + write!(f, "invalid terrain LOD mapping: {error}") + } } } } @@ -1016,23 +1021,15 @@ fn transform_normal( pub fn project_land_msh_to_static_mesh( terrain: &LandMeshDocument, ) -> Result<VulkanStaticMesh, VulkanAssetMeshError> { - let mut indices = Vec::with_capacity( - terrain - .faces - .len() - .checked_mul(3) - .ok_or(VulkanAssetMeshError::IndexOutOfRange)?, - ); - for face in &terrain.faces { - indices.extend(face.vertices.map(u32::from)); - } + let lod0 = static_terrain_lod0_assignments(terrain)?; + let indices = static_terrain_indices(terrain, &lod0)?; if indices.is_empty() { return Err(VulkanAssetMeshError::EmptyGeometry); } let (min_x, max_x, min_y, max_y) = static_mesh_xy_bounds(&terrain.positions, &indices)?; let frame = VulkanStaticXyFrame::from_bounds(min_x, max_x, min_y, max_y)?; - project_land_msh_to_static_mesh_in_xy_frame(terrain, frame) + project_land_msh_to_static_mesh_with_lod0(terrain, frame, &lod0) } /// Projects `Land.msh` terrain geometry into a shared diagnostic XY frame. @@ -1045,16 +1042,16 @@ pub fn project_land_msh_to_static_mesh_in_xy_frame( terrain: &LandMeshDocument, frame: VulkanStaticXyFrame, ) -> Result<VulkanStaticMesh, VulkanAssetMeshError> { - let mut indices = Vec::with_capacity( - terrain - .faces - .len() - .checked_mul(3) - .ok_or(VulkanAssetMeshError::IndexOutOfRange)?, - ); - for face in &terrain.faces { - indices.extend(face.vertices.map(u32::from)); - } + let lod0 = static_terrain_lod0_assignments(terrain)?; + project_land_msh_to_static_mesh_with_lod0(terrain, frame, &lod0) +} + +fn project_land_msh_to_static_mesh_with_lod0( + terrain: &LandMeshDocument, + frame: VulkanStaticXyFrame, + lod0: &[TerrainFaceLod], +) -> Result<VulkanStaticMesh, VulkanAssetMeshError> { + let indices = static_terrain_indices(terrain, lod0)?; if indices.is_empty() { return Err(VulkanAssetMeshError::EmptyGeometry); } @@ -1089,7 +1086,7 @@ pub fn project_land_msh_to_static_mesh_in_xy_frame( Ok(VulkanStaticMesh { vertices, indices, - draw_ranges: static_terrain_draw_ranges(terrain)?, + draw_ranges: static_terrain_draw_ranges(terrain, lod0)?, }) } @@ -1107,7 +1104,8 @@ pub fn project_land_msh_to_static_mesh_in_xy_frame( pub fn project_land_msh_to_static_mesh_in_world_space( terrain: &LandMeshDocument, ) -> Result<VulkanStaticMesh, VulkanAssetMeshError> { - let indices = static_terrain_indices(terrain)?; + let lod0 = static_terrain_lod0_assignments(terrain)?; + let indices = static_terrain_indices(terrain, &lod0)?; let vertices = terrain .positions .iter() @@ -1132,19 +1130,34 @@ pub fn project_land_msh_to_static_mesh_in_world_space( Ok(VulkanStaticMesh { vertices, indices, - draw_ranges: static_terrain_draw_ranges(terrain)?, + draw_ranges: static_terrain_draw_ranges(terrain, &lod0)?, }) } -fn static_terrain_indices(terrain: &LandMeshDocument) -> Result<Vec<u32>, VulkanAssetMeshError> { +fn static_terrain_lod0_assignments( + terrain: &LandMeshDocument, +) -> Result<Vec<TerrainFaceLod>, VulkanAssetMeshError> { + let mut assignments = terrain + .face_lod_assignments() + .map_err(VulkanAssetMeshError::InvalidTerrainLodMapping)?; + assignments.retain(|assignment| assignment.lod == TerrainLod::Lod0); + Ok(assignments) +} + +fn static_terrain_indices( + terrain: &LandMeshDocument, + lod0: &[TerrainFaceLod], +) -> Result<Vec<u32>, VulkanAssetMeshError> { let mut indices = Vec::with_capacity( - terrain - .faces - .len() + lod0.len() .checked_mul(3) .ok_or(VulkanAssetMeshError::IndexOutOfRange)?, ); - for face in &terrain.faces { + for assignment in lod0 { + let face = terrain + .faces + .get(assignment.face_index) + .ok_or(VulkanAssetMeshError::IndexOutOfRange)?; indices.extend(face.vertices.map(u32::from)); } if indices.is_empty() { @@ -1187,14 +1200,24 @@ fn terrain_overlay_alpha(terrain: &LandMeshDocument, index: usize) -> f32 { fn static_terrain_draw_ranges( terrain: &LandMeshDocument, + lod0: &[TerrainFaceLod], ) -> Result<Vec<VulkanStaticDrawRange>, VulkanAssetMeshError> { let mut ranges = Vec::new(); let mut start_face = 0usize; - while start_face < terrain.faces.len() { - let material_index = terrain.faces[start_face].material_tag; + while start_face < lod0.len() { + let material_index = terrain + .faces + .get(lod0[start_face].face_index) + .ok_or(VulkanAssetMeshError::IndexOutOfRange)? + .material_tag; let mut end_face = start_face + 1; - while end_face < terrain.faces.len() - && terrain.faces[end_face].material_tag == material_index + while end_face < lod0.len() + && terrain + .faces + .get(lod0[end_face].face_index) + .ok_or(VulkanAssetMeshError::IndexOutOfRange)? + .material_tag + == material_index { end_face += 1; } @@ -1998,7 +2021,7 @@ mod tests { #[test] fn projects_land_faces_in_source_order_with_packed_uv0() { - let terrain = LandMeshDocument { + let mut terrain = LandMeshDocument { streams: Vec::new(), nodes_raw: Vec::new(), slots: TerrainSlotTable { @@ -2018,6 +2041,7 @@ mod tests { aux18: Vec::new(), faces: vec![terrain_face([0, 1, 2]), terrain_face([1, 3, 2])], }; + set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 2]]); let mesh = project_land_msh_to_static_mesh(&terrain).expect("representable terrain"); @@ -2031,8 +2055,60 @@ mod tests { } #[test] + fn terrain_projection_filters_interleaved_lods_in_source_face_order() { + let mut terrain = LandMeshDocument { + streams: Vec::new(), + nodes_raw: Vec::new(), + slots: TerrainSlotTable { + header_raw: Vec::new(), + slots_raw: Vec::new(), + }, + positions: vec![ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [10.0, 10.0, 10.0], + [11.0, 10.0, 10.0], + [10.0, 11.0, 10.0], + ], + normals: Vec::new(), + uv0: Vec::new(), + accelerator: Vec::new(), + aux14: Vec::new(), + aux18: Vec::new(), + faces: vec![ + terrain_face_with_tag(0x0101, [0, 1, 2]), + terrain_face_with_tag(0x0202, [3, 4, 5]), + terrain_face_with_tag(0x0101, [0, 1, 2]), + terrain_face_with_tag(0x0202, [3, 4, 5]), + ], + }; + set_terrain_lod_slots( + &mut terrain, + &[[0, 1], [2, 3]], + &[[0, 1], [1, 1], [2, 1], [3, 1]], + ); + + let mesh = project_land_msh_to_static_mesh_in_world_space(&terrain) + .expect("LOD0 terrain projection"); + assert_eq!(mesh.indices, [0, 1, 2, 0, 1, 2]); + assert_eq!(mesh.draw_ranges.len(), 1); + assert_eq!(mesh.draw_ranges[0].first_index, 0); + assert_eq!(mesh.draw_ranges[0].index_count, 6); + assert_eq!(mesh.draw_ranges[0].material_index, 0x0101); + + terrain.nodes_raw.clear(); + assert!(matches!( + project_land_msh_to_static_mesh_in_world_space(&terrain), + Err(VulkanAssetMeshError::InvalidTerrainLodMapping( + TerrainLodError::MissingNodes { face_count: 4 } + )) + )); + } + + #[test] fn terrain_normals_keep_native_one_over_128_scale() { - let terrain = LandMeshDocument { + let mut terrain = LandMeshDocument { streams: Vec::new(), nodes_raw: Vec::new(), slots: TerrainSlotTable { @@ -2047,6 +2123,7 @@ mod tests { aux18: Vec::new(), faces: vec![terrain_face([0, 1, 2])], }; + set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 1]]); let mesh = project_land_msh_to_static_mesh_in_world_space(&terrain) .expect("representable terrain normals"); @@ -2055,7 +2132,7 @@ mod tests { #[test] fn world_space_terrain_preserves_nonzero_source_heights_and_faces() { - let terrain = LandMeshDocument { + let mut terrain = LandMeshDocument { streams: Vec::new(), nodes_raw: Vec::new(), slots: TerrainSlotTable { @@ -2074,6 +2151,7 @@ mod tests { aux18: Vec::new(), faces: vec![terrain_face([2, 0, 1])], }; + set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 1]]); let mesh = project_land_msh_to_static_mesh_in_world_space(&terrain) .expect("representable source-space terrain"); @@ -2101,9 +2179,30 @@ mod tests { } } + fn set_terrain_lod_slots( + terrain: &mut LandMeshDocument, + node_slots: &[[u16; 2]], + face_ranges: &[[u16; 2]], + ) { + const NODE_STRIDE: usize = 38; + let mut nodes = vec![u8::MAX; node_slots.len() * NODE_STRIDE]; + for (node_index, [lod0, lod1]) in node_slots.iter().copied().enumerate() { + let offset = node_index * NODE_STRIDE + 8; + nodes[offset..offset + 2].copy_from_slice(&lod0.to_le_bytes()); + nodes[offset + 2..offset + 4].copy_from_slice(&lod1.to_le_bytes()); + } + let mut slots = vec![[0_u8; 68]; face_ranges.len()]; + for (slot, [start, count]) in slots.iter_mut().zip(face_ranges.iter().copied()) { + slot[..2].copy_from_slice(&start.to_le_bytes()); + slot[2..4].copy_from_slice(&count.to_le_bytes()); + } + terrain.nodes_raw = nodes; + terrain.slots.slots_raw = slots; + } + #[test] fn terrain_ranges_preserve_face_order_and_packed_material_tags() { - let terrain = LandMeshDocument { + let mut terrain = LandMeshDocument { streams: Vec::new(), nodes_raw: Vec::new(), slots: TerrainSlotTable { @@ -2123,8 +2222,10 @@ mod tests { terrain_face_with_tag(0xff01, [0, 1, 2]), ], }; + set_terrain_lod_slots(&mut terrain, &[[0, u16::MAX]], &[[0, 4]]); - let ranges = static_terrain_draw_ranges(&terrain).expect("representable terrain"); + let lod0 = static_terrain_lod0_assignments(&terrain).expect("terrain LOD0"); + let ranges = static_terrain_draw_ranges(&terrain, &lod0).expect("representable terrain"); assert_eq!(ranges.len(), 3); assert_eq!(ranges[0].first_index, 0); diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs index c45c331..ac359c2 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke.rs @@ -3,7 +3,7 @@ use ash::vk; use fparkan_render::{LegacyDepthMode, LegacyPipelineState}; -use super::smoke_types::resolve_draw_texture_indices; +use super::smoke_types::{resolve_draw_texture_indices, VulkanStaticMaterial}; use super::update_material_texture_descriptor; use super::{ create_command_pool, create_frame_sync, create_index_buffer, create_static_mesh_index_buffer, @@ -361,18 +361,7 @@ impl VulkanSmokeRenderer { create_info .materials .iter() - .map(|material| { - let combiner_mode = if material.lightmap_mode { - 3.0 - } else if material.sky_nebula_stars { - 2.0 - } else if material.unlit { - 1.0 - } else { - 0.0 - }; - combiner_mode + if material.sky_far_depth { 4.0 } else { 0.0 } - }) + .map(VulkanStaticMaterial::encoded_material_mode) .collect() }; let material_specular = if create_info.materials.is_empty() { @@ -1091,10 +1080,10 @@ impl VulkanSmokeRenderer { .chain(additive_rgb) .chain(overlay_directional_rgb.into_iter().flatten()) .chain(overlay_additive_rgb.into_iter().flatten()) - .any(|value| !value.is_finite() || value < 0.0) + .any(|value| !value.is_finite()) { return Err(VulkanSmokeRendererError::InvalidStaticMesh { - context: "dynamic material lighting must be finite and non-negative", + context: "dynamic material lighting must be finite", }); } let lighting = self.material_lighting.get_mut(material_index).ok_or( @@ -1155,10 +1144,10 @@ impl VulkanSmokeRenderer { if specular_rgb .into_iter() .chain(overlay_specular_rgb.into_iter().flatten()) - .any(|value| !value.is_finite() || value < 0.0) + .any(|value| !value.is_finite()) { return Err(VulkanSmokeRendererError::InvalidStaticMesh { - context: "dynamic material specular RGB must be finite and non-negative", + context: "dynamic material specular RGB must be finite", }); } let specular = self.material_specular.get_mut(material_index).ok_or( diff --git a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs index c41eff9..7a8837d 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/smoke_types.rs @@ -505,6 +505,33 @@ pub struct VulkanStaticMaterial { /// renderer projection choice; it is not a claim about the original D3D /// depth-bit encoding. pub sky_far_depth: bool, + /// Push material mode bit 8 and treat positions as Vulkan clip-space + /// `XYZRHW` vertices. Their x/y coordinates are converted from viewport + /// pixels by the caller and z retains the native `0.999` depth. + pub screen_tl: bool, +} + +impl VulkanStaticMaterial { + pub(crate) fn encoded_material_mode(&self) -> f32 { + let combiner_mode = if self.screen_tl { + 1.0 + } else if self.lightmap_mode { + 3.0 + } else if self.sky_nebula_stars { + 2.0 + } else if self.unlit { + 1.0 + } else { + 0.0 + }; + combiner_mode + + if self.sky_far_depth && !self.screen_tl { + 4.0 + } else { + 0.0 + } + + if self.screen_tl { 8.0 } else { 0.0 } + } } /// One authored RGBA8 mip level accepted by the Vulkan texture upload path. @@ -622,9 +649,9 @@ pub(super) fn resolve_draw_texture_indices( .chain(material.additive_rgb) .chain(material.specular_rgb) .chain(material.overlay_specular_rgb.into_iter().flatten()) - .any(|value| !value.is_finite() || value < 0.0) + .any(|value| !value.is_finite()) { - return Err("static material lighting and specular RGB parameters must be finite and non-negative"); + return Err("static material lighting and specular RGB parameters must be finite"); } if material .uv_transform @@ -645,9 +672,9 @@ pub(super) fn resolve_draw_texture_indices( .into_iter() .flatten() .chain(material.overlay_additive_rgb.into_iter().flatten()) - .any(|value| !value.is_finite() || value < 0.0) + .any(|value| !value.is_finite()) { - return Err("static material overlay RGB parameters must be finite and non-negative"); + return Err("static material overlay RGB parameters must be finite"); } if material .overlay_uv_transform @@ -1012,15 +1039,15 @@ mod static_mesh_tests { overlay_detail_texture: None, diffuse_alpha: 1.0, overlay_diffuse_alpha: None, - directional_rgb: [1.0; 3], - additive_rgb: [0.0; 3], - specular_rgb: [0.0; 3], + directional_rgb: [-0.25; 3], + additive_rgb: [-0.5; 3], + specular_rgb: [-0.75; 3], specular_power: 0, - overlay_specular_rgb: None, + overlay_specular_rgb: Some([-1.0; 3]), overlay_specular_power: None, uv_transform: [0.0, 0.0, 1.0, 1.0], - overlay_directional_rgb: None, - overlay_additive_rgb: None, + overlay_directional_rgb: Some([-0.125; 3]), + overlay_additive_rgb: Some([-0.375; 3]), detail_uv_transform: [0.0, 0.0, 1.0, 1.0], overlay_uv_transform: None, overlay_detail_uv_transform: None, @@ -1028,6 +1055,7 @@ mod static_mesh_tests { sky_nebula_stars: false, lightmap_mode: false, sky_far_depth: false, + screen_tl: false, }, VulkanStaticMaterial { material_index: 7, @@ -1054,13 +1082,37 @@ mod static_mesh_tests { sky_nebula_stars: false, lightmap_mode: false, sky_far_depth: false, + screen_tl: false, }, ]; + let mut screen_tl = materials[0].clone(); + screen_tl.screen_tl = true; + screen_tl.sky_far_depth = true; + assert_eq!(screen_tl.encoded_material_mode(), 9.0); + let mut far_sky = materials[0].clone(); + far_sky.unlit = true; + far_sky.sky_far_depth = true; + assert_eq!(far_sky.encoded_material_mode(), 5.0); + assert_eq!( resolve_draw_texture_indices(&ranges, &materials), Ok(vec![1, 0]) ); + + let mut non_finite_base = materials.clone(); + non_finite_base[0].specular_rgb[0] = f32::NAN; + assert_eq!( + resolve_draw_texture_indices(&ranges, &non_finite_base), + Err("static material lighting and specular RGB parameters must be finite") + ); + + let mut non_finite_overlay = materials.clone(); + non_finite_overlay[0].overlay_additive_rgb = Some([f32::INFINITY, 0.0, 0.0]); + assert_eq!( + resolve_draw_texture_indices(&ranges, &non_finite_overlay), + Err("static material overlay RGB parameters must be finite") + ); } #[test] diff --git a/adapters/fparkan-render-vulkan/src/ffi/tests.rs b/adapters/fparkan-render-vulkan/src/ffi/tests.rs index b8a3d71..4ed61f7 100644 --- a/adapters/fparkan-render-vulkan/src/ffi/tests.rs +++ b/adapters/fparkan-render-vulkan/src/ffi/tests.rs @@ -533,7 +533,7 @@ fn native_specular_reference_uses_squared_power_and_total_overflow_curve() { fn shader_computes_radial_fog_per_vertex_and_interpolates_it() { let vertex = include_str!("../../shaders/triangle.vert"); let fragment = include_str!("../../shaders/triangle.frag"); - assert!(vertex.contains("out_fog_factor = native_fog_factor(in_position);")); + assert!(vertex.contains("out_fog_factor = screen_tl ? 0.0 : native_fog_factor(in_position);")); assert!(fragment.contains("layout(location = 4) in float in_fog_factor;")); assert!(!fragment.contains("distance(in_world_position")); assert!(!fragment.contains("float fog_factor()")); |
