aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-07-18 15:02:31 +0300
committerValentin Popov <valentin@popov.link>2026-07-18 15:02:31 +0300
commit16449430d1aac32f079fc76d3d0c549f36a2c6b6 (patch)
tree9887cdb35853ab45460d7b5c3c1a94adde14ce0e
parenta59f16d6b5895af58a36c16b041c3edaf882b3fd (diff)
downloadfparkan-16449430d1aac32f079fc76d3d0c549f36a2c6b6.tar.xz
fparkan-16449430d1aac32f079fc76d3d0c549f36a2c6b6.zip
feat(render): select static model LOD slots
-rw-r--r--adapters/fparkan-render-vulkan/src/asset_mesh.rs103
-rw-r--r--docs/tomes/05-render.md10
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<u32>, Vec<VulkanStaticDrawRange>), 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<Vec<&fparkan_msh::Batch>, 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<u16>, batches: Vec<Batch>) -> ModelAsset {
@@ -539,6 +583,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(
vec![
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 и