feat(render): compose terrain with preview root
This commit is contained in:
Generated
+1
@@ -451,6 +451,7 @@ dependencies = [
|
||||
"fparkan-render",
|
||||
"fparkan-render-vulkan",
|
||||
"fparkan-runtime",
|
||||
"fparkan-terrain",
|
||||
"fparkan-vfs",
|
||||
"fparkan-world",
|
||||
"winit",
|
||||
|
||||
@@ -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()) {
|
||||
/// 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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,
|
||||
],
|
||||
.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(
|
||||
[
|
||||
(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],
|
||||
),
|
||||
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,
|
||||
],
|
||||
.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(
|
||||
[
|
||||
(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],
|
||||
),
|
||||
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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,6 +12,7 @@ fparkan-render = { path = "../../crates/fparkan-render", version = "0.1.0" }
|
||||
fparkan-platform-winit = { path = "../../adapters/fparkan-platform-winit", version = "0.1.0" }
|
||||
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan", version = "0.1.0" }
|
||||
fparkan-runtime = { path = "../../crates/fparkan-runtime", version = "0.1.0" }
|
||||
fparkan-terrain = { path = "../../crates/fparkan-terrain", version = "0.1.0" }
|
||||
fparkan-vfs = { path = "../../crates/fparkan-vfs", version = "0.1.0" }
|
||||
fparkan-world = { path = "../../crates/fparkan-world", version = "0.1.0" }
|
||||
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
||||
|
||||
+123
-14
@@ -30,16 +30,18 @@ use fparkan_render::{
|
||||
RenderSnapshotDraw,
|
||||
};
|
||||
use fparkan_render_vulkan::{
|
||||
project_msh_to_static_mesh, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
|
||||
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticMaterial, VulkanStaticMesh,
|
||||
VulkanStaticTexture,
|
||||
project_land_msh_to_static_mesh_in_xz_frame, project_msh_to_static_mesh_in_xz_frame,
|
||||
VulkanPlanningBackend, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
|
||||
VulkanSmokeRendererCreateInfo, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture,
|
||||
VulkanStaticXzFrame,
|
||||
};
|
||||
use fparkan_runtime::{
|
||||
create, frame, load_mission, load_mission_static_preview,
|
||||
load_mission_static_preview_with_progress, load_mission_with_progress, loaded_mission_assets,
|
||||
EngineConfig, EngineMode, EngineServices, MissionAssets, MissionLoadPhase, MissionObjectDraft,
|
||||
MissionRequest,
|
||||
loaded_mission_object_drafts, loaded_terrain, EngineConfig, EngineMode, EngineServices,
|
||||
MissionAssets, MissionLoadPhase, MissionObjectDraft, MissionRequest,
|
||||
};
|
||||
use fparkan_terrain::TerrainWorld;
|
||||
use fparkan_vfs::DirectoryVfs;
|
||||
#[cfg(test)]
|
||||
use fparkan_world::OriginalObjectId;
|
||||
@@ -82,11 +84,17 @@ fn run(args: &[String]) -> Result<String, String> {
|
||||
if args.backend == RenderBackendMode::StaticVulkan {
|
||||
let mission_assets = loaded_mission_assets(&engine)
|
||||
.ok_or_else(|| "mission assets are unavailable after loading".to_string())?;
|
||||
let preview = static_preview_mesh_and_materials(mission_assets)?;
|
||||
let terrain = loaded_terrain(&engine)
|
||||
.ok_or_else(|| "mission terrain is unavailable after loading".to_string())?;
|
||||
let root = loaded_mission_object_drafts(&engine)
|
||||
.and_then(|drafts| drafts.first())
|
||||
.ok_or_else(|| "first mission object draft is unavailable after loading".to_string())?;
|
||||
let preview = static_preview_mesh_and_materials(mission_assets, terrain, root)?;
|
||||
return run_static_vulkan_mode(
|
||||
preview.mesh,
|
||||
preview.materials,
|
||||
preview.mesh_components,
|
||||
preview.terrain_components,
|
||||
args.frames,
|
||||
&args.mission,
|
||||
loaded.object_count,
|
||||
@@ -183,25 +191,45 @@ struct StaticPreviewScene {
|
||||
mesh: VulkanStaticMesh,
|
||||
materials: Vec<VulkanStaticMaterial>,
|
||||
mesh_components: usize,
|
||||
terrain_components: usize,
|
||||
}
|
||||
|
||||
/// Projects every MSH component of the explicitly bounded static-preview root
|
||||
/// into one mesh and selector-keyed diffuse material descriptors.
|
||||
/// Projects the mission terrain plus every MSH component of the explicitly
|
||||
/// bounded static-preview root into one diagnostic XZ frame.
|
||||
///
|
||||
/// This intentionally takes only the first MAT0 texture request for each MSH
|
||||
/// batch selector. Later material phases, animation, lightmaps, transforms,
|
||||
/// and gameplay visibility remain outside this preview bridge.
|
||||
fn static_preview_mesh_and_materials(assets: &MissionAssets) -> Result<StaticPreviewScene, String> {
|
||||
/// batch selector. The terrain uses an explicit white diagnostic texture;
|
||||
/// terrain slot/material selection, orientation, camera, later material phases,
|
||||
/// animation, lightmaps and gameplay visibility remain outside this bridge.
|
||||
fn static_preview_mesh_and_materials(
|
||||
assets: &MissionAssets,
|
||||
terrain: &TerrainWorld,
|
||||
root: &MissionObjectDraft,
|
||||
) -> Result<StaticPreviewScene, String> {
|
||||
let visual_ids = assets.visuals_for_object(0);
|
||||
if visual_ids.is_empty() {
|
||||
return Err("first static preview root has no prepared visuals".to_string());
|
||||
}
|
||||
let terrain_mesh = terrain
|
||||
.source_mesh()
|
||||
.ok_or_else(|| "runtime terrain does not retain its validated source mesh".to_string())?;
|
||||
let frame = static_preview_xz_frame(assets, visual_ids, terrain, root)?;
|
||||
let mut mesh = VulkanStaticMesh {
|
||||
vertices: Vec::new(),
|
||||
indices: Vec::new(),
|
||||
draw_ranges: Vec::new(),
|
||||
};
|
||||
let mut materials = Vec::new();
|
||||
let mut materials = vec![VulkanStaticMaterial {
|
||||
material_index: 0,
|
||||
texture: VulkanStaticTexture {
|
||||
width: 1,
|
||||
height: 1,
|
||||
rgba8: vec![255, 255, 255, 255],
|
||||
},
|
||||
}];
|
||||
let terrain_component = project_land_msh_to_static_mesh_in_xz_frame(terrain_mesh, frame)
|
||||
.map_err(|err| format!("project mission terrain for Vulkan: {err}"))?;
|
||||
append_static_preview_component(&mut mesh, terrain_component, &[(0, 0)])?;
|
||||
let mut mesh_components = 0;
|
||||
for visual_id in visual_ids {
|
||||
let visual = assets.visual_by_id(*visual_id).ok_or_else(|| {
|
||||
@@ -213,7 +241,12 @@ fn static_preview_mesh_and_materials(assets: &MissionAssets) -> Result<StaticPre
|
||||
let model = assets.model_by_id(model_id).ok_or_else(|| {
|
||||
format!("static preview visual {visual_id:?} references unknown model {model_id:?}")
|
||||
})?;
|
||||
let component = project_msh_to_static_mesh(&model.validated)
|
||||
let component = project_msh_to_static_mesh_in_xz_frame(
|
||||
&model.validated,
|
||||
frame,
|
||||
root.position,
|
||||
root.scale,
|
||||
)
|
||||
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
|
||||
let selector_remap =
|
||||
static_preview_component_materials(assets, visual, &component, &mut materials)?;
|
||||
@@ -227,9 +260,79 @@ fn static_preview_mesh_and_materials(assets: &MissionAssets) -> Result<StaticPre
|
||||
mesh,
|
||||
materials,
|
||||
mesh_components,
|
||||
terrain_components: 1,
|
||||
})
|
||||
}
|
||||
|
||||
fn static_preview_xz_frame(
|
||||
assets: &MissionAssets,
|
||||
visual_ids: &[fparkan_assets::AssetId<PreparedVisual>],
|
||||
terrain: &TerrainWorld,
|
||||
root: &MissionObjectDraft,
|
||||
) -> Result<VulkanStaticXzFrame, String> {
|
||||
if !root
|
||||
.position
|
||||
.iter()
|
||||
.chain(root.scale.iter())
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return Err("first static preview root has a non-finite position or scale".to_string());
|
||||
}
|
||||
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;
|
||||
let terrain_positions = terrain
|
||||
.source_positions()
|
||||
.ok_or_else(|| "runtime terrain does not retain source positions".to_string())?;
|
||||
for position in terrain_positions {
|
||||
extend_static_preview_xz_bounds(*position, &mut min_x, &mut max_x, &mut min_z, &mut max_z)?;
|
||||
}
|
||||
for visual_id in visual_ids {
|
||||
let visual = assets.visual_by_id(*visual_id).ok_or_else(|| {
|
||||
format!("first static preview root references unknown visual {visual_id:?}")
|
||||
})?;
|
||||
let Some(model_id) = visual.model_id else {
|
||||
continue;
|
||||
};
|
||||
let model = assets.model_by_id(model_id).ok_or_else(|| {
|
||||
format!("static preview visual {visual_id:?} references unknown model {model_id:?}")
|
||||
})?;
|
||||
for position in &model.validated.positions {
|
||||
extend_static_preview_xz_bounds(
|
||||
[
|
||||
position[0] * root.scale[0] + root.position[0],
|
||||
position[1] * root.scale[1] + root.position[1],
|
||||
position[2] * root.scale[2] + root.position[2],
|
||||
],
|
||||
&mut min_x,
|
||||
&mut max_x,
|
||||
&mut min_z,
|
||||
&mut max_z,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
VulkanStaticXzFrame::from_bounds(min_x, max_x, min_z, max_z)
|
||||
.map_err(|err| format!("build static preview XZ frame: {err}"))
|
||||
}
|
||||
|
||||
fn extend_static_preview_xz_bounds(
|
||||
position: [f32; 3],
|
||||
min_x: &mut f32,
|
||||
max_x: &mut f32,
|
||||
min_z: &mut f32,
|
||||
max_z: &mut f32,
|
||||
) -> Result<(), String> {
|
||||
if !position.iter().all(|value| value.is_finite()) {
|
||||
return Err("static preview contains a non-finite XZ position".to_string());
|
||||
}
|
||||
*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(())
|
||||
}
|
||||
|
||||
fn static_preview_component_materials(
|
||||
assets: &MissionAssets,
|
||||
visual: &PreparedVisual,
|
||||
@@ -361,6 +464,7 @@ fn run_static_vulkan_mode(
|
||||
mesh: VulkanStaticMesh,
|
||||
materials: Vec<VulkanStaticMaterial>,
|
||||
mesh_components: usize,
|
||||
terrain_components: usize,
|
||||
target_frames: u64,
|
||||
mission: &str,
|
||||
object_count: usize,
|
||||
@@ -371,6 +475,7 @@ fn run_static_vulkan_mode(
|
||||
mesh,
|
||||
materials,
|
||||
mesh_components,
|
||||
terrain_components,
|
||||
target_frames,
|
||||
mission,
|
||||
object_count,
|
||||
@@ -385,6 +490,7 @@ struct StaticVulkanApp {
|
||||
mesh: VulkanStaticMesh,
|
||||
materials: Vec<VulkanStaticMaterial>,
|
||||
mesh_components: usize,
|
||||
terrain_components: usize,
|
||||
target_frames: u64,
|
||||
mission: String,
|
||||
object_count: usize,
|
||||
@@ -401,6 +507,7 @@ impl StaticVulkanApp {
|
||||
mesh: VulkanStaticMesh,
|
||||
materials: Vec<VulkanStaticMaterial>,
|
||||
mesh_components: usize,
|
||||
terrain_components: usize,
|
||||
target_frames: u64,
|
||||
mission: &str,
|
||||
object_count: usize,
|
||||
@@ -409,6 +516,7 @@ impl StaticVulkanApp {
|
||||
mesh,
|
||||
materials,
|
||||
mesh_components,
|
||||
terrain_components,
|
||||
target_frames,
|
||||
mission: mission.to_string(),
|
||||
object_count,
|
||||
@@ -451,11 +559,12 @@ impl StaticVulkanApp {
|
||||
return;
|
||||
}
|
||||
self.output = Some(format!(
|
||||
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"mesh_components\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_bytes\":{},\"readback_hash\":{}}}",
|
||||
"{{\"report_kind\":\"rendered-static-mission\",\"backend\":\"vulkan-static\",\"window\":\"native\",\"mission\":{},\"objects\":{},\"frames\":{},\"mesh_components\":{},\"terrain_components\":{},\"material_descriptors\":{},\"swapchain\":[{},{}],\"swapchain_images\":{},\"validation_warnings\":{},\"validation_errors\":{},\"readback_bytes\":{},\"readback_hash\":{}}}",
|
||||
json_string(&self.mission),
|
||||
self.object_count,
|
||||
self.frames_presented,
|
||||
self.mesh_components,
|
||||
self.terrain_components,
|
||||
self.materials.len(),
|
||||
report.renderer_report.swapchain_extent.0,
|
||||
report.renderer_report.swapchain_extent.1,
|
||||
|
||||
@@ -30,6 +30,7 @@ pub struct TerrainWorld {
|
||||
grid: RuntimeGrid,
|
||||
adjacency: Vec<Vec<ArealId>>,
|
||||
surfaces: Vec<RuntimeTriangle>,
|
||||
source_mesh: Option<LandMeshDocument>,
|
||||
}
|
||||
|
||||
/// Surface hit.
|
||||
@@ -232,6 +233,7 @@ impl TerrainWorld {
|
||||
grid,
|
||||
adjacency,
|
||||
surfaces: Vec::new(),
|
||||
source_mesh: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -244,6 +246,7 @@ impl TerrainWorld {
|
||||
pub fn from_land_msh(mesh: &LandMeshDocument) -> Result<Self, TerrainError> {
|
||||
Ok(Self {
|
||||
surfaces: build_surfaces(mesh)?,
|
||||
source_mesh: Some(mesh.clone()),
|
||||
..Self::default()
|
||||
})
|
||||
}
|
||||
@@ -260,6 +263,7 @@ impl TerrainWorld {
|
||||
) -> Result<Self, TerrainError> {
|
||||
let mut world = Self::from_land_map(map)?;
|
||||
world.surfaces = build_surfaces(mesh)?;
|
||||
world.source_mesh = Some(mesh.clone());
|
||||
Ok(world)
|
||||
}
|
||||
|
||||
@@ -275,6 +279,24 @@ impl TerrainWorld {
|
||||
self.surfaces.len()
|
||||
}
|
||||
|
||||
/// Returns the validated source mesh retained for renderer integration.
|
||||
///
|
||||
/// Runtime collision queries use their own compact triangle representation;
|
||||
/// keeping this document preserves the original geometry and UV streams for
|
||||
/// rendering without asking the application layer to decode an archive again.
|
||||
#[must_use]
|
||||
pub fn source_mesh(&self) -> Option<&LandMeshDocument> {
|
||||
self.source_mesh.as_ref()
|
||||
}
|
||||
|
||||
/// Returns source terrain positions without exposing parser ownership to apps.
|
||||
#[must_use]
|
||||
pub fn source_positions(&self) -> Option<&[[f32; 3]]> {
|
||||
self.source_mesh
|
||||
.as_ref()
|
||||
.map(|mesh| mesh.positions.as_slice())
|
||||
}
|
||||
|
||||
fn locate_by_candidates(
|
||||
&self,
|
||||
position: [f32; 3],
|
||||
|
||||
@@ -979,6 +979,29 @@ probe timed out at `Graph`; this reduction is what allowed the successful GOG
|
||||
native run. If that first root has no usable MSH, static preview fails explicitly
|
||||
rather than expanding later roots or pretending to render the entire mission.
|
||||
|
||||
### Shared terrain/root diagnostic frame
|
||||
|
||||
The bounded native preview now retains the already validated `Land.msh` inside
|
||||
`TerrainWorld`; this lets the application consume source geometry through the
|
||||
runtime boundary instead of decoding archive formats a second time. It merges
|
||||
one terrain component with the first TMA root's 14 reachable MSH components in
|
||||
one explicit top-down XZ frame. The frame is computed from terrain positions
|
||||
and the root models after applying only the decoded TMA `position` and `scale`.
|
||||
Raw TMA orientation is deliberately excluded: its convention is not proven.
|
||||
|
||||
Terrain is assigned an explicit 1x1 white diagnostic texture, rather than a
|
||||
guessed terrain material. `Land.msh` slot selection, `material_tag`, masks,
|
||||
auxiliary streams, original terrain phases and camera remain unresolved. Thus
|
||||
this is an integrated geometry/provenance checkpoint, not a claim of original
|
||||
terrain shading or scene camera reconstruction.
|
||||
|
||||
Fresh canonical GOG `MISSIONS/Autodemo.00/data.tma` evidence: one native
|
||||
1280x720 Vulkan frame completed in 40.6 seconds with 14 root MSH components,
|
||||
one terrain component, 15 descriptors, a 7,372,800-byte readback (FNV-1a
|
||||
`10739087367165646439`) and validation warnings/errors `0/0`. The distinct hash
|
||||
from the earlier first-root-only preview is expected because the submitted
|
||||
geometry and descriptor set changed; it is not an original-frame comparison.
|
||||
|
||||
### Camera ownership boundary from the GOG renderer
|
||||
|
||||
The GOG `World3D.dll` export `LoadCamera` at RVA `0x1FB06` is only an import
|
||||
|
||||
Reference in New Issue
Block a user