feat(render): diagnose captured camera clipping

This commit is contained in:
2026-07-18 15:50:52 +04:00
parent 2a793f1854
commit fea984ddf5
4 changed files with 170 additions and 7 deletions
@@ -5,6 +5,9 @@ use fparkan_msh::ModelAsset;
use fparkan_render::{LegacyIron3dEulerTransform, LegacyPipelineState};
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.
#[derive(Clone, Debug, Eq, PartialEq)]
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> {
let mut indices = Vec::with_capacity(
terrain
@@ -699,6 +727,36 @@ mod tests {
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 {
TerrainFace28 {
flags: FullSurfaceMask(0),
+3 -3
View File
@@ -40,9 +40,9 @@ mod swapchain_resources;
mod validation;
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_in_xy_frame, project_msh_to_static_mesh,
project_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_world_space, project_land_msh_to_static_mesh_in_xy_frame,
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_xy_frame, VulkanAssetMeshError, VulkanStaticXyFrame,
};