use crate::math::{Fix128, QuatFix, Vec3Fix};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum JointType {
Ball,
Hinge,
Fixed,
Slider,
Spring,
D6,
ConeTwist,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BallJoint {
pub body_a: usize,
pub body_b: usize,
pub local_anchor_a: Vec3Fix,
pub local_anchor_b: Vec3Fix,
pub compliance: Fix128,
pub break_force: Option<Fix128>,
}
impl BallJoint {
#[inline]
#[must_use]
pub const fn new(body_a: usize, body_b: usize, anchor_a: Vec3Fix, anchor_b: Vec3Fix) -> Self {
Self {
body_a,
body_b,
local_anchor_a: anchor_a,
local_anchor_b: anchor_b,
compliance: Fix128::ZERO,
break_force: None,
}
}
#[must_use]
pub const fn with_compliance(mut self, compliance: Fix128) -> Self {
self.compliance = compliance;
self
}
#[must_use]
pub const fn with_break_force(mut self, force: Fix128) -> Self {
self.break_force = Some(force);
self
}
}
const fn fix128_min_max_violated(min: Fix128, max: Fix128) -> bool {
min.hi > max.hi || (min.hi == max.hi && min.lo > max.lo)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HingeJoint {
pub body_a: usize,
pub body_b: usize,
pub local_anchor_a: Vec3Fix,
pub local_anchor_b: Vec3Fix,
pub local_axis_a: Vec3Fix,
pub local_axis_b: Vec3Fix,
pub angle_min: Option<Fix128>,
pub angle_max: Option<Fix128>,
pub compliance: Fix128,
pub angular_compliance: Fix128,
pub break_force: Option<Fix128>,
}
impl HingeJoint {
#[inline]
#[must_use]
pub const fn new(
body_a: usize,
body_b: usize,
anchor_a: Vec3Fix,
anchor_b: Vec3Fix,
axis_a: Vec3Fix,
axis_b: Vec3Fix,
) -> Self {
Self {
body_a,
body_b,
local_anchor_a: anchor_a,
local_anchor_b: anchor_b,
local_axis_a: axis_a,
local_axis_b: axis_b,
angle_min: None,
angle_max: None,
compliance: Fix128::ZERO,
angular_compliance: Fix128::ZERO,
break_force: None,
}
}
#[must_use]
pub const fn with_limits(mut self, min: Fix128, max: Fix128) -> Self {
if fix128_min_max_violated(min, max) {
panic!("HingeJoint::with_limits: min must not exceed max");
}
self.angle_min = Some(min);
self.angle_max = Some(max);
self
}
#[must_use]
pub const fn with_compliance(mut self, compliance: Fix128) -> Self {
self.compliance = compliance;
self
}
#[must_use]
pub const fn with_break_force(mut self, force: Fix128) -> Self {
self.break_force = Some(force);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FixedJoint {
pub body_a: usize,
pub body_b: usize,
pub local_anchor_a: Vec3Fix,
pub local_anchor_b: Vec3Fix,
pub relative_rotation: QuatFix,
pub compliance: Fix128,
pub angular_compliance: Fix128,
pub break_force: Option<Fix128>,
}
impl FixedJoint {
#[inline]
#[must_use]
pub const fn new(
body_a: usize,
body_b: usize,
anchor_a: Vec3Fix,
anchor_b: Vec3Fix,
relative_rotation: QuatFix,
) -> Self {
Self {
body_a,
body_b,
local_anchor_a: anchor_a,
local_anchor_b: anchor_b,
relative_rotation,
compliance: Fix128::ZERO,
angular_compliance: Fix128::ZERO,
break_force: None,
}
}
#[must_use]
pub const fn with_break_force(mut self, force: Fix128) -> Self {
self.break_force = Some(force);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SliderJoint {
pub body_a: usize,
pub body_b: usize,
pub local_axis: Vec3Fix,
pub local_anchor_a: Vec3Fix,
pub local_anchor_b: Vec3Fix,
pub limit_min: Option<Fix128>,
pub limit_max: Option<Fix128>,
pub compliance: Fix128,
pub break_force: Option<Fix128>,
}
impl SliderJoint {
#[inline]
#[must_use]
pub const fn new(
body_a: usize,
body_b: usize,
axis: Vec3Fix,
anchor_a: Vec3Fix,
anchor_b: Vec3Fix,
) -> Self {
Self {
body_a,
body_b,
local_axis: axis,
local_anchor_a: anchor_a,
local_anchor_b: anchor_b,
limit_min: None,
limit_max: None,
compliance: Fix128::ZERO,
break_force: None,
}
}
#[must_use]
pub const fn with_limits(mut self, min: Fix128, max: Fix128) -> Self {
if fix128_min_max_violated(min, max) {
panic!("SliderJoint::with_limits: min must not exceed max");
}
self.limit_min = Some(min);
self.limit_max = Some(max);
self
}
#[must_use]
pub const fn with_break_force(mut self, force: Fix128) -> Self {
self.break_force = Some(force);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpringJoint {
pub body_a: usize,
pub body_b: usize,
pub local_anchor_a: Vec3Fix,
pub local_anchor_b: Vec3Fix,
pub rest_length: Fix128,
pub stiffness: Fix128,
pub damping: Fix128,
pub break_force: Option<Fix128>,
}
impl SpringJoint {
#[inline]
#[must_use]
pub const fn new(
body_a: usize,
body_b: usize,
anchor_a: Vec3Fix,
anchor_b: Vec3Fix,
rest_length: Fix128,
stiffness: Fix128,
damping: Fix128,
) -> Self {
Self {
body_a,
body_b,
local_anchor_a: anchor_a,
local_anchor_b: anchor_b,
rest_length,
stiffness,
damping,
break_force: None,
}
}
#[must_use]
pub const fn with_break_force(mut self, force: Fix128) -> Self {
self.break_force = Some(force);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum D6Motion {
Locked,
#[default]
Free,
Limited,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct D6Joint {
pub body_a: usize,
pub body_b: usize,
pub local_anchor_a: Vec3Fix,
pub local_anchor_b: Vec3Fix,
pub local_frame_a: QuatFix,
pub local_frame_b: QuatFix,
pub linear_x: D6Motion,
pub linear_y: D6Motion,
pub linear_z: D6Motion,
pub angular_x: D6Motion,
pub angular_y: D6Motion,
pub angular_z: D6Motion,
pub linear_limit_min: Vec3Fix,
pub linear_limit_max: Vec3Fix,
pub angular_limit_min: Vec3Fix,
pub angular_limit_max: Vec3Fix,
pub compliance: Fix128,
pub angular_compliance: Fix128,
pub break_force: Option<Fix128>,
}
impl D6Joint {
#[must_use]
pub fn new(body_a: usize, body_b: usize, anchor_a: Vec3Fix, anchor_b: Vec3Fix) -> Self {
Self {
body_a,
body_b,
local_anchor_a: anchor_a,
local_anchor_b: anchor_b,
local_frame_a: QuatFix::IDENTITY,
local_frame_b: QuatFix::IDENTITY,
linear_x: D6Motion::Free,
linear_y: D6Motion::Free,
linear_z: D6Motion::Free,
angular_x: D6Motion::Free,
angular_y: D6Motion::Free,
angular_z: D6Motion::Free,
linear_limit_min: Vec3Fix::new(-Fix128::ONE, -Fix128::ONE, -Fix128::ONE),
linear_limit_max: Vec3Fix::new(Fix128::ONE, Fix128::ONE, Fix128::ONE),
angular_limit_min: Vec3Fix::new(-Fix128::PI, -Fix128::PI, -Fix128::PI),
angular_limit_max: Vec3Fix::new(Fix128::PI, Fix128::PI, Fix128::PI),
compliance: Fix128::ZERO,
angular_compliance: Fix128::ZERO,
break_force: None,
}
}
#[must_use]
pub const fn with_linear_motion(mut self, x: D6Motion, y: D6Motion, z: D6Motion) -> Self {
self.linear_x = x;
self.linear_y = y;
self.linear_z = z;
self
}
#[must_use]
pub const fn with_angular_motion(mut self, x: D6Motion, y: D6Motion, z: D6Motion) -> Self {
self.angular_x = x;
self.angular_y = y;
self.angular_z = z;
self
}
#[must_use]
pub const fn with_linear_limits(mut self, min: Vec3Fix, max: Vec3Fix) -> Self {
if fix128_min_max_violated(min.x, max.x)
|| fix128_min_max_violated(min.y, max.y)
|| fix128_min_max_violated(min.z, max.z)
{
panic!("D6Joint::with_linear_limits: min must not exceed max on any axis");
}
self.linear_limit_min = min;
self.linear_limit_max = max;
self
}
#[must_use]
pub const fn with_angular_limits(mut self, min: Vec3Fix, max: Vec3Fix) -> Self {
if fix128_min_max_violated(min.x, max.x)
|| fix128_min_max_violated(min.y, max.y)
|| fix128_min_max_violated(min.z, max.z)
{
panic!("D6Joint::with_angular_limits: min must not exceed max on any axis");
}
self.angular_limit_min = min;
self.angular_limit_max = max;
self
}
#[must_use]
pub const fn with_break_force(mut self, force: Fix128) -> Self {
self.break_force = Some(force);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ConeTwistJoint {
pub body_a: usize,
pub body_b: usize,
pub local_anchor_a: Vec3Fix,
pub local_anchor_b: Vec3Fix,
pub twist_axis_a: Vec3Fix,
pub twist_axis_b: Vec3Fix,
pub cone_limit: Fix128,
pub twist_limit: Fix128,
pub compliance: Fix128,
pub angular_compliance: Fix128,
pub break_force: Option<Fix128>,
}
impl ConeTwistJoint {
#[must_use]
pub const fn new(
body_a: usize,
body_b: usize,
anchor_a: Vec3Fix,
anchor_b: Vec3Fix,
twist_axis_a: Vec3Fix,
twist_axis_b: Vec3Fix,
) -> Self {
Self {
body_a,
body_b,
local_anchor_a: anchor_a,
local_anchor_b: anchor_b,
twist_axis_a,
twist_axis_b,
cone_limit: Fix128::HALF_PI,
twist_limit: Fix128::PI,
compliance: Fix128::ZERO,
angular_compliance: Fix128::ZERO,
break_force: None,
}
}
#[must_use]
pub const fn with_limits(mut self, cone: Fix128, twist: Fix128) -> Self {
if cone.is_negative() {
panic!("ConeTwistJoint::with_limits: cone must not be negative");
}
if twist.is_negative() {
panic!("ConeTwistJoint::with_limits: twist must not be negative");
}
self.cone_limit = cone;
self.twist_limit = twist;
self
}
#[must_use]
pub const fn with_break_force(mut self, force: Fix128) -> Self {
self.break_force = Some(force);
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Joint {
Ball(BallJoint),
Hinge(HingeJoint),
Fixed(FixedJoint),
Slider(SliderJoint),
Spring(SpringJoint),
D6(D6Joint),
ConeTwist(ConeTwistJoint),
}
impl Joint {
#[inline]
#[must_use]
pub const fn bodies(&self) -> (usize, usize) {
match self {
Self::Ball(j) => (j.body_a, j.body_b),
Self::Hinge(j) => (j.body_a, j.body_b),
Self::Fixed(j) => (j.body_a, j.body_b),
Self::Slider(j) => (j.body_a, j.body_b),
Self::Spring(j) => (j.body_a, j.body_b),
Self::D6(j) => (j.body_a, j.body_b),
Self::ConeTwist(j) => (j.body_a, j.body_b),
}
}
#[inline]
#[must_use]
pub const fn joint_type(&self) -> JointType {
match self {
Self::Ball(_) => JointType::Ball,
Self::Hinge(_) => JointType::Hinge,
Self::Fixed(_) => JointType::Fixed,
Self::Slider(_) => JointType::Slider,
Self::Spring(_) => JointType::Spring,
Self::D6(_) => JointType::D6,
Self::ConeTwist(_) => JointType::ConeTwist,
}
}
#[inline]
#[must_use]
pub const fn break_force(&self) -> Option<Fix128> {
match self {
Self::Ball(j) => j.break_force,
Self::Hinge(j) => j.break_force,
Self::Fixed(j) => j.break_force,
Self::Slider(j) => j.break_force,
Self::Spring(j) => j.break_force,
Self::D6(j) => j.break_force,
Self::ConeTwist(j) => j.break_force,
}
}
#[must_use]
pub fn compute_force(&self, bodies: &[crate::solver::RigidBody]) -> Fix128 {
let (a_idx, b_idx) = self.bodies();
let body_a = &bodies[a_idx];
let body_b = &bodies[b_idx];
match self {
Self::Ball(j) => {
let anchor_a = body_a.position + body_a.rotation.rotate_vec(j.local_anchor_a);
let anchor_b = body_b.position + body_b.rotation.rotate_vec(j.local_anchor_b);
(anchor_b - anchor_a).length()
}
Self::Hinge(j) => {
let anchor_a = body_a.position + body_a.rotation.rotate_vec(j.local_anchor_a);
let anchor_b = body_b.position + body_b.rotation.rotate_vec(j.local_anchor_b);
(anchor_b - anchor_a).length()
}
Self::Fixed(j) => {
let anchor_a = body_a.position + body_a.rotation.rotate_vec(j.local_anchor_a);
let anchor_b = body_b.position + body_b.rotation.rotate_vec(j.local_anchor_b);
(anchor_b - anchor_a).length()
}
Self::Slider(j) => {
let anchor_a = body_a.position + body_a.rotation.rotate_vec(j.local_anchor_a);
let anchor_b = body_b.position + body_b.rotation.rotate_vec(j.local_anchor_b);
let delta = anchor_b - anchor_a;
let world_axis = body_a.rotation.rotate_vec(j.local_axis).normalize();
let along = delta.dot(world_axis);
let perp = delta - world_axis * along;
perp.length()
}
Self::Spring(j) => {
let anchor_a = body_a.position + body_a.rotation.rotate_vec(j.local_anchor_a);
let anchor_b = body_b.position + body_b.rotation.rotate_vec(j.local_anchor_b);
let dist = (anchor_b - anchor_a).length();
let displacement = dist - j.rest_length;
(j.stiffness * displacement).abs()
}
Self::D6(j) => {
let anchor_a = body_a.position + body_a.rotation.rotate_vec(j.local_anchor_a);
let anchor_b = body_b.position + body_b.rotation.rotate_vec(j.local_anchor_b);
(anchor_b - anchor_a).length()
}
Self::ConeTwist(j) => {
let anchor_a = body_a.position + body_a.rotation.rotate_vec(j.local_anchor_a);
let anchor_b = body_b.position + body_b.rotation.rotate_vec(j.local_anchor_b);
(anchor_b - anchor_a).length()
}
}
}
}
pub fn solve_joints(joints: &[Joint], bodies: &mut [crate::solver::RigidBody], dt: Fix128) {
for joint in joints {
let _ = solve_one(joint, bodies, dt);
}
}
fn solve_one(joint: &Joint, bodies: &mut [crate::solver::RigidBody], dt: Fix128) -> Vec3Fix {
let (a, b) = joint.bodies();
if a == b {
return Vec3Fix::ZERO;
}
match joint {
Joint::Ball(j) => solve_ball_joint(j, bodies, dt),
Joint::Hinge(j) => solve_hinge_joint(j, bodies, dt),
Joint::Fixed(j) => solve_fixed_joint(j, bodies, dt),
Joint::Slider(j) => solve_slider_joint(j, bodies, dt),
Joint::Spring(j) => solve_spring_joint(j, bodies, dt),
Joint::D6(j) => solve_d6_joint(j, bodies, dt),
Joint::ConeTwist(j) => solve_cone_twist_joint(j, bodies, dt),
}
}
#[deprecated(
since = "2.2.0",
note = "the threshold is the anchor separation (m), not a force; use solve_joints_breaking_on_force, whose break_force is the reaction force λ/dt² in N"
)]
pub fn solve_joints_breakable(
joints: &[Joint],
bodies: &mut [crate::solver::RigidBody],
dt: Fix128,
) -> Vec<usize> {
let mut broken = Vec::new();
for (i, joint) in joints.iter().enumerate() {
let (a, b) = joint.bodies();
if a == b {
continue;
}
if let Some(max_force) = joint.break_force() {
let force = joint.compute_force(bodies);
if force > max_force {
broken.push(i);
}
}
}
broken.sort_unstable();
for (i, joint) in joints.iter().enumerate() {
if broken.binary_search(&i).is_ok() {
continue;
}
let _ = solve_one(joint, bodies, dt);
}
broken.sort_unstable_by(|a, b| b.cmp(a));
broken
}
pub fn solve_joints_with_reaction_forces(
joints: &[Joint],
bodies: &mut [crate::solver::RigidBody],
dt: Fix128,
) -> Vec<Fix128> {
let h2 = dt * dt;
joints
.iter()
.map(|joint| {
let impulse = solve_one(joint, bodies, dt);
if h2.is_zero() {
Fix128::ZERO
} else {
impulse.length() / h2
}
})
.collect()
}
pub fn solve_joints_breaking_on_force(
joints: &[Joint],
bodies: &mut [crate::solver::RigidBody],
dt: Fix128,
) -> Vec<usize> {
let forces = solve_joints_with_reaction_forces(joints, bodies, dt);
let mut broken: Vec<usize> = joints
.iter()
.zip(&forces)
.enumerate()
.filter(|(_, (joint, force))| joint.break_force().is_some_and(|max| **force > max))
.map(|(i, _)| i)
.collect();
broken.reverse();
broken
}
fn solve_ball_joint(
joint: &BallJoint,
bodies: &mut [crate::solver::RigidBody],
dt: Fix128,
) -> Vec3Fix {
let mut impulse = Vec3Fix::ZERO;
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let r_a = body_a.rotation.rotate_vec(joint.local_anchor_a);
let r_b = body_b.rotation.rotate_vec(joint.local_anchor_b);
let anchor_a = body_a.position + r_a;
let anchor_b = body_b.position + r_b;
let delta = anchor_b - anchor_a;
let (normal, distance) = delta.normalize_with_length();
if distance.is_zero() {
return impulse;
}
let compliance_term = joint.compliance / (dt * dt);
let w_sum = point_w_sum(bodies, joint.body_a, joint.body_b, r_a, r_b, normal) + compliance_term;
if w_sum.is_zero() {
return impulse;
}
let lambda = distance / w_sum;
apply_point_correction(bodies, joint.body_a, joint.body_b, r_a, r_b, normal, lambda);
impulse = impulse + normal * lambda;
impulse
}
fn solve_hinge_joint(
joint: &HingeJoint,
bodies: &mut [crate::solver::RigidBody],
dt: Fix128,
) -> Vec3Fix {
let mut impulse = Vec3Fix::ZERO;
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let r_a = body_a.rotation.rotate_vec(joint.local_anchor_a);
let r_b = body_b.rotation.rotate_vec(joint.local_anchor_b);
let anchor_a = body_a.position + r_a;
let anchor_b = body_b.position + r_b;
let delta = anchor_b - anchor_a;
let (normal, distance) = delta.normalize_with_length();
if !distance.is_zero() {
let compliance_term = joint.compliance / (dt * dt);
let w_sum =
point_w_sum(bodies, joint.body_a, joint.body_b, r_a, r_b, normal) + compliance_term;
if !w_sum.is_zero() {
let lambda = distance / w_sum;
apply_point_correction(bodies, joint.body_a, joint.body_b, r_a, r_b, normal, lambda);
impulse = impulse + normal * lambda;
}
}
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let world_axis_a = body_a.rotation.rotate_vec(joint.local_axis_a);
let world_axis_b = body_b.rotation.rotate_vec(joint.local_axis_b);
let axis_error = world_axis_a.cross(world_axis_b);
let (correction_axis, sin_err) = axis_error.normalize_with_length();
if !sin_err.is_zero() {
let error_mag = Fix128::atan2(sin_err, world_axis_a.dot(world_axis_b));
let angular_compliance = joint.angular_compliance / (dt * dt);
let w_ang =
angular_w_sum(bodies, joint.body_a, joint.body_b, correction_axis) + angular_compliance;
if !w_ang.is_zero() {
let inv_w_ang = Fix128::ONE / w_ang;
let angular_lambda = error_mag * inv_w_ang;
apply_angular_correction(
bodies,
joint.body_a,
joint.body_b,
correction_axis,
angular_lambda,
);
}
}
if joint.angle_min.is_some() || joint.angle_max.is_some() {
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let world_axis_a = body_a.rotation.rotate_vec(joint.local_axis_a);
let rel_quat = body_b.rotation.mul(body_a.rotation.conjugate());
let angle = compute_twist_angle(rel_quat, world_axis_a);
if let Some(min_angle) = joint.angle_min.filter(|m| angle < *m) {
let error = min_angle - angle;
let w_ang = angular_w_sum(bodies, joint.body_a, joint.body_b, world_axis_a);
if !w_ang.is_zero() {
let inv_w_ang = Fix128::ONE / w_ang;
apply_angular_correction(
bodies,
joint.body_a,
joint.body_b,
world_axis_a,
-(error * inv_w_ang),
);
}
} else if let Some(max_angle) = joint.angle_max.filter(|m| angle > *m) {
let error = angle - max_angle;
let w_ang = angular_w_sum(bodies, joint.body_a, joint.body_b, world_axis_a);
if !w_ang.is_zero() {
let inv_w_ang = Fix128::ONE / w_ang;
apply_angular_correction(
bodies,
joint.body_a,
joint.body_b,
world_axis_a,
error * inv_w_ang,
);
}
}
}
impulse
}
fn solve_fixed_joint(
joint: &FixedJoint,
bodies: &mut [crate::solver::RigidBody],
dt: Fix128,
) -> Vec3Fix {
let mut impulse = Vec3Fix::ZERO;
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let r_a = body_a.rotation.rotate_vec(joint.local_anchor_a);
let r_b = body_b.rotation.rotate_vec(joint.local_anchor_b);
let anchor_a = body_a.position + r_a;
let anchor_b = body_b.position + r_b;
let delta = anchor_b - anchor_a;
let (normal, distance) = delta.normalize_with_length();
if !distance.is_zero() {
let compliance_term = joint.compliance / (dt * dt);
let w_sum =
point_w_sum(bodies, joint.body_a, joint.body_b, r_a, r_b, normal) + compliance_term;
if !w_sum.is_zero() {
let lambda = distance / w_sum;
apply_point_correction(bodies, joint.body_a, joint.body_b, r_a, r_b, normal, lambda);
impulse = impulse + normal * lambda;
}
}
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let target_rot_b = body_a.rotation.mul(joint.relative_rotation);
let rot_error = body_b.rotation.mul(target_rot_b.conjugate());
let error_vec = Vec3Fix::new(rot_error.x, rot_error.y, rot_error.z);
let (correction_axis, sin_half) = error_vec.normalize_with_length();
if !sin_half.is_zero() {
let (sin_half, cos_half) = if rot_error.w.is_negative() {
(-sin_half, -rot_error.w)
} else {
(sin_half, rot_error.w)
};
let error_mag = Fix128::atan2(sin_half, cos_half).double();
let angular_compliance = joint.angular_compliance / (dt * dt);
let w_ang =
angular_w_sum(bodies, joint.body_a, joint.body_b, correction_axis) + angular_compliance;
if !w_ang.is_zero() {
let inv_w_ang = Fix128::ONE / w_ang;
let angular_lambda = error_mag * inv_w_ang;
apply_angular_correction(
bodies,
joint.body_a,
joint.body_b,
correction_axis,
angular_lambda,
);
}
}
impulse
}
fn solve_slider_joint(
joint: &SliderJoint,
bodies: &mut [crate::solver::RigidBody],
dt: Fix128,
) -> Vec3Fix {
let mut impulse = Vec3Fix::ZERO;
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let world_axis = body_a.rotation.rotate_vec(joint.local_axis).normalize();
let r_a = body_a.rotation.rotate_vec(joint.local_anchor_a);
let r_b = body_b.rotation.rotate_vec(joint.local_anchor_b);
let anchor_a = body_a.position + r_a;
let anchor_b = body_b.position + r_b;
let delta = anchor_b - anchor_a;
let along_axis = delta.dot(world_axis);
let perp = delta - world_axis * along_axis;
let (perp_normal, perp_dist) = perp.normalize_with_length();
if !perp_dist.is_zero() {
let compliance_term = joint.compliance / (dt * dt);
let w_sum = point_w_sum(bodies, joint.body_a, joint.body_b, r_a, r_b, perp_normal)
+ compliance_term;
if !w_sum.is_zero() {
let lambda = perp_dist / w_sum;
apply_point_correction(
bodies,
joint.body_a,
joint.body_b,
r_a,
r_b,
perp_normal,
lambda,
);
impulse = impulse + perp_normal * lambda;
}
}
if joint.limit_min.is_some() || joint.limit_max.is_some() {
if let Some(min_d) = joint.limit_min.filter(|m| along_axis < *m) {
let error = min_d - along_axis;
let w_sum = body_a.inv_mass + body_b.inv_mass;
if !w_sum.is_zero() {
let inv_w_sum = Fix128::ONE / w_sum;
let correction = world_axis * (error * inv_w_sum);
impulse = impulse - correction;
if !body_a.inv_mass.is_zero() {
bodies[joint.body_a].position =
bodies[joint.body_a].position - correction * body_a.inv_mass;
}
if !body_b.inv_mass.is_zero() {
bodies[joint.body_b].position =
bodies[joint.body_b].position + correction * body_b.inv_mass;
}
}
} else if let Some(max_d) = joint.limit_max.filter(|m| along_axis > *m) {
let error = along_axis - max_d;
let w_sum = body_a.inv_mass + body_b.inv_mass;
if !w_sum.is_zero() {
let inv_w_sum = Fix128::ONE / w_sum;
let correction = world_axis * (error * inv_w_sum);
impulse = impulse + correction;
if !body_a.inv_mass.is_zero() {
bodies[joint.body_a].position =
bodies[joint.body_a].position + correction * body_a.inv_mass;
}
if !body_b.inv_mass.is_zero() {
bodies[joint.body_b].position =
bodies[joint.body_b].position - correction * body_b.inv_mass;
}
}
}
}
impulse
}
fn solve_spring_joint(
joint: &SpringJoint,
bodies: &mut [crate::solver::RigidBody],
dt: Fix128,
) -> Vec3Fix {
let mut impulse = Vec3Fix::ZERO;
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
if body_a.inv_mass.is_zero() && body_b.inv_mass.is_zero() {
return impulse;
}
let r_a = body_a.rotation.rotate_vec(joint.local_anchor_a);
let r_b = body_b.rotation.rotate_vec(joint.local_anchor_b);
let delta = (body_b.position + r_b) - (body_a.position + r_a);
let vel_a = body_a.velocity + body_a.angular_velocity.cross(r_a);
let vel_b = body_b.velocity + body_b.angular_velocity.cross(r_b);
let rel_vel = vel_b - vel_a;
let k = joint.stiffness;
let c = joint.damping;
let h2 = dt * dt;
let (normal, force) = if joint.rest_length.is_zero() {
let (normal, force) = (delta * k + rel_vel * c).normalize_with_length();
if force.is_zero() {
return impulse;
}
(normal, force)
} else {
let (normal, distance) = delta.normalize_with_length();
if distance.is_zero() {
return impulse;
}
let displacement = distance - joint.rest_length;
(normal, k * displacement + c * rel_vel.dot(normal))
};
let w = point_w_sum(bodies, joint.body_a, joint.body_b, r_a, r_b, normal);
let denom = Fix128::ONE + (k * h2 + c * dt) * w;
if denom.is_zero() {
return impulse;
}
let lambda = force * h2 / denom;
apply_point_correction(bodies, joint.body_a, joint.body_b, r_a, r_b, normal, lambda);
impulse = impulse + normal * lambda;
impulse
}
#[allow(clippy::too_many_lines)]
fn solve_d6_joint(joint: &D6Joint, bodies: &mut [crate::solver::RigidBody], dt: Fix128) -> Vec3Fix {
let mut impulse = Vec3Fix::ZERO;
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let anchor_a = body_a.position + body_a.rotation.rotate_vec(joint.local_anchor_a);
let anchor_b = body_b.position + body_b.rotation.rotate_vec(joint.local_anchor_b);
let delta = anchor_b - anchor_a;
let frame_a = body_a.rotation.mul(joint.local_frame_a);
let axis_x = frame_a.rotate_vec(Vec3Fix::UNIT_X);
let axis_y = frame_a.rotate_vec(Vec3Fix::UNIT_Y);
let axis_z = frame_a.rotate_vec(Vec3Fix::UNIT_Z);
let compliance_term = joint.compliance / (dt * dt);
let axes = [
(
joint.linear_x,
axis_x,
joint.linear_limit_min.x,
joint.linear_limit_max.x,
),
(
joint.linear_y,
axis_y,
joint.linear_limit_min.y,
joint.linear_limit_max.y,
),
(
joint.linear_z,
axis_z,
joint.linear_limit_min.z,
joint.linear_limit_max.z,
),
];
for &(motion, axis, limit_min, limit_max) in &axes {
let proj = delta.dot(axis);
let error = match motion {
D6Motion::Locked => proj,
D6Motion::Limited => {
if proj < limit_min {
proj - limit_min
} else if proj > limit_max {
proj - limit_max
} else {
Fix128::ZERO
}
}
D6Motion::Free => Fix128::ZERO,
};
if !error.is_zero() {
let r_a = bodies[joint.body_a]
.rotation
.rotate_vec(joint.local_anchor_a);
let r_b = bodies[joint.body_b]
.rotation
.rotate_vec(joint.local_anchor_b);
let w_sum =
point_w_sum(bodies, joint.body_a, joint.body_b, r_a, r_b, axis) + compliance_term;
if !w_sum.is_zero() {
let lambda = error / w_sum;
apply_point_correction(bodies, joint.body_a, joint.body_b, r_a, r_b, axis, lambda);
impulse = impulse + axis * lambda;
}
}
}
let angular_compliance = joint.angular_compliance / (dt * dt);
{
let frame_a_now = bodies[joint.body_a].rotation.mul(joint.local_frame_a);
let frame_b_now = bodies[joint.body_b].rotation.mul(joint.local_frame_b);
let rel_quat = frame_b_now.mul(frame_a_now.conjugate());
let ang_axes = [
(
joint.angular_x,
axis_x,
joint.angular_limit_min.x,
joint.angular_limit_max.x,
),
(
joint.angular_y,
axis_y,
joint.angular_limit_min.y,
joint.angular_limit_max.y,
),
(
joint.angular_z,
axis_z,
joint.angular_limit_min.z,
joint.angular_limit_max.z,
),
];
for &(motion, axis, limit_min, limit_max) in &ang_axes {
let angle = compute_twist_angle(rel_quat, axis);
let error = match motion {
D6Motion::Locked => angle,
D6Motion::Limited => {
if angle < limit_min {
angle - limit_min
} else if angle > limit_max {
angle - limit_max
} else {
Fix128::ZERO
}
}
D6Motion::Free => Fix128::ZERO,
};
if !error.is_zero() {
let w_ang =
angular_w_sum(bodies, joint.body_a, joint.body_b, axis) + angular_compliance;
if !w_ang.is_zero() {
apply_angular_correction(
bodies,
joint.body_a,
joint.body_b,
axis,
error / w_ang,
);
}
}
}
}
impulse
}
fn solve_cone_twist_joint(
joint: &ConeTwistJoint,
bodies: &mut [crate::solver::RigidBody],
dt: Fix128,
) -> Vec3Fix {
let mut impulse = Vec3Fix::ZERO;
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let r_a = body_a.rotation.rotate_vec(joint.local_anchor_a);
let r_b = body_b.rotation.rotate_vec(joint.local_anchor_b);
let anchor_a = body_a.position + r_a;
let anchor_b = body_b.position + r_b;
let delta = anchor_b - anchor_a;
let (normal, distance) = delta.normalize_with_length();
if !distance.is_zero() {
let compliance_term = joint.compliance / (dt * dt);
let w_sum =
point_w_sum(bodies, joint.body_a, joint.body_b, r_a, r_b, normal) + compliance_term;
if !w_sum.is_zero() {
let lambda = distance / w_sum;
apply_point_correction(bodies, joint.body_a, joint.body_b, r_a, r_b, normal, lambda);
impulse = impulse + normal * lambda;
}
}
let body_a = bodies[joint.body_a];
let body_b = bodies[joint.body_b];
let world_axis_a = body_a.rotation.rotate_vec(joint.twist_axis_a).normalize();
let world_axis_b = body_b.rotation.rotate_vec(joint.twist_axis_b).normalize();
let dot = world_axis_a.dot(world_axis_b);
let cross = world_axis_a.cross(world_axis_b);
let (correction_axis, cross_len) = cross.normalize_with_length();
let cone_angle = Fix128::atan2(cross_len, dot);
let cone_angle = if cone_angle < Fix128::ZERO {
-cone_angle
} else {
cone_angle
};
if cone_angle > joint.cone_limit {
let error = cone_angle - joint.cone_limit;
if !cross_len.is_zero() {
let angular_compliance = joint.angular_compliance / (dt * dt);
let w_ang = angular_w_sum(bodies, joint.body_a, joint.body_b, correction_axis)
+ angular_compliance;
if !w_ang.is_zero() {
let inv_w_ang = Fix128::ONE / w_ang;
apply_angular_correction(
bodies,
joint.body_a,
joint.body_b,
correction_axis,
error * inv_w_ang,
);
}
}
}
let rel_quat = bodies[joint.body_b]
.rotation
.mul(bodies[joint.body_a].rotation.conjugate());
let twist_angle = compute_twist_angle(rel_quat, world_axis_a);
if twist_angle.abs() > joint.twist_limit {
let error = if twist_angle > Fix128::ZERO {
twist_angle - joint.twist_limit
} else {
twist_angle + joint.twist_limit
};
let angular_compliance = joint.angular_compliance / (dt * dt);
let w_ang =
angular_w_sum(bodies, joint.body_a, joint.body_b, world_axis_a) + angular_compliance;
if !w_ang.is_zero() {
let inv_w_ang = Fix128::ONE / w_ang;
apply_angular_correction(
bodies,
joint.body_a,
joint.body_b,
world_axis_a,
error * inv_w_ang,
);
}
}
impulse
}
fn angular_inverse_mass(body: &crate::solver::RigidBody, axis: Vec3Fix) -> Fix128 {
if body.inv_mass.is_zero() {
return Fix128::ZERO;
}
let local = if crate::solver::inv_inertia_is_isotropic(body.inv_inertia) {
axis
} else {
body.rotation.conjugate().rotate_vec(axis)
};
local.x * local.x * body.inv_inertia.x
+ local.y * local.y * body.inv_inertia.y
+ local.z * local.z * body.inv_inertia.z
}
fn angular_w_sum(
bodies: &[crate::solver::RigidBody],
idx_a: usize,
idx_b: usize,
axis: Vec3Fix,
) -> Fix128 {
angular_inverse_mass(&bodies[idx_a], axis) + angular_inverse_mass(&bodies[idx_b], axis)
}
fn apply_angular_correction(
bodies: &mut [crate::solver::RigidBody],
idx_a: usize,
idx_b: usize,
axis: Vec3Fix,
lambda: Fix128,
) {
let w_a = angular_inverse_mass(&bodies[idx_a], axis);
let w_b = angular_inverse_mass(&bodies[idx_b], axis);
if !w_a.is_zero() {
bodies[idx_a].rotation = rotate_by_angle(bodies[idx_a].rotation, axis, w_a * lambda);
}
if !w_b.is_zero() {
bodies[idx_b].rotation = rotate_by_angle(bodies[idx_b].rotation, axis, -(w_b * lambda));
}
}
fn rotate_by_angle(q: QuatFix, axis: Vec3Fix, theta: Fix128) -> QuatFix {
if theta.is_zero() {
return q;
}
QuatFix::from_axis_angle(axis, theta).mul(q).normalize()
}
fn point_angular_w(body: &crate::solver::RigidBody, r: Vec3Fix, normal: Vec3Fix) -> Fix128 {
angular_inverse_mass(body, r.cross(normal))
}
fn point_w_sum(
bodies: &[crate::solver::RigidBody],
idx_a: usize,
idx_b: usize,
r_a: Vec3Fix,
r_b: Vec3Fix,
normal: Vec3Fix,
) -> Fix128 {
bodies[idx_a].inv_mass
+ bodies[idx_b].inv_mass
+ point_angular_w(&bodies[idx_a], r_a, normal)
+ point_angular_w(&bodies[idx_b], r_b, normal)
}
fn apply_point_rotation(
bodies: &mut [crate::solver::RigidBody],
idx: usize,
r: Vec3Fix,
normal: Vec3Fix,
lambda: Fix128,
) {
let body = bodies[idx];
if body.inv_mass.is_zero() {
return;
}
let omega = body.world_inv_inertia_apply(r.cross(normal)) * lambda;
let (axis, angle) = omega.normalize_with_length();
if !angle.is_zero() {
bodies[idx].rotation = rotate_by_angle(bodies[idx].rotation, axis, angle);
}
}
fn apply_point_correction(
bodies: &mut [crate::solver::RigidBody],
idx_a: usize,
idx_b: usize,
r_a: Vec3Fix,
r_b: Vec3Fix,
normal: Vec3Fix,
lambda: Fix128,
) {
let body_a = bodies[idx_a];
let body_b = bodies[idx_b];
if !body_a.inv_mass.is_zero() {
bodies[idx_a].position = body_a.position + normal * (lambda * body_a.inv_mass);
}
if !body_b.inv_mass.is_zero() {
bodies[idx_b].position = body_b.position - normal * (lambda * body_b.inv_mass);
}
apply_point_rotation(bodies, idx_a, r_a, normal, lambda);
apply_point_rotation(bodies, idx_b, r_b, normal, -lambda);
}
pub(crate) fn compute_twist_angle(q: QuatFix, axis: Vec3Fix) -> Fix128 {
let qv = Vec3Fix::new(q.x, q.y, q.z);
let s = qv.dot(axis);
let proj = axis * s;
let twist = QuatFix::new(proj.x, proj.y, proj.z, q.w).normalize();
let signed = Vec3Fix::new(twist.x, twist.y, twist.z).dot(axis);
let (signed, w) = if twist.w.is_negative() {
(-signed, -twist.w)
} else {
(signed, twist.w)
};
Fix128::atan2(signed, w).double()
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::solver::RigidBody;
#[test]
fn test_ball_joint_holds() {
let mut bodies = vec![
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new(Vec3Fix::from_int(5, 0, 0), Fix128::ONE),
];
let joint = Joint::Ball(BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO));
let dt = Fix128::from_ratio(1, 60);
for _ in 0..100 {
solve_joints(&[joint], &mut bodies, dt);
}
let dist = (bodies[1].position - bodies[0].position).length();
assert!(
dist < Fix128::from_int(5),
"Ball joint should pull bodies together"
);
}
#[test]
fn test_fixed_joint() {
let mut bodies = vec![
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new(Vec3Fix::from_int(3, 0, 0), Fix128::ONE),
];
let joint = Joint::Fixed(FixedJoint::new(
0,
1,
Vec3Fix::from_int(1, 0, 0),
Vec3Fix::ZERO,
QuatFix::IDENTITY,
));
let dt = Fix128::from_ratio(1, 60);
for _ in 0..100 {
solve_joints(&[joint], &mut bodies, dt);
}
let dist = (bodies[1].position - Vec3Fix::from_int(1, 0, 0)).length();
assert!(
dist < Fix128::from_int(3),
"Fixed joint should hold position"
);
}
#[test]
fn test_spring_joint() {
let mut bodies = vec![
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new(Vec3Fix::from_int(10, 0, 0), Fix128::ONE),
];
let joint = Joint::Spring(SpringJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Fix128::from_int(3), Fix128::from_int(10), Fix128::from_int(1), ));
let dt = Fix128::from_ratio(1, 60);
for _ in 0..200 {
solve_joints(&[joint], &mut bodies, dt);
}
let dist = bodies[1].position.length();
assert!(
dist < Fix128::from_int(10),
"Spring should pull body closer"
);
}
#[test]
fn test_slider_joint() {
let mut bodies = vec![
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new(Vec3Fix::from_int(3, 5, 0), Fix128::ONE),
];
let joint = Joint::Slider(SliderJoint::new(
0,
1,
Vec3Fix::UNIT_X,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
));
let dt = Fix128::from_ratio(1, 60);
for _ in 0..100 {
solve_joints(&[joint], &mut bodies, dt);
}
let y_abs = bodies[1].position.y.abs();
assert!(
y_abs < Fix128::from_int(5),
"Slider should constrain perpendicular motion"
);
}
#[test]
fn test_joint_bodies() {
let ball = Joint::Ball(BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO));
assert_eq!(ball.bodies(), (0, 1));
assert_eq!(ball.joint_type(), JointType::Ball);
}
#[allow(deprecated)] #[test]
fn test_breakable_joint() {
let mut bodies = vec![
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new(Vec3Fix::from_int(20, 0, 0), Fix128::ONE), ];
let joint = Joint::Ball(
BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO)
.with_break_force(Fix128::from_int(5)),
);
let dt = Fix128::from_ratio(1, 60);
let broken = solve_joints_breakable(&[joint], &mut bodies, dt);
assert_eq!(broken.len(), 1, "Joint should break under high force");
assert_eq!(broken[0], 0);
}
#[allow(deprecated)] #[test]
fn test_unbreakable_joint() {
let mut bodies = vec![
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new(Vec3Fix::from_int(1, 0, 0), Fix128::ONE),
];
let joint = Joint::Ball(
BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO)
.with_break_force(Fix128::from_int(100)),
);
let dt = Fix128::from_ratio(1, 60);
let broken = solve_joints_breakable(&[joint], &mut bodies, dt);
assert!(broken.is_empty(), "Joint should not break under low force");
}
#[test]
fn test_unbreakable_joint_on_force() {
let mut bodies = vec![
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new(Vec3Fix::from_int(1, 0, 0), Fix128::ONE),
];
let joint = Joint::Ball(
BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO)
.with_break_force(Fix128::from_int(4000)),
);
let dt = Fix128::from_ratio(1, 60);
let broken = solve_joints_breaking_on_force(&[joint], &mut bodies, dt);
assert!(broken.is_empty(), "Joint should not break under low force");
}
#[allow(deprecated)] #[test]
fn test_no_break_force() {
let mut bodies = vec![
RigidBody::new_static(Vec3Fix::ZERO),
RigidBody::new(Vec3Fix::from_int(100, 0, 0), Fix128::ONE),
];
let joint = Joint::Ball(BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO));
let dt = Fix128::from_ratio(1, 60);
let broken = solve_joints_breakable(&[joint], &mut bodies, dt);
assert!(
broken.is_empty(),
"Joint without break_force should never break"
);
}
fn fi(n: i64) -> Fix128 {
Fix128::from_int(n)
}
fn v3i(x: i64, y: i64, z: i64) -> Vec3Fix {
Vec3Fix::from_int(x, y, z)
}
const DT: Fix128 = Fix128 { hi: 0, lo: 1 << 62 };
fn pair(pa: Vec3Fix, ia: i64, pb: Vec3Fix, ib: i64) -> Vec<RigidBody> {
let mut a = RigidBody::new_dynamic(pa, Fix128::ONE);
let mut b = RigidBody::new_dynamic(pb, Fix128::ONE);
a.inv_mass = fi(ia);
b.inv_mass = fi(ib);
a.inv_inertia = v3i(1, 1, 1);
b.inv_inertia = v3i(1, 1, 1);
if ia == 0 {
a.body_type = crate::solver::BodyType::Static;
}
if ib == 0 {
b.body_type = crate::solver::BodyType::Static;
}
vec![a, b]
}
fn near(a: Fix128, b: Fix128) -> bool {
(a - b).abs() < Fix128 { hi: 0, lo: 1 << 24 }
}
fn near_v(a: Vec3Fix, b: Vec3Fix) -> bool {
near(a.x, b.x) && near(a.y, b.y) && near(a.z, b.z)
}
#[test]
fn ball_joint_splits_gap_by_inverse_mass() {
let mut bodies = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 3);
let j = BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
solve_ball_joint(&j, &mut bodies, DT);
assert_eq!(bodies[0].position, v3i(1, 0, 0));
assert_eq!(bodies[1].position, v3i(1, 0, 0));
}
#[test]
fn ball_joint_local_anchors_are_rotated_and_static_side_fixed() {
let mut bodies = pair(Vec3Fix::ZERO, 0, v3i(3, 0, 0), 1);
bodies[0].rotation = QuatFix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
let j = BallJoint::new(0, 1, v3i(1, 0, 0), Vec3Fix::ZERO);
solve_ball_joint(&j, &mut bodies, DT);
assert_eq!(bodies[0].position, Vec3Fix::ZERO);
assert_eq!(bodies[1].position, v3i(-1, 0, 0));
let mut bodies2 = pair(Vec3Fix::ZERO, 0, v3i(3, 0, 0), 1);
bodies2[0].rotation = QuatFix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
let j2 = BallJoint::new(0, 1, v3i(1, 0, 0), v3i(0, 2, 0));
solve_ball_joint(&j2, &mut bodies2, DT);
assert!(
near_v(
bodies2[1].position,
Vec3Fix::new(
Fix128::from_ratio(43, 21),
Fix128::from_ratio(-10, 21),
Fix128::ZERO
)
),
"{:?}",
bodies2[1].position
);
assert!(near(
compute_twist_angle(bodies2[1].rotation, Vec3Fix::UNIT_Z),
Fix128::from_ratio(40, 21)
));
}
#[test]
fn ball_joint_compliance_and_degenerate_cases() {
let mut bodies = pair(Vec3Fix::ZERO, 0, v3i(4, 0, 0), 1);
let mut j = BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
j.compliance = Fix128::from_ratio(1, 16);
solve_ball_joint(&j, &mut bodies, DT);
assert_eq!(bodies[1].position, v3i(2, 0, 0));
let mut same = pair(v3i(1, 1, 1), 1, v3i(1, 1, 1), 1);
solve_ball_joint(
&BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO),
&mut same,
DT,
);
assert_eq!(same[0].position, v3i(1, 1, 1));
let mut statics = pair(Vec3Fix::ZERO, 0, v3i(4, 0, 0), 0);
solve_ball_joint(
&BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO),
&mut statics,
DT,
);
assert_eq!(statics[1].position, v3i(4, 0, 0));
}
#[test]
fn hinge_joint_positional_part_matches_ball_joint() {
let mut bodies = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 3);
let j = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
);
solve_hinge_joint(&j, &mut bodies, DT);
assert_eq!(bodies[0].position, v3i(1, 0, 0));
assert_eq!(bodies[1].position, v3i(1, 0, 0));
assert_eq!(bodies[0].rotation, QuatFix::IDENTITY);
assert_eq!(bodies[1].rotation, QuatFix::IDENTITY);
}
#[test]
fn hinge_joint_aligns_axes_monotonically() {
let mut bodies = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
bodies[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_X, Fix128::from_ratio(1, 2));
let j = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
);
let err = |b: &[RigidBody]| {
let a = b[0].rotation.rotate_vec(Vec3Fix::UNIT_Z);
let c = b[1].rotation.rotate_vec(Vec3Fix::UNIT_Z);
a.cross(c).length()
};
let mut prev = err(&bodies);
assert!(prev > Fix128::from_ratio(1, 10));
for i in 0..30 {
solve_hinge_joint(&j, &mut bodies, DT);
let e = err(&bodies);
assert!(e <= prev, "iter {i}: {prev:?} -> {e:?}");
prev = e;
}
assert!(prev < Fix128::from_ratio(1, 1000));
assert_eq!(bodies[0].rotation, QuatFix::IDENTITY);
let mut both = pair(Vec3Fix::ZERO, 1, Vec3Fix::ZERO, 1);
both[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_X, Fix128::from_ratio(1, 2));
solve_hinge_joint(&j, &mut both, DT);
assert!(both[0].rotation != QuatFix::IDENTITY);
}
#[test]
fn hinge_joint_limits_push_angle_back_into_range() {
let j = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(Fix128::from_ratio(-1, 2), Fix128::from_ratio(1, 2));
let angle = |b: &[RigidBody]| {
let rel = b[1].rotation.mul(b[0].rotation.conjugate());
compute_twist_angle(rel, Vec3Fix::UNIT_Z)
};
let mut over = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
over[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::ONE);
let a0 = angle(&over);
assert!(near(a0, Fix128::ONE), "{a0:?}");
solve_hinge_joint(&j, &mut over, DT);
let a1 = angle(&over);
assert!(
a1 < a0 && a1 >= Fix128::from_ratio(1, 2) - Fix128::from_ratio(1, 100),
"{a0:?} -> {a1:?}"
);
let mut under = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
under[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, fi(-1));
let b0 = angle(&under);
solve_hinge_joint(&j, &mut under, DT);
let b1 = angle(&under);
assert!(b1 > b0, "{b0:?} -> {b1:?}");
let mut inside = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
inside[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::from_ratio(1, 4));
let r0 = inside[1].rotation;
solve_hinge_joint(&j, &mut inside, DT);
assert_eq!(inside[1].rotation, r0);
let mut free = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
free[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::ONE);
let f0 = free[1].rotation;
let nolimit = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
);
solve_hinge_joint(&nolimit, &mut free, DT);
assert_eq!(free[1].rotation, f0);
}
#[test]
fn fixed_joint_restores_relative_rotation_and_position() {
let mut bodies = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 3);
let j = FixedJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO, QuatFix::IDENTITY);
solve_fixed_joint(&j, &mut bodies, DT);
assert_eq!(bodies[0].position, v3i(1, 0, 0));
assert_eq!(bodies[1].position, v3i(1, 0, 0));
let mut rot = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
rot[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::from_ratio(1, 2));
let err = |b: &[RigidBody]| {
let e = b[1].rotation.mul(b[0].rotation.conjugate());
Vec3Fix::new(e.x, e.y, e.z).length()
};
let mut prev = err(&rot);
for i in 0..30 {
solve_fixed_joint(&j, &mut rot, DT);
let e = err(&rot);
assert!(e <= prev, "iter {i}");
prev = e;
}
assert!(prev < Fix128::from_ratio(1, 1000));
let target = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::HALF_PI);
let j90 = FixedJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO, target);
let mut ok = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
ok[1].rotation = target;
let before = ok[1].rotation;
solve_fixed_joint(&j90, &mut ok, DT);
assert!(
near(ok[1].rotation.x, before.x)
&& near(ok[1].rotation.z, before.z)
&& near(ok[1].rotation.w, before.w)
);
}
#[test]
fn slider_joint_removes_perpendicular_offset_only() {
let mut bodies = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
let j = SliderJoint::new(0, 1, Vec3Fix::UNIT_X, Vec3Fix::ZERO, Vec3Fix::ZERO);
solve_slider_joint(&j, &mut bodies, DT);
assert_eq!(bodies[1].position, v3i(3, 0, 0));
let mut both = pair(Vec3Fix::ZERO, 1, v3i(3, 4, 0), 3);
solve_slider_joint(&j, &mut both, DT);
assert_eq!(both[0].position, v3i(0, 1, 0));
assert_eq!(both[1].position, v3i(3, 1, 0));
let mut rot = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
rot[0].rotation = QuatFix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
solve_slider_joint(&j, &mut rot, DT);
assert_eq!(rot[1].position, v3i(3, 0, 0));
}
#[test]
fn slider_joint_limits_clamp_travel_along_axis() {
let j = SliderJoint::new(0, 1, Vec3Fix::UNIT_X, Vec3Fix::ZERO, Vec3Fix::ZERO)
.with_limits(fi(-1), fi(2));
let mut over = pair(Vec3Fix::ZERO, 0, v3i(5, 0, 0), 1);
solve_slider_joint(&j, &mut over, DT);
assert_eq!(over[1].position, v3i(2, 0, 0));
let mut under = pair(Vec3Fix::ZERO, 0, v3i(-4, 0, 0), 1);
solve_slider_joint(&j, &mut under, DT);
assert_eq!(under[1].position, v3i(-1, 0, 0));
let mut both = pair(Vec3Fix::ZERO, 1, v3i(6, 0, 0), 3);
solve_slider_joint(&j, &mut both, DT);
assert_eq!(both[0].position, v3i(1, 0, 0));
assert_eq!(both[1].position, v3i(3, 0, 0));
let mut edge = pair(Vec3Fix::ZERO, 0, v3i(2, 0, 0), 1);
solve_slider_joint(&j, &mut edge, DT);
assert_eq!(edge[1].position, v3i(2, 0, 0));
let mut inside = pair(Vec3Fix::ZERO, 0, v3i(1, 0, 0), 1);
solve_slider_joint(&j, &mut inside, DT);
assert_eq!(inside[1].position, v3i(1, 0, 0));
}
#[test]
fn spring_joint_force_is_stiffness_times_displacement_plus_damping() {
let mut bodies = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 3);
let j = SpringJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
fi(1),
fi(2),
Fix128::ZERO,
);
solve_spring_joint(&j, &mut bodies, DT);
assert_eq!(
bodies[0].position,
Vec3Fix::new(Fix128::from_ratio(1, 4), Fix128::ZERO, Fix128::ZERO)
);
assert_eq!(
bodies[1].position,
Vec3Fix::new(Fix128::from_ratio(13, 4), Fix128::ZERO, Fix128::ZERO)
);
let mut comp = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 1);
let jc = SpringJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
fi(8),
fi(2),
Fix128::ZERO,
);
solve_spring_joint(&jc, &mut comp, DT);
assert!(near_v(
comp[0].position,
Vec3Fix::new(Fix128::from_ratio(-2, 5), Fix128::ZERO, Fix128::ZERO)
));
assert!(near_v(
comp[1].position,
Vec3Fix::new(Fix128::from_ratio(22, 5), Fix128::ZERO, Fix128::ZERO)
));
let mut damp = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 1);
damp[1].velocity = v3i(3, 0, 0);
let jd = SpringJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO, fi(4), fi(2), fi(2));
solve_spring_joint(&jd, &mut damp, DT);
assert!(near_v(
damp[0].position,
Vec3Fix::new(Fix128::from_ratio(1, 6), Fix128::ZERO, Fix128::ZERO)
));
assert!(near_v(
damp[1].position,
Vec3Fix::new(Fix128::from_ratio(23, 6), Fix128::ZERO, Fix128::ZERO)
));
let mut rest = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 1);
solve_spring_joint(&jd, &mut rest, DT);
assert_eq!(rest[1].position, v3i(4, 0, 0));
}
#[test]
fn d6_joint_linear_locked_limited_and_free_axes() {
let mut j = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
j.linear_x = D6Motion::Locked;
j.linear_y = D6Motion::Limited; j.linear_z = D6Motion::Free;
let mut bodies = pair(Vec3Fix::ZERO, 0, v3i(4, 3, 5), 1);
solve_d6_joint(&j, &mut bodies, DT);
assert_eq!(bodies[1].position, v3i(0, 1, 5));
let mut low = pair(Vec3Fix::ZERO, 0, v3i(0, -6, 0), 1);
solve_d6_joint(&j, &mut low, DT);
assert_eq!(low[1].position, v3i(0, -1, 0));
let mut both = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 3);
solve_d6_joint(&j, &mut both, DT);
assert_eq!(both[0].position, v3i(1, 0, 0));
assert_eq!(both[1].position, v3i(1, 0, 0));
let mut jf = j;
jf.local_frame_a = QuatFix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
let mut flipped = pair(Vec3Fix::ZERO, 0, v3i(4, 0, 0), 1);
solve_d6_joint(&jf, &mut flipped, DT);
assert_eq!(flipped[1].position, Vec3Fix::ZERO);
}
#[test]
fn d6_joint_second_locked_axis_uses_the_rotation_from_the_first() {
let mut j = D6Joint::new(0, 1, Vec3Fix::ZERO, v3i(0, 2, 0));
j.linear_x = D6Motion::Locked;
j.linear_y = D6Motion::Locked;
let mut bodies = pair(Vec3Fix::ZERO, 0, v3i(4, 3, 0), 1);
solve_d6_joint(&j, &mut bodies, DT);
let theta = Fix128::from_ratio(8, 5);
let sin_theta = theta.sin();
let want_y = Fix128::from_int(3)
- Fix128::from_int(5) / (Fix128::ONE + Fix128::from_int(4) * sin_theta * sin_theta);
assert!(
near(bodies[1].position.x, Fix128::from_ratio(16, 5)),
"x {:?}",
bodies[1].position.x
);
assert!(
near(bodies[1].position.y, want_y),
"y {:?} (want {want_y:?})",
bodies[1].position.y
);
assert!(
!near(bodies[1].position.y, Fix128::from_int(-2)),
"y landed on the stale-cache value: {:?}",
bodies[1].position.y
);
}
#[test]
fn d6_joint_angular_locked_axis_pulls_rotation_back() {
let mut j = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
j.angular_z = D6Motion::Locked;
let angle = |b: &[RigidBody]| {
let rel = b[1].rotation.mul(b[0].rotation.conjugate());
compute_twist_angle(rel, Vec3Fix::UNIT_Z)
};
let mut bodies = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
bodies[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::from_ratio(1, 2));
let mut prev = angle(&bodies).abs();
let noise = Fix128::from_raw(0, 1 << 24);
for i in 0..30 {
solve_d6_joint(&j, &mut bodies, DT);
let a = angle(&bodies).abs();
assert!(a <= prev + noise, "iter {i}");
prev = a;
}
assert!(prev < Fix128::from_ratio(1, 1000));
let mut jl = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
jl.angular_z = D6Motion::Limited;
jl.angular_limit_min = Vec3Fix::new(
Fix128::from_ratio(-1, 4),
Fix128::from_ratio(-1, 4),
Fix128::from_ratio(-1, 4),
);
jl.angular_limit_max = Vec3Fix::new(
Fix128::from_ratio(1, 4),
Fix128::from_ratio(1, 4),
Fix128::from_ratio(1, 4),
);
let mut over = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
over[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::from_ratio(1, 2));
let o0 = angle(&over);
solve_d6_joint(&jl, &mut over, DT);
assert!(angle(&over) < o0);
let mut inside = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
inside[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::from_ratio(1, 10));
let r0 = inside[1].rotation;
solve_d6_joint(&jl, &mut inside, DT);
assert_eq!(inside[1].rotation, r0);
let free = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
let mut f = pair(Vec3Fix::ZERO, 0, v3i(4, 3, 5), 1);
f[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::from_ratio(1, 2));
let (p0, q0) = (f[1].position, f[1].rotation);
solve_d6_joint(&free, &mut f, DT);
assert_eq!((f[1].position, f[1].rotation), (p0, q0));
}
#[test]
fn cone_twist_joint_enforces_cone_and_twist_limits() {
let cone_angle = |b: &[RigidBody]| {
let a = b[0].rotation.rotate_vec(Vec3Fix::UNIT_Z);
let c = b[1].rotation.rotate_vec(Vec3Fix::UNIT_Z);
Fix128::atan2(a.cross(c).length(), a.dot(c))
};
let j = ConeTwistJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(Fix128::from_ratio(1, 4), Fix128::from_ratio(1, 4));
let mut tilted = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
tilted[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_X, Fix128::ONE);
let c0 = cone_angle(&tilted);
assert!(near(c0, Fix128::ONE), "{c0:?}");
solve_cone_twist_joint(&j, &mut tilted, DT);
let c1 = cone_angle(&tilted);
assert!(c1 < c0, "{c0:?} -> {c1:?}");
let mut prev = c1;
for _ in 0..40 {
solve_cone_twist_joint(&j, &mut tilted, DT);
let c = cone_angle(&tilted);
assert!(c <= prev + Fix128 { hi: 0, lo: 1 << 30 });
prev = c;
}
assert!(
prev < Fix128::from_ratio(1, 4) + Fix128::from_ratio(1, 50),
"{prev:?}"
);
let twist = |b: &[RigidBody]| {
let rel = b[1].rotation.mul(b[0].rotation.conjugate());
compute_twist_angle(rel, Vec3Fix::UNIT_Z)
};
let mut twisted = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
twisted[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::ONE);
let t0 = twist(&twisted);
solve_cone_twist_joint(&j, &mut twisted, DT);
assert!(twist(&twisted) < t0);
let mut inside = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
inside[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_X, Fix128::from_ratio(1, 10));
let r0 = inside[1].rotation;
solve_cone_twist_joint(&j, &mut inside, DT);
assert_eq!(inside[1].rotation, r0);
let mut pos = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 3);
solve_cone_twist_joint(&j, &mut pos, DT);
assert_eq!(pos[0].position, v3i(1, 0, 0));
assert_eq!(pos[1].position, v3i(1, 0, 0));
}
#[test]
fn joint_bodies_and_compute_force_per_kind() {
let bodies = pair(Vec3Fix::ZERO, 1, v3i(3, 4, 0), 1); let ball = Joint::Ball(BallJoint::new(2, 7, Vec3Fix::ZERO, Vec3Fix::ZERO));
assert_eq!(ball.bodies(), (2, 7));
let ball01 = Joint::Ball(BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO));
assert_eq!(ball01.compute_force(&bodies), fi(5));
let hinge = Joint::Hinge(HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
));
assert_eq!(hinge.compute_force(&bodies), fi(5));
let fixed = Joint::Fixed(FixedJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
QuatFix::IDENTITY,
));
assert_eq!(fixed.compute_force(&bodies), fi(5));
let slider = Joint::Slider(SliderJoint::new(
0,
1,
Vec3Fix::UNIT_X,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
));
assert_eq!(slider.compute_force(&bodies), fi(4));
let spring = Joint::Spring(SpringJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
fi(8),
fi(2),
Fix128::ZERO,
));
assert_eq!(spring.compute_force(&bodies), fi(6));
let d6 = Joint::D6(D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO));
assert_eq!(d6.compute_force(&bodies), fi(5));
let ct = Joint::ConeTwist(ConeTwistJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
));
assert_eq!(ct.compute_force(&bodies), fi(5));
let anchored = Joint::Ball(BallJoint::new(0, 1, v3i(3, 4, 0), Vec3Fix::ZERO));
assert_eq!(anchored.compute_force(&bodies), Fix128::ZERO);
}
#[allow(deprecated)] #[test]
fn solve_joints_breakable_breaks_strictly_above_threshold_and_skips_broken() {
let mk = |bf: i64| {
Joint::Ball(BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO).with_break_force(fi(bf)))
};
let mut hold = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
let broken = solve_joints_breakable(&[mk(5)], &mut hold, DT);
assert!(broken.is_empty());
assert!(
near_v(hold[1].position, Vec3Fix::ZERO),
"解かれて A に一致 {:?}",
hold[1].position
);
let mut snap = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
let broken2 = solve_joints_breakable(&[mk(4)], &mut snap, DT);
assert_eq!(broken2, vec![0]);
assert_eq!(snap[1].position, v3i(3, 4, 0), "壊れた joint は解かれない");
let mut multi = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
let js = [mk(4), mk(100), mk(1)];
let broken3 = solve_joints_breakable(&js, &mut multi, DT);
assert_eq!(broken3, vec![2, 0]);
assert!(near_v(multi[1].position, Vec3Fix::ZERO));
let mut all = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
solve_joints(&[mk(1)], &mut all, DT);
assert!(near_v(all[1].position, Vec3Fix::ZERO));
}
#[test]
fn breaking_on_force_breaks_strictly_above_threshold_and_skips_broken() {
let mk = |bf: i64| {
Joint::Ball(BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO).with_break_force(fi(bf)))
};
let mut hold = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
let broken = solve_joints_breaking_on_force(&[mk(80)], &mut hold, DT);
assert!(broken.is_empty());
assert!(
near_v(hold[1].position, Vec3Fix::ZERO),
"解かれて A に一致 {:?}",
hold[1].position
);
let mut snap = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
let broken2 = solve_joints_breaking_on_force(&[mk(64)], &mut snap, DT);
assert_eq!(broken2, vec![0]);
assert!(
near_v(snap[1].position, Vec3Fix::ZERO),
"{:?}",
snap[1].position
);
let mut multi = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
let js = [mk(64), mk(1600), mk(16)];
let broken3 = solve_joints_breaking_on_force(&js, &mut multi, DT);
assert_eq!(broken3, vec![0]);
assert!(near_v(multi[1].position, Vec3Fix::ZERO));
let mut all = pair(Vec3Fix::ZERO, 0, v3i(3, 4, 0), 1);
solve_joints(&[mk(16)], &mut all, DT);
assert!(near_v(all[1].position, Vec3Fix::ZERO));
}
#[test]
fn d6_with_linear_motion_and_limits_builders_drive_solver() {
let j = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO)
.with_linear_motion(D6Motion::Locked, D6Motion::Limited, D6Motion::Free)
.with_linear_limits(v3i(-7, -2, -9), v3i(7, 2, 9));
assert_eq!(
(j.linear_x, j.linear_y, j.linear_z),
(D6Motion::Locked, D6Motion::Limited, D6Motion::Free)
);
assert_eq!(j.linear_limit_min, v3i(-7, -2, -9));
assert_eq!(j.linear_limit_max, v3i(7, 2, 9));
assert_eq!(
(j.angular_x, j.angular_y, j.angular_z),
(D6Motion::Free, D6Motion::Free, D6Motion::Free)
);
assert_eq!(j.angular_limit_max.y, Fix128::PI);
let mut manual = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
manual.linear_x = D6Motion::Locked;
manual.linear_y = D6Motion::Limited;
manual.linear_z = D6Motion::Free;
manual.linear_limit_min = v3i(-7, -2, -9);
manual.linear_limit_max = v3i(7, 2, 9);
assert_eq!(j, manual);
let mut bodies = pair(Vec3Fix::ZERO, 0, v3i(4, 3, 5), 1);
solve_d6_joint(&j, &mut bodies, DT);
assert_eq!(bodies[1].position, v3i(0, 2, 5));
let mut low = pair(Vec3Fix::ZERO, 0, v3i(0, -6, 0), 1);
solve_d6_joint(&j, &mut low, DT);
assert_eq!(low[1].position, v3i(0, -2, 0));
let mut inside = pair(Vec3Fix::ZERO, 0, v3i(0, 1, 0), 1);
solve_d6_joint(&j, &mut inside, DT);
assert_eq!(inside[1].position, v3i(0, 1, 0));
}
#[test]
fn d6_with_angular_motion_and_limits_builders_drive_solver() {
let q = Fix128::from_ratio(1, 4);
let j = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO)
.with_angular_motion(D6Motion::Free, D6Motion::Locked, D6Motion::Limited)
.with_angular_limits(Vec3Fix::new(-q, -q, -q), Vec3Fix::new(q, q, q));
assert_eq!(
(j.angular_x, j.angular_y, j.angular_z),
(D6Motion::Free, D6Motion::Locked, D6Motion::Limited)
);
assert_eq!(j.angular_limit_min, Vec3Fix::new(-q, -q, -q));
assert_eq!(j.angular_limit_max, Vec3Fix::new(q, q, q));
assert_eq!(
(j.linear_x, j.linear_y, j.linear_z),
(D6Motion::Free, D6Motion::Free, D6Motion::Free)
);
assert_eq!(j.linear_limit_min, v3i(-1, -1, -1));
let angle = |b: &[RigidBody], axis: Vec3Fix| {
let rel = b[1].rotation.mul(b[0].rotation.conjugate());
compute_twist_angle(rel, axis)
};
let mut over = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
over[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::from_ratio(1, 2));
let o0 = angle(&over, Vec3Fix::UNIT_Z);
solve_d6_joint(&j, &mut over, DT);
let o1 = angle(&over, Vec3Fix::UNIT_Z);
assert!(o1 < o0, "{o1:?} < {o0:?}");
assert!(
o1 >= q - Fix128::from_raw(0, 1 << 24),
"limit は over-correct しない: {o1:?}"
);
let mut inside = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
inside[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Z, Fix128::from_ratio(1, 10));
let z0 = angle(&inside, Vec3Fix::UNIT_Z);
solve_d6_joint(&j, &mut inside, DT);
let z1 = angle(&inside, Vec3Fix::UNIT_Z);
assert!(near(z0, z1), "{z0:?} vs {z1:?}");
let mut locked = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
locked[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_Y, Fix128::from_ratio(1, 10));
let l0 = angle(&locked, Vec3Fix::UNIT_Y).abs();
solve_d6_joint(&j, &mut locked, DT);
assert!(angle(&locked, Vec3Fix::UNIT_Y).abs() < l0);
let mut free = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
free[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_X, Fix128::from_ratio(1, 2));
let x0 = angle(&free, Vec3Fix::UNIT_X);
solve_d6_joint(&j, &mut free, DT);
let x1 = angle(&free, Vec3Fix::UNIT_X);
assert!(near(x0, x1), "{x0:?} vs {x1:?}");
assert!(
x1 > Fix128::from_ratio(49, 100),
"x Free は 1/2 のまま: {x1:?}"
);
}
fn anchored_pair() -> Vec<RigidBody> {
let mut b = pair(Vec3Fix::ZERO, 0, v3i(3, -2, 0), 1);
b[0].rotation = QuatFix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
b
}
const A_ANCHOR: Vec3Fix = Vec3Fix {
x: Fix128::ONE,
y: Fix128::ZERO,
z: Fix128::ZERO,
};
const B_ANCHOR: Vec3Fix = Vec3Fix {
x: Fix128::ZERO,
y: Fix128 { hi: 2, lo: 0 },
z: Fix128::ZERO,
};
#[test]
fn every_joint_positional_part_uses_rotated_local_anchors() {
let want_pos = Vec3Fix::new(
Fix128::from_ratio(11, 5),
-Fix128::from_int(2),
Fix128::ZERO,
);
let want_twist = Fix128::from_ratio(8, 5);
let mut b = anchored_pair();
solve_ball_joint(&BallJoint::new(0, 1, A_ANCHOR, B_ANCHOR), &mut b, DT);
assert!(near_v(b[1].position, want_pos), "ball {:?}", b[1].position);
assert!(near(
compute_twist_angle(b[1].rotation, Vec3Fix::UNIT_Z),
want_twist
));
let mut h = anchored_pair();
solve_hinge_joint(
&HingeJoint::new(0, 1, A_ANCHOR, B_ANCHOR, Vec3Fix::UNIT_Z, Vec3Fix::UNIT_Z),
&mut h,
DT,
);
assert!(near_v(h[1].position, want_pos), "hinge {:?}", h[1].position);
assert!(near(
compute_twist_angle(h[1].rotation, Vec3Fix::UNIT_Z),
want_twist
));
let mut f = anchored_pair();
solve_fixed_joint(
&FixedJoint::new(
0,
1,
A_ANCHOR,
B_ANCHOR,
QuatFix::new(Fix128::ZERO, Fix128::ZERO, -Fix128::ONE, Fix128::ZERO),
),
&mut f,
DT,
);
assert!(near_v(f[1].position, want_pos), "fixed {:?}", f[1].position);
assert_eq!(
f[1].rotation,
QuatFix::IDENTITY,
"fixed rotation {:?}",
f[1].rotation
);
let mut ct = anchored_pair();
solve_cone_twist_joint(
&ConeTwistJoint::new(0, 1, A_ANCHOR, B_ANCHOR, Vec3Fix::UNIT_Z, Vec3Fix::UNIT_Z),
&mut ct,
DT,
);
assert!(
near_v(ct[1].position, want_pos),
"cone {:?}",
ct[1].position
);
assert!(near(
compute_twist_angle(ct[1].rotation, Vec3Fix::UNIT_Z),
want_twist
));
let mut d6j = D6Joint::new(0, 1, A_ANCHOR, B_ANCHOR);
d6j.linear_x = D6Motion::Locked;
d6j.linear_y = D6Motion::Locked;
d6j.linear_z = D6Motion::Locked;
let mut d = anchored_pair();
solve_d6_joint(&d6j, &mut d, DT);
assert!(near_v(d[1].position, want_pos), "d6 {:?}", d[1].position);
assert!(near(
compute_twist_angle(d[1].rotation, Vec3Fix::UNIT_Z),
want_twist
));
let mut sl = anchored_pair();
solve_slider_joint(
&SliderJoint::new(0, 1, Vec3Fix::UNIT_X, A_ANCHOR, v3i(0, 3, 0)),
&mut sl,
DT,
);
assert!(
near_v(sl[1].position, v3i(3, -3, 0)),
"slider {:?}",
sl[1].position
);
let mut sp0 = anchored_pair();
solve_spring_joint(
&SpringJoint::new(0, 1, A_ANCHOR, B_ANCHOR, fi(4), Fix128::ONE, Fix128::ZERO),
&mut sp0,
DT,
);
assert_eq!(sp0[1].position, v3i(3, -2, 0));
let mut sp = anchored_pair();
solve_spring_joint(
&SpringJoint::new(0, 1, A_ANCHOR, B_ANCHOR, fi(2), Fix128::ONE, Fix128::ZERO),
&mut sp,
DT,
);
assert!(
near_v(
sp[1].position,
Vec3Fix::new(Fix128::from_ratio(61, 21), fi(-2), Fix128::ZERO)
),
"spring {:?}",
sp[1].position
);
assert!(near(
compute_twist_angle(sp[1].rotation, Vec3Fix::UNIT_Z),
Fix128::from_ratio(4, 21)
));
}
#[test]
fn compliance_softens_every_positional_joint_identically() {
let expect = v3i(2, 0, 0);
let mut h = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
);
h.compliance = Fix128::from_ratio(1, 16);
let mut bh = pair(Vec3Fix::ZERO, 0, v3i(4, 0, 0), 1);
solve_hinge_joint(&h, &mut bh, DT);
assert_eq!(bh[1].position, expect, "hinge");
let mut f = FixedJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO, QuatFix::IDENTITY);
f.compliance = Fix128::from_ratio(1, 16);
let mut bf = pair(Vec3Fix::ZERO, 0, v3i(4, 0, 0), 1);
solve_fixed_joint(&f, &mut bf, DT);
assert_eq!(bf[1].position, expect, "fixed");
let mut c = ConeTwistJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
);
c.compliance = Fix128::from_ratio(1, 16);
let mut bc = pair(Vec3Fix::ZERO, 0, v3i(4, 0, 0), 1);
solve_cone_twist_joint(&c, &mut bc, DT);
assert_eq!(bc[1].position, expect, "cone");
let mut s = SliderJoint::new(0, 1, Vec3Fix::UNIT_X, Vec3Fix::ZERO, Vec3Fix::ZERO);
s.compliance = Fix128::from_ratio(1, 16);
let mut bs = pair(Vec3Fix::ZERO, 0, v3i(0, 4, 0), 1); solve_slider_joint(&s, &mut bs, DT);
assert_eq!(bs[1].position, v3i(0, 2, 0), "slider");
let mut d = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
d.linear_x = D6Motion::Locked;
d.compliance = Fix128::from_ratio(1, 16);
let mut bd = pair(Vec3Fix::ZERO, 0, v3i(4, 0, 0), 1);
solve_d6_joint(&d, &mut bd, DT);
assert_eq!(bd[1].position, expect, "d6");
let mk = |ac: Fix128| {
let mut j = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
);
j.angular_compliance = ac;
let mut b = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
b[1].rotation = QuatFix::from_axis_angle(Vec3Fix::UNIT_X, Fix128::from_ratio(1, 2));
solve_hinge_joint(&j, &mut b, DT);
let a = b[0].rotation.rotate_vec(Vec3Fix::UNIT_Z);
let c = b[1].rotation.rotate_vec(Vec3Fix::UNIT_Z);
a.cross(c).length()
};
assert!(
mk(Fix128::from_ratio(1, 4)) > mk(Fix128::ZERO),
"angular compliance で補正が弱まる"
);
}
#[test]
fn limit_equality_is_inside_the_allowed_range() {
let sj = SliderJoint::new(0, 1, Vec3Fix::UNIT_X, Vec3Fix::ZERO, Vec3Fix::ZERO)
.with_limits(fi(-1), fi(2));
let mut at_min = pair(Vec3Fix::ZERO, 0, v3i(-1, 0, 0), 1);
solve_slider_joint(&sj, &mut at_min, DT);
assert_eq!(at_min[1].position, v3i(-1, 0, 0));
let mut dj = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
dj.linear_y = D6Motion::Limited; let mut at_max = pair(Vec3Fix::ZERO, 0, v3i(0, 1, 0), 1);
solve_d6_joint(&dj, &mut at_max, DT);
assert_eq!(at_max[1].position, v3i(0, 1, 0));
let mut at_lo = pair(Vec3Fix::ZERO, 0, v3i(0, -1, 0), 1);
solve_d6_joint(&dj, &mut at_lo, DT);
assert_eq!(at_lo[1].position, v3i(0, -1, 0));
let mut below = pair(Vec3Fix::ZERO, 0, v3i(0, -3, 0), 1);
solve_d6_joint(&dj, &mut below, DT);
assert_eq!(below[1].position, v3i(0, -1, 0));
}
#[test]
fn compute_force_honours_anchors_and_rotation_for_every_kind() {
let bodies = anchored_pair(); assert_eq!(
Joint::Ball(BallJoint::new(0, 1, A_ANCHOR, B_ANCHOR)).compute_force(&bodies),
fi(4)
);
assert_eq!(
Joint::Hinge(HingeJoint::new(
0,
1,
A_ANCHOR,
B_ANCHOR,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z
))
.compute_force(&bodies),
fi(4)
);
assert_eq!(
Joint::Fixed(FixedJoint::new(0, 1, A_ANCHOR, B_ANCHOR, QuatFix::IDENTITY))
.compute_force(&bodies),
fi(4)
);
assert_eq!(
Joint::D6(D6Joint::new(0, 1, A_ANCHOR, B_ANCHOR)).compute_force(&bodies),
fi(4)
);
assert_eq!(
Joint::ConeTwist(ConeTwistJoint::new(
0,
1,
A_ANCHOR,
B_ANCHOR,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z
))
.compute_force(&bodies),
fi(4)
);
assert_eq!(
Joint::Spring(SpringJoint::new(
0,
1,
A_ANCHOR,
B_ANCHOR,
fi(1),
fi(3),
Fix128::ZERO
))
.compute_force(&bodies),
fi(9)
);
assert_eq!(
Joint::Slider(SliderJoint::new(0, 1, Vec3Fix::UNIT_Y, A_ANCHOR, B_ANCHOR))
.compute_force(&bodies),
fi(4)
);
assert_eq!(
Joint::Slider(SliderJoint::new(0, 1, Vec3Fix::UNIT_X, A_ANCHOR, B_ANCHOR))
.compute_force(&bodies),
Fix128::ZERO
);
assert_ne!(
Joint::Ball(BallJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO)).compute_force(&bodies),
fi(4)
);
}
fn set_inv_inertia(b: &mut [RigidBody], ia: Fix128, ib: Fix128) {
b[0].inv_inertia = Vec3Fix::new(ia, ia, ia);
b[1].inv_inertia = Vec3Fix::new(ib, ib, ib);
}
fn q35(axis: Vec3Fix) -> QuatFix {
let s = Fix128::from_ratio(3, 5);
QuatFix::new(axis.x * s, axis.y * s, axis.z * s, Fix128::from_ratio(4, 5))
}
fn rel_twist(b: &[RigidBody], axis: Vec3Fix) -> Fix128 {
compute_twist_angle(b[1].rotation.mul(b[0].rotation.conjugate()), axis)
}
fn tilt_z(b: &[RigidBody]) -> Fix128 {
let a = b[0].rotation.rotate_vec(Vec3Fix::UNIT_Z);
let c = b[1].rotation.rotate_vec(Vec3Fix::UNIT_Z);
Fix128::atan2(a.cross(c).length(), a.dot(c))
}
fn tilt_from_world_z(q: QuatFix) -> Fix128 {
let c = q.rotate_vec(Vec3Fix::UNIT_Z);
Fix128::atan2(Vec3Fix::UNIT_Z.cross(c).length(), Vec3Fix::UNIT_Z.dot(c))
}
fn theta35() -> Fix128 {
Fix128::atan2(fi(3), fi(4)).double()
}
#[test]
fn positional_body_a_update_multiplies_by_inverse_mass() {
let expect = v3i(3, 0, 0);
let mut h = pair(Vec3Fix::ZERO, 3, v3i(4, 0, 0), 1);
solve_hinge_joint(
&HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
),
&mut h,
DT,
);
assert_eq!((h[0].position, h[1].position), (expect, expect), "hinge");
let mut f = pair(Vec3Fix::ZERO, 3, v3i(4, 0, 0), 1);
solve_fixed_joint(
&FixedJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO, QuatFix::IDENTITY),
&mut f,
DT,
);
assert_eq!((f[0].position, f[1].position), (expect, expect), "fixed");
let mut c = pair(Vec3Fix::ZERO, 3, v3i(4, 0, 0), 1);
solve_cone_twist_joint(
&ConeTwistJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
),
&mut c,
DT,
);
assert_eq!((c[0].position, c[1].position), (expect, expect), "cone");
let mut d6 = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
d6.linear_x = D6Motion::Locked;
let mut d = pair(Vec3Fix::ZERO, 3, v3i(4, 0, 0), 1);
solve_d6_joint(&d6, &mut d, DT);
assert_eq!((d[0].position, d[1].position), (expect, expect), "d6");
let mut s = pair(Vec3Fix::ZERO, 3, v3i(0, 4, 0), 1);
solve_slider_joint(
&SliderJoint::new(0, 1, Vec3Fix::UNIT_X, Vec3Fix::ZERO, Vec3Fix::ZERO),
&mut s,
DT,
);
assert_eq!(
(s[0].position, s[1].position),
(v3i(0, 3, 0), v3i(0, 3, 0)),
"slider"
);
let mut sp = pair(Vec3Fix::ZERO, 3, v3i(4, 0, 0), 1);
solve_spring_joint(
&SpringJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
fi(1),
fi(2),
Fix128::ZERO,
),
&mut sp,
DT,
);
assert_eq!(
sp[0].position,
Vec3Fix::new(Fix128::from_ratio(3, 4), Fix128::ZERO, Fix128::ZERO),
"spring A"
);
assert_eq!(
sp[1].position,
Vec3Fix::new(Fix128::from_ratio(15, 4), Fix128::ZERO, Fix128::ZERO),
"spring B"
);
}
#[test]
fn hinge_axis_alignment_uses_angular_compliance_and_inertia_sum() {
let mut j = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
);
j.angular_compliance = Fix128::from_ratio(1, 64);
let mut b = pair(Vec3Fix::ZERO, 1, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut b, Fix128::from_ratio(1, 2), Fix128::from_ratio(1, 4));
b[1].rotation = q35(Vec3Fix::UNIT_X);
let theta = theta35();
assert!(near(tilt_z(&b), theta));
solve_hinge_joint(&j, &mut b, DT);
let a_tilt = tilt_from_world_z(b[0].rotation);
let b_tilt = tilt_from_world_z(b[1].rotation);
let rel = tilt_z(&b);
assert!(near(a_tilt, theta.half()), "A tilt {a_tilt:?} vs θ/2");
assert!(
near(b_tilt, theta - theta / fi(4)),
"B tilt {b_tilt:?} vs 3θ/4"
);
assert!(near(rel, theta / fi(4)), "residual {rel:?} vs θ/4");
assert!(b[0].rotation.rotate_vec(Vec3Fix::UNIT_Z).y < Fix128::ZERO);
}
#[test]
fn fixed_joint_rotation_lock_uses_angular_compliance_and_inertia_sum() {
let mut j = FixedJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO, QuatFix::IDENTITY);
j.angular_compliance = Fix128::from_ratio(1, 64);
let mut b = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut b, Fix128::ZERO, Fix128::from_ratio(3, 4));
b[1].rotation = q35(Vec3Fix::UNIT_Z);
let theta = theta35();
assert!(near(rel_twist(&b, Vec3Fix::UNIT_Z), theta));
solve_fixed_joint(&j, &mut b, DT);
let after = rel_twist(&b, Vec3Fix::UNIT_Z);
assert!(near(after, theta / fi(4)), "{after:?} vs θ/4");
assert_eq!(b[0].rotation, QuatFix::IDENTITY, "static A never rotates");
let rigid = FixedJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO, QuatFix::IDENTITY);
let mut r = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut r, Fix128::ZERO, Fix128::ONE);
r[1].rotation = q35(Vec3Fix::UNIT_Z);
solve_fixed_joint(&rigid, &mut r, DT);
assert!(near(rel_twist(&r, Vec3Fix::UNIT_Z), Fix128::ZERO));
assert!(near(r[1].rotation.w.abs(), Fix128::ONE));
}
#[test]
fn hinge_min_limit_lands_exactly_on_the_limit() {
let j = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(fi(2), fi(3));
let theta = theta35();
let e = fi(2) - theta;
let mut b = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut b, Fix128::ZERO, fi(2));
b[1].rotation = q35(Vec3Fix::UNIT_Z);
assert!(near(rel_twist(&b, Vec3Fix::UNIT_Z), theta));
solve_hinge_joint(&j, &mut b, DT);
let after = rel_twist(&b, Vec3Fix::UNIT_Z);
assert!(near(after, fi(2)), "{after:?} vs 2");
assert_eq!(b[0].rotation, QuatFix::IDENTITY);
let mut both = pair(Vec3Fix::ZERO, 1, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut both, fi(2), fi(6));
both[1].rotation = q35(Vec3Fix::UNIT_Z);
solve_hinge_joint(&j, &mut both, DT);
let rel = rel_twist(&both, Vec3Fix::UNIT_Z);
let a_angle = compute_twist_angle(both[0].rotation, Vec3Fix::UNIT_Z);
let b_angle = compute_twist_angle(both[1].rotation, Vec3Fix::UNIT_Z);
assert!(near(rel, fi(2)), "rel {rel:?} vs 2");
assert!(near(a_angle, -(e / fi(4))), "A {a_angle:?} vs −e/4");
assert!(
near(b_angle, theta + (e * fi(3)) / fi(4)),
"B {b_angle:?} vs θ + 3e/4"
);
}
#[test]
fn hinge_max_limit_lands_exactly_on_the_limit() {
let j = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(Fix128::from_ratio(1, 4), Fix128::from_ratio(1, 2));
let mut b = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut b, Fix128::ZERO, fi(2));
b[1].rotation = q35(Vec3Fix::UNIT_Z);
solve_hinge_joint(&j, &mut b, DT);
let after = rel_twist(&b, Vec3Fix::UNIT_Z);
assert!(near(after, Fix128::from_ratio(1, 2)), "{after:?} vs 1/2");
}
#[test]
fn hinge_min_limit_with_negative_angle_pushes_toward_min() {
let theta = theta35();
let q = q35(Vec3Fix::UNIT_Z);
let neg_q = QuatFix::new(-q.x, -q.y, -q.z, -q.w);
assert!(near(compute_twist_angle(q, Vec3Fix::UNIT_Z), theta));
assert!(near(compute_twist_angle(neg_q, Vec3Fix::UNIT_Z), theta));
assert!(near(
compute_twist_angle(q.conjugate(), Vec3Fix::UNIT_Z),
-theta
));
let j = HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(Fix128::from_ratio(-1, 2), Fix128::from_ratio(1, 2));
let mut b = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut b, Fix128::ZERO, Fix128::ONE);
b[1].rotation = q.conjugate();
assert!(near(rel_twist(&b, Vec3Fix::UNIT_Z), -theta));
solve_hinge_joint(&j, &mut b, DT);
let after = rel_twist(&b, Vec3Fix::UNIT_Z);
assert!(near(after, Fix128::from_ratio(-1, 2)), "{after:?} vs −1/2");
let mut p = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut p, Fix128::ZERO, Fix128::ONE);
p[1].rotation = q;
solve_hinge_joint(&j, &mut p, DT);
assert!(near(
rel_twist(&p, Vec3Fix::UNIT_Z),
Fix128::from_ratio(1, 2)
));
}
#[test]
fn hinge_limit_equality_is_a_bitwise_no_op() {
let mk = |min: Fix128, max: Fix128| {
HingeJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(min, max)
};
let mut b = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut b, fi(3), fi(1));
b[1].rotation = q35(Vec3Fix::UNIT_Z);
let before = b[1].rotation;
let theta = rel_twist(&b, Vec3Fix::UNIT_Z);
solve_hinge_joint(&mk(theta, theta + fi(1)), &mut b, DT);
assert_eq!(b[1].rotation, before, "angle == min must not touch B");
solve_hinge_joint(&mk(theta - fi(1), theta), &mut b, DT);
assert_eq!(b[1].rotation, before, "angle == max must not touch B");
}
#[test]
fn slider_adds_anchor_a_and_uses_b_minus_a() {
let j = SliderJoint::new(0, 1, Vec3Fix::UNIT_X, v3i(0, 1, 0), Vec3Fix::ZERO);
let mut b = pair(Vec3Fix::ZERO, 0, v3i(3, 5, 0), 1);
solve_slider_joint(&j, &mut b, DT);
assert_eq!(b[1].position, v3i(3, 1, 0));
}
#[test]
fn slider_limit_corrections_split_by_inverse_mass() {
let j = SliderJoint::new(0, 1, Vec3Fix::UNIT_X, Vec3Fix::ZERO, Vec3Fix::ZERO)
.with_limits(fi(-1), fi(2));
let half = Fix128::from_ratio(1, 2);
let mut under = pair(Vec3Fix::ZERO, 3, v3i(-5, 0, 0), 5);
solve_slider_joint(&j, &mut under, DT);
assert_eq!(
under[0].position,
Vec3Fix::new(-(fi(3) * half), Fix128::ZERO, Fix128::ZERO)
);
assert_eq!(
under[1].position,
Vec3Fix::new(-(fi(5) * half), Fix128::ZERO, Fix128::ZERO)
);
assert_eq!(under[1].position.x - under[0].position.x, fi(-1));
let mut over = pair(Vec3Fix::ZERO, 3, v3i(6, 0, 0), 5);
solve_slider_joint(&j, &mut over, DT);
assert_eq!(
over[0].position,
Vec3Fix::new(fi(3) * half, Fix128::ZERO, Fix128::ZERO)
);
assert_eq!(
over[1].position,
Vec3Fix::new(fi(7) * half, Fix128::ZERO, Fix128::ZERO)
);
assert_eq!(over[1].position.x - over[0].position.x, fi(2));
}
#[test]
fn spring_damping_uses_relative_velocity_b_minus_a() {
let j = SpringJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO, fi(4), fi(2), fi(2));
let mut b = pair(Vec3Fix::ZERO, 1, v3i(4, 0, 0), 1);
b[0].velocity = v3i(1, 0, 0);
b[1].velocity = v3i(3, 0, 0);
solve_spring_joint(&j, &mut b, DT);
assert!(
near_v(
b[0].position,
Vec3Fix::new(Fix128::from_ratio(1, 9), Fix128::ZERO, Fix128::ZERO)
),
"A {:?}",
b[0].position
);
assert!(
near_v(
b[1].position,
Vec3Fix::new(Fix128::from_ratio(35, 9), Fix128::ZERO, Fix128::ZERO)
),
"B {:?}",
b[1].position
);
}
#[test]
fn d6_locked_angular_axis_uses_angular_compliance_and_inertia_sum() {
let mut j = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
j.angular_z = D6Motion::Locked;
j.angular_compliance = Fix128::from_ratio(3, 64);
let mut b = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut b, Fix128::ZERO, Fix128::from_ratio(3, 4));
b[1].rotation = q35(Vec3Fix::UNIT_Z);
let theta = theta35();
solve_d6_joint(&j, &mut b, DT);
let after = rel_twist(&b, Vec3Fix::UNIT_Z);
assert!(near(after, theta.half()), "{after:?} vs θ/2");
}
#[test]
fn d6_limited_angular_axis_lands_exactly_on_the_limit() {
let mk = |min: Fix128, max: Fix128| {
let mut j = D6Joint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO);
j.angular_z = D6Motion::Limited;
j.angular_limit_min.z = min;
j.angular_limit_max.z = max;
j
};
let half = Fix128::from_ratio(1, 2);
let run = |rot: QuatFix, min: Fix128, max: Fix128| {
let mut b = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut b, Fix128::ZERO, Fix128::ONE);
b[1].rotation = rot;
solve_d6_joint(&mk(min, max), &mut b, DT);
rel_twist(&b, Vec3Fix::UNIT_Z)
};
let q = q35(Vec3Fix::UNIT_Z);
let under = run(q, fi(2), fi(3));
assert!(near(under, fi(2)), "min: {under:?} vs 2");
let over = run(q, Fix128::from_ratio(1, 4), half);
assert!(near(over, half), "max: {over:?} vs 1/2");
let negative = run(q.conjugate(), -half, half);
assert!(near(negative, -half), "negative: {negative:?} vs −1/2");
}
#[test]
fn cone_limit_correction_uses_angular_compliance_and_inertia_sum() {
let half = Fix128::from_ratio(1, 2);
let mut j = ConeTwistJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(half, half);
j.angular_compliance = Fix128::from_ratio(1, 64);
let mut b = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut b, Fix128::ZERO, half);
b[1].rotation = q35(Vec3Fix::UNIT_X);
let theta = theta35();
assert!(near(tilt_z(&b), theta));
let expected = (theta + Fix128::ONE) / fi(3);
solve_cone_twist_joint(&j, &mut b, DT);
let after = tilt_z(&b);
assert!(near(after, expected), "{after:?} vs {expected:?}");
assert!(near(rel_twist(&b, Vec3Fix::UNIT_Z), Fix128::ZERO));
let rigid = ConeTwistJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(half, half);
let mut r = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut r, Fix128::ZERO, Fix128::ONE);
r[1].rotation = q35(Vec3Fix::UNIT_X);
solve_cone_twist_joint(&rigid, &mut r, DT);
assert!(near(tilt_z(&r), half));
}
#[test]
fn twist_limit_correction_error_arithmetic() {
let half = Fix128::from_ratio(1, 2);
let mut j = ConeTwistJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(half, half);
j.angular_compliance = Fix128::from_ratio(1, 64);
let theta = theta35();
let expected = (theta + Fix128::ONE) / fi(3);
let q = q35(Vec3Fix::UNIT_Z);
let mut pos = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut pos, Fix128::ZERO, half);
pos[1].rotation = q;
solve_cone_twist_joint(&j, &mut pos, DT);
let after = rel_twist(&pos, Vec3Fix::UNIT_Z);
assert!(near(after, expected), "positive: {after:?} vs {expected:?}");
assert!(near(tilt_z(&pos), Fix128::ZERO));
let mut neg = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut neg, Fix128::ZERO, half);
neg[1].rotation = q.conjugate();
assert!(near(rel_twist(&neg, Vec3Fix::UNIT_Z), -theta));
solve_cone_twist_joint(&j, &mut neg, DT);
let after = rel_twist(&neg, Vec3Fix::UNIT_Z);
assert!(
near(after, -expected),
"negative: {after:?} vs {:?}",
-expected
);
let rigid = ConeTwistJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(half, half);
for (rot, limit) in [(q, half), (q.conjugate(), -half)] {
let mut r = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
set_inv_inertia(&mut r, Fix128::ZERO, Fix128::ONE);
r[1].rotation = rot;
solve_cone_twist_joint(&rigid, &mut r, DT);
let t = rel_twist(&r, Vec3Fix::UNIT_Z);
assert!(near(t, limit), "{t:?} vs {limit:?}");
}
}
#[test]
fn cone_and_twist_limit_equality_is_a_bitwise_no_op() {
let mk = |cone: Fix128, twist: Fix128| {
ConeTwistJoint::new(
0,
1,
Vec3Fix::ZERO,
Vec3Fix::ZERO,
Vec3Fix::UNIT_Z,
Vec3Fix::UNIT_Z,
)
.with_limits(cone, twist)
};
let mut c = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
c[1].rotation = q35(Vec3Fix::UNIT_X);
let a = c[0].rotation.rotate_vec(Vec3Fix::UNIT_Z).normalize();
let bz = c[1].rotation.rotate_vec(Vec3Fix::UNIT_Z).normalize();
let (_, cross_len) = a.cross(bz).normalize_with_length();
let cone = Fix128::atan2(cross_len, a.dot(bz));
let before = c[1].rotation;
solve_cone_twist_joint(&mk(cone, Fix128::PI), &mut c, DT);
assert_eq!(c[1].rotation, before, "cone == limit must not touch B");
let mut t = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
t[1].rotation = q35(Vec3Fix::UNIT_Z);
let twist = rel_twist(&t, Vec3Fix::UNIT_Z);
let before = t[1].rotation;
solve_cone_twist_joint(&mk(Fix128::HALF_PI, twist), &mut t, DT);
assert_eq!(t[1].rotation, before, "twist == limit must not touch B");
}
#[test]
fn angular_inverse_mass_is_anisotropic_and_split_by_body() {
let local_axis = Vec3Fix::new(
Fix128::ZERO,
Fix128::from_ratio(24, 25),
Fix128::from_ratio(7, 25),
);
let j = HingeJoint::new(0, 1, Vec3Fix::ZERO, Vec3Fix::ZERO, local_axis, local_axis)
.with_limits(fi(2), fi(3));
let theta = theta35();
let e = fi(2) - theta;
let qa = q35(Vec3Fix::UNIT_X);
let mut b = pair(Vec3Fix::ZERO, 1, Vec3Fix::ZERO, 1);
b[0].inv_inertia = v3i(5, 2, 7);
b[1].inv_inertia = v3i(11, 13, 3);
b[0].rotation = qa;
b[1].rotation = q35(Vec3Fix::UNIT_Z).mul(qa).normalize();
assert!(near_v(qa.rotate_vec(local_axis), Vec3Fix::UNIT_Z));
assert!(near_v(
b[1].rotation.rotate_vec(local_axis),
Vec3Fix::UNIT_Z
));
assert!(near(rel_twist(&b, Vec3Fix::UNIT_Z), theta));
let w_a = Fix128::from_ratio(1152 + 343, 625);
let w_b = Fix128::from_ratio(7488 + 147, 625);
assert!(near(angular_inverse_mass(&b[0], Vec3Fix::UNIT_Z), w_a));
assert!(near(angular_inverse_mass(&b[1], Vec3Fix::UNIT_Z), w_b));
let a_before = b[0].rotation;
solve_hinge_joint(&j, &mut b, DT);
let rel = rel_twist(&b, Vec3Fix::UNIT_Z);
assert!(near(rel, fi(2)), "rel {rel:?} vs 2");
let a_delta = b[0].rotation.mul(a_before.conjugate());
let a_angle = compute_twist_angle(a_delta, Vec3Fix::UNIT_Z);
let expected_a = -(w_a * e) / (w_a + w_b);
assert!(near(a_angle, expected_a), "A {a_angle:?} vs {expected_a:?}");
assert!(near_v(
b[0].rotation.rotate_vec(local_axis),
Vec3Fix::UNIT_Z
));
let mut s = pair(Vec3Fix::ZERO, 0, Vec3Fix::ZERO, 1);
s[0].inv_inertia = v3i(5, 2, 7);
assert_eq!(angular_inverse_mass(&s[0], Vec3Fix::UNIT_Z), Fix128::ZERO);
}
#[test]
fn angular_inverse_mass_of_an_isotropic_body_does_not_depend_on_orientation() {
let c = Fix128::from_ratio(5, 2);
let n = Vec3Fix::new(
Fix128::from_ratio(3, 7),
Fix128::from_ratio(-11, 13),
Fix128::from_ratio(1, 3),
);
let expected = n.x * n.x * c + n.y * n.y * c + n.z * n.z * c;
let mut differ = 0;
for i in 0..16_i64 {
let axis = Vec3Fix::new(
Fix128::from_ratio(1 + i, 3),
Fix128::from_ratio(2 - i, 5),
Fix128::from_ratio(3 + 2 * i, 7),
)
.normalize();
for k in 1..=4_i64 {
let q =
QuatFix::from_axis_angle(axis, Fix128::from_ratio(7 * k + i, 9)).normalize();
let mut b = RigidBody::new_dynamic(Vec3Fix::ZERO, Fix128::ONE);
b.inv_inertia = Vec3Fix::new(c, c, c);
b.rotation = q;
differ += usize::from(angular_inverse_mass(&b, n) != expected);
}
}
assert_eq!(differ, 0, "{differ}/64 orientations differ from c·(n·n)");
}
}