aboutsummaryrefslogtreecommitdiff
path: root/adapters/fparkan-render-vulkan
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 09:21:43 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 09:21:43 +0300
commit8ac6a11100cfe41a8a139f9b6d19c4e82a3bf1c5 (patch)
tree6243ceed21daa0d68dbd1a50932a63cbb094d5b3 /adapters/fparkan-render-vulkan
parent46851518fe1fde39d81ad4cf6805aa404f05016c (diff)
downloadfparkan-8ac6a11100cfe41a8a139f9b6d19c4e82a3bf1c5.tar.xz
fparkan-8ac6a11100cfe41a8a139f9b6d19c4e82a3bf1c5.zip
feat(render): compose terrain with preview root
Diffstat (limited to 'adapters/fparkan-render-vulkan')
-rw-r--r--adapters/fparkan-render-vulkan/src/asset_mesh.rs275
-rw-r--r--adapters/fparkan-render-vulkan/src/ffi.rs4
2 files changed, 186 insertions, 93 deletions
diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
index e7bfff4..28d8567 100644
--- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs
+++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs
@@ -18,6 +18,58 @@ pub enum VulkanAssetMeshError {
IndexOutOfRange,
}
+/// Shared XZ frame for a deliberately top-down diagnostic static scene.
+///
+/// It is a CPU-side viewer transform, not evidence of the original camera or
+/// object transform convention. Keeping it explicit prevents separately
+/// normalized terrain and model components from being incorrectly overlaid.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct VulkanStaticXzFrame {
+ center_x: f32,
+ center_z: f32,
+ extent: f32,
+}
+
+impl VulkanStaticXzFrame {
+ /// Builds a frame that maps the supplied XZ bounds into the static viewer.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`VulkanAssetMeshError::DegenerateViewExtent`] for non-finite
+ /// or zero-sized bounds.
+ pub fn from_bounds(
+ min_x: f32,
+ max_x: f32,
+ min_z: f32,
+ max_z: f32,
+ ) -> Result<Self, VulkanAssetMeshError> {
+ let extent = (max_x - min_x).max(max_z - min_z);
+ if !extent.is_finite() || extent <= f32::EPSILON {
+ return Err(VulkanAssetMeshError::DegenerateViewExtent);
+ }
+ Ok(Self {
+ center_x: (min_x + max_x) * 0.5,
+ center_z: (min_z + max_z) * 0.5,
+ extent,
+ })
+ }
+
+ fn project(self, position: [f32; 3]) -> [f32; 2] {
+ let scale = 1.6 / self.extent;
+ [
+ (position[0] - self.center_x) * scale,
+ (position[2] - self.center_z) * scale,
+ ]
+ }
+
+ fn planar_uv(self, position: [f32; 3]) -> [f32; 2] {
+ [
+ (position[0] - self.center_x) / self.extent + 0.5,
+ (position[2] - self.center_z) / self.extent + 0.5,
+ ]
+ }
+}
+
impl std::fmt::Display for VulkanAssetMeshError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
@@ -46,85 +98,65 @@ impl std::error::Error for VulkanAssetMeshError {}
pub fn project_msh_to_static_mesh(
model: &ModelAsset,
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
- let mut indices = Vec::new();
- let mut draw_ranges = Vec::with_capacity(model.batches.len());
- for batch in &model.batches {
- let first_index =
- u32::try_from(indices.len()).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?;
- let start = usize::try_from(batch.index_start)
- .map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?;
- let end = start
- .checked_add(usize::from(batch.index_count))
- .ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
- for &raw_index in model
- .indices
- .get(start..end)
- .ok_or(VulkanAssetMeshError::IndexOutOfRange)?
- {
- let index = batch
- .base_vertex
- .checked_add(u32::from(raw_index))
- .ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
- indices.push(u16::try_from(index).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?);
- }
- draw_ranges.push(VulkanStaticDrawRange {
- first_index,
- index_count: u32::from(batch.index_count),
- material_index: batch.material_index,
- pipeline_state: LegacyPipelineState::default(),
- alpha_test_reference: 0,
- });
- }
- if indices.is_empty() {
- return Err(VulkanAssetMeshError::EmptyGeometry);
- }
+ let (indices, _) = static_model_indices_and_ranges(model)?;
+ let (min_x, max_x, min_z, max_z) = static_mesh_xz_bounds(&model.positions, &indices)?;
+ let frame = VulkanStaticXzFrame::from_bounds(min_x, max_x, min_z, max_z)?;
+ project_msh_to_static_mesh_in_xz_frame(model, frame, [0.0; 3], [1.0; 3])
+}
- 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 = model
- .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]);
- }
- let extent = (max_x - min_x).max(max_z - min_z);
- if !extent.is_finite() || extent <= f32::EPSILON {
- return Err(VulkanAssetMeshError::DegenerateViewExtent);
+/// Projects MSH geometry with a known translation/scale into a shared XZ frame.
+///
+/// The caller supplies only transforms already decoded from mission data. Raw
+/// orientation is intentionally not interpreted until its original convention
+/// is established.
+///
+/// # Errors
+///
+/// Returns [`VulkanAssetMeshError`] when geometry, transform values or the
+/// static Vulkan input contract cannot represent the source model.
+pub fn project_msh_to_static_mesh_in_xz_frame(
+ model: &ModelAsset,
+ frame: VulkanStaticXzFrame,
+ translation: [f32; 3],
+ scale: [f32; 3],
+) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
+ if !translation
+ .iter()
+ .chain(scale.iter())
+ .all(|value| value.is_finite())
+ {
+ return Err(VulkanAssetMeshError::NonFinitePosition);
}
- let center_x = (min_x + max_x) * 0.5;
- let center_z = (min_z + max_z) * 0.5;
- let scale = 1.6 / extent;
+ let (indices, draw_ranges) = static_model_indices_and_ranges(model)?;
let vertices = model
.positions
.iter()
.enumerate()
- .map(|(index, position)| VulkanStaticVertex {
- position: [
- (position[0] - center_x) * scale,
- (position[2] - center_z) * scale,
- ],
- color: [0.82, 0.72, 0.31],
- // Iron3D stores Res5 UV0 as signed fixed point with 1/1024 units.
- // Models that omit this optional stream retain the static viewer's
- // XZ planar fallback instead of receiving fabricated raw UV values.
- uv: model.uv0.as_ref().and_then(|uv0| 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],
- ),
+ .map(|(index, position)| {
+ if !position.iter().all(|value| value.is_finite()) {
+ return Err(VulkanAssetMeshError::NonFinitePosition);
+ }
+ let transformed = [
+ position[0] * scale[0] + translation[0],
+ position[1] * scale[1] + translation[1],
+ position[2] * scale[2] + translation[2],
+ ];
+ Ok(VulkanStaticVertex {
+ position: frame.project(transformed),
+ color: [0.82, 0.72, 0.31],
+ // Iron3D stores Res5 UV0 as signed fixed point with 1/1024 units.
+ // Models that omit this optional stream retain the static viewer's
+ // XZ planar fallback instead of receiving fabricated raw UV values.
+ uv: model
+ .uv0
+ .as_ref()
+ .and_then(|uv0| uv0.get(index))
+ .map_or(frame.planar_uv(transformed), |uv| {
+ [f32::from(uv[0]) / 1024.0, f32::from(uv[1]) / 1024.0]
+ }),
+ })
})
- .collect();
+ .collect::<Result<Vec<_>, _>>()?;
Ok(VulkanStaticMesh {
vertices,
@@ -164,32 +196,53 @@ pub fn project_land_msh_to_static_mesh(
}
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 frame = VulkanStaticXzFrame::from_bounds(min_x, max_x, min_z, max_z)?;
+ project_land_msh_to_static_mesh_in_xz_frame(terrain, frame)
+}
+
+/// Projects `Land.msh` terrain geometry into a shared diagnostic XZ frame.
+///
+/// # Errors
+///
+/// Returns [`VulkanAssetMeshError`] when terrain geometry or the static Vulkan
+/// input contract cannot represent the source mesh.
+pub fn project_land_msh_to_static_mesh_in_xz_frame(
+ terrain: &LandMeshDocument,
+ frame: VulkanStaticXzFrame,
+) -> 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 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],
- ),
+ .map(|(index, position)| {
+ if !position.iter().all(|value| value.is_finite()) {
+ return Err(VulkanAssetMeshError::NonFinitePosition);
+ }
+ Ok(VulkanStaticVertex {
+ position: frame.project(*position),
+ color: [0.31, 0.58, 0.27],
+ uv: terrain
+ .uv0
+ .get(index)
+ .map_or(frame.planar_uv(*position), |uv| {
+ [f32::from(uv[0]) / 1024.0, f32::from(uv[1]) / 1024.0]
+ }),
+ })
})
- .collect();
+ .collect::<Result<Vec<_>, _>>()?;
Ok(VulkanStaticMesh {
vertices,
@@ -207,6 +260,44 @@ pub fn project_land_msh_to_static_mesh(
})
}
+fn static_model_indices_and_ranges(
+ model: &ModelAsset,
+) -> Result<(Vec<u16>, Vec<VulkanStaticDrawRange>), VulkanAssetMeshError> {
+ let mut indices = Vec::new();
+ let mut draw_ranges = Vec::with_capacity(model.batches.len());
+ for batch in &model.batches {
+ let first_index =
+ u32::try_from(indices.len()).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?;
+ let start = usize::try_from(batch.index_start)
+ .map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?;
+ let end = start
+ .checked_add(usize::from(batch.index_count))
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
+ for &raw_index in model
+ .indices
+ .get(start..end)
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?
+ {
+ let index = batch
+ .base_vertex
+ .checked_add(u32::from(raw_index))
+ .ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
+ indices.push(u16::try_from(index).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?);
+ }
+ draw_ranges.push(VulkanStaticDrawRange {
+ first_index,
+ index_count: u32::from(batch.index_count),
+ material_index: batch.material_index,
+ pipeline_state: LegacyPipelineState::default(),
+ alpha_test_reference: 0,
+ });
+ }
+ if indices.is_empty() {
+ return Err(VulkanAssetMeshError::EmptyGeometry);
+ }
+ Ok((indices, draw_ranges))
+}
+
fn static_mesh_xz_bounds(
positions: &[[f32; 3]],
indices: &[u16],
diff --git a/adapters/fparkan-render-vulkan/src/ffi.rs b/adapters/fparkan-render-vulkan/src/ffi.rs
index 7b53c92..4e6a79f 100644
--- a/adapters/fparkan-render-vulkan/src/ffi.rs
+++ b/adapters/fparkan-render-vulkan/src/ffi.rs
@@ -40,7 +40,9 @@ mod swapchain_resources;
mod validation;
pub use self::asset_mesh::{
- project_land_msh_to_static_mesh, project_msh_to_static_mesh, VulkanAssetMeshError,
+ project_land_msh_to_static_mesh, project_land_msh_to_static_mesh_in_xz_frame,
+ project_msh_to_static_mesh, project_msh_to_static_mesh_in_xz_frame, VulkanAssetMeshError,
+ VulkanStaticXzFrame,
};
pub use self::capabilities::{
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,