fix(terrain): use xy ground coordinates
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
clippy::unwrap_used
|
clippy::unwrap_used
|
||||||
)
|
)
|
||||||
)]
|
)]
|
||||||
//! Validated terrain runtime queries.
|
//! Validated terrain runtime queries using XY ground coordinates and Z height.
|
||||||
|
|
||||||
use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument};
|
use fparkan_terrain_format::{FullSurfaceMask, LandMapDocument, LandMeshDocument};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
@@ -36,7 +36,7 @@ pub struct TerrainWorld {
|
|||||||
/// Surface hit.
|
/// Surface hit.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct SurfaceHit {
|
pub struct SurfaceHit {
|
||||||
/// Height.
|
/// Source Z height.
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
/// Hit position.
|
/// Hit position.
|
||||||
pub position: [f32; 3],
|
pub position: [f32; 3],
|
||||||
@@ -135,7 +135,7 @@ impl std::error::Error for TerrainError {}
|
|||||||
|
|
||||||
/// Surface query.
|
/// Surface query.
|
||||||
pub trait SurfaceQuery {
|
pub trait SurfaceQuery {
|
||||||
/// Height at position.
|
/// Source Z height at an XY ground position.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
@@ -157,7 +157,7 @@ pub trait SurfaceQuery {
|
|||||||
|
|
||||||
/// Navigation query.
|
/// Navigation query.
|
||||||
pub trait NavigationQuery {
|
pub trait NavigationQuery {
|
||||||
/// Locate areal.
|
/// Locates an areal from the XY components of a world position.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
@@ -195,7 +195,7 @@ impl TerrainWorld {
|
|||||||
polygon: areal
|
polygon: areal
|
||||||
.vertices
|
.vertices
|
||||||
.iter()
|
.iter()
|
||||||
.map(|vertex| [vertex[0], vertex[2]])
|
.map(|vertex| [vertex[0], vertex[1]])
|
||||||
.collect(),
|
.collect(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -302,7 +302,7 @@ impl TerrainWorld {
|
|||||||
position: [f32; 3],
|
position: [f32; 3],
|
||||||
candidates: &[ArealId],
|
candidates: &[ArealId],
|
||||||
) -> Result<Option<ArealId>, TerrainError> {
|
) -> Result<Option<ArealId>, TerrainError> {
|
||||||
let point = [position[0], position[2]];
|
let point = [position[0], position[1]];
|
||||||
for candidate in candidates {
|
for candidate in candidates {
|
||||||
let Some(areal) = usize::try_from(candidate.0)
|
let Some(areal) = usize::try_from(candidate.0)
|
||||||
.ok()
|
.ok()
|
||||||
@@ -427,7 +427,7 @@ impl SurfaceQuery for TerrainWorld {
|
|||||||
origin[2] + direction[2] * distance,
|
origin[2] + direction[2] * distance,
|
||||||
];
|
];
|
||||||
best = Some(SurfaceHit {
|
best = Some(SurfaceHit {
|
||||||
height: position[1],
|
height: position[2],
|
||||||
position,
|
position,
|
||||||
distance,
|
distance,
|
||||||
face: triangle.face,
|
face: triangle.face,
|
||||||
@@ -468,18 +468,18 @@ struct RuntimeTriangle {
|
|||||||
|
|
||||||
impl RuntimeTriangle {
|
impl RuntimeTriangle {
|
||||||
fn height_at(&self, position: [f32; 2]) -> Option<f32> {
|
fn height_at(&self, position: [f32; 2]) -> Option<f32> {
|
||||||
let a = [self.vertices[0][0], self.vertices[0][2]];
|
let a = [self.vertices[0][0], self.vertices[0][1]];
|
||||||
let b = [self.vertices[1][0], self.vertices[1][2]];
|
let b = [self.vertices[1][0], self.vertices[1][1]];
|
||||||
let c = [self.vertices[2][0], self.vertices[2][2]];
|
let c = [self.vertices[2][0], self.vertices[2][1]];
|
||||||
let weights = barycentric_2d(position, a, b, c)?;
|
let weights = barycentric_2d(position, a, b, c)?;
|
||||||
if weights
|
if weights
|
||||||
.iter()
|
.iter()
|
||||||
.all(|weight| *weight >= -1.0e-4 && *weight <= 1.0001)
|
.all(|weight| *weight >= -1.0e-4 && *weight <= 1.0001)
|
||||||
{
|
{
|
||||||
Some(
|
Some(
|
||||||
weights[0] * self.vertices[0][1]
|
weights[0] * self.vertices[0][2]
|
||||||
+ weights[1] * self.vertices[1][1]
|
+ weights[1] * self.vertices[1][2]
|
||||||
+ weights[2] * self.vertices[2][1],
|
+ weights[2] * self.vertices[2][2],
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -570,9 +570,9 @@ impl RuntimeGrid {
|
|||||||
for areal in &map.areals {
|
for areal in &map.areals {
|
||||||
for vertex in &areal.vertices {
|
for vertex in &areal.vertices {
|
||||||
bounds_min[0] = bounds_min[0].min(vertex[0]);
|
bounds_min[0] = bounds_min[0].min(vertex[0]);
|
||||||
bounds_min[1] = bounds_min[1].min(vertex[2]);
|
bounds_min[1] = bounds_min[1].min(vertex[1]);
|
||||||
bounds_max[0] = bounds_max[0].max(vertex[0]);
|
bounds_max[0] = bounds_max[0].max(vertex[0]);
|
||||||
bounds_max[1] = bounds_max[1].max(vertex[2]);
|
bounds_max[1] = bounds_max[1].max(vertex[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !bounds_min[0].is_finite()
|
if !bounds_min[0].is_finite()
|
||||||
@@ -622,7 +622,7 @@ impl RuntimeGrid {
|
|||||||
if self.cells_x == 0 || self.cells_y == 0 || self.cells.is_empty() {
|
if self.cells_x == 0 || self.cells_y == 0 || self.cells.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let point = [position[0], position[2]];
|
let point = [position[0], position[1]];
|
||||||
if point[0] < self.min[0]
|
if point[0] < self.min[0]
|
||||||
|| point[0] > self.max[0]
|
|| point[0] > self.max[0]
|
||||||
|| point[1] < self.min[1]
|
|| point[1] < self.min[1]
|
||||||
@@ -770,18 +770,18 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(world.areal_count(), 2);
|
assert_eq!(world.areal_count(), 2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
world.locate_areal([0.25, 0.0, 0.25]).expect("locate"),
|
world.locate_areal([0.25, 0.25, 41.0]).expect("locate"),
|
||||||
Some(ArealId(0))
|
Some(ArealId(0))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
world.locate_areal([1.75, 0.0, 0.25]).expect("locate"),
|
world.locate_areal([1.75, 0.25, -12.0]).expect("locate"),
|
||||||
Some(ArealId(1))
|
Some(ArealId(1))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
world
|
world
|
||||||
.route(RouteRequest {
|
.route(RouteRequest {
|
||||||
start: [0.25, 0.0, 0.25],
|
start: [0.25, 0.25, 0.0],
|
||||||
goal: [1.75, 0.0, 0.25],
|
goal: [1.75, 0.25, 0.0],
|
||||||
})
|
})
|
||||||
.expect("route"),
|
.expect("route"),
|
||||||
Some(ArealRoute {
|
Some(ArealRoute {
|
||||||
@@ -797,8 +797,8 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
world
|
world
|
||||||
.route(RouteRequest {
|
.route(RouteRequest {
|
||||||
start: [10.0, 0.0, 10.0],
|
start: [10.0, 10.0, 0.0],
|
||||||
goal: [1.75, 0.0, 0.25],
|
goal: [1.75, 0.25, 0.0],
|
||||||
})
|
})
|
||||||
.expect("route"),
|
.expect("route"),
|
||||||
None
|
None
|
||||||
@@ -815,8 +815,8 @@ mod tests {
|
|||||||
|
|
||||||
let hit = world
|
let hit = world
|
||||||
.raycast(
|
.raycast(
|
||||||
[0.25, 2.0, 0.25],
|
[0.25, 0.25, 2.0],
|
||||||
[0.0, -1.0, 0.0],
|
[0.0, 0.0, -1.0],
|
||||||
FullSurfaceMask(0x0000_0001),
|
FullSurfaceMask(0x0000_0001),
|
||||||
)
|
)
|
||||||
.expect("raycast")
|
.expect("raycast")
|
||||||
@@ -828,8 +828,8 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
world
|
world
|
||||||
.raycast(
|
.raycast(
|
||||||
[0.25, 2.0, 0.25],
|
[0.25, 0.25, 2.0],
|
||||||
[0.0, -1.0, 0.0],
|
[0.0, 0.0, -1.0],
|
||||||
FullSurfaceMask(0x8000_0000)
|
FullSurfaceMask(0x8000_0000)
|
||||||
)
|
)
|
||||||
.expect("raycast"),
|
.expect("raycast"),
|
||||||
@@ -942,9 +942,9 @@ mod tests {
|
|||||||
},
|
},
|
||||||
positions: vec![
|
positions: vec![
|
||||||
[0.0, 0.0, 0.0],
|
[0.0, 0.0, 0.0],
|
||||||
[1.0, 1.0, 0.0],
|
[1.0, 0.0, 1.0],
|
||||||
[0.0, 1.0, 1.0],
|
[0.0, 1.0, 1.0],
|
||||||
[1.0, 2.0, 1.0],
|
[1.0, 1.0, 2.0],
|
||||||
],
|
],
|
||||||
normals: Vec::new(),
|
normals: Vec::new(),
|
||||||
uv0: Vec::new(),
|
uv0: Vec::new(),
|
||||||
@@ -987,10 +987,10 @@ mod tests {
|
|||||||
areals: vec![
|
areals: vec![
|
||||||
Areal {
|
Areal {
|
||||||
prefix_raw: [0; 56],
|
prefix_raw: [0; 56],
|
||||||
anchor: [0.5, 0.0, 0.5],
|
anchor: [0.5, 0.5, 0.0],
|
||||||
reserved_12: 0.0,
|
reserved_12: 0.0,
|
||||||
area_metric: 1.0,
|
area_metric: 1.0,
|
||||||
normal: [0.0, 1.0, 0.0],
|
normal: [0.0, 0.0, 1.0],
|
||||||
logic_flag: 0,
|
logic_flag: 0,
|
||||||
reserved_36: 0,
|
reserved_36: 0,
|
||||||
class_id: 0,
|
class_id: 0,
|
||||||
@@ -998,8 +998,8 @@ mod tests {
|
|||||||
vertices: vec![
|
vertices: vec![
|
||||||
[0.0, 0.0, 0.0],
|
[0.0, 0.0, 0.0],
|
||||||
[1.0, 0.0, 0.0],
|
[1.0, 0.0, 0.0],
|
||||||
[1.0, 0.0, 1.0],
|
[1.0, 1.0, 0.0],
|
||||||
[0.0, 0.0, 1.0],
|
[0.0, 1.0, 0.0],
|
||||||
],
|
],
|
||||||
links: vec![
|
links: vec![
|
||||||
EdgeLink {
|
EdgeLink {
|
||||||
@@ -1031,10 +1031,10 @@ mod tests {
|
|||||||
},
|
},
|
||||||
Areal {
|
Areal {
|
||||||
prefix_raw: [0; 56],
|
prefix_raw: [0; 56],
|
||||||
anchor: [1.5, 0.0, 0.5],
|
anchor: [1.5, 0.5, 0.0],
|
||||||
reserved_12: 0.0,
|
reserved_12: 0.0,
|
||||||
area_metric: 1.0,
|
area_metric: 1.0,
|
||||||
normal: [0.0, 1.0, 0.0],
|
normal: [0.0, 0.0, 1.0],
|
||||||
logic_flag: 0,
|
logic_flag: 0,
|
||||||
reserved_36: 0,
|
reserved_36: 0,
|
||||||
class_id: 0,
|
class_id: 0,
|
||||||
@@ -1042,8 +1042,8 @@ mod tests {
|
|||||||
vertices: vec![
|
vertices: vec![
|
||||||
[1.0, 0.0, 0.0],
|
[1.0, 0.0, 0.0],
|
||||||
[2.0, 0.0, 0.0],
|
[2.0, 0.0, 0.0],
|
||||||
[2.0, 0.0, 1.0],
|
[2.0, 1.0, 0.0],
|
||||||
[1.0, 0.0, 1.0],
|
[1.0, 1.0, 0.0],
|
||||||
],
|
],
|
||||||
links: vec![
|
links: vec![
|
||||||
EdgeLink {
|
EdgeLink {
|
||||||
|
|||||||
@@ -1038,6 +1038,14 @@ to 120 seconds but remained at the existing `Graph` loading checkpoint and were
|
|||||||
terminated without a native frame/report; therefore no new readback hash or GPU
|
terminated without a native frame/report; therefore no new readback hash or GPU
|
||||||
acceptance result is claimed for the corrected projection yet.
|
acceptance result is claimed for the corrected projection yet.
|
||||||
|
|
||||||
|
The same source-axis proof now applies to `TerrainWorld`: its `Land.msh` surface
|
||||||
|
height query and `Land.map` areal/grid lookup use XY ground coordinates, return
|
||||||
|
source Z height, and leave raycasts as full 3D intersections. The Part 1 and
|
||||||
|
Part 2 licensed `Land.map` gate successfully locates sampled polygon vertices
|
||||||
|
under that contract. This closes the former XZ mismatch in terrain queries; it
|
||||||
|
does not establish object orientation, physics/gravity, original culling or the
|
||||||
|
camera matrix.
|
||||||
|
|
||||||
### 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