feat(render): compose terrain with preview root
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
Test / Lint (push) Failing after 2m6s
Test / Test (push) Skipped
Test / Render parity (push) Skipped

This commit is contained in:
2026-07-18 10:21:43 +04:00
parent 46851518fe
commit 8ac6a11100
7 changed files with 357 additions and 108 deletions
Generated
+1
View File
@@ -451,6 +451,7 @@ dependencies = [
"fparkan-render",
"fparkan-render-vulkan",
"fparkan-runtime",
"fparkan-terrain",
"fparkan-vfs",
"fparkan-world",
"winit",
+176 -85
View File
@@ -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],
+3 -1
View File
@@ -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,
+1
View File
@@ -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
View File
@@ -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,
+22
View File
@@ -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],
+23
View File
@@ -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