feat(render): select static model LOD slots
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
//! Format-to-GPU geometry bridge for the initial static asset renderer.
|
//! Format-to-GPU geometry bridge for the initial static asset renderer.
|
||||||
|
|
||||||
use crate::{VulkanStaticDrawRange, VulkanStaticMesh, VulkanStaticVertex};
|
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_render::{LegacyIron3dEulerTransform, LegacyPipelineState};
|
||||||
use fparkan_terrain_format::LandMeshDocument;
|
use fparkan_terrain_format::LandMeshDocument;
|
||||||
|
|
||||||
@@ -447,8 +447,9 @@ fn static_model_indices_and_ranges(
|
|||||||
model: &ModelAsset,
|
model: &ModelAsset,
|
||||||
) -> Result<(Vec<u32>, Vec<VulkanStaticDrawRange>), VulkanAssetMeshError> {
|
) -> Result<(Vec<u32>, Vec<VulkanStaticDrawRange>), VulkanAssetMeshError> {
|
||||||
let mut indices = Vec::new();
|
let mut indices = Vec::new();
|
||||||
let mut draw_ranges = Vec::with_capacity(model.batches.len());
|
let batches = static_model_preview_batches(model)?;
|
||||||
for batch in &model.batches {
|
let mut draw_ranges = Vec::with_capacity(batches.len());
|
||||||
|
for batch in batches {
|
||||||
let first_index =
|
let first_index =
|
||||||
u32::try_from(indices.len()).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?;
|
u32::try_from(indices.len()).map_err(|_| VulkanAssetMeshError::IndexOutOfRange)?;
|
||||||
let start = usize::try_from(batch.index_start)
|
let start = usize::try_from(batch.index_start)
|
||||||
@@ -481,6 +482,49 @@ fn static_model_indices_and_ranges(
|
|||||||
Ok((indices, draw_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(
|
fn static_mesh_xy_bounds(
|
||||||
positions: &[[f32; 3]],
|
positions: &[[f32; 3]],
|
||||||
indices: &[u32],
|
indices: &[u32],
|
||||||
@@ -507,7 +551,7 @@ fn static_mesh_xy_bounds(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use fparkan_msh::{Batch, ModelAsset};
|
use fparkan_msh::{Batch, ModelAsset, Slot};
|
||||||
use fparkan_terrain_format::{FullSurfaceMask, TerrainFace28, TerrainSlotTable};
|
use fparkan_terrain_format::{FullSurfaceMask, TerrainFace28, TerrainSlotTable};
|
||||||
|
|
||||||
fn model(positions: Vec<[f32; 3]>, indices: Vec<u16>, batches: Vec<Batch>) -> ModelAsset {
|
fn model(positions: Vec<[f32; 3]>, indices: Vec<u16>, batches: Vec<Batch>) -> 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]
|
#[test]
|
||||||
fn projects_xy_geometry_and_applies_base_vertex() {
|
fn projects_xy_geometry_and_applies_base_vertex() {
|
||||||
let mesh = project_msh_to_static_mesh(&model(
|
let mesh = project_msh_to_static_mesh(&model(
|
||||||
|
|||||||
@@ -1588,6 +1588,16 @@ coefficients. После исправления того же capture диагн
|
|||||||
camera timing/selection, source model-node transforms, terrain material phases,
|
camera timing/selection, source model-node transforms, terrain material phases,
|
||||||
lighting и visibility, а не объявление pixel parity.
|
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 теперь переводит только высоту
|
Параллельно bridge для legacy-camera path теперь переводит только высоту
|
||||||
`Land.msh` в world units с масштабом `1/32`: raw AutoDemo heights
|
`Land.msh` в world units с масштабом `1/32`: raw AutoDemo heights
|
||||||
`95.29412..288.23532` становятся `2.978..9.007`, что согласуется с TMA Z и
|
`95.29412..288.23532` становятся `2.978..9.007`, что согласуется с TMA Z и
|
||||||
|
|||||||
Reference in New Issue
Block a user