From 16449430d1aac32f079fc76d3d0c549f36a2c6b6 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 16:02:31 +0400 Subject: [PATCH] feat(render): select static model LOD slots --- .../fparkan-render-vulkan/src/asset_mesh.rs | 103 +++++++++++++++++- docs/tomes/05-render.md | 10 ++ 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/adapters/fparkan-render-vulkan/src/asset_mesh.rs b/adapters/fparkan-render-vulkan/src/asset_mesh.rs index 413eba6..bd43a81 100644 --- a/adapters/fparkan-render-vulkan/src/asset_mesh.rs +++ b/adapters/fparkan-render-vulkan/src/asset_mesh.rs @@ -1,7 +1,7 @@ //! Format-to-GPU geometry bridge for the initial static asset renderer. use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex}; -use fparkan_msh::ModelAsset; +use fparkan_msh::{selected_slot, Group, Lod, ModelAsset, NodeId}; use fparkan_render::{LegacyIron3dEulerTransform, LegacyPipelineState}; use fparkan_terrain_format::LandMeshDocument; @@ -447,8 +447,9 @@ fn static_model_indices_and_ranges( model: &ModelAsset, ) -> Result<(Vec, Vec), VulkanAssetMeshError> { let mut indices = Vec::new(); - let mut draw_ranges = Vec::with_capacity(model.batches.len()); - for batch in &model.batches { + let batches = static_model_preview_batches(model)?; + let mut draw_ranges = Vec::with_capacity(batches.len()); + for batch in batches { let first_index = u32::try_from(indices.len()).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?; let start = usize::try_from(batch.index_start) @@ -481,6 +482,49 @@ fn static_model_indices_and_ranges( Ok((indices, draw_ranges)) } +/// Returns the source batches selected by the initial static model pose. +/// +/// A `Node38` has a three-LOD by five-group slot map. The static bridge has no +/// recovered animation or group-selection controller, so it uses the source's +/// first LOD/group entry for every node instead of submitting every batch in the +/// file (which also includes alternate LODs and groups). Models without the +/// standard node layout retain the prior all-batches diagnostic fallback. +fn static_model_preview_batches( + model: &ModelAsset, +) -> Result, VulkanAssetMeshError> { + if model.node_stride != 38 || model.node_count == 0 { + return Ok(model.batches.iter().collect()); + } + + let mut selected = Vec::new(); + for node_index in 0..model.node_count { + let node = + NodeId(u32::try_from(node_index).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?); + let Some(slot) = selected_slot(model, node, Lod(0), Group(0)) else { + continue; + }; + let slot = model + .slots + .get(usize::try_from(slot.0).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?) + .ok_or(VulkanAssetMeshError::IndexOutOfRange)?; + let start = usize::from(slot.batch_start); + let end = start + .checked_add(usize::from(slot.batch_count)) + .ok_or(VulkanAssetMeshError::IndexOutOfRange)?; + selected.extend( + model + .batches + .get(start..end) + .ok_or(VulkanAssetMeshError::IndexOutOfRange)?, + ); + } + + if selected.is_empty() { + return Ok(model.batches.iter().collect()); + } + Ok(selected) +} + fn static_mesh_xy_bounds( positions: &[[f32; 3]], indices: &[u32], @@ -507,7 +551,7 @@ fn static_mesh_xy_bounds( #[cfg(test)] mod tests { use super::*; - use fparkan_msh::{Batch, ModelAsset}; + use fparkan_msh::{Batch, ModelAsset, Slot}; use fparkan_terrain_format::{FullSurfaceMask, TerrainFace28, TerrainSlotTable}; fn model(positions: Vec<[f32; 3]>, indices: Vec, batches: Vec) -> ModelAsset { @@ -538,6 +582,57 @@ mod tests { } } + #[test] + fn static_preview_uses_first_lod_group_slot_for_standard_nodes() { + let mut node = vec![0_u8; 38]; + node[8..10].copy_from_slice(&1_u16.to_le_bytes()); + let model = ModelAsset { + node_stride: 38, + node_count: 1, + nodes_raw: node, + slots: vec![ + Slot { + tri_start: 0, + tri_count: 0, + batch_start: 0, + batch_count: 1, + aabb_min: [0.0; 3], + aabb_max: [1.0; 3], + sphere_center: [0.0; 3], + sphere_radius: 1.0, + opaque: [0; 5], + }, + Slot { + tri_start: 0, + tri_count: 0, + batch_start: 1, + batch_count: 1, + aabb_min: [0.0; 3], + aabb_max: [1.0; 3], + sphere_center: [0.0; 3], + sphere_radius: 1.0, + opaque: [0; 5], + }, + ], + positions: Vec::new(), + normals: None, + uv0: None, + indices: Vec::new(), + batches: vec![ + batch(0, 0, 0), + Batch { + material_index: 1, + ..batch(0, 0, 0) + }, + ], + node_names: None, + }; + + let selected = static_model_preview_batches(&model).expect("valid selected slot"); + assert_eq!(selected.len(), 1); + assert_eq!(selected[0].material_index, 1); + } + #[test] fn projects_xy_geometry_and_applies_base_vertex() { let mesh = project_msh_to_static_mesh(&model( diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index 7c1fba7..3ee2998 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -1588,6 +1588,16 @@ coefficients. После исправления того же capture диагн camera timing/selection, source model-node transforms, terrain material phases, lighting и visibility, а не объявление pixel parity. +MSH static bridge теперь также перестал отправлять каждый `Batch20` файла. +Для стандартного `Node38` он выбирает `slot_index[LOD0, group0]`, затем +соответствующий `Slot68.batch_start..batch_count`; alternate LOD/groups больше +не подмешиваются в initial static pose. Regression test фиксирует selection, +а fresh AutoDemo run меняет readback hash при тех же 938 clip-visible vertices +и остаётся validation-clean. Новый capture по-прежнему показывает сильное +смещение частей, поэтому одной selection недостаточно: нужны local node poses, +parent hierarchy и animation-key/fallback semantics. Модели без layout Node38 +сохраняют прежний all-batches diagnostic fallback. + Параллельно bridge для legacy-camera path теперь переводит только высоту `Land.msh` в world units с масштабом `1/32`: raw AutoDemo heights `95.29412..288.23532` становятся `2.978..9.007`, что согласуется с TMA Z и