feat(render): compose terrain with preview root
This commit is contained in:
Generated
+1
@@ -451,6 +451,7 @@ dependencies = [
|
|||||||
"fparkan-render",
|
"fparkan-render",
|
||||||
"fparkan-render-vulkan",
|
"fparkan-render-vulkan",
|
||||||
"fparkan-runtime",
|
"fparkan-runtime",
|
||||||
|
"fparkan-terrain",
|
||||||
"fparkan-vfs",
|
"fparkan-vfs",
|
||||||
"fparkan-world",
|
"fparkan-world",
|
||||||
"winit",
|
"winit",
|
||||||
|
|||||||
@@ -18,6 +18,58 @@ pub enum VulkanAssetMeshError {
|
|||||||
IndexOutOfRange,
|
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 {
|
impl std::fmt::Display for VulkanAssetMeshError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -46,85 +98,65 @@ impl std::error::Error for VulkanAssetMeshError {}
|
|||||||
pub fn project_msh_to_static_mesh(
|
pub fn project_msh_to_static_mesh(
|
||||||
model: &ModelAsset,
|
model: &ModelAsset,
|
||||||
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
|
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
|
||||||
let mut indices = Vec::new();
|
let (indices, _) = static_model_indices_and_ranges(model)?;
|
||||||
let mut draw_ranges = Vec::with_capacity(model.batches.len());
|
let (min_x, max_x, min_z, max_z) = static_mesh_xz_bounds(&model.positions, &indices)?;
|
||||||
for batch in &model.batches {
|
let frame = VulkanStaticXzFrame::from_bounds(min_x, max_x, min_z, max_z)?;
|
||||||
let first_index =
|
project_msh_to_static_mesh_in_xz_frame(model, frame, [0.0; 3], [1.0; 3])
|
||||||
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 mut min_x = f32::INFINITY;
|
/// Projects MSH geometry with a known translation/scale into a shared XZ frame.
|
||||||
let mut max_x = f32::NEG_INFINITY;
|
///
|
||||||
let mut min_z = f32::INFINITY;
|
/// The caller supplies only transforms already decoded from mission data. Raw
|
||||||
let mut max_z = f32::NEG_INFINITY;
|
/// orientation is intentionally not interpreted until its original convention
|
||||||
for &index in &indices {
|
/// is established.
|
||||||
let position = model
|
///
|
||||||
.positions
|
/// # Errors
|
||||||
.get(usize::from(index))
|
///
|
||||||
.ok_or(VulkanAssetMeshError::IndexOutOfRange)?;
|
/// Returns [`VulkanAssetMeshError`] when geometry, transform values or the
|
||||||
if !position.iter().all(|value| value.is_finite()) {
|
/// static Vulkan input contract cannot represent the source model.
|
||||||
return Err(VulkanAssetMeshError::NonFinitePosition);
|
pub fn project_msh_to_static_mesh_in_xz_frame(
|
||||||
}
|
model: &ModelAsset,
|
||||||
min_x = min_x.min(position[0]);
|
frame: VulkanStaticXzFrame,
|
||||||
max_x = max_x.max(position[0]);
|
translation: [f32; 3],
|
||||||
min_z = min_z.min(position[2]);
|
scale: [f32; 3],
|
||||||
max_z = max_z.max(position[2]);
|
) -> Result<VulkanStaticMesh, VulkanAssetMeshError> {
|
||||||
|
if !translation
|
||||||
|
.iter()
|
||||||
|
.chain(scale.iter())
|
||||||
|
.all(|value| value.is_finite())
|
||||||
|
{
|
||||||
|
return Err(VulkanAssetMeshError::NonFinitePosition);
|
||||||
}
|
}
|
||||||
let extent = (max_x - min_x).max(max_z - min_z);
|
let (indices, draw_ranges) = static_model_indices_and_ranges(model)?;
|
||||||
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 vertices = model
|
let vertices = model
|
||||||
.positions
|
.positions
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, position)| VulkanStaticVertex {
|
.map(|(index, position)| {
|
||||||
position: [
|
if !position.iter().all(|value| value.is_finite()) {
|
||||||
(position[0] - center_x) * scale,
|
return Err(VulkanAssetMeshError::NonFinitePosition);
|
||||||
(position[2] - center_z) * scale,
|
}
|
||||||
],
|
let transformed = [
|
||||||
color: [0.82, 0.72, 0.31],
|
position[0] * scale[0] + translation[0],
|
||||||
// Iron3D stores Res5 UV0 as signed fixed point with 1/1024 units.
|
position[1] * scale[1] + translation[1],
|
||||||
// Models that omit this optional stream retain the static viewer's
|
position[2] * scale[2] + translation[2],
|
||||||
// XZ planar fallback instead of receiving fabricated raw UV values.
|
];
|
||||||
uv: model.uv0.as_ref().and_then(|uv0| uv0.get(index)).map_or(
|
Ok(VulkanStaticVertex {
|
||||||
[
|
position: frame.project(transformed),
|
||||||
(position[0] - center_x) / extent + 0.5,
|
color: [0.82, 0.72, 0.31],
|
||||||
(position[2] - center_z) / extent + 0.5,
|
// Iron3D stores Res5 UV0 as signed fixed point with 1/1024 units.
|
||||||
],
|
// Models that omit this optional stream retain the static viewer's
|
||||||
|uv| [f32::from(uv[0]) / 1024.0, f32::from(uv[1]) / 1024.0],
|
// 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 {
|
Ok(VulkanStaticMesh {
|
||||||
vertices,
|
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 (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);
|
let frame = VulkanStaticXzFrame::from_bounds(min_x, max_x, min_z, max_z)?;
|
||||||
if !extent.is_finite() || extent <= f32::EPSILON {
|
project_land_msh_to_static_mesh_in_xz_frame(terrain, frame)
|
||||||
return Err(VulkanAssetMeshError::DegenerateViewExtent);
|
}
|
||||||
|
|
||||||
|
/// 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
|
let vertices = terrain
|
||||||
.positions
|
.positions
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, position)| VulkanStaticVertex {
|
.map(|(index, position)| {
|
||||||
position: [
|
if !position.iter().all(|value| value.is_finite()) {
|
||||||
(position[0] - center_x) * scale,
|
return Err(VulkanAssetMeshError::NonFinitePosition);
|
||||||
(position[2] - center_z) * scale,
|
}
|
||||||
],
|
Ok(VulkanStaticVertex {
|
||||||
color: [0.31, 0.58, 0.27],
|
position: frame.project(*position),
|
||||||
uv: terrain.uv0.get(index).map_or(
|
color: [0.31, 0.58, 0.27],
|
||||||
[
|
uv: terrain
|
||||||
(position[0] - center_x) / extent + 0.5,
|
.uv0
|
||||||
(position[2] - center_z) / extent + 0.5,
|
.get(index)
|
||||||
],
|
.map_or(frame.planar_uv(*position), |uv| {
|
||||||
|uv| [f32::from(uv[0]) / 1024.0, f32::from(uv[1]) / 1024.0],
|
[f32::from(uv[0]) / 1024.0, f32::from(uv[1]) / 1024.0]
|
||||||
),
|
}),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
Ok(VulkanStaticMesh {
|
Ok(VulkanStaticMesh {
|
||||||
vertices,
|
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(
|
fn static_mesh_xz_bounds(
|
||||||
positions: &[[f32; 3]],
|
positions: &[[f32; 3]],
|
||||||
indices: &[u16],
|
indices: &[u16],
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ mod swapchain_resources;
|
|||||||
mod validation;
|
mod validation;
|
||||||
|
|
||||||
pub use self::asset_mesh::{
|
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::{
|
pub use self::capabilities::{
|
||||||
probe_vulkan_runtime_capabilities, probe_vulkan_runtime_capabilities_for_request,
|
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-platform-winit = { path = "../../adapters/fparkan-platform-winit", version = "0.1.0" }
|
||||||
fparkan-render-vulkan = { path = "../../adapters/fparkan-render-vulkan", 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-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-vfs = { path = "../../crates/fparkan-vfs", version = "0.1.0" }
|
||||||
fparkan-world = { path = "../../crates/fparkan-world", version = "0.1.0" }
|
fparkan-world = { path = "../../crates/fparkan-world", version = "0.1.0" }
|
||||||
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
winit = { version = "0.30", default-features = false, features = ["rwh_06"] }
|
||||||
|
|||||||
+124
-15
@@ -30,16 +30,18 @@ use fparkan_render::{
|
|||||||
RenderSnapshotDraw,
|
RenderSnapshotDraw,
|
||||||
};
|
};
|
||||||
use fparkan_render_vulkan::{
|
use fparkan_render_vulkan::{
|
||||||
project_msh_to_static_mesh, VulkanPlanningBackend, VulkanSmokeFrameOutcome,
|
project_land_msh_to_static_mesh_in_xz_frame, project_msh_to_static_mesh_in_xz_frame,
|
||||||
VulkanSmokeRenderer, VulkanSmokeRendererCreateInfo, VulkanStaticMaterial, VulkanStaticMesh,
|
VulkanPlanningBackend, VulkanSmokeFrameOutcome, VulkanSmokeRenderer,
|
||||||
VulkanStaticTexture,
|
VulkanSmokeRendererCreateInfo, VulkanStaticMaterial, VulkanStaticMesh, VulkanStaticTexture,
|
||||||
|
VulkanStaticXzFrame,
|
||||||
};
|
};
|
||||||
use fparkan_runtime::{
|
use fparkan_runtime::{
|
||||||
create, frame, load_mission, load_mission_static_preview,
|
create, frame, load_mission, load_mission_static_preview,
|
||||||
load_mission_static_preview_with_progress, load_mission_with_progress, loaded_mission_assets,
|
load_mission_static_preview_with_progress, load_mission_with_progress, loaded_mission_assets,
|
||||||
EngineConfig, EngineMode, EngineServices, MissionAssets, MissionLoadPhase, MissionObjectDraft,
|
loaded_mission_object_drafts, loaded_terrain, EngineConfig, EngineMode, EngineServices,
|
||||||
MissionRequest,
|
MissionAssets, MissionLoadPhase, MissionObjectDraft, MissionRequest,
|
||||||
};
|
};
|
||||||
|
use fparkan_terrain::TerrainWorld;
|
||||||
use fparkan_vfs::DirectoryVfs;
|
use fparkan_vfs::DirectoryVfs;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use fparkan_world::OriginalObjectId;
|
use fparkan_world::OriginalObjectId;
|
||||||
@@ -82,11 +84,17 @@ fn run(args: &[String]) -> Result<String, String> {
|
|||||||
if args.backend == RenderBackendMode::StaticVulkan {
|
if args.backend == RenderBackendMode::StaticVulkan {
|
||||||
let mission_assets = loaded_mission_assets(&engine)
|
let mission_assets = loaded_mission_assets(&engine)
|
||||||
.ok_or_else(|| "mission assets are unavailable after loading".to_string())?;
|
.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(
|
return run_static_vulkan_mode(
|
||||||
preview.mesh,
|
preview.mesh,
|
||||||
preview.materials,
|
preview.materials,
|
||||||
preview.mesh_components,
|
preview.mesh_components,
|
||||||
|
preview.terrain_components,
|
||||||
args.frames,
|
args.frames,
|
||||||
&args.mission,
|
&args.mission,
|
||||||
loaded.object_count,
|
loaded.object_count,
|
||||||
@@ -183,25 +191,45 @@ struct StaticPreviewScene {
|
|||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
materials: Vec<VulkanStaticMaterial>,
|
materials: Vec<VulkanStaticMaterial>,
|
||||||
mesh_components: usize,
|
mesh_components: usize,
|
||||||
|
terrain_components: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Projects every MSH component of the explicitly bounded static-preview root
|
/// Projects the mission terrain plus every MSH component of the explicitly
|
||||||
/// into one mesh and selector-keyed diffuse material descriptors.
|
/// bounded static-preview root into one diagnostic XZ frame.
|
||||||
///
|
///
|
||||||
/// This intentionally takes only the first MAT0 texture request for each MSH
|
/// This intentionally takes only the first MAT0 texture request for each MSH
|
||||||
/// batch selector. Later material phases, animation, lightmaps, transforms,
|
/// batch selector. The terrain uses an explicit white diagnostic texture;
|
||||||
/// and gameplay visibility remain outside this preview bridge.
|
/// terrain slot/material selection, orientation, camera, later material phases,
|
||||||
fn static_preview_mesh_and_materials(assets: &MissionAssets) -> Result<StaticPreviewScene, String> {
|
/// 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);
|
let visual_ids = assets.visuals_for_object(0);
|
||||||
if visual_ids.is_empty() {
|
if visual_ids.is_empty() {
|
||||||
return Err("first static preview root has no prepared visuals".to_string());
|
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 {
|
let mut mesh = VulkanStaticMesh {
|
||||||
vertices: Vec::new(),
|
vertices: Vec::new(),
|
||||||
indices: Vec::new(),
|
indices: Vec::new(),
|
||||||
draw_ranges: 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;
|
let mut mesh_components = 0;
|
||||||
for visual_id in visual_ids {
|
for visual_id in visual_ids {
|
||||||
let visual = assets.visual_by_id(*visual_id).ok_or_else(|| {
|
let visual = assets.visual_by_id(*visual_id).ok_or_else(|| {
|
||||||
@@ -213,8 +241,13 @@ fn static_preview_mesh_and_materials(assets: &MissionAssets) -> Result<StaticPre
|
|||||||
let model = assets.model_by_id(model_id).ok_or_else(|| {
|
let model = assets.model_by_id(model_id).ok_or_else(|| {
|
||||||
format!("static preview visual {visual_id:?} references unknown model {model_id:?}")
|
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(
|
||||||
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
|
&model.validated,
|
||||||
|
frame,
|
||||||
|
root.position,
|
||||||
|
root.scale,
|
||||||
|
)
|
||||||
|
.map_err(|err| format!("project mission MSH for Vulkan: {err}"))?;
|
||||||
let selector_remap =
|
let selector_remap =
|
||||||
static_preview_component_materials(assets, visual, &component, &mut materials)?;
|
static_preview_component_materials(assets, visual, &component, &mut materials)?;
|
||||||
append_static_preview_component(&mut mesh, component, &selector_remap)?;
|
append_static_preview_component(&mut mesh, component, &selector_remap)?;
|
||||||
@@ -227,9 +260,79 @@ fn static_preview_mesh_and_materials(assets: &MissionAssets) -> Result<StaticPre
|
|||||||
mesh,
|
mesh,
|
||||||
materials,
|
materials,
|
||||||
mesh_components,
|
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(
|
fn static_preview_component_materials(
|
||||||
assets: &MissionAssets,
|
assets: &MissionAssets,
|
||||||
visual: &PreparedVisual,
|
visual: &PreparedVisual,
|
||||||
@@ -361,6 +464,7 @@ fn run_static_vulkan_mode(
|
|||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
materials: Vec<VulkanStaticMaterial>,
|
materials: Vec<VulkanStaticMaterial>,
|
||||||
mesh_components: usize,
|
mesh_components: usize,
|
||||||
|
terrain_components: usize,
|
||||||
target_frames: u64,
|
target_frames: u64,
|
||||||
mission: &str,
|
mission: &str,
|
||||||
object_count: usize,
|
object_count: usize,
|
||||||
@@ -371,6 +475,7 @@ fn run_static_vulkan_mode(
|
|||||||
mesh,
|
mesh,
|
||||||
materials,
|
materials,
|
||||||
mesh_components,
|
mesh_components,
|
||||||
|
terrain_components,
|
||||||
target_frames,
|
target_frames,
|
||||||
mission,
|
mission,
|
||||||
object_count,
|
object_count,
|
||||||
@@ -385,6 +490,7 @@ struct StaticVulkanApp {
|
|||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
materials: Vec<VulkanStaticMaterial>,
|
materials: Vec<VulkanStaticMaterial>,
|
||||||
mesh_components: usize,
|
mesh_components: usize,
|
||||||
|
terrain_components: usize,
|
||||||
target_frames: u64,
|
target_frames: u64,
|
||||||
mission: String,
|
mission: String,
|
||||||
object_count: usize,
|
object_count: usize,
|
||||||
@@ -401,6 +507,7 @@ impl StaticVulkanApp {
|
|||||||
mesh: VulkanStaticMesh,
|
mesh: VulkanStaticMesh,
|
||||||
materials: Vec<VulkanStaticMaterial>,
|
materials: Vec<VulkanStaticMaterial>,
|
||||||
mesh_components: usize,
|
mesh_components: usize,
|
||||||
|
terrain_components: usize,
|
||||||
target_frames: u64,
|
target_frames: u64,
|
||||||
mission: &str,
|
mission: &str,
|
||||||
object_count: usize,
|
object_count: usize,
|
||||||
@@ -409,6 +516,7 @@ impl StaticVulkanApp {
|
|||||||
mesh,
|
mesh,
|
||||||
materials,
|
materials,
|
||||||
mesh_components,
|
mesh_components,
|
||||||
|
terrain_components,
|
||||||
target_frames,
|
target_frames,
|
||||||
mission: mission.to_string(),
|
mission: mission.to_string(),
|
||||||
object_count,
|
object_count,
|
||||||
@@ -451,11 +559,12 @@ impl StaticVulkanApp {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.output = Some(format!(
|
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),
|
json_string(&self.mission),
|
||||||
self.object_count,
|
self.object_count,
|
||||||
self.frames_presented,
|
self.frames_presented,
|
||||||
self.mesh_components,
|
self.mesh_components,
|
||||||
|
self.terrain_components,
|
||||||
self.materials.len(),
|
self.materials.len(),
|
||||||
report.renderer_report.swapchain_extent.0,
|
report.renderer_report.swapchain_extent.0,
|
||||||
report.renderer_report.swapchain_extent.1,
|
report.renderer_report.swapchain_extent.1,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ pub struct TerrainWorld {
|
|||||||
grid: RuntimeGrid,
|
grid: RuntimeGrid,
|
||||||
adjacency: Vec<Vec<ArealId>>,
|
adjacency: Vec<Vec<ArealId>>,
|
||||||
surfaces: Vec<RuntimeTriangle>,
|
surfaces: Vec<RuntimeTriangle>,
|
||||||
|
source_mesh: Option<LandMeshDocument>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Surface hit.
|
/// Surface hit.
|
||||||
@@ -232,6 +233,7 @@ impl TerrainWorld {
|
|||||||
grid,
|
grid,
|
||||||
adjacency,
|
adjacency,
|
||||||
surfaces: Vec::new(),
|
surfaces: Vec::new(),
|
||||||
|
source_mesh: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +246,7 @@ impl TerrainWorld {
|
|||||||
pub fn from_land_msh(mesh: &LandMeshDocument) -> Result<Self, TerrainError> {
|
pub fn from_land_msh(mesh: &LandMeshDocument) -> Result<Self, TerrainError> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
surfaces: build_surfaces(mesh)?,
|
surfaces: build_surfaces(mesh)?,
|
||||||
|
source_mesh: Some(mesh.clone()),
|
||||||
..Self::default()
|
..Self::default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -260,6 +263,7 @@ impl TerrainWorld {
|
|||||||
) -> Result<Self, TerrainError> {
|
) -> Result<Self, TerrainError> {
|
||||||
let mut world = Self::from_land_map(map)?;
|
let mut world = Self::from_land_map(map)?;
|
||||||
world.surfaces = build_surfaces(mesh)?;
|
world.surfaces = build_surfaces(mesh)?;
|
||||||
|
world.source_mesh = Some(mesh.clone());
|
||||||
Ok(world)
|
Ok(world)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,6 +279,24 @@ impl TerrainWorld {
|
|||||||
self.surfaces.len()
|
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(
|
fn locate_by_candidates(
|
||||||
&self,
|
&self,
|
||||||
position: [f32; 3],
|
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
|
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.
|
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
|
### Camera ownership boundary from the GOG renderer
|
||||||
|
|
||||||
The GOG `World3D.dll` export `LoadCamera` at RVA `0x1FB06` is only an import
|
The GOG `World3D.dll` export `LoadCamera` at RVA `0x1FB06` is only an import
|
||||||
|
|||||||
Reference in New Issue
Block a user