feat: complete map viewer scene and static CTL pose preview

Complete the interactive mission viewer with environment rendering, audio events, dynamic shadows, free-flight camera controls, and per-component CTL pose sampling.
This commit is contained in:
2026-10-11 13:12:50 +04:00
parent aa51f3574d
commit a8faba8aad
42 changed files with 24411 additions and 1234 deletions
+70 -3
View File
@@ -1,14 +1,16 @@
#![forbid(unsafe_code)]
//! Validated terrain runtime queries using XY ground coordinates and Z height.
use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument};
use fparkan_terrain_format::{LandMapDocument, LandMeshDocument};
use std::collections::VecDeque;
/// Terrain material-selector contract preserved from the decoded map mesh.
///
/// Applications access this semantic terrain API through this terrain crate
/// rather than taking a direct dependency on the binary-format parser.
pub use fparkan_terrain_format::{TerrainMaterialLayers, TerrainMaterialSelection};
pub use fparkan_terrain_format::{
FullSurfaceMask, TerrainMaterialLayers, TerrainMaterialSelection,
};
/// Terrain world.
#[derive(Clone, Debug, Default)]
@@ -141,6 +143,21 @@ pub trait SurfaceQuery {
direction: [f32; 3],
mask: FullSurfaceMask,
) -> Result<Option<SurfaceHit>, TerrainError>;
/// Raycast while requiring every bit in `include` and rejecting every
/// face carrying a bit in `exclude`.
///
/// The native landscape query has both predicates. The legacy
/// [`Self::raycast`] shorthand keeps its historical include-only form;
/// callers that model a native receiver or visibility test should use
/// this method so exclusion flags cannot be mistaken for inclusion bits.
fn raycast_excluding(
&self,
origin: [f32; 3],
direction: [f32; 3],
include: FullSurfaceMask,
exclude: FullSurfaceMask,
) -> Result<Option<SurfaceHit>, TerrainError>;
}
/// Navigation query.
@@ -404,6 +421,28 @@ impl SurfaceQuery for TerrainWorld {
origin: [f32; 3],
direction: [f32; 3],
mask: FullSurfaceMask,
) -> Result<Option<SurfaceHit>, TerrainError> {
self.raycast_filtered(origin, direction, mask, FullSurfaceMask(0))
}
fn raycast_excluding(
&self,
origin: [f32; 3],
direction: [f32; 3],
include: FullSurfaceMask,
exclude: FullSurfaceMask,
) -> Result<Option<SurfaceHit>, TerrainError> {
self.raycast_filtered(origin, direction, include, exclude)
}
}
impl TerrainWorld {
fn raycast_filtered(
&self,
origin: [f32; 3],
direction: [f32; 3],
include: FullSurfaceMask,
exclude: FullSurfaceMask,
) -> Result<Option<SurfaceHit>, TerrainError> {
if self.surfaces.is_empty() {
return Err(TerrainError::Unsupported);
@@ -413,7 +452,10 @@ impl SurfaceQuery for TerrainWorld {
let mut best: Option<SurfaceHit> = None;
for index in candidates {
let triangle = &self.surfaces[index];
if mask.0 != 0 && triangle.mask.0 & mask.0 == 0 {
if include.0 != 0 && triangle.mask.0 & include.0 != include.0 {
continue;
}
if triangle.mask.0 & exclude.0 != 0 {
continue;
}
let Some(distance) = triangle.raycast(origin, direction) else {
@@ -1032,6 +1074,31 @@ mod tests {
.expect("raycast"),
None
);
// Native include masks require every requested bit, while exclusion
// masks reject any matching face bit.
assert_eq!(
world
.raycast_excluding(
[0.25, 0.25, 2.0],
[0.0, 0.0, -1.0],
FullSurfaceMask(0x0000_0003),
FullSurfaceMask(0),
)
.expect("raycast include all bits"),
None
);
assert_eq!(
world
.raycast_excluding(
[0.25, 0.25, 2.0],
[0.0, 0.0, -1.0],
FullSurfaceMask(0),
FullSurfaceMask(0x0000_0001),
)
.expect("raycast exclusion"),
None
);
}
#[test]