diff options
Diffstat (limited to 'apps/fparkan-game/src')
| -rw-r--r-- | apps/fparkan-game/src/main.rs | 574 |
1 files changed, 550 insertions, 24 deletions
diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 438e345..3b3969e 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -32,8 +32,8 @@ use fparkan_path::ResourceName; use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan}; use fparkan_prototype::{unit_component_tree, PROTOTYPE_TYPE_EXTO}; use fparkan_render::{ - LegacyBlendMode, LegacyD3d7Projection, LegacyDepthMode, LegacyIron3dEulerTransform, - LegacyPipelineState, RawCameraTransform, + LegacyBlendMode, LegacyCullMode, LegacyD3d7Projection, LegacyDepthMode, + LegacyIron3dEulerTransform, LegacyPipelineState, RawCameraTransform, }; use fparkan_render_vulkan::{ node38_pose, node38_pose_from_hierarchy, node38_pose_relative_to_root, @@ -924,6 +924,113 @@ fn native_world_transparent(range: &VulkanStaticDrawRange, diffuse_alpha: f32) - diffuse_alpha < 1.0 || (range.batch_flags & 0x108) != 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; + }; + let Some(x_axis) = transform + .try_transform_scaled_point([1.0, 0.0, 0.0], scale) + .map(|point| sub3(point, origin)) + else { + return 0; + }; + let Some(y_axis) = transform + .try_transform_scaled_point([0.0, 1.0, 0.0], scale) + .map(|point| sub3(point, origin)) + else { + return 0; + }; + let Some(z_axis) = transform + .try_transform_scaled_point([0.0, 0.0, 1.0], scale) + .map(|point| sub3(point, origin)) + else { + return 0; + }; + // Node and mount poses in the preview are unit-quaternion rotations, so + // they have determinant +1 and preserve this exact object-transform sign. + let determinant_sign = dot3(x_axis, cross3(y_axis, z_axis)); + if !determinant_sign.is_finite() || determinant_sign == 0.0 { + 0 + } else if determinant_sign < 0.0 { + -1 + } else { + 1 + } +} + +fn native_msh_cull_mode(batch_flags: u16, handedness: i8) -> LegacyCullMode { + if batch_flags & 0x2 != 0 || batch_flags & 0x4000 != 0 || handedness == 0 { + LegacyCullMode::Disabled + } else if handedness > 0 { + // The Y-negated projection maps native d > 0 faces to Vulkan's back + // under the renderer's CLOCKWISE front-face convention. + LegacyCullMode::FrontFace + } else { + LegacyCullMode::BackFace + } +} + +fn native_msh_first_triangle_visible( + mesh: &VulkanStaticMesh, + range: &VulkanStaticDrawRange, + eye_world: [f32; 3], + handedness: i8, +) -> bool { + if range.batch_flags & 0x2 != 0 || range.batch_flags & 0x4000 == 0 { + return true; + } + if handedness == 0 { + return false; + } + let Some(indices) = usize::try_from(range.first_index) + .ok() + .and_then(|first| mesh.indices.get(first..first.checked_add(3)?)) + else { + return false; + }; + let Some(v0) = usize::try_from(indices[0]) + .ok() + .and_then(|index| mesh.vertices.get(index)) + else { + return false; + }; + let Some(v1) = usize::try_from(indices[1]) + .ok() + .and_then(|index| mesh.vertices.get(index)) + else { + return false; + }; + let Some(v2) = usize::try_from(indices[2]) + .ok() + .and_then(|index| mesh.vertices.get(index)) + else { + return false; + }; + let [v0, v1, v2] = [v0.position, v1.position, v2.position]; + let face = cross3(sub3(v1, v0), sub3(v2, v0)); + let facing = dot3(sub3(eye_world, v0), face) * f32::from(handedness); + facing.is_finite() && facing > 0.0 +} + +fn world_draw_range_index_count( + mesh: &VulkanStaticMesh, + range: &VulkanStaticDrawRange, + original_index_count: u32, + diffuse_alpha: f32, + eye_world: [f32; 3], + native_msh_handedness: Option<i8>, +) -> u32 { + if !(diffuse_alpha > 0.0) { + return 0; + } + if native_msh_handedness.is_some_and(|handedness| { + !native_msh_first_triangle_visible(mesh, range, eye_world, handedness) + }) { + return 0; + } + original_index_count +} + fn configure_world_material_categories( mesh: &mut VulkanStaticMesh, materials: &[VulkanStaticMaterial], @@ -1261,6 +1368,7 @@ fn append_environment_material( sky_nebula_stars: false, lightmap_mode: false, sky_far_depth: matches!(fallback_row, 0..=2), + screen_tl: ENVIRONMENT_SPRITE_ROWS.contains(&fallback_row), }); phase_material_indices.push(index); } @@ -1321,6 +1429,7 @@ fn append_environment_gradient_material( sky_nebula_stars: false, lightmap_mode: false, sky_far_depth: true, + screen_tl: false, }); Ok(index) } @@ -1371,6 +1480,7 @@ fn append_shadow_material(materials: &mut Vec<VulkanStaticMaterial>) -> Result<u sky_nebula_stars: false, lightmap_mode: false, sky_far_depth: false, + screen_tl: false, }); Ok(index) } @@ -1990,6 +2100,7 @@ impl EnvironmentGpuScene { fn new( mesh: &mut VulkanStaticMesh, + native_handedness_by_range: &mut HashMap<usize, i8>, materials: &mut Vec<VulkanStaticMaterial>, preview_material_animations: &mut Vec<MaterialAnimationBinding>, sun_occlusion_range_indices: &mut Vec<usize>, @@ -2409,6 +2520,14 @@ impl EnvironmentGpuScene { old_order.extend(sprite_indices.iter().copied()); old_order.extend([rain_index, snow_index, lightning_index]); let remap = reorder_draw_ranges(mesh, &old_order)?; + let mut remapped_handedness = HashMap::with_capacity(native_handedness_by_range.len()); + for (old_range_index, handedness) in std::mem::take(native_handedness_by_range) { + let new_range_index = remap.get(old_range_index).copied().ok_or_else(|| { + format!("native MSH draw range remap is missing range {old_range_index}") + })?; + remapped_handedness.insert(new_range_index, handedness); + } + *native_handedness_by_range = remapped_handedness; for layer in &mut scene.sky_layers { layer.range.range_index = *remap .get(layer.range.range_index) @@ -2648,17 +2767,33 @@ impl EnvironmentGpuScene { viewport, horizontal_fov(camera, viewport[0] / viewport[1].max(1.0)), ) { - vertices = Some(sprite_quad_vertices_pixels( - camera, - frame.direction, - (camera.far_plane * 0.5).max(camera.near_plane * 2.0), - half_width, - half_height, - viewport, - frame.color, - frame.alpha, - frame.uv, - )); + if ENVIRONMENT_SPRITE_ROWS.contains(&sprite.row) { + if let Some(center_pixels) = projected { + vertices = Some(screen_tl_quad_vertices_pixels( + center_pixels, + half_width, + half_height, + viewport, + frame.color, + frame.alpha, + frame.uv, + )); + } else { + active = false; + } + } else { + vertices = Some(sprite_quad_vertices_pixels( + camera, + frame.direction, + (camera.far_plane * 0.5).max(camera.near_plane * 2.0), + half_width, + half_height, + viewport, + frame.color, + frame.alpha, + frame.uv, + )); + } } else { active = false; } @@ -3083,6 +3218,50 @@ fn sprite_quad_vertices_pixels( ] } +fn screen_tl_quad_vertices_pixels( + center_pixels: [f32; 2], + half_width_pixels: f32, + half_height_pixels: f32, + viewport: [f32; 2], + color: [f32; 3], + alpha: f32, + uv: [[f32; 2]; 4], +) -> [VulkanStaticVertex; 4] { + let width = viewport[0].max(1.0); + let height = viewport[1].max(1.0); + let half_width = half_width_pixels.max(0.0); + let half_height = half_height_pixels.max(0.0); + let pixel_corners = [ + [ + center_pixels[0] - half_width, + center_pixels[1] + half_height, + ], + [ + center_pixels[0] + half_width, + center_pixels[1] + half_height, + ], + [ + center_pixels[0] - half_width, + center_pixels[1] - half_height, + ], + [ + center_pixels[0] + half_width, + center_pixels[1] - half_height, + ], + ]; + // Positive-height Vulkan viewports map NDC -Y to top-down screen pixels. + let uv = [uv[1], uv[2], uv[0], uv[3]]; + std::array::from_fn(|index| { + let pixel = pixel_corners[index]; + let position = [ + pixel[0] * (2.0 / width) - 1.0, + pixel[1] * (2.0 / height) - 1.0, + 0.999, + ]; + environment_vertex_with_alpha(position, color, alpha, uv[index]) + }) +} + fn precipitation_quad_vertices( kind: PrecipitationKind, screen: &ScreenBillboard, @@ -3594,6 +3773,7 @@ fn multiply_row_major(left: [f32; 16], right: [f32; 16]) -> [f32; 16] { struct StaticPreviewScene { mesh: VulkanStaticMesh, + native_handedness_by_range: HashMap<usize, i8>, materials: Vec<VulkanStaticMaterial>, material_animations: Vec<MaterialAnimationBinding>, sun_occlusion_range_indices: Vec<usize>, @@ -3661,6 +3841,11 @@ const PROTOTYPE_TYPE_FORT: u32 = u32::from_le_bytes(*b"FORT"); const PROTOTYPE_TYPE_BTLU: u32 = u32::from_le_bytes(*b"BTLU"); const PROTOTYPE_TYPE_STAT: u32 = u32::from_le_bytes(*b"STAT"); +// shortcut: the preview does not load shade.cfg yet, so use CShade's native +// defaults until the settings loader is implemented. +const NATIVE_DEFAULT_ROB_SHADOWS_ON: bool = true; +const NATIVE_DEFAULT_BLD_SHADOWS_ON: bool = false; + /// Maps the resolved registry type to the native object-kind selector used by /// CSun/CShade. The mission object's raw kind is an unrelated TMA field; the /// renderer receives the registry provenance from the prepared visual. @@ -3673,6 +3858,14 @@ fn native_shadow_kind(source_type: Option<u32>) -> Option<u32> { } } +fn native_shadow_caster_enabled(native_kind: u32) -> bool { + if native_kind == 4 { + NATIVE_DEFAULT_ROB_SHADOWS_ON + } else { + NATIVE_DEFAULT_BLD_SHADOWS_ON + } +} + fn compose_unit_pose(parent: VulkanNodePose, child: VulkanNodePose) -> VulkanNodePose { let rotated = rotate_unit_vector(child.translation, parent.rotation); VulkanNodePose { @@ -3805,6 +3998,7 @@ fn static_preview_mesh_and_materials( indices: Vec::new(), draw_ranges: Vec::new(), }; + let mut native_handedness_by_range = HashMap::new(); let mut materials = Vec::new(); let mut material_animations = Vec::new(); let mut sun_occlusion_range_indices = Vec::new(); @@ -4003,7 +4197,7 @@ fn static_preview_mesh_and_materials( }; if should_render { let pose_buffer = component_pose_buffers[component_index].as_ref(); - let component = match (pose_buffer, mount_pose, static_animation_frame) { + let mut component = match (pose_buffer, mount_pose, static_animation_frame) { (Some(poses), Some(mount), _) => { project_msh_to_static_mesh_in_world_space_with_node_pose_buffer_and_mount( &model.validated, @@ -4055,6 +4249,11 @@ fn static_preview_mesh_and_materials( } } .map_err(|err| format!("project mission MSH for Vulkan: {err}"))?; + let component_handedness = native_msh_handedness(transform, mission_root.scale); + for range in &mut component.draw_ranges { + range.pipeline_state.cull = + native_msh_cull_mode(range.batch_flags, component_handedness); + } let component_shadow_triangles = static_mesh_shadow_triangles(&component); if root_native_kind.is_some() { root_shadow_triangles.extend(component_shadow_triangles); @@ -4093,13 +4292,18 @@ fn static_preview_mesh_and_materials( &lightmap_textures, Some(&mut material_animations), )?; + for range_index in range_start..mesh.draw_ranges.len() { + native_handedness_by_range.insert(range_index, component_handedness); + } if root_sun_eligible { sun_occlusion_range_indices.extend(range_start..mesh.draw_ranges.len()); } mesh_components += 1; } } - if let Some(native_kind) = root_native_kind { + if let Some(native_kind) = + root_native_kind.filter(|kind| native_shadow_caster_enabled(*kind)) + { let caster_id = u32::try_from(shadow_casters.len()) .map_err(|_| "shadow caster id exceeds u32".to_string())?; if let Some(caster) = aggregate_shadow_caster( @@ -4140,6 +4344,7 @@ fn static_preview_mesh_and_materials( let shadow_material_index = append_shadow_material(&mut materials)?; Ok(StaticPreviewScene { mesh, + native_handedness_by_range, materials, material_animations, sun_occlusion_range_indices, @@ -4305,6 +4510,7 @@ fn static_preview_terrain_base_materials( sky_nebula_stars: false, lightmap_mode: false, sky_far_depth: false, + screen_tl: false, }); phase_material_indices.push(usize::from(preview_selector)); } @@ -4510,6 +4716,7 @@ fn static_preview_component_materials( sky_nebula_stars: false, lightmap_mode: false, sky_far_depth: false, + screen_tl: false, }); phase_material_indices.push(usize::from(preview_selector)); } @@ -4863,6 +5070,7 @@ fn run_static_vulkan_mode( let sky_mesh = environment.sky.mesh().clone(); Some(EnvironmentGpuScene::new( &mut preview.mesh, + &mut preview.native_handedness_by_range, &mut preview.materials, &mut preview.material_animations, &mut preview.sun_occlusion_range_indices, @@ -4900,6 +5108,7 @@ fn run_static_vulkan_mode( struct StaticVulkanApp { mesh: VulkanStaticMesh, + native_handedness_by_range: HashMap<usize, i8>, materials: Vec<VulkanStaticMaterial>, material_animations: Vec<MaterialAnimationBinding>, sun_occlusion_range_indices: Vec<usize>, @@ -4980,6 +5189,7 @@ impl StaticVulkanApp { .collect(); Self { mesh: preview.mesh, + native_handedness_by_range: preview.native_handedness_by_range, materials: preview.materials, material_animations: preview.material_animations, sun_occlusion_range_indices: preview.sun_occlusion_range_indices, @@ -5058,7 +5268,14 @@ impl StaticVulkanApp { renderer .set_draw_range_index_count( range_index, - if alpha > 0.0 { range.index_count } else { 0 }, + world_draw_range_index_count( + &self.mesh, + range, + range.index_count, + alpha, + self.view_camera.position, + self.native_handedness_by_range.get(&range_index).copied(), + ), ) .map_err(|error| { format!("configure world draw range {range_index} count: {error}") @@ -5072,6 +5289,8 @@ impl StaticVulkanApp { materials: &[VulkanStaticMaterial], world_range_indices: &[usize], world_range_index_counts: &HashMap<usize, u32>, + native_handedness_by_range: &HashMap<usize, i8>, + eye_world: [f32; 3], active_materials: &HashMap<usize, ActiveMaterialState>, renderer: &mut VulkanSmokeRenderer, ) -> Result<(), String> { @@ -5092,13 +5311,17 @@ impl StaticVulkanApp { renderer .set_draw_range_transparency(range_index, native_world_transparent(range, alpha)) .map_err(|error| format!("update world draw range {range_index}: {error}"))?; - let count = if alpha > 0.0 { - *world_range_index_counts - .get(&range_index) - .ok_or_else(|| "world draw range count is missing".to_string())? - } else { - 0 - }; + let original_index_count = *world_range_index_counts + .get(&range_index) + .ok_or_else(|| "world draw range count is missing".to_string())?; + let count = world_draw_range_index_count( + mesh, + range, + original_index_count, + alpha, + eye_world, + native_handedness_by_range.get(&range_index).copied(), + ); renderer .set_draw_range_index_count(range_index, count) .map_err(|error| format!("update world draw range {range_index} count: {error}"))?; @@ -5257,6 +5480,8 @@ impl StaticVulkanApp { &self.materials, &self.world_range_indices, &self.world_range_index_counts, + &self.native_handedness_by_range, + self.view_camera.position, &active_materials, renderer, )?; @@ -6229,10 +6454,13 @@ mod tests { } #[test] - fn native_sun_object_source_types_map_to_world_kinds() { + fn native_shadow_kind_mapping_keeps_sun_eligibility_separate_from_caster_defaults() { assert_eq!(native_shadow_kind(Some(PROTOTYPE_TYPE_FORT)), Some(3)); assert_eq!(native_shadow_kind(Some(PROTOTYPE_TYPE_BTLU)), Some(4)); assert_eq!(native_shadow_kind(Some(PROTOTYPE_TYPE_STAT)), Some(10)); + assert!(!native_shadow_caster_enabled(3)); + assert!(native_shadow_caster_enabled(4)); + assert!(!native_shadow_caster_enabled(10)); assert!(native_sun_object_kind_eligible(Some(PROTOTYPE_TYPE_FORT))); assert!(native_sun_object_kind_eligible(Some(PROTOTYPE_TYPE_BTLU))); assert!(native_sun_object_kind_eligible(Some(PROTOTYPE_TYPE_STAT))); @@ -7336,4 +7564,302 @@ mod tests { [uv[1], uv[2], uv[0], uv[3]] ); } + + #[test] + fn native_tl_sprite_pixels_use_current_viewport_and_preserve_depth_999() { + let uv = [[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]; + let small = screen_tl_quad_vertices_pixels( + [160.0, 120.0], + 40.0, + 30.0, + [640.0, 480.0], + [1.0; 3], + 1.0, + uv, + ); + let resized = screen_tl_quad_vertices_pixels( + [320.0, 240.0], + 80.0, + 60.0, + [1280.0, 960.0], + [1.0; 3], + 1.0, + uv, + ); + let wide = screen_tl_quad_vertices_pixels( + [640.0, 360.0], + 100.0, + 45.0, + [1280.0, 720.0], + [1.0; 3], + 1.0, + uv, + ); + + for (small, resized) in small.iter().zip(resized) { + for axis in 0..2 { + assert!((small.position[axis] - resized.position[axis]).abs() < 1.0e-6); + } + assert_eq!(small.position[2], 0.999); + assert_eq!(resized.position[2], 0.999); + } + assert_eq!(small[0].uv, [0.0, 1.0]); + assert_eq!(small[2].uv, [0.0, 0.0]); + for (actual, expected) in [ + (wide[0].position, [-0.15625, 0.125, 0.999]), + (wide[2].position, [-0.15625, -0.125, 0.999]), + ] { + for axis in 0..3 { + assert!((actual[axis] - expected[axis]).abs() < 1.0e-6); + } + } + } + + #[test] + fn native_tl_depth_999_is_nearer_than_opaque_geometry_at_320_units() { + let camera = FreeFlightCamera { + position: [0.0; 3], + yaw: 0.0, + pitch: 0.0, + horizontal_fov: 1.3, + near_plane: 0.5, + far_plane: 700.0, + move_speed: 1.0, + }; + let frame = camera.preview_frame(16.0 / 9.0); + let opaque_clip = row_vector_transform([320.0, 0.0, 0.0, 1.0], frame.clip_from_world); + let opaque_depth = opaque_clip[2] / opaque_clip[3]; + + assert!((opaque_depth - 0.999151_18).abs() < 1.0e-6); + assert!(0.999 < opaque_depth); + } + + fn facing_mesh( + batch_flags: u16, + positions: &[[f32; 3]], + ) -> (VulkanStaticMesh, VulkanStaticDrawRange) { + let range = VulkanStaticDrawRange { + first_index: 0, + index_count: u32::try_from(positions.len()).expect("fixture index count fits u32"), + material_index: 0, + lightmap_index: 0, + batch_flags, + pipeline_state: LegacyPipelineState::default(), + alpha_test_reference: VulkanStaticDrawRange::NATIVE_DEFAULT_ALPHA_TEST_REFERENCE, + }; + let mesh = VulkanStaticMesh { + vertices: positions + .iter() + .copied() + .map(|position| VulkanStaticVertex { + position, + color: [1.0; 3], + normal: [0.0, 0.0, 1.0], + uv: [0.0; 2], + detail_uv: [0.0; 2], + overlay_alpha: 0.0, + }) + .collect(), + indices: (0..positions.len()) + .map(|index| u32::try_from(index).expect("fixture index fits u32")) + .collect(), + draw_ranges: vec![range], + }; + (mesh, range) + } + + fn native_front_triangle() -> [[f32; 3]; 3] { + [[-0.2, -0.2, -1.0], [0.2, -0.2, -1.0], [0.0, 0.2, -1.0]] + } + + fn camera_face_triangle(camera: PreviewCameraFrame, mirror: bool) -> [[f32; 3]; 3] { + let center = add3(camera.position, scale3(camera.forward, 10.0)); + let mirror = if mirror { -1.0 } else { 1.0 }; + [ + add3( + add3(center, scale3(camera.right, -2.0 * mirror)), + scale3(camera.up, -2.0), + ), + add3( + add3(center, scale3(camera.right, 2.0 * mirror)), + scale3(camera.up, -2.0), + ), + add3(center, scale3(camera.up, 2.0)), + ] + } + + fn projected_vulkan_area(camera: PreviewCameraFrame, triangle: [[f32; 3]; 3]) -> f32 { + let screen = triangle.map(|point| { + let clip = + row_vector_transform([point[0], point[1], point[2], 1.0], camera.clip_from_world); + assert!(clip[3] > 0.0 && clip.iter().all(|value| value.is_finite())); + // This is the positive-height viewport transform used by the Vulkan renderer. + [ + (clip[0] / clip[3] + 1.0) * 0.5, + (clip[1] / clip[3] + 1.0) * 0.5, + ] + }); + let shoelace = (0..3) + .map(|index| { + let next = (index + 1) % 3; + screen[index][0] * screen[next][1] - screen[next][0] * screen[index][1] + }) + .sum::<f32>() + * 0.5; + // Vulkan's framebuffer signed-area definition is the negative shoelace area. + -shoelace + } + + #[test] + fn native_msh_cull_mode_matches_legacy_and_free_flight_camera_projection() { + let identity = RawCameraTransform { + words: [ + 1.0_f32.to_bits(), + 0.0_f32.to_bits(), + 0.0_f32.to_bits(), + 0.0_f32.to_bits(), + 0.0_f32.to_bits(), + 1.0_f32.to_bits(), + 0.0_f32.to_bits(), + 0.0_f32.to_bits(), + 0.0_f32.to_bits(), + 0.0_f32.to_bits(), + 1.0_f32.to_bits(), + 0.0_f32.to_bits(), + 0.0_f32.to_bits(), + 0.0_f32.to_bits(), + 0.0_f32.to_bits(), + 1.0_f32.to_bits(), + ], + }; + let projection = LegacyD3d7Projection { + viewport: [0, 0, 1280, 1024], + near_plane: 0.5, + far_plane: 700.0, + field_of_view_radians: 1.3, + }; + let legacy = PreviewCameraFrame::from_legacy_d3d7(identity, projection) + .expect("valid legacy camera projection"); + let free_flight = FreeFlightCamera { + position: legacy.position, + yaw: 0.0, + pitch: 0.0, + horizontal_fov: 1.3, + near_plane: 0.5, + far_plane: 700.0, + move_speed: 1.0, + } + .preview_frame(1280.0 / 1024.0); + + for camera in [legacy, free_flight] { + let front = camera_face_triangle(camera, false); + let back = [front[0], front[2], front[1]]; + let front_d = dot3( + sub3(camera.position, front[0]), + cross3(sub3(front[1], front[0]), sub3(front[2], front[0])), + ); + let back_d = dot3( + sub3(camera.position, back[0]), + cross3(sub3(back[1], back[0]), sub3(back[2], back[0])), + ); + assert!(front_d > 0.0); + assert!(back_d < 0.0); + + let front_vk_area = projected_vulkan_area(camera, front); + let back_vk_area = projected_vulkan_area(camera, back); + // Vulkan CLOCKWISE front faces have negative framebuffer area. + // The native-visible d>0 face is therefore a Vulkan back face. + assert!(front_vk_area > 0.0, "front area: {front_vk_area}"); + assert!(back_vk_area < 0.0, "back area: {back_vk_area}"); + assert_eq!(native_msh_cull_mode(0, 1), LegacyCullMode::FrontFace); + + let mirrored_front = camera_face_triangle(camera, true); + let mirrored_d = dot3( + sub3(camera.position, mirrored_front[0]), + cross3( + sub3(mirrored_front[1], mirrored_front[0]), + sub3(mirrored_front[2], mirrored_front[0]), + ), + ); + assert!(mirrored_d < 0.0); + assert!(projected_vulkan_area(camera, mirrored_front) < 0.0); + assert_eq!(native_msh_cull_mode(0, -1), LegacyCullMode::BackFace); + } + } + + #[test] + fn native_msh_handedness_uses_full_placement_transform_and_handles_singular_scale() { + let rotated = LegacyIron3dEulerTransform { + translation: [8.0, -4.0, 3.0], + orientation_radians: [0.4, -0.7, 1.2], + }; + assert_eq!(native_msh_handedness(rotated, [2.0, 3.0, 4.0]), 1); + assert_eq!(native_msh_handedness(rotated, [-2.0, 3.0, 4.0]), -1); + assert_eq!(native_msh_handedness(rotated, [0.0, 3.0, 4.0]), 0); + assert_eq!(native_msh_cull_mode(0, 0), LegacyCullMode::Disabled); + } + + #[test] + fn native_msh_4000_samples_only_first_triangle_and_raw_two_sided_bypasses_it() { + let front = native_front_triangle(); + let back = front.map(|point| [-point[0], point[1], point[2]]); + let front_then_back = [front[0], front[1], front[2], back[0], back[1], back[2]]; + let (mesh, range) = facing_mesh(0x4000, &front_then_back); + assert!(native_msh_first_triangle_visible( + &mesh, &range, [0.0; 3], 1 + )); + + let back_then_front = [back[0], back[1], back[2], front[0], front[1], front[2]]; + let (mesh, range) = facing_mesh(0x4000, &back_then_front); + assert!(!native_msh_first_triangle_visible( + &mesh, &range, [0.0; 3], 1 + )); + + let (mesh, range) = facing_mesh(0x4002, &back_then_front); + assert!(native_msh_first_triangle_visible( + &mesh, &range, [0.0; 3], 1 + )); + assert_eq!( + native_msh_cull_mode(range.batch_flags, 1), + LegacyCullMode::Disabled + ); + } + + #[test] + fn world_draw_range_count_restores_after_alpha_and_msh_facing_changes() { + let triangle = native_front_triangle(); + let (mesh, range) = facing_mesh(0x4000, &triangle); + let original_count = range.index_count; + + assert_eq!( + world_draw_range_index_count(&mesh, &range, original_count, 0.0, [0.0; 3], Some(1)), + 0 + ); + assert_eq!( + world_draw_range_index_count( + &mesh, + &range, + original_count, + 1.0, + [0.0, 0.0, -2.0], + Some(1), + ), + 0 + ); + assert_eq!( + world_draw_range_index_count(&mesh, &range, original_count, 1.0, [0.0; 3], Some(1)), + original_count + ); + assert_eq!( + world_draw_range_index_count( + &mesh, + &range, + original_count, + 1.0, + [0.0, 0.0, -2.0], + None, + ), + original_count + ); + } } |
