use crate::math::jolt_length;
use crate::{PhysicsWorld, Quat, RVec3, Real, Vec3};
pub const MAX_POSITION: Real = (if core::mem::size_of::<Real>() == 8 {
1.0e7_f64
} else {
5.0e3_f64
}) as Real;
pub const MAX_SHAPE_EXTENT: f32 = 2000.0;
pub const MAX_EXPANDED_SUB_SHAPES: u32 = 1 << 20;
pub const MAX_LINEAR_VELOCITY: f32 = 500.0;
pub const MAX_ANGULAR_VELOCITY: f32 = 0.25 * core::f32::consts::PI * 60.0;
pub const MAX_ACCELERATION: f32 = MAX_LINEAR_VELOCITY / PhysicsWorld::MIN_DELTA_TIME;
pub const MAX_ANGULAR_ACCELERATION: f32 = MAX_ANGULAR_VELOCITY / PhysicsWorld::MIN_DELTA_TIME;
pub const MAX_VELOCITY_CHANGE: f32 = 2.0 * MAX_LINEAR_VELOCITY;
pub const MAX_ANGULAR_VELOCITY_CHANGE: f32 = 2.0 * MAX_ANGULAR_VELOCITY;
pub const MAX_GRAVITY_FACTOR: f32 = 1000.0;
pub const MAX_FRICTION: f32 = 1000.0;
pub const MIN_MASS: f32 = 1.0e-3;
pub const MAX_VERTEX_INVERSE_MASS: f32 = 1000.0;
pub const MAX_MASS: f32 = 1.0e6;
pub const MIN_CONTACT_SCALE: f32 = 1.0e-9;
pub const MAX_SPRING_COEFFICIENT: f32 = 1.0e30;
pub const MAX_WEIGHT_IMPULSE: f32 = 1.0e9;
pub const MAX_RATIO: f32 = 1.0e4;
pub const MAX_GEAR_RATIO: f32 = 10.0;
pub const MAX_NORMALIZED_TORQUE: f32 = 10.0;
pub const MIN_TORQUE_CURVE_SPACING: f32 = 1.0e-3;
pub const MIN_TRACK_INERTIA: f32 = 1.0e-3;
pub const MAX_TRACK_INERTIA: f32 = 1.0e6;
pub const MAX_TRACK_INERTIA_RATIO: f32 = 100.0;
pub const MAX_TRACK_MASS_RATIO: f32 = 0.25;
pub const MAX_LEVER_ARM_RATIO: f32 = 1000.0;
pub const MIN_SOFT_BODY_EDGE_LENGTH: f32 = 1.0e-3;
pub const MIN_MAPPED_CHAIN_LENGTH: f32 = 1.0e-3;
pub const MAX_COMPLIANCE: f32 = 1.0e20;
pub const MAX_SOFT_BODY_PRESSURE: f32 = 1.0e6;
const MAX_PRESSURE_VERTEX_FORCE: f64 = MAX_ACCELERATION as f64 / MAX_VERTEX_INVERSE_MASS as f64;
const F32_UNIT_ROUNDOFF: f64 = f32::EPSILON as f64 / 2.0;
pub(crate) const F32_PRODUCT_HEADROOM: f64 = 1.0e37;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct SoftBodyPressureGeometry {
six_volume_low: f64,
largest_vertex_face_area: f64,
}
impl SoftBodyPressureGeometry {
pub(crate) fn new(positions: &[Vec3], faces: &[[u32; 3]]) -> Self {
const ROTATION_VOLUME: f64 = 4.0e-5;
const ROTATION_LENGTH: f64 = 1.0001;
const POSITION_ERROR: f64 = 15.0;
const TERM_ERROR: f64 = 64.0;
const UNDERFLOW_PER_FACE: f64 = 1.0e-36;
let u = F32_UNIT_ROUNDOFF;
let point = |index: u32| {
let p = positions[index as usize];
[p.x, p.y, p.z].map(f64::from)
};
let edge = |from: [f64; 3], to: [f64; 3]| {
let e = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
ROTATION_LENGTH * (norm(e) + POSITION_ERROR * u * (norm(from) + norm(to)))
};
let mut six_volume = 0.0;
let mut partial_sums = 0.0;
let mut length_products = 0.0;
let mut vertex_face_area = vec![0.0_f64; positions.len()];
for &face in faces {
let [x1, x2, x3] = face.map(point);
let c = [
x1[1] * x2[2] - x1[2] * x2[1],
x1[2] * x2[0] - x1[0] * x2[2],
x1[0] * x2[1] - x1[1] * x2[0],
];
six_volume += c[0] * x3[0] + c[1] * x3[1] + c[2] * x3[2];
partial_sums += six_volume.abs();
length_products += norm(x1) * norm(x2) * norm(x3);
let area = edge(x1, x2) * edge(x1, x3);
for index in face {
vertex_face_area[index as usize] += area;
}
}
let face_count = faces.len() as f64;
let growth = (1.0 - u).powf(-face_count);
let error = growth
* (u * (TERM_ERROR * length_products + (1.0 + ROTATION_VOLUME) * partial_sums)
+ UNDERFLOW_PER_FACE * face_count);
Self {
six_volume_low: (1.0 - ROTATION_VOLUME) * six_volume - error,
largest_vertex_face_area: vertex_face_area.into_iter().fold(0.0, f64::max),
}
}
}
fn norm(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
pub(crate) fn is_soft_body_pressure(pressure: f32, geometry: &SoftBodyPressureGeometry) -> bool {
pressure == 0.0
|| (geometry.six_volume_low > 0.0
&& f64::from(pressure) * geometry.largest_vertex_face_area
<= MAX_PRESSURE_VERTEX_FORCE * geometry.six_volume_low)
}
pub(crate) fn is_soft_body_force(
force: [f64; 3],
largest_inverse_mass: f32,
vertex_count: u32,
) -> bool {
let length = norm(force);
let per_vertex = f64::from(largest_inverse_mass) / f64::from(vertex_count.max(1));
length <= f64::from(MAX_ACCELERATION) * f64::from(MAX_MASS)
&& length * per_vertex <= f64::from(MAX_ACCELERATION)
}
const MIN_INERTIA_RATIO: f64 = 1001.0 * 8.0 * F32_UNIT_ROUNDOFF;
pub(crate) fn is_rigid_body_inertia(tensor: [[f64; 3]; 3]) -> bool {
let [[a, b, c], [d, e, f], [g, h, i]] = tensor;
let det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
let minors = (a * e - b * d) + (a * i - c * g) + (e * i - f * h);
let frobenius = tensor.iter().flatten().map(|v| v * v).sum::<f64>().sqrt();
a + e + i > 0.0 && det > 0.0 && minors > 0.0 && det / minors >= MIN_INERTIA_RATIO * frobenius
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct SoftBodyMassDistribution {
second_moment: [[f64; 3]; 3],
vertex_count: usize,
has_kinematic_vertex: bool,
on_axes: bool,
}
impl SoftBodyMassDistribution {
pub(crate) fn new(vertices: impl IntoIterator<Item = (Vec3, f32)>) -> Self {
let mut distribution = Self {
second_moment: [[0.0; 3]; 3],
vertex_count: 0,
has_kinematic_vertex: false,
on_axes: true,
};
for (position, inverse_mass) in vertices {
distribution.vertex_count += 1;
if inverse_mass == 0.0 {
distribution.has_kinematic_vertex = true;
continue;
}
let mass = f64::from(1.0 / inverse_mass);
let p = [position.x, position.y, position.z].map(f64::from);
for (i, row) in distribution.second_moment.iter_mut().enumerate() {
for (j, entry) in row.iter_mut().enumerate() {
*entry += mass * p[i] * p[j];
}
}
distribution.on_axes &= p.iter().filter(|&&c| c != 0.0).count() <= 1;
}
distribution
}
}
pub(crate) fn is_soft_body_inertia(
distribution: &SoftBodyMassDistribution,
rotation: Option<Quat>,
) -> bool {
if distribution.has_kinematic_vertex {
return true;
}
let u = F32_UNIT_ROUNDOFF;
let summed_terms = (distribution.vertex_count + 3) as f64 * u;
if summed_terms >= 0.5 {
return false;
}
let rotation = rotation.filter(|q| !(q.x == 0.0 && q.y == 0.0 && q.z == 0.0));
let c = match rotation {
Some(q) => {
let r = jolt_rotation_matrix(q);
let rc = multiply(r, distribution.second_moment);
multiply(rc, transpose(r))
}
None => distribution.second_moment,
};
let trace = c[0][0] + c[1][1] + c[2][2];
let inertia: [[f64; 3]; 3] = std::array::from_fn(|i| {
std::array::from_fn(|j| if i == j { trace - c[i][j] } else { -c[i][j] })
});
if rotation.is_none() && distribution.on_axes {
let diagonal = [inertia[0][0], inertia[1][1], inertia[2][2]];
let length_sq: f64 = diagonal.iter().map(|d| d * d).sum();
return length_sq <= 0.5e-12 || diagonal.iter().all(|&d| d >= 1.0e-30);
}
let position_error = if rotation.is_some() { 16.0 * u } else { 0.0 };
let error = trace
* ((1.0 + 3.0_f64.sqrt()) * (2.0 * position_error + position_error * position_error)
+ 3.0 * summed_terms / (1.0 - summed_terms) * (1.0 + position_error).powi(2));
let frobenius = inertia.iter().flatten().map(|v| v * v).sum::<f64>().sqrt();
let [[a, b, cc], [d, e, f], [g, h, i]] = inertia;
let det = a * (e * i - f * h) - b * (d * i - f * g) + cc * (d * h - e * g);
let minors = (a * e - b * d) + (a * i - cc * g) + (e * i - f * h);
if !(det > 0.0 && minors > 0.0 && frobenius >= 1.0e-30) {
return false;
}
det / minors - error >= MIN_INERTIA_RATIO * (frobenius + error)
}
fn jolt_rotation_matrix(q: Quat) -> [[f64; 3]; 3] {
let [x, y, z, w] = [q.x, q.y, q.z, q.w].map(f64::from);
[
[
1.0 - 2.0 * (y * y + z * z),
2.0 * (x * y - z * w),
2.0 * (x * z + y * w),
],
[
2.0 * (x * y + z * w),
1.0 - 2.0 * (x * x + z * z),
2.0 * (y * z - x * w),
],
[
2.0 * (x * z - y * w),
2.0 * (y * z + x * w),
1.0 - 2.0 * (x * x + y * y),
],
]
}
fn multiply(a: [[f64; 3]; 3], b: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|k| a[i][k] * b[k][j]).sum()))
}
fn transpose(a: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
std::array::from_fn(|i| std::array::from_fn(|j| a[j][i]))
}
pub(crate) const POSITION_RULE: &str = "position must be finite and within limits::MAX_POSITION";
pub(crate) fn is_in_frame(position: RVec3) -> bool {
[position.x, position.y, position.z]
.iter()
.all(|c| c.abs() <= MAX_POSITION)
}
pub(crate) fn is_frame_displacement(displacement: RVec3) -> bool {
[displacement.x, displacement.y, displacement.z]
.iter()
.all(|c| c.abs() <= 2.0 * MAX_POSITION)
}
pub(crate) fn is_frame_span(span: Vec3) -> bool {
[span.x, span.y, span.z]
.iter()
.all(|&c| Real::from(c).abs() <= 2.0 * MAX_POSITION)
}
pub(crate) fn is_local_offset(offset: Vec3) -> bool {
[offset.x, offset.y, offset.z]
.iter()
.all(|c| c.abs() <= MAX_SHAPE_EXTENT)
}
pub(crate) fn is_local_distance(distance: f32) -> bool {
(0.0..=MAX_SHAPE_EXTENT).contains(&distance)
}
pub(crate) const LINEAR_VELOCITY_RULE: &str =
"linear velocity must be finite and at most limits::MAX_LINEAR_VELOCITY long";
pub(crate) fn is_linear_velocity(velocity: Vec3) -> bool {
velocity.is_finite() && jolt_length(velocity) <= MAX_LINEAR_VELOCITY
}
pub(crate) const ANGULAR_VELOCITY_RULE: &str =
"angular velocity must be finite and at most limits::MAX_ANGULAR_VELOCITY long";
pub(crate) fn is_angular_velocity(velocity: Vec3) -> bool {
velocity.is_finite() && jolt_length(velocity) <= MAX_ANGULAR_VELOCITY
}
pub(crate) const GRAVITY_RULE: &str =
"gravity must be finite and at most limits::MAX_ACCELERATION long";
pub(crate) fn is_acceleration(acceleration: Vec3) -> bool {
acceleration.is_finite() && f64_length(acceleration) <= f64::from(MAX_ACCELERATION)
}
pub(crate) const GRAVITY_FACTOR_RULE: &str =
"gravity factor must be finite and within limits::MAX_GRAVITY_FACTOR";
pub(crate) fn is_gravity_factor(factor: f32) -> bool {
factor.abs() <= MAX_GRAVITY_FACTOR
}
pub(crate) fn is_contact_scale(scale: f32) -> bool {
scale == 0.0 || (MIN_CONTACT_SCALE..=1.0).contains(&scale)
}
pub(crate) const FRICTION_RULE: &str =
"friction must be finite and between 0 and limits::MAX_FRICTION";
pub(crate) fn is_friction(friction: f32) -> bool {
(0.0..=MAX_FRICTION).contains(&friction)
}
pub(crate) fn is_weight_impulse(mass: f32, gravity: Vec3, delta_time: f32) -> bool {
f64::from(mass) * f64_length(gravity) * f64::from(delta_time) <= f64::from(MAX_WEIGHT_IMPULSE)
}
pub(crate) const VELOCITY_CHANGE_RULE: &str =
"impulse would change the velocity by more than limits::MAX_VELOCITY_CHANGE";
pub(crate) fn is_velocity_change(impulse: [f64; 3], inverse_mass: f32) -> bool {
norm(impulse) * f64::from(inverse_mass) <= f64::from(MAX_VELOCITY_CHANGE)
}
pub(crate) const ANGULAR_VELOCITY_CHANGE_RULE: &str =
"impulse would change the angular velocity by more than limits::MAX_ANGULAR_VELOCITY_CHANGE";
pub(crate) fn is_angular_velocity_change(
angular_impulse: [f64; 3],
largest_inverse_inertia: f32,
) -> bool {
norm(angular_impulse) * f64::from(largest_inverse_inertia)
<= f64::from(MAX_ANGULAR_VELOCITY_CHANGE)
}
pub(crate) const LEAN_SPRING_RULE: &str =
"lean spring would exceed limits::MAX_ANGULAR_ACCELERATION for this chassis";
pub(crate) fn is_lean_spring(constant: f32, damping: f32, largest_inverse_inertia: f32) -> bool {
let torque = f64::from(constant) * std::f64::consts::PI
+ f64::from(damping) * f64::from(MAX_ANGULAR_VELOCITY_CHANGE);
let headroom = 1.0 + 4.0 * f64::from(crate::math::UNIT_TOLERANCE);
torque * f64::from(largest_inverse_inertia) * headroom <= f64::from(MAX_ANGULAR_ACCELERATION)
}
pub(crate) const MASS_RULE: &str = "mass must be between limits::MIN_MASS and limits::MAX_MASS";
pub(crate) fn is_mass(mass: f32) -> bool {
(MIN_MASS..=MAX_MASS).contains(&mass)
}
pub(crate) fn is_vertex_inverse_mass(inverse_mass: f32) -> bool {
(1.0 / MAX_MASS..=MAX_VERTEX_INVERSE_MASS).contains(&inverse_mass)
}
pub(crate) fn is_compliance(compliance: f32) -> bool {
(0.0..=MAX_COMPLIANCE).contains(&compliance)
}
pub(crate) fn is_ratio(ratio: f32) -> bool {
(1.0 / MAX_RATIO..=MAX_RATIO).contains(&ratio.abs())
}
pub(crate) const TORQUE_CURVE_RULE: &str = "engine torque curve needs x in 0..=1, \
limits::MIN_TORQUE_CURVE_SPACING apart, and y in 0..=limits::MAX_NORMALIZED_TORQUE";
pub(crate) fn is_torque_curve(points: &[(f32, f32)]) -> bool {
let spacing = f64::from(MIN_TORQUE_CURVE_SPACING) - f64::from(f32::EPSILON);
!points.is_empty()
&& points
.iter()
.all(|&(x, y)| (0.0..=1.0).contains(&x) && (0.0..=MAX_NORMALIZED_TORQUE).contains(&y))
&& points
.windows(2)
.all(|pair| f64::from(pair[1].0) - f64::from(pair[0].0) >= spacing)
}
pub(crate) fn is_track_inertia(inertia: f32) -> bool {
(MIN_TRACK_INERTIA..=MAX_TRACK_INERTIA).contains(&inertia)
}
pub(crate) fn is_track_inertia_ratio(first: f32, second: f32) -> bool {
first.max(second) <= MAX_TRACK_INERTIA_RATIO * first.min(second)
}
pub(crate) const TRACK_MASS_RATIO_RULE: &str =
"a track is too heavy for its chassis: see limits::MAX_TRACK_MASS_RATIO";
pub(crate) fn is_track_mass_ratio(ratio: f64) -> bool {
ratio <= f64::from(MAX_TRACK_MASS_RATIO)
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct PrincipalMass {
pub(crate) inverse_mass: f64,
pub(crate) inverse_inertia: [f64; 3],
}
impl PrincipalMass {
pub(crate) fn lever_in_plane(&self, lever: [f64; 3], first: [f64; 3], second: [f64; 3]) -> f64 {
let (a, b) = (cross(lever, first), cross(lever, second));
let (aa, ab, bb) = (self.form(a, a), self.form(a, b), self.form(b, b));
let half_gap = 0.5 * (aa - bb);
(0.5 * (aa + bb) + (half_gap * half_gap + ab * ab).sqrt()).sqrt()
}
pub(crate) fn lever_per_metre(&self) -> f64 {
self.inverse_inertia.into_iter().fold(0.0, f64::max).sqrt()
}
fn form(&self, a: [f64; 3], b: [f64; 3]) -> f64 {
(0..3).map(|k| self.inverse_inertia[k] * a[k] * b[k]).sum()
}
}
pub(crate) fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
pub(crate) fn normalized(v: [f64; 3]) -> [f64; 3] {
let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
v.map(|c| c / length)
}
pub(crate) fn lever_arm_ratio(inverse_mass: f32, inverse_inertia: Vec3, lever: Vec3) -> f64 {
let d = [inverse_inertia.x, inverse_inertia.y, inverse_inertia.z].map(f64::from);
let r = [lever.x, lever.y, lever.z].map(f64::from);
let squared = r.iter().map(|c| c * c).sum::<f64>();
let trace: f64 = (0..3).map(|k| d[k] * (squared - r[k] * r[k])).sum();
trace / f64::from(inverse_mass)
}
pub(crate) fn lever_arm_ratio_within(
inverse_mass: f32,
inverse_inertia: Vec3,
distance: f64,
) -> f64 {
let mut d = [inverse_inertia.x, inverse_inertia.y, inverse_inertia.z].map(f64::from);
d.sort_by(f64::total_cmp);
(d[1] + d[2]) * distance * distance / f64::from(inverse_mass)
}
pub(crate) fn is_lever_arm_ratio(ratio: f64) -> bool {
ratio <= f64::from(MAX_LEVER_ARM_RATIO)
}
pub(crate) fn f64_length(v: Vec3) -> f64 {
let [x, y, z] = [v.x, v.y, v.z].map(f64::from);
(x * x + y * y + z * z).sqrt()
}
pub(crate) mod buoyancy;
#[cfg(test)]
mod tests;