feat(render): diagnose captured camera clipping
This commit is contained in:
@@ -5,6 +5,9 @@ use fparkan_msh::ModelAsset;
|
|||||||
use fparkan_render::{LegacyIron3dEulerTransform, LegacyPipelineState};
|
use fparkan_render::{LegacyIron3dEulerTransform, LegacyPipelineState};
|
||||||
use fparkan_terrain_format::LandMeshDocument;
|
use fparkan_terrain_format::LandMeshDocument;
|
||||||
|
|
||||||
|
/// Legacy `Land.msh` stored-height to mission-world-height scale.
|
||||||
|
const LEGACY_LAND_HEIGHT_SCALE: f32 = 1.0 / 32.0;
|
||||||
|
|
||||||
/// Error returned when a validated MSH cannot enter the current static GPU path.
|
/// Error returned when a validated MSH cannot enter the current static GPU path.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum VulkanAssetMeshError {
|
pub enum VulkanAssetMeshError {
|
||||||
@@ -383,6 +386,31 @@ pub fn project_land_msh_to_static_mesh_in_world_space(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Projects terrain into the mission space consumed by the legacy D3D7 camera.
|
||||||
|
///
|
||||||
|
/// `Land.msh` stores horizontal coordinates directly, while its height stream
|
||||||
|
/// uses 1/32 units. The scale is evidenced by the GOG `AutoDemo` map: raw terrain
|
||||||
|
/// heights `95.29412..288.23532` become `2.978..9.007`, matching its placed
|
||||||
|
/// object heights and the live Ngi32 camera's world-space Z coordinate.
|
||||||
|
///
|
||||||
|
/// This conversion is intentionally confined to the captured legacy-camera
|
||||||
|
/// path. [`project_land_msh_to_static_mesh_in_world_space`] remains the raw
|
||||||
|
/// source-coordinate bridge for format inspection.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`VulkanAssetMeshError`] when the terrain cannot enter the static
|
||||||
|
/// Vulkan input contract.
|
||||||
|
pub fn project_land_msh_to_static_mesh_in_legacy_world_space(
|
||||||
|
terrain: &LandMeshDocument,
|
||||||
|
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
|
||||||
|
let mut mesh = project_land_msh_to_static_mesh_in_world_space(terrain)?;
|
||||||
|
for vertex in &mut mesh.vertices {
|
||||||
|
vertex.position[2] *= LEGACY_LAND_HEIGHT_SCALE;
|
||||||
|
}
|
||||||
|
Ok(mesh)
|
||||||
|
}
|
||||||
|
|
||||||
fn static_terrain_indices(terrain: &LandMeshDocument) -> Result<Vec<u32>, VulkanAssetMeshError> {
|
fn static_terrain_indices(terrain: &LandMeshDocument) -> Result<Vec<u32>, VulkanAssetMeshError> {
|
||||||
let mut indices = Vec::with_capacity(
|
let mut indices = Vec::with_capacity(
|
||||||
terrain
|
terrain
|
||||||
@@ -699,6 +727,36 @@ mod tests {
|
|||||||
assert_eq!(mesh.vertices[0].uv, [1.0, -0.5]);
|
assert_eq!(mesh.vertices[0].uv, [1.0, -0.5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn legacy_world_space_terrain_scales_only_stored_height() {
|
||||||
|
let terrain = LandMeshDocument {
|
||||||
|
streams: Vec::new(),
|
||||||
|
nodes_raw: Vec::new(),
|
||||||
|
slots: TerrainSlotTable {
|
||||||
|
header_raw: Vec::new(),
|
||||||
|
slots_raw: Vec::new(),
|
||||||
|
},
|
||||||
|
positions: vec![
|
||||||
|
[100.0, 200.0, 96.0],
|
||||||
|
[300.0, 400.0, 288.0],
|
||||||
|
[500.0, 600.0, 192.0],
|
||||||
|
],
|
||||||
|
normals: Vec::new(),
|
||||||
|
uv0: vec![[0, 0]; 3],
|
||||||
|
accelerator: Vec::new(),
|
||||||
|
aux14: Vec::new(),
|
||||||
|
aux18: Vec::new(),
|
||||||
|
faces: vec![terrain_face([0, 1, 2])],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mesh = project_land_msh_to_static_mesh_in_legacy_world_space(&terrain)
|
||||||
|
.expect("representable terrain");
|
||||||
|
|
||||||
|
assert_eq!(mesh.vertices[0].position, [100.0, 200.0, 3.0]);
|
||||||
|
assert_eq!(mesh.vertices[1].position, [300.0, 400.0, 9.0]);
|
||||||
|
assert_eq!(mesh.vertices[2].position, [500.0, 600.0, 6.0]);
|
||||||
|
}
|
||||||
|
|
||||||
fn terrain_face(vertices: [u16; 3]) -> TerrainFace28 {
|
fn terrain_face(vertices: [u16; 3]) -> TerrainFace28 {
|
||||||
TerrainFace28 {
|
TerrainFace28 {
|
||||||
flags: FullSurfaceMask(0),
|
flags: FullSurfaceMask(0),
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ mod swapchain_resources;
|
|||||||
mod validation;
|
mod validation;
|
||||||
|
|
||||||
pub use self::asset_mesh::{
|
pub use self::asset_mesh::{
|
||||||
project_land_msh_to_static_mesh, project_land_msh_to_static_mesh_in_world_space,
|
project_land_msh_to_static_mesh, project_land_msh_to_static_mesh_in_legacy_world_space,
|
||||||
project_land_msh_to_static_mesh_in_xy_frame, project_msh_to_static_mesh,
|
project_land_msh_to_static_mesh_in_world_space, project_land_msh_to_static_mesh_in_xy_frame,
|
||||||
project_msh_to_static_mesh_in_world_space,
|
project_msh_to_static_mesh, project_msh_to_static_mesh_in_world_space,
|
||||||
project_msh_to_static_mesh_in_world_space_with_transform,
|
project_msh_to_static_mesh_in_world_space_with_transform,
|
||||||
project_msh_to_static_mesh_in_xy_frame, VulkanAssetMeshError, VulkanStaticXyFrame,
|
project_msh_to_static_mesh_in_xy_frame, VulkanAssetMeshError, VulkanStaticXyFrame,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ use fparkan_render::{
|
|||||||
RenderSnapshotDraw,
|
RenderSnapshotDraw,
|
||||||
};
|
};
|
||||||
use fparkan_render_vulkan::{
|
use fparkan_render_vulkan::{
|
||||||
project_land_msh_to_static_mesh_in_world_space, project_land_msh_to_static_mesh_in_xy_frame,
|
project_land_msh_to_static_mesh_in_legacy_world_space,
|
||||||
|
project_land_msh_to_static_mesh_in_xy_frame,
|
||||||
project_msh_to_static_mesh_in_world_space_with_transform,
|
project_msh_to_static_mesh_in_world_space_with_transform,
|
||||||
project_msh_to_static_mesh_in_xy_frame, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
|
project_msh_to_static_mesh_in_xy_frame, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
|
||||||
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticCamera, VulkanStaticMaterial,
|
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticCamera, VulkanStaticMaterial,
|
||||||
@@ -226,6 +227,7 @@ struct StaticPreviewScene {
|
|||||||
materials: Vec<VulkanStaticMaterial>,
|
materials: Vec<VulkanStaticMaterial>,
|
||||||
camera: VulkanStaticCamera,
|
camera: VulkanStaticCamera,
|
||||||
camera_mode: &'static str,
|
camera_mode: &'static str,
|
||||||
|
clip_visible_vertices: usize,
|
||||||
mesh_components: usize,
|
mesh_components: usize,
|
||||||
terrain_components: usize,
|
terrain_components: usize,
|
||||||
}
|
}
|
||||||
@@ -264,7 +266,7 @@ fn static_preview_mesh_and_materials(
|
|||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
let terrain_component = if legacy_camera.is_some() {
|
let terrain_component = if legacy_camera.is_some() {
|
||||||
project_land_msh_to_static_mesh_in_world_space(terrain_mesh)
|
project_land_msh_to_static_mesh_in_legacy_world_space(terrain_mesh)
|
||||||
} else {
|
} else {
|
||||||
let frame = xy_frame.ok_or_else(|| "missing diagnostic XY frame".to_string())?;
|
let frame = xy_frame.ok_or_else(|| "missing diagnostic XY frame".to_string())?;
|
||||||
project_land_msh_to_static_mesh_in_xy_frame(terrain_mesh, frame)
|
project_land_msh_to_static_mesh_in_xy_frame(terrain_mesh, frame)
|
||||||
@@ -313,10 +315,12 @@ fn static_preview_mesh_and_materials(
|
|||||||
if mesh_components == 0 {
|
if mesh_components == 0 {
|
||||||
return Err("selected static preview roots have no mesh-backed visual".to_string());
|
return Err("selected static preview roots have no mesh-backed visual".to_string());
|
||||||
}
|
}
|
||||||
|
let camera = legacy_camera.unwrap_or_default();
|
||||||
Ok(StaticPreviewScene {
|
Ok(StaticPreviewScene {
|
||||||
|
clip_visible_vertices: static_preview_clip_visible_vertices(&mesh, camera),
|
||||||
mesh,
|
mesh,
|
||||||
materials,
|
materials,
|
||||||
camera: legacy_camera.unwrap_or_default(),
|
camera,
|
||||||
camera_mode: if legacy_camera.is_some() {
|
camera_mode: if legacy_camera.is_some() {
|
||||||
"legacy-d3d7-capture"
|
"legacy-d3d7-capture"
|
||||||
} else {
|
} else {
|
||||||
@@ -327,6 +331,46 @@ fn static_preview_mesh_and_materials(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Counts source vertices inside the Vulkan clip volume for a static preview.
|
||||||
|
///
|
||||||
|
/// The static vertex shader receives row-major D3D7 matrix data as a GLSL
|
||||||
|
/// column-major `mat4`; its multiplication is consequently equivalent to this
|
||||||
|
/// row-vector calculation. This remains CPU-only diagnostic evidence and does
|
||||||
|
/// not change the renderer's clipping rules.
|
||||||
|
fn static_preview_clip_visible_vertices(
|
||||||
|
mesh: &VulkanStaticMesh,
|
||||||
|
camera: VulkanStaticCamera,
|
||||||
|
) -> usize {
|
||||||
|
mesh.vertices
|
||||||
|
.iter()
|
||||||
|
.filter(|vertex| {
|
||||||
|
let [x, y, z] = vertex.position;
|
||||||
|
let matrix = camera.clip_from_world;
|
||||||
|
let clip_x = x.mul_add(
|
||||||
|
matrix[0],
|
||||||
|
y.mul_add(matrix[4], z.mul_add(matrix[8], matrix[12])),
|
||||||
|
);
|
||||||
|
let clip_y = x.mul_add(
|
||||||
|
matrix[1],
|
||||||
|
y.mul_add(matrix[5], z.mul_add(matrix[9], matrix[13])),
|
||||||
|
);
|
||||||
|
let clip_z = x.mul_add(
|
||||||
|
matrix[2],
|
||||||
|
y.mul_add(matrix[6], z.mul_add(matrix[10], matrix[14])),
|
||||||
|
);
|
||||||
|
let clip_w = x.mul_add(
|
||||||
|
matrix[3],
|
||||||
|
y.mul_add(matrix[7], z.mul_add(matrix[11], matrix[15])),
|
||||||
|
);
|
||||||
|
clip_w.is_finite()
|
||||||
|
&& clip_w > 0.0
|
||||||
|
&& (-clip_w..=clip_w).contains(&clip_x)
|
||||||
|
&& (-clip_w..=clip_w).contains(&clip_y)
|
||||||
|
&& (0.0..=clip_w).contains(&clip_z)
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
fn static_preview_xy_frame(
|
fn static_preview_xy_frame(
|
||||||
assets: &MissionAssets,
|
assets: &MissionAssets,
|
||||||
terrain: &TerrainWorld,
|
terrain: &TerrainWorld,
|
||||||
@@ -558,6 +602,7 @@ struct StaticVulkanApp {
|
|||||||
materials: Vec<VulkanStaticMaterial>,
|
materials: Vec<VulkanStaticMaterial>,
|
||||||
camera: VulkanStaticCamera,
|
camera: VulkanStaticCamera,
|
||||||
camera_mode: &'static str,
|
camera_mode: &'static str,
|
||||||
|
clip_visible_vertices: usize,
|
||||||
mesh_components: usize,
|
mesh_components: usize,
|
||||||
terrain_components: usize,
|
terrain_components: usize,
|
||||||
preview_roots: usize,
|
preview_roots: usize,
|
||||||
@@ -585,6 +630,7 @@ impl StaticVulkanApp {
|
|||||||
materials: preview.materials,
|
materials: preview.materials,
|
||||||
camera: preview.camera,
|
camera: preview.camera,
|
||||||
camera_mode: preview.camera_mode,
|
camera_mode: preview.camera_mode,
|
||||||
|
clip_visible_vertices: preview.clip_visible_vertices,
|
||||||
mesh_components: preview.mesh_components,
|
mesh_components: preview.mesh_components,
|
||||||
terrain_components: preview.terrain_components,
|
terrain_components: preview.terrain_components,
|
||||||
preview_roots,
|
preview_roots,
|
||||||
@@ -630,7 +676,7 @@ impl StaticVulkanApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.output = Some(format!(
|
self.output = Some(format!(
|
||||||
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"camera_mode\":{},\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"preview_roots\":{},\"mesh_components\":{},\"terrain_components\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_bytes\":{},\"readback_hash\":{}}}",
|
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"camera_mode\":{},\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"preview_roots\":{},\"mesh_components\":{},\"terrain_components\":{},\"clip_visible_vertices\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_bytes\":{},\"readback_hash\":{}}}",
|
||||||
json_string(self.camera_mode),
|
json_string(self.camera_mode),
|
||||||
json_string(&self.mission),
|
json_string(&self.mission),
|
||||||
self.object_count,
|
self.object_count,
|
||||||
@@ -638,6 +684,7 @@ impl StaticVulkanApp {
|
|||||||
self.preview_roots,
|
self.preview_roots,
|
||||||
self.mesh_components,
|
self.mesh_components,
|
||||||
self.terrain_components,
|
self.terrain_components,
|
||||||
|
self.clip_visible_vertices,
|
||||||
self.materials.len(),
|
self.materials.len(),
|
||||||
report.renderer_report.swapchain_extent.0,
|
report.renderer_report.swapchain_extent.0,
|
||||||
report.renderer_report.swapchain_extent.1,
|
report.renderer_report.swapchain_extent.1,
|
||||||
@@ -1061,6 +1108,40 @@ mod tests {
|
|||||||
values.iter().map(|value| (*value).to_string()).collect()
|
values.iter().map(|value| (*value).to_string()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn static_preview_clip_diagnostic_matches_vulkan_clip_bounds() {
|
||||||
|
let mesh = VulkanStaticMesh {
|
||||||
|
vertices: vec![
|
||||||
|
fparkan_render_vulkan::VulkanStaticVertex {
|
||||||
|
position: [0.0, 0.0, 0.0],
|
||||||
|
color: [0.0; 3],
|
||||||
|
uv: [0.0; 2],
|
||||||
|
},
|
||||||
|
fparkan_render_vulkan::VulkanStaticVertex {
|
||||||
|
position: [1.0, -1.0, 1.0],
|
||||||
|
color: [0.0; 3],
|
||||||
|
uv: [0.0; 2],
|
||||||
|
},
|
||||||
|
fparkan_render_vulkan::VulkanStaticVertex {
|
||||||
|
position: [1.01, 0.0, 0.0],
|
||||||
|
color: [0.0; 3],
|
||||||
|
uv: [0.0; 2],
|
||||||
|
},
|
||||||
|
fparkan_render_vulkan::VulkanStaticVertex {
|
||||||
|
position: [0.0, 0.0, -0.01],
|
||||||
|
color: [0.0; 3],
|
||||||
|
uv: [0.0; 2],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
indices: Vec::new(),
|
||||||
|
draw_ranges: Vec::new(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
static_preview_clip_visible_vertices(&mesh, VulkanStaticCamera::default()),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_required_args() {
|
fn parses_required_args() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -1561,3 +1561,27 @@ hashes промежуточных buffers. Без такой трассиров
|
|||||||
Связанные справочные страницы с таблицами форматов: [MSH](../reference/msh.md),
|
Связанные справочные страницы с таблицами форматов: [MSH](../reference/msh.md),
|
||||||
[materials](../reference/materials.md), [Texm](../reference/texm.md) и
|
[materials](../reference/materials.md), [Texm](../reference/texm.md) и
|
||||||
[render frame](../reference/render-frame.md).
|
[render frame](../reference/render-frame.md).
|
||||||
|
|
||||||
|
### Live AutoDemo: видимый результат пока не достигнут
|
||||||
|
|
||||||
|
Read-only `PrintWindow` capture работающего GOG AutoDemo даёт полноценный
|
||||||
|
оригинальный кадр (terrain, варбот и близкая geometry), поэтому теперь есть
|
||||||
|
безвводный source для будущего visual comparison. Текущий полный static-Vulkan
|
||||||
|
preview нельзя описывать как видимую сцену: хотя он загружает 8 TMA objects,
|
||||||
|
66 MSH components и 67 descriptors, его изображение состоит только из clear
|
||||||
|
color. Validation остаётся `0/0`, но это доказывает корректность GPU lifecycle,
|
||||||
|
а не совпадение с оригинальным renderer.
|
||||||
|
|
||||||
|
Новая CPU-диагностика применяет к тем же source vertices ту же row-major D3D7
|
||||||
|
matrix, которую GLSL получает как column-major push constant. Для свежего
|
||||||
|
captured Ngi32 camera (`viewport 1024x768`, near `5`, far `700`, FOV `1.3`)
|
||||||
|
она насчитала `clip_visible_vertices=0`. Следовательно, current empty frame не
|
||||||
|
объясняется texture selection, face culling (baseline state already disables
|
||||||
|
it) или Vulkan validation: следующий исследовательский шаг — восстановить
|
||||||
|
точный live camera transform/clip-space boundary.
|
||||||
|
|
||||||
|
Параллельно bridge для legacy-camera path теперь переводит только высоту
|
||||||
|
`Land.msh` в world units с масштабом `1/32`: raw AutoDemo heights
|
||||||
|
`95.29412..288.23532` становятся `2.978..9.007`, что согласуется с TMA Z и
|
||||||
|
live camera height. Это доказанное coordinate conversion, но оно само по себе
|
||||||
|
не сделало вершины видимыми и не является заявлением о pixel parity.
|
||||||
|
|||||||
Reference in New Issue
Block a user