193 lines
7.6 KiB
Rust
193 lines
7.6 KiB
Rust
// Port of box3d/test/test_body.c
|
|
//
|
|
// b3UpdateBodyMassData shifts each shape's inertia to the body center of mass
|
|
// with the parallel axis theorem. When shapes sit far from the body origin the
|
|
// shift term dwarfs the central inertia, so any error in the per shape framing
|
|
// blows up the tensor. Spheres make a clean oracle: the central inertia is
|
|
// isotropic and independent of placement, so the shift is the only thing tested.
|
|
|
|
use makepad_box3d::body::{
|
|
body_apply_mass_from_shapes, body_apply_torque, body_get_angular_velocity, body_get_mass_data,
|
|
body_get_world_inverse_rotational_inertia, body_set_mass_data, body_set_transform, create_body,
|
|
};
|
|
use makepad_box3d::ensure_small;
|
|
use makepad_box3d::math_functions::{make_quat_from_axis_angle, pos, vec3, Matrix3, Vec3, PI};
|
|
use makepad_box3d::physics_world::{create_world, destroy_world, world_step};
|
|
use makepad_box3d::shape::create_sphere_shape;
|
|
use makepad_box3d::types::*;
|
|
|
|
fn sphere_body_mass(centers: &[Vec3], radius: f32, density: f32) -> MassData {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
let mut shape_def = default_shape_def();
|
|
shape_def.density = density;
|
|
|
|
for center in centers {
|
|
let sphere = Sphere { center: *center, radius };
|
|
create_sphere_shape(&mut world, body_id, &shape_def, &sphere);
|
|
}
|
|
|
|
body_apply_mass_from_shapes(&mut world, body_id);
|
|
let mass_data = body_get_mass_data(&world, body_id);
|
|
|
|
destroy_world(world);
|
|
mass_data
|
|
}
|
|
|
|
// One sphere far from the body origin. The center of mass lands on the sphere
|
|
// and the inertia about it must be the bare central inertia, with no trace of
|
|
// the offset.
|
|
#[test]
|
|
fn far_single_sphere_mass() {
|
|
let radius = 0.5f32;
|
|
let density = 1.0f32;
|
|
let center = vec3(100.0, -50.0, 75.0);
|
|
let md = sphere_body_mass(&[center], radius, density);
|
|
|
|
let mass = density * (4.0 / 3.0) * PI * radius * radius * radius;
|
|
let central = 0.4 * mass * radius * radius;
|
|
|
|
ensure_small!(md.mass - mass, 1e-4);
|
|
|
|
ensure_small!(md.center.x - center.x, 1e-3);
|
|
ensure_small!(md.center.y - center.y, 1e-3);
|
|
ensure_small!(md.center.z - center.z, 1e-3);
|
|
|
|
ensure_small!(md.inertia.cx.x - central, 1e-3);
|
|
ensure_small!(md.inertia.cy.y - central, 1e-3);
|
|
ensure_small!(md.inertia.cz.z - central, 1e-3);
|
|
|
|
ensure_small!(md.inertia.cy.x, 1e-3);
|
|
ensure_small!(md.inertia.cz.x, 1e-3);
|
|
ensure_small!(md.inertia.cz.y, 1e-3);
|
|
}
|
|
|
|
// Eight equal spheres on the corners of a cube, the whole cube parked far from
|
|
// the body origin. The center of mass is the cube center and the products of
|
|
// inertia cancel by symmetry, so the tensor stays diagonal no matter how far
|
|
// out the cube sits.
|
|
#[test]
|
|
fn far_cube_sphere_mass() {
|
|
let radius = 0.5f32;
|
|
let density = 1.0f32;
|
|
let h = 1.0f32;
|
|
let p = vec3(100.0, 100.0, 100.0);
|
|
|
|
let mut centers = Vec::new();
|
|
for sx in [-1.0f32, 1.0] {
|
|
for sy in [-1.0f32, 1.0] {
|
|
for sz in [-1.0f32, 1.0] {
|
|
centers.push(vec3(p.x + sx * h, p.y + sy * h, p.z + sz * h));
|
|
}
|
|
}
|
|
}
|
|
|
|
let md = sphere_body_mass(¢ers, radius, density);
|
|
|
|
let mass = density * (4.0 / 3.0) * PI * radius * radius * radius;
|
|
let total_mass = 8.0 * mass;
|
|
|
|
// Per sphere central inertia summed, plus the parallel axis term for each
|
|
// corner offset (dy^2 + dz^2) = (h^2 + h^2) about every axis.
|
|
let diag = 8.0 * 0.4 * mass * radius * radius + 16.0 * mass * h * h;
|
|
|
|
ensure_small!(md.mass - total_mass, 1e-3);
|
|
|
|
ensure_small!(md.center.x - p.x, 1e-2);
|
|
ensure_small!(md.center.y - p.y, 1e-2);
|
|
ensure_small!(md.center.z - p.z, 1e-2);
|
|
|
|
ensure_small!(md.inertia.cx.x - diag, 1e-2);
|
|
ensure_small!(md.inertia.cy.y - diag, 1e-2);
|
|
ensure_small!(md.inertia.cz.z - diag, 1e-2);
|
|
|
|
ensure_small!(md.inertia.cy.x, 1e-2);
|
|
ensure_small!(md.inertia.cz.x, 1e-2);
|
|
ensure_small!(md.inertia.cz.y, 1e-2);
|
|
}
|
|
|
|
// body_set_mass_data must refresh the cached world space inverse inertia.
|
|
// The solver consumes the cached tensor at the start of the next step (torque
|
|
// integration, contact and joint prepare), and only recomputes it at the end
|
|
// of the step or in body_set_transform. Before the fix, calling
|
|
// body_set_mass_data on a body whose transform was already set left the tensor
|
|
// derived from the old (shape) mass data, so the first step ran with a wildly
|
|
// wrong rotational response.
|
|
//
|
|
// Oracle: the same body with the same final mass data and transform, but with
|
|
// the calls in the other order (mass data first, then transform — the path
|
|
// that always recomputed the cache). Both orders must yield the identical
|
|
// cached tensor and the identical angular velocity after one step.
|
|
fn spin_with_mass_data(transform_first: bool) -> (Matrix3, Vec3) {
|
|
let world_def = default_world_def();
|
|
let mut world = create_world(&world_def);
|
|
|
|
let mut body_def = default_body_def();
|
|
body_def.body_type = BodyType::Dynamic;
|
|
let body_id = create_body(&mut world, &body_def);
|
|
|
|
// Shape mass data is the stale value the bug leaves behind: a small sphere,
|
|
// orders of magnitude lighter than the mass data set below.
|
|
let sphere = Sphere { center: vec3(0.0, 0.0, 0.0), radius: 0.5 };
|
|
create_sphere_shape(&mut world, body_id, &default_shape_def(), &sphere);
|
|
|
|
let position = pos(2.0, 5.0, -1.0);
|
|
let rotation = make_quat_from_axis_angle(vec3(1.0 / 3.0, 2.0 / 3.0, 2.0 / 3.0), 0.7);
|
|
let mass_data = MassData {
|
|
mass: 500.0,
|
|
center: vec3(0.0, 0.0, 0.0),
|
|
inertia: Matrix3 {
|
|
cx: vec3(40.0, 0.0, 0.0),
|
|
cy: vec3(0.0, 90.0, 0.0),
|
|
cz: vec3(0.0, 0.0, 160.0),
|
|
},
|
|
};
|
|
|
|
if transform_first {
|
|
body_set_transform(&mut world, body_id, position, rotation);
|
|
body_set_mass_data(&mut world, body_id, mass_data);
|
|
} else {
|
|
body_set_mass_data(&mut world, body_id, mass_data);
|
|
body_set_transform(&mut world, body_id, position, rotation);
|
|
}
|
|
|
|
let inv_inertia_world = body_get_world_inverse_rotational_inertia(&world, body_id);
|
|
|
|
body_apply_torque(&mut world, body_id, vec3(1200.0, -450.0, 800.0), true);
|
|
world_step(&mut world, 1.0 / 60.0, 4);
|
|
let angular_velocity = body_get_angular_velocity(&world, body_id);
|
|
|
|
destroy_world(world);
|
|
(inv_inertia_world, angular_velocity)
|
|
}
|
|
|
|
#[test]
|
|
fn set_mass_data_refreshes_world_inverse_inertia() {
|
|
let (stale_candidate, spin_a) = spin_with_mass_data(true);
|
|
let (reference, spin_b) = spin_with_mass_data(false);
|
|
|
|
// Cached world space inverse inertia must not depend on call order.
|
|
ensure_small!(stale_candidate.cx.x - reference.cx.x, 1e-6);
|
|
ensure_small!(stale_candidate.cx.y - reference.cx.y, 1e-6);
|
|
ensure_small!(stale_candidate.cx.z - reference.cx.z, 1e-6);
|
|
ensure_small!(stale_candidate.cy.x - reference.cy.x, 1e-6);
|
|
ensure_small!(stale_candidate.cy.y - reference.cy.y, 1e-6);
|
|
ensure_small!(stale_candidate.cy.z - reference.cy.z, 1e-6);
|
|
ensure_small!(stale_candidate.cz.x - reference.cz.x, 1e-6);
|
|
ensure_small!(stale_candidate.cz.y - reference.cz.y, 1e-6);
|
|
ensure_small!(stale_candidate.cz.z - reference.cz.z, 1e-6);
|
|
|
|
// Sanity: the torque actually spun the body.
|
|
let speed = (spin_b.x * spin_b.x + spin_b.y * spin_b.y + spin_b.z * spin_b.z).sqrt();
|
|
assert!(speed > 0.01, "torque produced no spin: {:?}", spin_b);
|
|
|
|
// One step of torque must produce the same angular velocity either way.
|
|
ensure_small!(spin_a.x - spin_b.x, 1e-6);
|
|
ensure_small!(spin_a.y - spin_b.y, 1e-6);
|
|
ensure_small!(spin_a.z - spin_b.z, 1e-6);
|
|
}
|