This commit is contained in:
2 files changed
+432
-11
No files matched your search
+385
-11
@@ -32,8 +32,8 @@ use fparkan_path::ResourceName;
|
|||||||
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
use fparkan_platform_winit::{window_native_handles, WinitWindow, WinitWindowPlan};
|
||||||
use fparkan_prototype::{unit_component_tree, PROTOTYPE_TYPE_EXTO};
|
use fparkan_prototype::{unit_component_tree, PROTOTYPE_TYPE_EXTO};
|
||||||
use fparkan_render::{
|
use fparkan_render::{
|
||||||
LegacyBlendMode, LegacyD3d7Projection, LegacyDepthMode, LegacyIron3dEulerTransform,
|
LegacyBlendMode, LegacyCullMode, LegacyD3d7Projection, LegacyDepthMode,
|
||||||
LegacyPipelineState, RawCameraTransform,
|
LegacyIron3dEulerTransform, LegacyPipelineState, RawCameraTransform,
|
||||||
};
|
};
|
||||||
use fparkan_render_vulkan::{
|
use fparkan_render_vulkan::{
|
||||||
node38_pose, node38_pose_from_hierarchy, node38_pose_relative_to_root,
|
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
|
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(
|
fn configure_world_material_categories(
|
||||||
mesh: &mut VulkanStaticMesh,
|
mesh: &mut VulkanStaticMesh,
|
||||||
materials: &[VulkanStaticMaterial],
|
materials: &[VulkanStaticMaterial],
|
||||||
@@ -1993,6 +2100,7 @@ impl EnvironmentGpuScene {
|
|||||||
|
|
||||||
fn new(
|
fn new(
|
||||||
mesh: &mut VulkanStaticMesh,
|
mesh: &mut VulkanStaticMesh,
|
||||||
|
native_handedness_by_range: &mut HashMap<usize, i8>,
|
||||||
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>,
|
||||||
@@ -2412,6 +2520,14 @@ impl EnvironmentGpuScene {
|
|||||||
old_order.extend(sprite_indices.iter().copied());
|
old_order.extend(sprite_indices.iter().copied());
|
||||||
old_order.extend([rain_index, snow_index, lightning_index]);
|
old_order.extend([rain_index, snow_index, lightning_index]);
|
||||||
let remap = reorder_draw_ranges(mesh, &old_order)?;
|
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 {
|
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)
|
||||||
@@ -3657,6 +3773,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>,
|
||||||
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>,
|
||||||
@@ -3868,6 +3985,7 @@ fn static_preview_mesh_and_materials(
|
|||||||
indices: Vec::new(),
|
indices: Vec::new(),
|
||||||
draw_ranges: Vec::new(),
|
draw_ranges: Vec::new(),
|
||||||
};
|
};
|
||||||
|
let mut native_handedness_by_range = HashMap::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();
|
||||||
@@ -4066,7 +4184,7 @@ fn static_preview_mesh_and_materials(
|
|||||||
};
|
};
|
||||||
if should_render {
|
if should_render {
|
||||||
let pose_buffer = component_pose_buffers[component_index].as_ref();
|
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), _) => {
|
(Some(poses), Some(mount), _) => {
|
||||||
project_msh_to_static_mesh_in_world_space_with_node_pose_buffer_and_mount(
|
project_msh_to_static_mesh_in_world_space_with_node_pose_buffer_and_mount(
|
||||||
&model.validated,
|
&model.validated,
|
||||||
@@ -4118,6 +4236,11 @@ fn static_preview_mesh_and_materials(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
|
.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);
|
let component_shadow_triangles = static_mesh_shadow_triangles(&component);
|
||||||
if root_native_kind.is_some() {
|
if root_native_kind.is_some() {
|
||||||
root_shadow_triangles.extend(component_shadow_triangles);
|
root_shadow_triangles.extend(component_shadow_triangles);
|
||||||
@@ -4156,6 +4279,9 @@ fn static_preview_mesh_and_materials(
|
|||||||
&lightmap_textures,
|
&lightmap_textures,
|
||||||
Some(&mut material_animations),
|
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 {
|
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());
|
||||||
}
|
}
|
||||||
@@ -4203,6 +4329,7 @@ fn static_preview_mesh_and_materials(
|
|||||||
let shadow_material_index = append_shadow_material(&mut materials)?;
|
let shadow_material_index = append_shadow_material(&mut materials)?;
|
||||||
Ok(StaticPreviewScene {
|
Ok(StaticPreviewScene {
|
||||||
mesh,
|
mesh,
|
||||||
|
native_handedness_by_range,
|
||||||
materials,
|
materials,
|
||||||
material_animations,
|
material_animations,
|
||||||
sun_occlusion_range_indices,
|
sun_occlusion_range_indices,
|
||||||
@@ -4928,6 +5055,7 @@ fn run_static_vulkan_mode(
|
|||||||
let sky_mesh = environment.sky.mesh().clone();
|
let sky_mesh = environment.sky.mesh().clone();
|
||||||
Some(EnvironmentGpuScene::new(
|
Some(EnvironmentGpuScene::new(
|
||||||
&mut preview.mesh,
|
&mut preview.mesh,
|
||||||
|
&mut preview.native_handedness_by_range,
|
||||||
&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,
|
||||||
@@ -4965,6 +5093,7 @@ fn run_static_vulkan_mode(
|
|||||||
|
|
||||||
struct StaticVulkanApp {
|
struct StaticVulkanApp {
|
||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
|
native_handedness_by_range: HashMap<usize, i8>,
|
||||||
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>,
|
||||||
@@ -5045,6 +5174,7 @@ impl StaticVulkanApp {
|
|||||||
.collect();
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
mesh: preview.mesh,
|
mesh: preview.mesh,
|
||||||
|
native_handedness_by_range: preview.native_handedness_by_range,
|
||||||
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,
|
||||||
@@ -5123,7 +5253,14 @@ impl StaticVulkanApp {
|
|||||||
renderer
|
renderer
|
||||||
.set_draw_range_index_count(
|
.set_draw_range_index_count(
|
||||||
range_index,
|
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| {
|
.map_err(|error| {
|
||||||
format!("configure world draw range {range_index} count: {error}")
|
format!("configure world draw range {range_index} count: {error}")
|
||||||
@@ -5137,6 +5274,8 @@ impl StaticVulkanApp {
|
|||||||
materials: &[VulkanStaticMaterial],
|
materials: &[VulkanStaticMaterial],
|
||||||
world_range_indices: &[usize],
|
world_range_indices: &[usize],
|
||||||
world_range_index_counts: &HashMap<usize, u32>,
|
world_range_index_counts: &HashMap<usize, u32>,
|
||||||
|
native_handedness_by_range: &HashMap<usize, i8>,
|
||||||
|
eye_world: [f32; 3],
|
||||||
active_materials: &HashMap<usize, ActiveMaterialState>,
|
active_materials: &HashMap<usize, ActiveMaterialState>,
|
||||||
renderer: &mut VulkanSmokeRenderer,
|
renderer: &mut VulkanSmokeRenderer,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -5157,13 +5296,17 @@ impl StaticVulkanApp {
|
|||||||
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}"))?;
|
||||||
let count = if alpha > 0.0 {
|
let original_index_count = *world_range_index_counts
|
||||||
*world_range_index_counts
|
.get(&range_index)
|
||||||
.get(&range_index)
|
.ok_or_else(|| "world draw range count is missing".to_string())?;
|
||||||
.ok_or_else(|| "world draw range count is missing".to_string())?
|
let count = world_draw_range_index_count(
|
||||||
} else {
|
mesh,
|
||||||
0
|
range,
|
||||||
};
|
original_index_count,
|
||||||
|
alpha,
|
||||||
|
eye_world,
|
||||||
|
native_handedness_by_range.get(&range_index).copied(),
|
||||||
|
);
|
||||||
renderer
|
renderer
|
||||||
.set_draw_range_index_count(range_index, count)
|
.set_draw_range_index_count(range_index, count)
|
||||||
.map_err(|error| format!("update world draw range {range_index} count: {error}"))?;
|
.map_err(|error| format!("update world draw range {range_index} count: {error}"))?;
|
||||||
@@ -5322,6 +5465,8 @@ impl StaticVulkanApp {
|
|||||||
&self.materials,
|
&self.materials,
|
||||||
&self.world_range_indices,
|
&self.world_range_indices,
|
||||||
&self.world_range_index_counts,
|
&self.world_range_index_counts,
|
||||||
|
&self.native_handedness_by_range,
|
||||||
|
self.view_camera.position,
|
||||||
&active_materials,
|
&active_materials,
|
||||||
renderer,
|
renderer,
|
||||||
)?;
|
)?;
|
||||||
@@ -7470,4 +7615,233 @@ mod tests {
|
|||||||
assert!((opaque_depth - 0.999151_18).abs() < 1.0e-6);
|
assert!((opaque_depth - 0.999151_18).abs() < 1.0e-6);
|
||||||
assert!(0.999 < opaque_depth);
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -153,3 +153,50 @@ Byte `material_index` выбирает строку WEAR; high word at `+0x02`
|
|||||||
`index_start + index_count` остаётся внутри type 6, а каждый использованный
|
`index_start + index_count` остаётся внутри type 6, а каждый использованный
|
||||||
индекс через `base_vertex` попадает в position stream. `vertex_count` и
|
индекс через `base_vertex` попадает в position stream. `vertex_count` и
|
||||||
`local_batch_index` доступны renderer для исходного draw contract.
|
`local_batch_index` доступны renderer для исходного draw contract.
|
||||||
|
|
||||||
|
## Сторона грани в цветовом проходе
|
||||||
|
|
||||||
|
В цветном проходе `Terrain.dll`, функция `0x100296F0`, сравнивает камеру с
|
||||||
|
каждой гранью по знаку тройного произведения:
|
||||||
|
|
||||||
|
```c
|
||||||
|
d = dot(eye - v0, cross(v1 - v0, v2 - v0));
|
||||||
|
```
|
||||||
|
|
||||||
|
Проход оставляет грань только при `d > 0`. Raw `Batch20.flags` задаёт два
|
||||||
|
исключения: `0x0002` пропускает программную проверку стороны, поэтому обратная
|
||||||
|
грань не отбрасывается этим условием. С `0x4000` проверяется только первая
|
||||||
|
грань записи: если `d <= 0`, цикл её граней прекращается; если `d > 0`,
|
||||||
|
оставшиеся грани этой записи уже не проходят повторную программную проверку.
|
||||||
|
`Terrain.dll!0x100296F0` также настраивает пакет этого цветного пути: flags по
|
||||||
|
адресу `packet+0x128` равны `0x404` или `0x414`. Путь отправки в
|
||||||
|
`Terrain.dll!0x1001D660` перед вызовом renderer vtable `+0x48` очищает только
|
||||||
|
маску `0x1`; `Ngi32.dll!0x10007470` проверяет маску `0x4` этого аргумента. При
|
||||||
|
установленной маске render state 22 получает `D3DCULL_NONE`, иначе —
|
||||||
|
`D3DCULL_CW`. Оба значения CShade сохраняют маску `0x4`, поэтому в этом
|
||||||
|
цветном пути аппаратное отсечение выключено, а набор обычных граней определяет
|
||||||
|
программный тест `d > 0`. Raw-флаги `0x0002` и `0x4000` меняют этот тест, как
|
||||||
|
описано выше.
|
||||||
|
|
||||||
|
Статический Vulkan preview сохраняет каждую исходную запись `Batch20` одним
|
||||||
|
диапазоном рисования:
|
||||||
|
`append_static_preview_component_with_lightmaps` добавляет исходные индексы без
|
||||||
|
перестановки, а в диапазон переносит `index_count` и исходный `first_index` с
|
||||||
|
общим смещением компонента. Поэтому `0x4000` выбирает ту же первую грань, даже
|
||||||
|
если диапазон получил другой материал для lightmap.
|
||||||
|
|
||||||
|
Поскольку вершины preview переведены в мировые координаты, знак `d` для MSH
|
||||||
|
диапазона корректируется знаком детерминанта полной матрицы размещения. Повороты
|
||||||
|
узлов и mounts — единичные кватернионы с determinant `+1`; отрицательный object
|
||||||
|
scale меняет знак. Renderer Vulkan задаёт `front_face=CLOCKWISE` и viewport с
|
||||||
|
положительной высотой. Для используемых матриц legacy и free-flight preview
|
||||||
|
при положительном determinant native-visible `d > 0` относится к Vulkan
|
||||||
|
back-face; поэтому preview отсекает `FrontFace`. При отрицательном determinant
|
||||||
|
режим меняется на `BackFace`.
|
||||||
|
Для `0x0002` и `0x4000` аппаратное отсечение выключено, чтобы сохранить их
|
||||||
|
программную политику. Отклонённый `0x4000` диапазон получает число индексов 0;
|
||||||
|
каждый кадр это число заново вычисляется из исходного значения, поэтому после
|
||||||
|
изменения положения камеры или opacity материала диапазон может вернуться в
|
||||||
|
draw queue. Проверка первой грани
|
||||||
|
применяется только к MSH-диапазонам цветовой мировой очереди; terrain и
|
||||||
|
shadow/sun запросы её не используют.
|
||||||
Reference in new issue
Block a user