feat(vulkan): draw original terrain geometry
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Test / Lint (push) Failing after 2m11s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 08:46:16 +04:00
parent 4feada1bf4
commit b1408b0907
9 changed files with 221 additions and 13 deletions
@@ -13,6 +13,7 @@ fparkan-binary = { path = "../../crates/fparkan-binary", version = "0.1.0" }
fparkan-msh = { path = "../../crates/fparkan-msh", version = "0.1.0" }
fparkan-platform = { path = "../../crates/fparkan-platform", version = "0.1.0" }
fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
fparkan-terrain-format = { path = "../../crates/fparkan-terrain-format", version = "0.1.0" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@@ -3,6 +3,7 @@
use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex};
use fparkan_msh::ModelAsset;
use fparkan_render::LegacyPipelineState;
use fparkan_terrain_format::LandMeshDocument;
/// Error returned when a validated MSH cannot enter the current static GPU path.
#[derive(Clone, Debug, Eq, PartialEq)]
@@ -132,10 +133,108 @@ pub fn project_msh_to_static_mesh(
})
}
/// Projects validated `Land.msh` terrain geometry into the static Vulkan path.
///
/// This bridge preserves the source triangle order from `TerrainFace28` and
/// consumes its validated position and UV0 streams. It is deliberately a
/// geometry-only terrain slice: source slot/material selection, camera
/// transforms, fog and surface-mask shading need their own evidence before
/// they can be modeled as renderer state.
///
/// # Errors
///
/// Returns [`VulkanAssetMeshError`] if the terrain has no triangles, contains
/// non-finite positions, has no usable XZ extent, or references data outside
/// the current static Vulkan input contract.
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);
}
if indices.is_empty() {
return Err(VulkanAssetMeshError::EmptyGeometry);
}
let (min_x, max_x, min_z, max_z) = static_mesh_xz_bounds(&terrain.positions, &indices)?;
let extent = (max_x - min_x).max(max_z - min_z);
if !extent.is_finite() || extent <= f32::EPSILON {
return Err(VulkanAssetMeshError::DegenerateViewExtent);
}
let center_x = (min_x + max_x) * 0.5;
let center_z = (min_z + max_z) * 0.5;
let scale = 1.6 / extent;
let vertices = terrain
.positions
.iter()
.enumerate()
.map(|(index, position)| VulkanStaticVertex {
position: [
(position[0] - center_x) * scale,
(position[2] - center_z) * scale,
],
color: [0.31, 0.58, 0.27],
uv: terrain.uv0.get(index).map_or(
[
(position[0] - center_x) / extent + 0.5,
(position[2] - center_z) / extent + 0.5,
],
|uv| [f32::from(uv[0]) / 1024.0, f32::from(uv[1]) / 1024.0],
),
})
.collect();
Ok(VulkanStaticMesh {
vertices,
indices,
draw_ranges: vec![VulkanStaticDrawRange {
first_index: 0,
index_count: u32::try_from(terrain.faces.len())
.map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?
.checked_mul(3)
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?,
material_index: 0,
pipeline_state: LegacyPipelineState::default(),
alpha_test_reference: 0,
}],
})
}
fn static_mesh_xz_bounds(
positions: &[[f32; 3]],
indices: &[u16],
) -> Result<(f32, f32, f32, f32), VulkanAssetMeshError> {
let mut min_x = f32::INFINITY;
let mut max_x = f32::NEG_INFINITY;
let mut min_z = f32::INFINITY;
let mut max_z = f32::NEG_INFINITY;
for &index in indices {
let position = positions
.get(usize::from(index))
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
if !position.iter().all(|value| value.is_finite()) {
return Err(VulkanAssetMeshError::NonFinitePosition);
}
min_x = min_x.min(position[0]);
max_x = max_x.max(position[0]);
min_z = min_z.min(position[2]);
max_z = max_z.max(position[2]);
}
Ok((min_x, max_x, min_z, max_z))
}
#[cfg(test)]
mod tests {
use super::*;
use fparkan_msh::{Batch, ModelAsset};
use fparkan_terrain_format::{FullSurfaceMask, TerrainFace28, TerrainSlotTable};
fn model(positions: Vec<[f32; 3]>, indices: Vec<u16>, batches: Vec<Batch>) -> ModelAsset {
ModelAsset {
@@ -247,4 +346,50 @@ mod tests {
]
);
}
#[test]
fn projects_land_faces_in_source_order_with_packed_uv0() {
let terrain = LandMeshDocument {
streams: Vec::new(),
nodes_raw: Vec::new(),
slots: TerrainSlotTable {
header_raw: Vec::new(),
slots_raw: Vec::new(),
},
positions: vec![
[-2.0, 5.0, -1.0],
[2.0, 3.0, -1.0],
[-2.0, 9.0, 3.0],
[2.0, 1.0, 3.0],
],
normals: Vec::new(),
uv0: vec![[1024, -512], [0, 2048], [-1024, 512], [512, 0]],
accelerator: Vec::new(),
aux14: Vec::new(),
aux18: Vec::new(),
faces: vec![terrain_face([0, 1, 2]), terrain_face([1, 3, 2])],
};
let mesh = project_land_msh_to_static_mesh(&terrain).expect("representable terrain");
assert_eq!(mesh.indices, vec![0, 1, 2, 1, 3, 2]);
assert_eq!(mesh.draw_ranges.len(), 1);
assert_eq!(mesh.draw_ranges[0].index_count, 6);
assert_eq!(mesh.vertices[0].position, [-0.8, -0.8]);
assert_eq!(mesh.vertices[3].position, [0.8, 0.8]);
assert_eq!(mesh.vertices[0].uv, [1.0, -0.5]);
assert_eq!(mesh.vertices[2].uv, [-1.0, 0.5]);
}
fn terrain_face(vertices: [u16; 3]) -> TerrainFace28 {
TerrainFace28 {
flags: FullSurfaceMask(0),
material_tag: 0,
aux_tag: 0,
vertices,
neighbors: [None; 3],
tail_raw: [0; 8],
raw: [0; 28],
}
}
}
+3 -1
View File
@@ -39,7 +39,9 @@ mod swapchain;
mod swapchain_resources;
mod validation;
pub use self::asset_mesh::{project_msh_to_static_mesh, VulkanAssetMeshError};
pub use self::asset_mesh::{
project_land_msh_to_static_mesh, project_msh_to_static_mesh, VulkanAssetMeshError,
};
pub use self::capabilities::{
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
VulkanRuntimeCapabilityError, VulkanRuntimeCapabilityProbe,