use std::ptr::null;
use oxijolt_sys::*;
use super::geometry::{cross, length, length_sq, sub, v3, V3};
use super::{initialize, Shape, ShapeSettings};
use crate::{limits, MeshError, PhysicsMaterial, ShapeError, Vec3};
const MAX_MESH_MATERIALS: usize = 32;
const MAX_TRIANGLES_PER_LEAF: u32 = 8;
const MIN_TRIANGLE_CROSS: f64 = 1.000_001e-6;
const CROSS_ERROR_MARGIN: f64 = 2.0;
const QUANTIZATION_STEPS: f64 = ((1u32 << 21) - 1) as f64;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum MeshBuildQuality {
#[default]
FavorRuntimePerformance,
FavorBuildSpeed,
}
impl MeshBuildQuality {
fn to_jph(self) -> JPH_Mesh_Shape_BuildQuality {
match self {
Self::FavorRuntimePerformance => JPH_Mesh_Shape_BuildQuality_FavorRuntimePerformance,
Self::FavorBuildSpeed => JPH_Mesh_Shape_BuildQuality_FavorBuildSpeed,
}
}
}
#[derive(Clone, Debug)]
pub struct MeshSettings<'a> {
materials: Option<(&'a [&'a PhysicsMaterial], &'a [u8])>,
active_edge_cos_threshold_angle: f32,
max_triangles_per_leaf: u32,
build_quality: MeshBuildQuality,
max_convex_extent: f32,
}
impl Default for MeshSettings<'_> {
fn default() -> Self {
Self {
materials: None,
active_edge_cos_threshold_angle: 0.996195,
max_triangles_per_leaf: 8,
build_quality: MeshBuildQuality::default(),
max_convex_extent: Self::DEFAULT_MAX_CONVEX_EXTENT,
}
}
}
impl<'a> MeshSettings<'a> {
pub const DEFAULT_MAX_CONVEX_EXTENT: f32 = 300.0;
#[must_use]
pub fn materials(mut self, list: &'a [&'a PhysicsMaterial], indices: &'a [u8]) -> Self {
self.materials = Some((list, indices));
self
}
#[must_use]
pub fn active_edge_cos_threshold_angle(mut self, value: f32) -> Self {
self.active_edge_cos_threshold_angle = value;
self
}
#[must_use]
pub fn max_triangles_per_leaf(mut self, value: u32) -> Self {
self.max_triangles_per_leaf = value;
self
}
#[must_use]
pub fn build_quality(mut self, value: MeshBuildQuality) -> Self {
self.build_quality = value;
self
}
#[must_use]
pub fn max_convex_extent(mut self, metres: f32) -> Self {
self.max_convex_extent = if metres == 0.0 { 0.0 } else { metres };
self
}
fn validate(&self, triangle_count: usize) -> Result<(), ShapeError> {
let invalid = |what| Err(ShapeError::InvalidValue(what));
let threshold = self.active_edge_cos_threshold_angle;
if !(threshold.is_finite() && (-1.0..=1.0).contains(&threshold)) {
return invalid("active_edge_cos_threshold_angle must be between -1 and 1");
}
if !(1..=MAX_TRIANGLES_PER_LEAF).contains(&self.max_triangles_per_leaf) {
return invalid("max_triangles_per_leaf must be between 1 and 8");
}
if !(0.0..=2.0 * limits::MAX_SHAPE_EXTENT).contains(&self.max_convex_extent) {
return invalid("max_convex_extent must be between 0 and 2 * limits::MAX_SHAPE_EXTENT");
}
if let Some((list, indices)) = self.materials {
if !(1..=MAX_MESH_MATERIALS).contains(&list.len()) {
return invalid("mesh material list must hold 1 to 32 materials");
}
if indices.len() != triangle_count {
return invalid("mesh material indices must hold one value per triangle");
}
if indices
.iter()
.any(|&index| usize::from(index) >= list.len())
{
return invalid("material indices must name a material of the list");
}
}
Ok(())
}
}
fn mesh_counts_fit(vertices: usize, triangles: usize) -> bool {
let max = i32::MAX as usize;
vertices <= max && triangles <= max
}
fn validate_geometry(vertices: &[Vec3], triangles: &[[u32; 3]]) -> Result<(), ShapeError> {
if vertices.is_empty() || triangles.is_empty() {
return Err(ShapeError::InvalidValue(
"a mesh needs vertices and triangles",
));
}
if !mesh_counts_fit(vertices.len(), triangles.len()) {
return Err(ShapeError::InvalidValue(
"a mesh has at most i32::MAX vertices and triangles",
));
}
if !vertices
.iter()
.all(|&vertex| limits::is_local_offset(vertex))
{
return Err(ShapeError::InvalidValue(
"mesh vertices must be finite and within limits::MAX_SHAPE_EXTENT",
));
}
if triangles
.iter()
.flatten()
.any(|&index| index as usize >= vertices.len())
{
return Err(ShapeError::InvalidValue(
"triangle index beyond the vertex list",
));
}
Ok(())
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct DroppedTriangles {
indices: Vec<usize>,
area: f64,
}
impl DroppedTriangles {
pub fn count(&self) -> usize {
self.indices.len()
}
pub fn is_empty(&self) -> bool {
self.indices.is_empty()
}
pub fn indices(&self) -> &[usize] {
&self.indices
}
pub fn area(&self) -> f32 {
self.area as f32
}
fn push(&mut self, index: usize, [a, b, c]: [V3; 3]) {
self.indices.push(index);
self.area += 0.5 * length(cross(sub(b, a), sub(c, a)));
}
}
impl Shape {
pub fn new_mesh(
vertices: &[Vec3],
triangles: &[[u32; 3]],
) -> Result<(Self, DroppedTriangles), ShapeError> {
Self::new_mesh_with_settings(vertices, triangles, &MeshSettings::default())
}
pub fn new_mesh_with_settings(
vertices: &[Vec3],
triangles: &[[u32; 3]],
settings: &MeshSettings<'_>,
) -> Result<(Self, DroppedTriangles), ShapeError> {
validate_geometry(vertices, triangles)?;
settings.validate(triangles.len())?;
initialize()?;
let (kept, dropped) = collidable_triangles(vertices, triangles, settings.max_convex_extent);
if kept.is_empty() {
return Err(ShapeError::Mesh(MeshError::NoTriangles));
}
let jolt_settings = mesh_settings(vertices, triangles, &kept, settings)?;
let mesh: *mut JPH_MeshShapeSettings = jolt_settings.as_ptr();
if unsafe { JPH_MeshShapeSettings_GetTriangleCount(mesh) } == 0 {
return Err(ShapeError::Mesh(MeshError::NoTriangles));
}
let shape = jolt_settings.create()?.within_extent_bounds()?;
shape.record_convex_extent(settings.max_convex_extent);
Ok((shape, dropped))
}
fn record_convex_extent(&self, extent: f32) {
let data = CONVEX_EXTENT_TAG | u64::from(extent.to_bits());
unsafe { JPH_Shape_SetUserData(self.0.as_ptr(), data) };
}
}
const CONVEX_EXTENT_TAG: u64 = 1 << 32;
pub(super) unsafe fn convex_extent_of(leaf: *const JPH_Shape) -> f32 {
let data = unsafe { JPH_Shape_GetUserData(leaf) };
if data >> 32 == CONVEX_EXTENT_TAG >> 32 {
f32::from_bits(data as u32)
} else {
MeshSettings::DEFAULT_MAX_CONVEX_EXTENT
}
}
fn collidable_triangles(
vertices: &[Vec3],
triangles: &[[u32; 3]],
convex_extent: f32,
) -> (Vec<usize>, DroppedTriangles) {
let corners = |triangle: &[u32; 3]| triangle.map(|index| v3(vertices[index as usize]));
let candidates = triangles
.iter()
.map(corners)
.filter(|&corners| is_collidable(corners, [0.0; 3], convex_extent));
let step = quantization_step(candidates);
let mut kept = Vec::new();
let mut dropped = DroppedTriangles::default();
for (index, triangle) in triangles.iter().enumerate() {
let corners = corners(triangle);
if is_collidable(corners, step, convex_extent) {
kept.push(index);
} else {
dropped.push(index, corners);
}
}
(kept, dropped)
}
fn quantization_step(triangles: impl Iterator<Item = [V3; 3]>) -> V3 {
let mut low = [f64::INFINITY; 3];
let mut high = [f64::NEG_INFINITY; 3];
for vertex in triangles.flatten() {
for axis in 0..3 {
low[axis] = low[axis].min(vertex[axis]);
high[axis] = high[axis].max(vertex[axis]);
}
}
[0, 1, 2].map(|axis| ((high[axis] - low[axis]) / QUANTIZATION_STEPS).max(0.0))
}
fn rounding_displacement(distance: f64, convex_extent: f64, longest_edge: f64) -> f64 {
let epsilon = f64::from(f32::EPSILON);
4.0 * epsilon * distance + epsilon * (convex_extent + longest_edge)
}
pub(super) fn is_collidable(corners: [V3; 3], step: V3, convex_extent: f32) -> bool {
let [a, b, c] = corners;
let normal = cross(sub(b, a), sub(c, a));
let twice_area = length(normal);
if twice_area < MIN_TRIANGLE_CROSS {
return false;
}
let unit = normal.map(|component| component / twice_area);
let distance = corners.map(length).into_iter().fold(0.0, f64::max);
let edge_pairs = [
(sub(b, a), sub(c, a)),
(sub(c, b), sub(a, b)),
(sub(a, c), sub(b, c)),
];
let longest_edge = edge_pairs
.iter()
.map(|&(edge, _)| length(edge))
.fold(0.0, f64::max);
let rounding = rounding_displacement(distance, f64::from(convex_extent), longest_edge);
let edge_move = step.map(|axis_step| 2.0 * (axis_step + rounding));
let shrink = edge_pairs
.iter()
.map(|&(first, second)| {
reach(cross(unit, first), edge_move) + reach(cross(second, unit), edge_move)
})
.fold(f64::INFINITY, f64::min);
let cross_rounding = edge_pairs
.iter()
.map(|&(first, second)| 2.0 * f64::from(f32::EPSILON) * length(first) * length(second))
.fold(0.0, f64::max);
let change = shrink + length_sq(edge_move) + cross_rounding;
twice_area >= MIN_TRIANGLE_CROSS + CROSS_ERROR_MARGIN * change
}
fn reach(direction: V3, bound: V3) -> f64 {
(0..3).map(|axis| bound[axis] * direction[axis].abs()).sum()
}
fn mesh_settings(
vertices: &[Vec3],
triangles: &[[u32; 3]],
kept: &[usize],
settings: &MeshSettings<'_>,
) -> Result<ShapeSettings, ShapeError> {
let jolt_vertices: Vec<JPH_Vec3> = vertices.iter().map(|vertex| vertex.to_jph()).collect();
let material_indices = settings.materials.map(|(_, indices)| indices);
let jolt_triangles: Vec<JPH_IndexedTriangle> = kept
.iter()
.map(|&i| {
let [i1, i2, i3] = triangles[i];
JPH_IndexedTriangle {
i1,
i2,
i3,
materialIndex: material_indices.map_or(0, |indices| u32::from(indices[i])),
userData: 0,
}
})
.collect();
let list: Vec<*const JPH_PhysicsMaterial> = settings
.materials
.map(|(list, _)| list.iter().map(|material| material.as_ptr()).collect())
.unwrap_or_default();
let jolt_settings = unsafe {
ShapeSettings::from_raw(
JPH_MeshShapeSettings_Create3(
jolt_vertices.as_ptr(),
jolt_vertices.len() as u32,
jolt_triangles.as_ptr(),
jolt_triangles.len() as u32,
if list.is_empty() {
null()
} else {
list.as_ptr()
},
list.len() as u32,
)
.cast(),
)
}?;
let mesh: *mut JPH_MeshShapeSettings = jolt_settings.as_ptr();
unsafe {
JPH_MeshShapeSettings_SetActiveEdgeCosThresholdAngle(
mesh,
settings.active_edge_cos_threshold_angle,
);
JPH_MeshShapeSettings_SetMaxTrianglesPerLeaf(mesh, settings.max_triangles_per_leaf);
JPH_MeshShapeSettings_SetBuildQuality(mesh, settings.build_quality.to_jph());
}
Ok(jolt_settings)
}
#[cfg(test)]
mod tests;