feat(terrain): build BAS basement foundations
Docs Deploy / Build and Deploy MkDocs (push) Successful in 31s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 31s
This commit is contained in:
4 files changed
+2970
-24
No files matched your search
@@ -0,0 +1,809 @@
|
|||||||
|
/// One triangle fragment retained after subtracting a building contour.
|
||||||
|
/// `source_triangle` indexes the triangle array passed to
|
||||||
|
/// [`subtract_contour`].
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub(crate) struct TerrainTriangleFragment {
|
||||||
|
/// Source triangle index, for preserving its material and vertex channels.
|
||||||
|
pub source_triangle: usize,
|
||||||
|
/// Fragment triangle vertices in the input coordinate space.
|
||||||
|
pub positions: [[f32; 3]; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error returned when a terrain contour cannot be subtracted safely.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) enum TerrainContourError {
|
||||||
|
/// The contour is non-finite, degenerate, or self-intersecting.
|
||||||
|
UnsupportedContour,
|
||||||
|
/// A source triangle has non-finite coordinates or a degenerate XY plane.
|
||||||
|
InvalidSourceTriangle {
|
||||||
|
/// Index of the source triangle.
|
||||||
|
triangle: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TerrainContourError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::UnsupportedContour => write!(
|
||||||
|
f,
|
||||||
|
"terrain contour must be finite, simple, and non-degenerate"
|
||||||
|
),
|
||||||
|
Self::InvalidSourceTriangle { triangle } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"terrain source triangle {triangle} is not a finite XY surface"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for TerrainContourError {}
|
||||||
|
|
||||||
|
/// Subtracts a finite simple contour using the native per-source-face point set:
|
||||||
|
/// the three source vertices plus the actual contour segments clipped to that
|
||||||
|
/// source triangle. It does not extend cuts across the triangle or triangulate
|
||||||
|
/// the contour into unrelated internal edges.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`TerrainContourError::UnsupportedContour`] for malformed or
|
||||||
|
/// self-intersecting contours, and
|
||||||
|
/// [`TerrainContourError::InvalidSourceTriangle`] when a source face cannot be
|
||||||
|
/// triangulated or lifted onto its original plane.
|
||||||
|
pub(crate) fn subtract_contour(
|
||||||
|
triangles: &[[[f32; 3]; 3]],
|
||||||
|
contour: &[[f32; 2]],
|
||||||
|
) -> Result<Vec<TerrainTriangleFragment>, TerrainContourError> {
|
||||||
|
let ring = normalize_ring(contour)?;
|
||||||
|
let contour_min: [f64; 2] = std::array::from_fn(|axis| {
|
||||||
|
ring.iter()
|
||||||
|
.map(|point| point[axis])
|
||||||
|
.fold(f64::INFINITY, f64::min)
|
||||||
|
});
|
||||||
|
let contour_max: [f64; 2] = std::array::from_fn(|axis| {
|
||||||
|
ring.iter()
|
||||||
|
.map(|point| point[axis])
|
||||||
|
.fold(f64::NEG_INFINITY, f64::max)
|
||||||
|
});
|
||||||
|
let mut output = Vec::new();
|
||||||
|
|
||||||
|
for (source_triangle, source) in triangles.iter().copied().enumerate() {
|
||||||
|
if !source.iter().flatten().all(|value| value.is_finite()) {
|
||||||
|
return Err(TerrainContourError::InvalidSourceTriangle {
|
||||||
|
triangle: source_triangle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let source_xy = source.map(|point| [f64::from(point[0]), f64::from(point[1])]);
|
||||||
|
let source_area = cross2(source_xy[0], source_xy[1], source_xy[2]);
|
||||||
|
if source_area.abs() <= 1.0e-10 {
|
||||||
|
return Err(TerrainContourError::InvalidSourceTriangle {
|
||||||
|
triangle: source_triangle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let source_min: [f64; 2] = std::array::from_fn(|axis| {
|
||||||
|
source_xy
|
||||||
|
.iter()
|
||||||
|
.map(|point| point[axis])
|
||||||
|
.fold(f64::INFINITY, f64::min)
|
||||||
|
});
|
||||||
|
let source_max: [f64; 2] = std::array::from_fn(|axis| {
|
||||||
|
source_xy
|
||||||
|
.iter()
|
||||||
|
.map(|point| point[axis])
|
||||||
|
.fold(f64::NEG_INFINITY, f64::max)
|
||||||
|
});
|
||||||
|
if (0..2).any(|axis| {
|
||||||
|
source_max[axis] < contour_min[axis] || contour_max[axis] < source_min[axis]
|
||||||
|
}) {
|
||||||
|
output.push(TerrainTriangleFragment {
|
||||||
|
source_triangle,
|
||||||
|
positions: source,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut vertices = source_xy.to_vec();
|
||||||
|
let initial = if source_area > 0.0 {
|
||||||
|
[0, 1, 2]
|
||||||
|
} else {
|
||||||
|
[0, 2, 1]
|
||||||
|
};
|
||||||
|
let mut triangulation = vec![initial];
|
||||||
|
let mut clipped_segments = Vec::new();
|
||||||
|
for index in 0..ring.len() {
|
||||||
|
let first = ring[index];
|
||||||
|
let second = ring[(index + 1) % ring.len()];
|
||||||
|
if let Some((start, end)) = clip_segment_to_triangle(first, second, source_xy) {
|
||||||
|
let start_id = insert_unique_point(&mut vertices, start);
|
||||||
|
let end_id = insert_unique_point(&mut vertices, end);
|
||||||
|
if start_id != end_id {
|
||||||
|
clipped_segments.push((start_id, end_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if clipped_segments.is_empty() {
|
||||||
|
let center = [
|
||||||
|
(source_xy[0][0] + source_xy[1][0] + source_xy[2][0]) / 3.0,
|
||||||
|
(source_xy[0][1] + source_xy[1][1] + source_xy[2][1]) / 3.0,
|
||||||
|
];
|
||||||
|
if !point_in_ring(center, &ring) {
|
||||||
|
output.push(TerrainTriangleFragment {
|
||||||
|
source_triangle,
|
||||||
|
positions: source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for point_id in 3..vertices.len() {
|
||||||
|
insert_triangulation_point(point_id, &vertices, &mut triangulation).map_err(|()| {
|
||||||
|
TerrainContourError::InvalidSourceTriangle {
|
||||||
|
triangle: source_triangle,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut constraints = source_boundary_constraints(&vertices, source_xy);
|
||||||
|
for (first, second) in clipped_segments {
|
||||||
|
let mut on_segment = vertices
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, point)| {
|
||||||
|
point_on_segment(point, vertices[first], vertices[second]).then_some((
|
||||||
|
segment_parameter(point, vertices[first], vertices[second]),
|
||||||
|
index,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
on_segment.sort_by(|left, right| left.0.total_cmp(&right.0));
|
||||||
|
on_segment.dedup_by_key(|(_, index)| *index);
|
||||||
|
for pair in on_segment.windows(2) {
|
||||||
|
let edge = canonical_edge(pair[0].1, pair[1].1);
|
||||||
|
if edge.0 == edge.1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
recover_constraint(edge, &mut triangulation, &vertices, &constraints).map_err(
|
||||||
|
|()| TerrainContourError::InvalidSourceTriangle {
|
||||||
|
triangle: source_triangle,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
constraints.insert(edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
legalize_constrained_delaunay(&mut triangulation, &vertices, &constraints).map_err(
|
||||||
|
|()| TerrainContourError::InvalidSourceTriangle {
|
||||||
|
triangle: source_triangle,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for triangle in triangulation {
|
||||||
|
let center = triangle.map(|index| vertices[index]);
|
||||||
|
let center = [
|
||||||
|
(center[0][0] + center[1][0] + center[2][0]) / 3.0,
|
||||||
|
(center[0][1] + center[1][1] + center[2][1]) / 3.0,
|
||||||
|
];
|
||||||
|
if point_in_ring(center, &ring) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut positions: [[f32; 3]; 3] = triangle
|
||||||
|
.map(|index| lift_to_source_plane(vertices[index], source))
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Option<Vec<_>>>()
|
||||||
|
.and_then(|points| points.try_into().ok())
|
||||||
|
.ok_or(TerrainContourError::InvalidSourceTriangle {
|
||||||
|
triangle: source_triangle,
|
||||||
|
})?;
|
||||||
|
if source_area < 0.0 {
|
||||||
|
positions.swap(1, 2);
|
||||||
|
}
|
||||||
|
output.push(TerrainTriangleFragment {
|
||||||
|
source_triangle,
|
||||||
|
positions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_ring(contour: &[[f32; 2]]) -> Result<Vec<[f64; 2]>, TerrainContourError> {
|
||||||
|
let mut ring = contour
|
||||||
|
.iter()
|
||||||
|
.map(|point| [f64::from(point[0]), f64::from(point[1])])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if ring.iter().flatten().any(|value| !value.is_finite()) {
|
||||||
|
return Err(TerrainContourError::UnsupportedContour);
|
||||||
|
}
|
||||||
|
if ring.len() > 1 && distance_squared(ring[0], *ring.last().expect("nonempty")) <= 1.0e-16 {
|
||||||
|
ring.pop();
|
||||||
|
}
|
||||||
|
let mut normalized = Vec::with_capacity(ring.len());
|
||||||
|
for point in ring {
|
||||||
|
if normalized
|
||||||
|
.last()
|
||||||
|
.is_none_or(|previous| distance_squared(*previous, point) > 1.0e-16)
|
||||||
|
{
|
||||||
|
normalized.push(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if normalized.len() > 1
|
||||||
|
&& distance_squared(normalized[0], *normalized.last().expect("nonempty")) <= 1.0e-16
|
||||||
|
{
|
||||||
|
normalized.pop();
|
||||||
|
}
|
||||||
|
if normalized.len() < 3 {
|
||||||
|
return Err(TerrainContourError::UnsupportedContour);
|
||||||
|
}
|
||||||
|
let signed_area = polygon_area(&normalized);
|
||||||
|
if !signed_area.is_finite() || signed_area.abs() <= 1.0e-10 {
|
||||||
|
return Err(TerrainContourError::UnsupportedContour);
|
||||||
|
}
|
||||||
|
for first in 0..normalized.len() {
|
||||||
|
let first_next = (first + 1) % normalized.len();
|
||||||
|
for second in (first + 1)..normalized.len() {
|
||||||
|
let second_next = (second + 1) % normalized.len();
|
||||||
|
if first_next == second || second_next == first {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if segments_intersect(
|
||||||
|
normalized[first],
|
||||||
|
normalized[first_next],
|
||||||
|
normalized[second],
|
||||||
|
normalized[second_next],
|
||||||
|
) {
|
||||||
|
return Err(TerrainContourError::UnsupportedContour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if signed_area < 0.0 {
|
||||||
|
normalized.reverse();
|
||||||
|
}
|
||||||
|
Ok(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clip_segment_to_triangle(
|
||||||
|
first: [f64; 2],
|
||||||
|
second: [f64; 2],
|
||||||
|
triangle: [[f64; 2]; 3],
|
||||||
|
) -> Option<([f64; 2], [f64; 2])> {
|
||||||
|
let mut triangle = triangle;
|
||||||
|
if cross2(triangle[0], triangle[1], triangle[2]) < 0.0 {
|
||||||
|
triangle.swap(1, 2);
|
||||||
|
}
|
||||||
|
let mut start = 0.0_f64;
|
||||||
|
let mut end = 1.0_f64;
|
||||||
|
for edge in 0..3 {
|
||||||
|
let edge_first = triangle[edge];
|
||||||
|
let edge_second = triangle[(edge + 1) % 3];
|
||||||
|
let at_start = cross2(edge_first, edge_second, first);
|
||||||
|
let at_end = cross2(edge_first, edge_second, second);
|
||||||
|
let change = at_end - at_start;
|
||||||
|
if change.abs() <= 1.0e-14 {
|
||||||
|
if at_start < -1.0e-10 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let crossing = -at_start / change;
|
||||||
|
if change > 0.0 {
|
||||||
|
start = start.max(crossing);
|
||||||
|
} else {
|
||||||
|
end = end.min(crossing);
|
||||||
|
}
|
||||||
|
if start > end + 1.0e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
start = start.clamp(0.0, 1.0);
|
||||||
|
end = end.clamp(0.0, 1.0);
|
||||||
|
if end - start <= 1.0e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((
|
||||||
|
interpolate(first, second, start),
|
||||||
|
interpolate(first, second, end),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_unique_point(vertices: &mut Vec<[f64; 2]>, point: [f64; 2]) -> usize {
|
||||||
|
// Terrain positions are f32 throughout the native data and renderer. Make
|
||||||
|
// clipped crossings use that same representable coordinate before building
|
||||||
|
// the local CDT, so distinct f64 intersections cannot collapse to one XY
|
||||||
|
// vertex only after fragment emission.
|
||||||
|
let point = [f64::from(point[0] as f32), f64::from(point[1] as f32)];
|
||||||
|
if let Some(index) = vertices
|
||||||
|
.iter()
|
||||||
|
.position(|candidate| distance_squared(*candidate, point) <= 1.0e-18)
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
let index = vertices.len();
|
||||||
|
vertices.push(point);
|
||||||
|
index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_triangulation_point(
|
||||||
|
point_id: usize,
|
||||||
|
vertices: &[[f64; 2]],
|
||||||
|
triangles: &mut Vec<[usize; 3]>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let point = vertices[point_id];
|
||||||
|
let edge = triangulation_edges(triangles)
|
||||||
|
.keys()
|
||||||
|
.copied()
|
||||||
|
.find(|&(first, second)| point_on_open_segment(point, vertices[first], vertices[second]));
|
||||||
|
if let Some(edge) = edge {
|
||||||
|
let mut split = Vec::new();
|
||||||
|
triangles.retain(|triangle| {
|
||||||
|
if triangle_has_edge(*triangle, edge) {
|
||||||
|
let opposite = triangle
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|index| *index != edge.0 && *index != edge.1)
|
||||||
|
.expect("triangle has opposite vertex");
|
||||||
|
split.push(opposite);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if split.is_empty() {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
for opposite in split {
|
||||||
|
let first = make_ccw([edge.0, point_id, opposite], vertices).ok_or(())?;
|
||||||
|
let second = make_ccw([point_id, edge.1, opposite], vertices).ok_or(())?;
|
||||||
|
triangles.extend([first, second]);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let containing = triangles.iter().position(|triangle| {
|
||||||
|
point_in_triangle(
|
||||||
|
point,
|
||||||
|
vertices[triangle[0]],
|
||||||
|
vertices[triangle[1]],
|
||||||
|
vertices[triangle[2]],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let Some(containing) = containing else {
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
|
let triangle = triangles.swap_remove(containing);
|
||||||
|
for edge in 0..3 {
|
||||||
|
let split = make_ccw(
|
||||||
|
[triangle[edge], triangle[(edge + 1) % 3], point_id],
|
||||||
|
vertices,
|
||||||
|
)
|
||||||
|
.ok_or(())?;
|
||||||
|
triangles.push(split);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_boundary_constraints(
|
||||||
|
vertices: &[[f64; 2]],
|
||||||
|
triangle: [[f64; 2]; 3],
|
||||||
|
) -> std::collections::BTreeSet<(usize, usize)> {
|
||||||
|
let mut constraints = std::collections::BTreeSet::new();
|
||||||
|
for edge in 0..3 {
|
||||||
|
let first = triangle[edge];
|
||||||
|
let second = triangle[(edge + 1) % 3];
|
||||||
|
let mut points = vertices
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, point)| {
|
||||||
|
point_on_segment(point, first, second)
|
||||||
|
.then_some((segment_parameter(point, first, second), index))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
points.sort_by(|left, right| left.0.total_cmp(&right.0));
|
||||||
|
points.dedup_by_key(|(_, index)| *index);
|
||||||
|
for pair in points.windows(2) {
|
||||||
|
constraints.insert(canonical_edge(pair[0].1, pair[1].1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
constraints
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recover_constraint(
|
||||||
|
target: (usize, usize),
|
||||||
|
triangles: &mut [[usize; 3]],
|
||||||
|
vertices: &[[f64; 2]],
|
||||||
|
constraints: &std::collections::BTreeSet<(usize, usize)>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let limit = vertices
|
||||||
|
.len()
|
||||||
|
.saturating_mul(vertices.len())
|
||||||
|
.saturating_mul(8)
|
||||||
|
.max(1);
|
||||||
|
for _ in 0..limit {
|
||||||
|
if edge_exists(triangles, target) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut crossed = triangulation_edges(triangles)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(edge, adjacent)| {
|
||||||
|
if adjacent.len() != 2
|
||||||
|
|| edge.0 == target.0
|
||||||
|
|| edge.0 == target.1
|
||||||
|
|| edge.1 == target.0
|
||||||
|
|| edge.1 == target.1
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
proper_intersection_parameter(
|
||||||
|
vertices[target.0],
|
||||||
|
vertices[target.1],
|
||||||
|
vertices[edge.0],
|
||||||
|
vertices[edge.1],
|
||||||
|
)
|
||||||
|
.map(|parameter| (parameter, edge, adjacent))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
crossed.sort_by(|left, right| left.0.total_cmp(&right.0));
|
||||||
|
let mut flipped = false;
|
||||||
|
for (_, edge, adjacent) in crossed {
|
||||||
|
if constraints.contains(&edge) {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let first_index = adjacent[0].0;
|
||||||
|
let second_index = adjacent[1].0;
|
||||||
|
let first_opposite = adjacent[0].1;
|
||||||
|
let second_opposite = adjacent[1].1;
|
||||||
|
if !convex_quadrilateral(
|
||||||
|
vertices[edge.0],
|
||||||
|
vertices[edge.1],
|
||||||
|
vertices[first_opposite],
|
||||||
|
vertices[second_opposite],
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let replacement = canonical_edge(first_opposite, second_opposite);
|
||||||
|
if constraints.contains(&replacement)
|
||||||
|
|| proper_intersection_parameter(
|
||||||
|
vertices[target.0],
|
||||||
|
vertices[target.1],
|
||||||
|
vertices[replacement.0],
|
||||||
|
vertices[replacement.1],
|
||||||
|
)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (a, b) = directed_edge(triangles[first_index], edge).ok_or(())?;
|
||||||
|
triangles[first_index] =
|
||||||
|
make_ccw([first_opposite, second_opposite, b], vertices).ok_or(())?;
|
||||||
|
triangles[second_index] =
|
||||||
|
make_ccw([second_opposite, first_opposite, a], vertices).ok_or(())?;
|
||||||
|
flipped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !flipped {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn legalize_constrained_delaunay(
|
||||||
|
triangles: &mut [[usize; 3]],
|
||||||
|
vertices: &[[f64; 2]],
|
||||||
|
constraints: &std::collections::BTreeSet<(usize, usize)>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let limit = triangles
|
||||||
|
.len()
|
||||||
|
.saturating_mul(triangles.len())
|
||||||
|
.saturating_mul(8)
|
||||||
|
.max(1);
|
||||||
|
for _ in 0..limit {
|
||||||
|
let edges = triangulation_edges(triangles);
|
||||||
|
let mut flipped = false;
|
||||||
|
for (edge, adjacent) in edges {
|
||||||
|
if adjacent.len() != 2 || constraints.contains(&edge) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let first_index = adjacent[0].0;
|
||||||
|
let second_index = adjacent[1].0;
|
||||||
|
let c = adjacent[0].1;
|
||||||
|
let d = adjacent[1].1;
|
||||||
|
if !convex_quadrilateral(vertices[edge.0], vertices[edge.1], vertices[c], vertices[d]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (a, b) = directed_edge(triangles[first_index], edge).ok_or(())?;
|
||||||
|
if in_circle(vertices[a], vertices[b], vertices[c], vertices[d]) <= 1.0e-10 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let replacement = canonical_edge(c, d);
|
||||||
|
if constraints.iter().any(|constraint| {
|
||||||
|
proper_intersection_parameter(
|
||||||
|
vertices[replacement.0],
|
||||||
|
vertices[replacement.1],
|
||||||
|
vertices[constraint.0],
|
||||||
|
vertices[constraint.1],
|
||||||
|
)
|
||||||
|
.is_some()
|
||||||
|
}) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
triangles[first_index] = make_ccw([c, d, b], vertices).ok_or(())?;
|
||||||
|
triangles[second_index] = make_ccw([d, c, a], vertices).ok_or(())?;
|
||||||
|
flipped = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !flipped {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn triangulation_edges(
|
||||||
|
triangles: &[[usize; 3]],
|
||||||
|
) -> std::collections::BTreeMap<(usize, usize), Vec<(usize, usize)>> {
|
||||||
|
let mut edges = std::collections::BTreeMap::new();
|
||||||
|
for (triangle_index, triangle) in triangles.iter().copied().enumerate() {
|
||||||
|
for edge in 0..3 {
|
||||||
|
let first = triangle[edge];
|
||||||
|
let second = triangle[(edge + 1) % 3];
|
||||||
|
let opposite = triangle[(edge + 2) % 3];
|
||||||
|
edges
|
||||||
|
.entry(canonical_edge(first, second))
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push((triangle_index, opposite));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edges
|
||||||
|
}
|
||||||
|
|
||||||
|
fn triangle_has_edge(triangle: [usize; 3], edge: (usize, usize)) -> bool {
|
||||||
|
triangle.contains(&edge.0) && triangle.contains(&edge.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edge_exists(triangles: &[[usize; 3]], edge: (usize, usize)) -> bool {
|
||||||
|
triangles
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.any(|triangle| triangle_has_edge(triangle, edge))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn directed_edge(triangle: [usize; 3], edge: (usize, usize)) -> Option<(usize, usize)> {
|
||||||
|
(0..3).find_map(|index| {
|
||||||
|
let first = triangle[index];
|
||||||
|
let second = triangle[(index + 1) % 3];
|
||||||
|
(canonical_edge(first, second) == edge).then_some((first, second))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn canonical_edge(first: usize, second: usize) -> (usize, usize) {
|
||||||
|
if first < second {
|
||||||
|
(first, second)
|
||||||
|
} else {
|
||||||
|
(second, first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_ccw(triangle: [usize; 3], vertices: &[[f64; 2]]) -> Option<[usize; 3]> {
|
||||||
|
let area = cross2(
|
||||||
|
vertices[triangle[0]],
|
||||||
|
vertices[triangle[1]],
|
||||||
|
vertices[triangle[2]],
|
||||||
|
);
|
||||||
|
if !area.is_finite() || area.abs() <= 1.0e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(if area > 0.0 {
|
||||||
|
triangle
|
||||||
|
} else {
|
||||||
|
[triangle[0], triangle[2], triangle[1]]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_in_triangle(point: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
|
||||||
|
let edges = [(a, b), (b, c), (c, a)];
|
||||||
|
edges.into_iter().all(|(first, second)| {
|
||||||
|
let scale = distance_squared(first, second).sqrt();
|
||||||
|
cross2(first, second, point) >= -linear_tolerance(first, second, point) * scale
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_on_open_segment(point: [f64; 2], first: [f64; 2], second: [f64; 2]) -> bool {
|
||||||
|
if !point_on_segment(point, first, second) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let t = segment_parameter(point, first, second);
|
||||||
|
t > 1.0e-9 && t < 1.0 - 1.0e-9
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_on_segment(point: [f64; 2], first: [f64; 2], second: [f64; 2]) -> bool {
|
||||||
|
let length_squared = distance_squared(first, second);
|
||||||
|
if length_squared <= 1.0e-20 {
|
||||||
|
return distance_squared(point, first) <= 1.0e-18;
|
||||||
|
}
|
||||||
|
let cross = cross2(first, second, point).abs();
|
||||||
|
let dot = (point[0] - first[0]) * (second[0] - first[0])
|
||||||
|
+ (point[1] - first[1]) * (second[1] - first[1]);
|
||||||
|
let tolerance = linear_tolerance(first, second, point);
|
||||||
|
let length = length_squared.sqrt();
|
||||||
|
cross <= tolerance * length
|
||||||
|
&& dot >= -tolerance * length
|
||||||
|
&& dot <= length_squared + tolerance * length
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linear_tolerance(first: [f64; 2], second: [f64; 2], point: [f64; 2]) -> f64 {
|
||||||
|
let coordinate_scale = first
|
||||||
|
.into_iter()
|
||||||
|
.chain(second)
|
||||||
|
.chain(point)
|
||||||
|
.map(f64::abs)
|
||||||
|
.fold(1.0_f64, f64::max);
|
||||||
|
2.0 * f64::from(f32::EPSILON) * coordinate_scale
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segment_parameter(point: [f64; 2], first: [f64; 2], second: [f64; 2]) -> f64 {
|
||||||
|
let delta = [second[0] - first[0], second[1] - first[1]];
|
||||||
|
let length_squared = delta[0] * delta[0] + delta[1] * delta[1];
|
||||||
|
if length_squared <= 1.0e-20 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
((point[0] - first[0]) * delta[0] + (point[1] - first[1]) * delta[1]) / length_squared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proper_intersection_parameter(
|
||||||
|
first: [f64; 2],
|
||||||
|
second: [f64; 2],
|
||||||
|
edge_first: [f64; 2],
|
||||||
|
edge_second: [f64; 2],
|
||||||
|
) -> Option<f64> {
|
||||||
|
let first_delta = [second[0] - first[0], second[1] - first[1]];
|
||||||
|
let edge_delta = [
|
||||||
|
edge_second[0] - edge_first[0],
|
||||||
|
edge_second[1] - edge_first[1],
|
||||||
|
];
|
||||||
|
let denominator = first_delta[0] * edge_delta[1] - first_delta[1] * edge_delta[0];
|
||||||
|
if denominator.abs() <= 1.0e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let offset = [edge_first[0] - first[0], edge_first[1] - first[1]];
|
||||||
|
let t = (offset[0] * edge_delta[1] - offset[1] * edge_delta[0]) / denominator;
|
||||||
|
let u = (offset[0] * first_delta[1] - offset[1] * first_delta[0]) / denominator;
|
||||||
|
(t > 1.0e-9 && t < 1.0 - 1.0e-9 && u > 1.0e-9 && u < 1.0 - 1.0e-9).then_some(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convex_quadrilateral(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> bool {
|
||||||
|
cross2(a, b, c) * cross2(a, b, d) < -1.0e-12 && cross2(c, d, a) * cross2(c, d, b) < -1.0e-12
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_circle(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> f64 {
|
||||||
|
let a = [a[0] - d[0], a[1] - d[1]];
|
||||||
|
let b = [b[0] - d[0], b[1] - d[1]];
|
||||||
|
let c = [c[0] - d[0], c[1] - d[1]];
|
||||||
|
let a2 = a[0] * a[0] + a[1] * a[1];
|
||||||
|
let b2 = b[0] * b[0] + b[1] * b[1];
|
||||||
|
let c2 = c[0] * c[0] + c[1] * c[1];
|
||||||
|
a2 * (b[0] * c[1] - b[1] * c[0]) - b2 * (a[0] * c[1] - a[1] * c[0])
|
||||||
|
+ c2 * (a[0] * b[1] - a[1] * b[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn segments_intersect(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> bool {
|
||||||
|
let ab_c = cross2(a, b, c);
|
||||||
|
let ab_d = cross2(a, b, d);
|
||||||
|
let cd_a = cross2(c, d, a);
|
||||||
|
let cd_b = cross2(c, d, b);
|
||||||
|
let eps = 1.0e-8;
|
||||||
|
(ab_c.abs() <= eps && point_on_segment(c, a, b))
|
||||||
|
|| (ab_d.abs() <= eps && point_on_segment(d, a, b))
|
||||||
|
|| (cd_a.abs() <= eps && point_on_segment(a, c, d))
|
||||||
|
|| (cd_b.abs() <= eps && point_on_segment(b, c, d))
|
||||||
|
|| (ab_c * ab_d < 0.0 && cd_a * cd_b < 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_in_ring(point: [f64; 2], ring: &[[f64; 2]]) -> bool {
|
||||||
|
let mut winding = 0_i32;
|
||||||
|
for index in 0..ring.len() {
|
||||||
|
let first = ring[index];
|
||||||
|
let second = ring[(index + 1) % ring.len()];
|
||||||
|
let side = cross2(first, second, point);
|
||||||
|
if first[1] <= point[1] {
|
||||||
|
if second[1] > point[1] && side > 0.0 {
|
||||||
|
winding += 1;
|
||||||
|
}
|
||||||
|
} else if second[1] <= point[1] && side < 0.0 {
|
||||||
|
winding -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
winding != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polygon_area(points: &[[f64; 2]]) -> f64 {
|
||||||
|
(0..points.len())
|
||||||
|
.map(|index| {
|
||||||
|
let first = points[index];
|
||||||
|
let second = points[(index + 1) % points.len()];
|
||||||
|
first[0] * second[1] - second[0] * first[1]
|
||||||
|
})
|
||||||
|
.sum::<f64>()
|
||||||
|
* 0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lift_to_source_plane(point: [f64; 2], source: [[f32; 3]; 3]) -> Option<[f32; 3]> {
|
||||||
|
for vertex in source {
|
||||||
|
if distance_squared(point, [f64::from(vertex[0]), f64::from(vertex[1])]) <= 1.0e-18 {
|
||||||
|
return Some(vertex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let a = source[0].map(f64::from);
|
||||||
|
let b = source[1].map(f64::from);
|
||||||
|
let c = source[2].map(f64::from);
|
||||||
|
let denominator = cross2([a[0], a[1]], [b[0], b[1]], [c[0], c[1]]);
|
||||||
|
if denominator.abs() <= 1.0e-10 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let second_weight = cross2([a[0], a[1]], point, [c[0], c[1]]) / denominator;
|
||||||
|
let third_weight = cross2([a[0], a[1]], [b[0], b[1]], point) / denominator;
|
||||||
|
let first_weight = 1.0 - second_weight - third_weight;
|
||||||
|
let z = first_weight * a[2] + second_weight * b[2] + third_weight * c[2];
|
||||||
|
let position = [point[0] as f32, point[1] as f32, z as f32];
|
||||||
|
position
|
||||||
|
.iter()
|
||||||
|
.all(|value| value.is_finite())
|
||||||
|
.then_some(position)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cross2(first: [f64; 2], second: [f64; 2], third: [f64; 2]) -> f64 {
|
||||||
|
(second[0] - first[0]) * (third[1] - first[1]) - (second[1] - first[1]) * (third[0] - first[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn interpolate(first: [f64; 2], second: [f64; 2], amount: f64) -> [f64; 2] {
|
||||||
|
[
|
||||||
|
first[0] + (second[0] - first[0]) * amount,
|
||||||
|
first[1] + (second[1] - first[1]) * amount,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn distance_squared(first: [f64; 2], second: [f64; 2]) -> f64 {
|
||||||
|
let delta_x = first[0] - second[0];
|
||||||
|
let delta_y = first[1] - second[1];
|
||||||
|
delta_x * delta_x + delta_y * delta_y
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::subtract_contour;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn translated_thin_outside_strip_keeps_its_area() {
|
||||||
|
let base = 10_000.0_f32;
|
||||||
|
let source = [[
|
||||||
|
[base, base, 0.0],
|
||||||
|
[base + 10.0, base, 0.0],
|
||||||
|
[base, base + 10.0, 0.0],
|
||||||
|
]];
|
||||||
|
let outer = [
|
||||||
|
[base + 0.003, base + 0.003],
|
||||||
|
[base + 1.0, base + 0.003],
|
||||||
|
[base + 1.0, base + 0.997],
|
||||||
|
[base + 0.003, base + 0.997],
|
||||||
|
];
|
||||||
|
let output = subtract_contour(&source, &outer).expect("simple contour");
|
||||||
|
let retained_area = output
|
||||||
|
.iter()
|
||||||
|
.map(|fragment| {
|
||||||
|
let [a, b, c] = fragment.positions;
|
||||||
|
let ab = [f64::from(b[0] - a[0]), f64::from(b[1] - a[1])];
|
||||||
|
let ac = [f64::from(c[0] - a[0]), f64::from(c[1] - a[1])];
|
||||||
|
(ab[0] * ac[1] - ab[1] * ac[0]).abs() * 0.5
|
||||||
|
})
|
||||||
|
.sum::<f64>();
|
||||||
|
let expected_area =
|
||||||
|
50.0 - (f64::from(outer[1][0] - outer[0][0]) * f64::from(outer[2][1] - outer[1][1]));
|
||||||
|
assert!(
|
||||||
|
(retained_area - expected_area).abs() < 0.001,
|
||||||
|
"retained area {retained_area}, expected {expected_area}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -4,6 +4,15 @@
|
|||||||
use fparkan_terrain_format::{LandMapDocument, LandMeshDocument};
|
use fparkan_terrain_format::{LandMapDocument, LandMeshDocument};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
mod contour;
|
||||||
|
mod foundation;
|
||||||
|
|
||||||
|
use foundation::basement_surface_mask;
|
||||||
|
pub use foundation::{
|
||||||
|
build_basement_foundations, BasementFootprint, BasementFoundation, BasementFoundationError,
|
||||||
|
BasementFoundationFragment, BasementPlacement,
|
||||||
|
};
|
||||||
|
|
||||||
/// Terrain material-selector contract preserved from the decoded map mesh.
|
/// Terrain material-selector contract preserved from the decoded map mesh.
|
||||||
///
|
///
|
||||||
/// Applications access this semantic terrain API through this terrain crate
|
/// Applications access this semantic terrain API through this terrain crate
|
||||||
@@ -21,6 +30,7 @@ pub struct TerrainWorld {
|
|||||||
surfaces: Vec<RuntimeTriangle>,
|
surfaces: Vec<RuntimeTriangle>,
|
||||||
surface_bvh: SurfaceBvh,
|
surface_bvh: SurfaceBvh,
|
||||||
source_mesh: Option<LandMeshDocument>,
|
source_mesh: Option<LandMeshDocument>,
|
||||||
|
basement_foundation: Option<BasementFoundation>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Surface hit.
|
/// Surface hit.
|
||||||
@@ -91,6 +101,13 @@ pub enum TerrainError {
|
|||||||
/// Position count.
|
/// Position count.
|
||||||
position_count: usize,
|
position_count: usize,
|
||||||
},
|
},
|
||||||
|
/// A basement fragment does not preserve a valid LOD0 source face and mask.
|
||||||
|
InvalidFoundationFragment {
|
||||||
|
/// Fragment index.
|
||||||
|
fragment: usize,
|
||||||
|
},
|
||||||
|
/// Foundation fragments require a retained source Land.msh document.
|
||||||
|
FoundationRequiresMesh,
|
||||||
/// Terrain Node38/Slot68 records do not assign every face to one LOD.
|
/// Terrain Node38/Slot68 records do not assign every face to one LOD.
|
||||||
InvalidLodMapping(TerrainLodError),
|
InvalidLodMapping(TerrainLodError),
|
||||||
}
|
}
|
||||||
@@ -119,6 +136,12 @@ impl std::fmt::Display for TerrainError {
|
|||||||
f,
|
f,
|
||||||
"terrain face {face} references vertex {vertex} outside {position_count} positions"
|
"terrain face {face} references vertex {vertex} outside {position_count} positions"
|
||||||
),
|
),
|
||||||
|
Self::InvalidFoundationFragment { fragment } => {
|
||||||
|
write!(f, "invalid basement foundation fragment {fragment}")
|
||||||
|
}
|
||||||
|
Self::FoundationRequiresMesh => {
|
||||||
|
write!(f, "basement foundation requires a source Land.msh")
|
||||||
|
}
|
||||||
Self::InvalidLodMapping(error) => write!(f, "invalid terrain LOD mapping: {error}"),
|
Self::InvalidLodMapping(error) => write!(f, "invalid terrain LOD mapping: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +155,9 @@ impl std::error::Error for TerrainError {
|
|||||||
| Self::TooManyAreals { .. }
|
| Self::TooManyAreals { .. }
|
||||||
| Self::InvalidGridReference { .. }
|
| Self::InvalidGridReference { .. }
|
||||||
| Self::InvalidArealReference { .. }
|
| Self::InvalidArealReference { .. }
|
||||||
| Self::InvalidSurfaceVertex { .. } => None,
|
| Self::InvalidSurfaceVertex { .. }
|
||||||
|
| Self::InvalidFoundationFragment { .. }
|
||||||
|
| Self::FoundationRequiresMesh => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,6 +279,7 @@ impl TerrainWorld {
|
|||||||
surfaces: Vec::new(),
|
surfaces: Vec::new(),
|
||||||
surface_bvh: SurfaceBvh::default(),
|
surface_bvh: SurfaceBvh::default(),
|
||||||
source_mesh: None,
|
source_mesh: None,
|
||||||
|
basement_foundation: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,6 +316,87 @@ impl TerrainWorld {
|
|||||||
Ok(world)
|
Ok(world)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a new world whose surface queries use exactly the canonical
|
||||||
|
/// source and generated triangles from a basement foundation.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`TerrainError`] if this world has no source mesh or any
|
||||||
|
/// fragment does not preserve valid LOD0 identity, flags, and geometry.
|
||||||
|
pub fn with_basement_foundation(
|
||||||
|
mut self,
|
||||||
|
foundation: BasementFoundation,
|
||||||
|
) -> Result<Self, TerrainError> {
|
||||||
|
let mesh = self
|
||||||
|
.source_mesh
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(TerrainError::FoundationRequiresMesh)?;
|
||||||
|
let assignments = mesh
|
||||||
|
.face_lod_assignments()
|
||||||
|
.map_err(TerrainError::InvalidLodMapping)?;
|
||||||
|
let is_lod0 = assignments
|
||||||
|
.iter()
|
||||||
|
.map(|assignment| assignment.lod == TerrainLod::Lod0)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut surfaces = Vec::with_capacity(foundation.fragments.len());
|
||||||
|
let mut generated = 0_usize;
|
||||||
|
for (fragment_index, fragment) in foundation.fragments.iter().enumerate() {
|
||||||
|
if !valid_foundation_triangle(fragment.positions) {
|
||||||
|
return Err(TerrainError::InvalidFoundationFragment {
|
||||||
|
fragment: fragment_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let face = if let Some(source_face) = fragment.source_face {
|
||||||
|
let Some(source) = mesh.faces.get(source_face) else {
|
||||||
|
return Err(TerrainError::InvalidFoundationFragment {
|
||||||
|
fragment: fragment_index,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if !is_lod0.get(source_face).copied().unwrap_or(false)
|
||||||
|
|| source.flags != fragment.surface_mask
|
||||||
|
{
|
||||||
|
return Err(TerrainError::InvalidFoundationFragment {
|
||||||
|
fragment: fragment_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
source_face
|
||||||
|
} else {
|
||||||
|
if basement_surface_mask(fragment.positions) != fragment.surface_mask {
|
||||||
|
return Err(TerrainError::InvalidFoundationFragment {
|
||||||
|
fragment: fragment_index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let face = mesh.faces.len().checked_add(generated).ok_or(
|
||||||
|
TerrainError::InvalidFoundationFragment {
|
||||||
|
fragment: fragment_index,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
generated =
|
||||||
|
generated
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or(TerrainError::InvalidFoundationFragment {
|
||||||
|
fragment: fragment_index,
|
||||||
|
})?;
|
||||||
|
face
|
||||||
|
};
|
||||||
|
surfaces.push(RuntimeTriangle {
|
||||||
|
face,
|
||||||
|
mask: fragment.surface_mask,
|
||||||
|
vertices: fragment.positions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.surfaces = surfaces;
|
||||||
|
self.surface_bvh = SurfaceBvh::from_surfaces(&self.surfaces);
|
||||||
|
self.basement_foundation = Some(foundation);
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the canonical basement geometry used by the surface query index.
|
||||||
|
#[must_use]
|
||||||
|
pub fn basement_foundation(&self) -> Option<&BasementFoundation> {
|
||||||
|
self.basement_foundation.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the number of navigation areas.
|
/// Returns the number of navigation areas.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn areal_count(&self) -> usize {
|
pub fn areal_count(&self) -> usize {
|
||||||
@@ -740,26 +847,31 @@ impl RuntimeTriangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn raycast(&self, origin: [f32; 3], direction: [f32; 3]) -> Option<f32> {
|
fn raycast(&self, origin: [f32; 3], direction: [f32; 3]) -> Option<f32> {
|
||||||
let edge1 = sub3(self.vertices[1], self.vertices[0]);
|
let edge1 = sub3_f64(self.vertices[1], self.vertices[0]);
|
||||||
let edge2 = sub3(self.vertices[2], self.vertices[0]);
|
let edge2 = sub3_f64(self.vertices[2], self.vertices[0]);
|
||||||
let pvec = cross3(direction, edge2);
|
let direction = direction.map(f64::from);
|
||||||
let det = dot3(edge1, pvec);
|
let pvec = cross3_f64(direction, edge2);
|
||||||
if det.abs() <= 1.0e-6 {
|
let det = dot3_f64(edge1, pvec);
|
||||||
|
if det == 0.0 || !det.is_finite() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let inv_det = 1.0 / det;
|
let inv_det = 1.0 / det;
|
||||||
let tvec = sub3(origin, self.vertices[0]);
|
let tvec = sub3_f64(origin, self.vertices[0]);
|
||||||
let u = dot3(tvec, pvec) * inv_det;
|
let u = dot3_f64(tvec, pvec) * inv_det;
|
||||||
if !(-1.0e-5..=1.00001).contains(&u) {
|
if !(-1.0e-5..=1.00001).contains(&u) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let qvec = cross3(tvec, edge1);
|
let qvec = cross3_f64(tvec, edge1);
|
||||||
let v = dot3(direction, qvec) * inv_det;
|
let v = dot3_f64(direction, qvec) * inv_det;
|
||||||
if v < -1.0e-5 || u + v > 1.00001 {
|
if v < -1.0e-5 || u + v > 1.00001 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let distance = dot3(edge2, qvec) * inv_det;
|
let distance = dot3_f64(edge2, qvec) * inv_det;
|
||||||
(distance >= 0.0).then_some(distance)
|
if distance >= 0.0 && distance.is_finite() && distance <= f64::from(f32::MAX) {
|
||||||
|
Some(distance as f32)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -919,6 +1031,21 @@ fn build_surfaces(mesh: &LandMeshDocument) -> Result<Vec<RuntimeTriangle>, Terra
|
|||||||
Ok(triangles)
|
Ok(triangles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn valid_foundation_triangle(vertices: [[f32; 3]; 3]) -> bool {
|
||||||
|
if !vertices.iter().flatten().all(|value| value.is_finite()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let first = [
|
||||||
|
f64::from(vertices[1][0]) - f64::from(vertices[0][0]),
|
||||||
|
f64::from(vertices[1][1]) - f64::from(vertices[0][1]),
|
||||||
|
];
|
||||||
|
let second = [
|
||||||
|
f64::from(vertices[2][0]) - f64::from(vertices[0][0]),
|
||||||
|
f64::from(vertices[2][1]) - f64::from(vertices[0][1]),
|
||||||
|
];
|
||||||
|
first[0] * second[1] - first[1] * second[0] != 0.0
|
||||||
|
}
|
||||||
|
|
||||||
fn surface_vertex(
|
fn surface_vertex(
|
||||||
mesh: &LandMeshDocument,
|
mesh: &LandMeshDocument,
|
||||||
face: usize,
|
face: usize,
|
||||||
@@ -940,25 +1067,41 @@ fn barycentric_2d(
|
|||||||
second: [f32; 2],
|
second: [f32; 2],
|
||||||
third: [f32; 2],
|
third: [f32; 2],
|
||||||
) -> Option<[f32; 3]> {
|
) -> Option<[f32; 3]> {
|
||||||
let edge_second = [second[0] - first[0], second[1] - first[1]];
|
let edge_second = [
|
||||||
let edge_third = [third[0] - first[0], third[1] - first[1]];
|
f64::from(second[0]) - f64::from(first[0]),
|
||||||
let point_delta = [point[0] - first[0], point[1] - first[1]];
|
f64::from(second[1]) - f64::from(first[1]),
|
||||||
|
];
|
||||||
|
let edge_third = [
|
||||||
|
f64::from(third[0]) - f64::from(first[0]),
|
||||||
|
f64::from(third[1]) - f64::from(first[1]),
|
||||||
|
];
|
||||||
|
let point_delta = [
|
||||||
|
f64::from(point[0]) - f64::from(first[0]),
|
||||||
|
f64::from(point[1]) - f64::from(first[1]),
|
||||||
|
];
|
||||||
let denom = edge_second[0] * edge_third[1] - edge_third[0] * edge_second[1];
|
let denom = edge_second[0] * edge_third[1] - edge_third[0] * edge_second[1];
|
||||||
if denom.abs() <= 1.0e-6 {
|
if denom == 0.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let inv = 1.0 / denom;
|
let second_weight = (point_delta[0] * edge_third[1] - edge_third[0] * point_delta[1]) / denom;
|
||||||
let second_weight = (point_delta[0] * edge_third[1] - edge_third[0] * point_delta[1]) * inv;
|
let third_weight = (edge_second[0] * point_delta[1] - point_delta[0] * edge_second[1]) / denom;
|
||||||
let third_weight = (edge_second[0] * point_delta[1] - point_delta[0] * edge_second[1]) * inv;
|
|
||||||
let first_weight = 1.0 - second_weight - third_weight;
|
let first_weight = 1.0 - second_weight - third_weight;
|
||||||
Some([first_weight, second_weight, third_weight])
|
let weights = [first_weight, second_weight, third_weight];
|
||||||
|
if weights.iter().any(|weight| !weight.is_finite()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(weights.map(|weight| weight as f32))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
|
fn sub3_f64(a: [f32; 3], b: [f32; 3]) -> [f64; 3] {
|
||||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
[
|
||||||
|
f64::from(a[0]) - f64::from(b[0]),
|
||||||
|
f64::from(a[1]) - f64::from(b[1]),
|
||||||
|
f64::from(a[2]) - f64::from(b[2]),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
|
fn cross3_f64(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||||
[
|
[
|
||||||
a[1] * b[2] - a[2] * b[1],
|
a[1] * b[2] - a[2] * b[1],
|
||||||
a[2] * b[0] - a[0] * b[2],
|
a[2] * b[0] - a[0] * b[2],
|
||||||
@@ -966,7 +1109,7 @@ fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
|
fn dot3_f64(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1126,6 +1269,177 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn foundation_world_queries_use_the_canonical_cut_and_generated_band() {
|
||||||
|
use crate::{build_basement_foundations, BasementFootprint};
|
||||||
|
|
||||||
|
let mesh = synthetic_land_mesh();
|
||||||
|
let outer = [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]];
|
||||||
|
let inner = [
|
||||||
|
[0.4, 0.4, 1.0],
|
||||||
|
[0.6, 0.4, 1.0],
|
||||||
|
[0.6, 0.6, 1.0],
|
||||||
|
[0.4, 0.6, 1.0],
|
||||||
|
];
|
||||||
|
let foundation = build_basement_foundations(
|
||||||
|
&mesh,
|
||||||
|
&[BasementFootprint {
|
||||||
|
outer: &outer,
|
||||||
|
inner: &inner,
|
||||||
|
flatten_inner_to_mean_outer_z: true,
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.expect("foundation");
|
||||||
|
let expected_fragments = foundation.fragments.clone();
|
||||||
|
let expected_fragment_count = foundation.fragments.len();
|
||||||
|
let world = TerrainWorld::from_land_msh(&mesh)
|
||||||
|
.expect("source world")
|
||||||
|
.with_basement_foundation(foundation)
|
||||||
|
.expect("foundation world");
|
||||||
|
|
||||||
|
assert_eq!(world.surface_count(), expected_fragment_count);
|
||||||
|
assert_eq!(
|
||||||
|
world
|
||||||
|
.basement_foundation()
|
||||||
|
.map(|value| value.fragments.as_slice()),
|
||||||
|
Some(expected_fragments.as_slice())
|
||||||
|
);
|
||||||
|
assert!(world
|
||||||
|
.height_at([0.05, 0.05])
|
||||||
|
.expect("band height")
|
||||||
|
.is_some());
|
||||||
|
assert!(world.height_at([0.3, 0.3]).expect("band height").is_some());
|
||||||
|
assert_eq!(world.height_at([0.5, 0.5]).expect("hole height"), None);
|
||||||
|
assert_eq!(
|
||||||
|
world
|
||||||
|
.raycast([0.5, 0.5, 4.0], [0.0, 0.0, -1.0], FullSurfaceMask(0),)
|
||||||
|
.expect("hole ray"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut generated_face = mesh.faces.len();
|
||||||
|
for (runtime, fragment) in world.surfaces.iter().zip(&expected_fragments) {
|
||||||
|
assert_eq!(runtime.mask, fragment.surface_mask);
|
||||||
|
assert_eq!(runtime.vertices, fragment.positions);
|
||||||
|
if let Some(source_face) = fragment.source_face {
|
||||||
|
assert_eq!(runtime.face, source_face);
|
||||||
|
assert_eq!(runtime.mask, mesh.faces[source_face].flags);
|
||||||
|
} else {
|
||||||
|
assert_eq!(runtime.face, generated_face);
|
||||||
|
generated_face += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(generated_face > mesh.faces.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn foundation_install_accepts_thin_nonzero_fragments_for_surface_queries() {
|
||||||
|
let mesh = synthetic_land_mesh();
|
||||||
|
let fragment = BasementFoundationFragment {
|
||||||
|
source_face: Some(0),
|
||||||
|
surface_mask: mesh.faces[0].flags,
|
||||||
|
positions: [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.000_000_5, 0.0]],
|
||||||
|
};
|
||||||
|
let foundation = BasementFoundation {
|
||||||
|
fragments: vec![fragment],
|
||||||
|
placements: Vec::new(),
|
||||||
|
};
|
||||||
|
let world = TerrainWorld::from_land_msh(&mesh)
|
||||||
|
.expect("source world")
|
||||||
|
.with_basement_foundation(foundation)
|
||||||
|
.expect("thin but non-degenerate triangle");
|
||||||
|
|
||||||
|
assert_eq!(world.surface_count(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
world.height_at([0.25, 0.000_000_125]).expect("height"),
|
||||||
|
Some(0.0)
|
||||||
|
);
|
||||||
|
assert!(world
|
||||||
|
.raycast(
|
||||||
|
[0.25, 0.000_000_125, 1.0],
|
||||||
|
[0.0, 0.0, -1.0],
|
||||||
|
FullSurfaceMask(0),
|
||||||
|
)
|
||||||
|
.expect("raycast")
|
||||||
|
.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn foundation_install_rejects_invalid_source_identity_masks_and_generated_flags() {
|
||||||
|
let mesh = synthetic_land_mesh();
|
||||||
|
let source_triangle = [[0.0, 0.0, 0.0], [1.0, 0.0, 1.0], [0.0, 1.0, 1.0]];
|
||||||
|
let generated_triangle = [[0.0, 0.0, 0.0], [0.25, 0.0, 0.0], [0.0, 0.25, 0.0]];
|
||||||
|
let invalid = [
|
||||||
|
BasementFoundationFragment {
|
||||||
|
source_face: Some(mesh.faces.len()),
|
||||||
|
surface_mask: mesh.faces[0].flags,
|
||||||
|
positions: source_triangle,
|
||||||
|
},
|
||||||
|
BasementFoundationFragment {
|
||||||
|
source_face: Some(0),
|
||||||
|
surface_mask: FullSurfaceMask(mesh.faces[0].flags.0 ^ 1),
|
||||||
|
positions: source_triangle,
|
||||||
|
},
|
||||||
|
BasementFoundationFragment {
|
||||||
|
source_face: None,
|
||||||
|
surface_mask: FullSurfaceMask(0x0300),
|
||||||
|
positions: generated_triangle,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (index, fragment) in invalid.into_iter().enumerate() {
|
||||||
|
let foundation = BasementFoundation {
|
||||||
|
fragments: vec![fragment],
|
||||||
|
placements: Vec::new(),
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
matches!(
|
||||||
|
TerrainWorld::from_land_msh(&mesh)
|
||||||
|
.expect("source world")
|
||||||
|
.with_basement_foundation(foundation),
|
||||||
|
Err(TerrainError::InvalidFoundationFragment { fragment: 0 })
|
||||||
|
),
|
||||||
|
"invalid fragment case {index}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn foundation_install_rejects_lod1_source_faces() {
|
||||||
|
use fparkan_terrain_format::TerrainLod;
|
||||||
|
|
||||||
|
let mut mesh = synthetic_land_mesh();
|
||||||
|
mesh.positions.push([0.0, 0.0, 2.0]);
|
||||||
|
mesh.positions.push([1.0, 0.0, 2.0]);
|
||||||
|
mesh.positions.push([0.0, 1.0, 2.0]);
|
||||||
|
mesh.faces
|
||||||
|
.push(terrain_face(FullSurfaceMask(0x0000_0003), [4, 5, 6]));
|
||||||
|
mesh.nodes_raw[10..12].copy_from_slice(&1_u16.to_le_bytes());
|
||||||
|
let mut lod1_slot = [0_u8; 68];
|
||||||
|
lod1_slot[0..2].copy_from_slice(&2_u16.to_le_bytes());
|
||||||
|
lod1_slot[2..4].copy_from_slice(&1_u16.to_le_bytes());
|
||||||
|
mesh.slots.slots_raw.push(lod1_slot);
|
||||||
|
assert_eq!(
|
||||||
|
mesh.face_lod_assignments().expect("LOD assignment")[2].lod,
|
||||||
|
TerrainLod::Lod1
|
||||||
|
);
|
||||||
|
|
||||||
|
let foundation = BasementFoundation {
|
||||||
|
fragments: vec![BasementFoundationFragment {
|
||||||
|
source_face: Some(2),
|
||||||
|
surface_mask: mesh.faces[2].flags,
|
||||||
|
positions: [[0.0, 0.0, 2.0], [1.0, 0.0, 2.0], [0.0, 1.0, 2.0]],
|
||||||
|
}],
|
||||||
|
placements: Vec::new(),
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
TerrainWorld::from_land_msh(&mesh)
|
||||||
|
.expect("source world")
|
||||||
|
.with_basement_foundation(foundation),
|
||||||
|
Err(TerrainError::InvalidFoundationFragment { fragment: 0 })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn surface_bvh_reduces_candidates_without_changing_surface_math() {
|
fn surface_bvh_reduces_candidates_without_changing_surface_math() {
|
||||||
let surfaces: Vec<_> = (0_u16..16)
|
let surfaces: Vec<_> = (0_u16..16)
|
||||||
|
|||||||
@@ -955,6 +955,23 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Подготовка BAS вынесена в `fparkan-terrain::build_basement_foundations`.
|
||||||
|
Функция получает внешние и внутренние кольца в порядке размещения, строит
|
||||||
|
упорядоченные пересечения внешней границы с текущей поверхностью и возвращает
|
||||||
|
исходные фрагменты рельефа вместе с треугольниками отмостки в одном наборе.
|
||||||
|
`TerrainWorld::with_basement_foundation` принимает этот набор во владение и
|
||||||
|
устанавливает те же координаты и маски в BVH высотных запросов и пересечений.
|
||||||
|
Предпросмотр миссии на Vulkan пока строит только LOD0: связывание BAS-записей
|
||||||
|
с этой подготовкой ещё не подключено, поэтому наличие API не означает, что
|
||||||
|
геометрия миссии уже обрезана.
|
||||||
|
|
||||||
|
Внешнее кольцо должно лежать на области LOD0, а каждый его сегмент — оставаться
|
||||||
|
покрытым текущей поверхностью. Непокрытый сегмент или пересекающиеся контуры
|
||||||
|
дают ошибку; карта не остаётся частично обрезанной. Первичная нормализация
|
||||||
|
использует исходные грани и соседей LOD0, последующие пересечения и вырезания —
|
||||||
|
канонические фрагменты после предыдущих размещений. Соответствие динамических
|
||||||
|
соседей, создаваемых нативным движком, пока не моделируется.
|
||||||
|
|
||||||
## Материалы ландшафта
|
## Материалы ландшафта
|
||||||
|
|
||||||
Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Старший байт
|
Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Старший байт
|
||||||
|
|||||||
Reference in new issue
Block a user