Physics Joints
A joint is a constraint between two rigid bodies. The constraint solver enforces the relationship every step. Different joint types constrain different degrees of freedom. A fixed joint constrains all six (three translation, three rotation). A revolute joint constrains five and leaves one rotation axis free. A prismatic joint constrains five and leaves one translation axis free.
Joint Types
| Joint | Description | Use Cases |
|---|---|---|
| Fixed | Rigid connection | Welded objects |
| Spherical | Ball-and-socket | Pendulums, ragdolls |
| Revolute | Hinge | Doors, wheels |
| Prismatic | Slider | Drawers, pistons |
| Rope | Max distance | Ropes, chains |
| Spring | Elastic | Suspension, bouncy connections |
Fixed Joint
A fixed joint welds two bodies into one. They move and rotate together as if they were a single body, but mass and collision stay separate.
#![allow(unused)] fn main() { use nightshade::ecs::physics::joints::*; create_fixed_joint( world, body_a, body_b, FixedJoint::new() .with_local_anchor1(Vec3::new(0.5, 0.0, 0.0)) .with_local_anchor2(Vec3::new(-0.5, 0.0, 0.0)), ); }
The two local anchors are the attachment points in each body's local space. The solver keeps those points coincident.
Spherical Joint (Ball-and-Socket)
A spherical joint locks position but allows all three rotation axes. The two bodies pivot freely around the joint point.
#![allow(unused)] fn main() { let anchor = spawn_cube_at(world, Vec3::new(0.0, 5.0, 0.0)); let ball = spawn_sphere_at(world, Vec3::new(0.0, 3.0, 0.0)); create_spherical_joint( world, anchor, ball, SphericalJoint::new() .with_local_anchor1(Vec3::new(0.0, -0.15, 0.0)) .with_local_anchor2(Vec3::new(0.0, 1.0, 0.0)), ); }
The example above is a pendulum. The anchor is fixed, the ball swings.
Revolute Joint (Hinge)
A revolute joint allows rotation around a single axis. Everything else is constrained.
#![allow(unused)] fn main() { let door_frame = spawn_cube_at(world, Vec3::zeros()); let door = spawn_cube_at(world, Vec3::new(0.5, 1.0, 0.0)); create_revolute_joint( world, door_frame, door, RevoluteJoint::new(JointAxisDirection::Y) .with_local_anchor1(Vec3::new(0.0, 0.0, 0.0)) .with_local_anchor2(Vec3::new(-0.5, 0.0, 0.0)) .with_limits(JointLimits::new(-1.5, 1.5)), ); }
The Y-axis revolute joint above is a door hinge. The limits (in radians) prevent it from swinging through the wall behind it.
Adding Motor
A joint motor drives the joint toward a target position or velocity. For a position motor on the door, that turns the hinge into something that can open and close on command.
#![allow(unused)] fn main() { RevoluteJoint::new(JointAxisDirection::Y) .with_motor(JointMotor::position(0.0, 5.0, 100.0)) }
The three arguments are target position, stiffness, and damping. Stiffness controls how hard the motor pushes toward the target. Damping resists the resulting velocity.
Prismatic Joint (Slider)
A prismatic joint allows translation along a single axis and locks everything else.
#![allow(unused)] fn main() { let cabinet = spawn_cube_at(world, Vec3::zeros()); let drawer = spawn_cube_at(world, Vec3::new(0.0, 0.0, 0.5)); create_prismatic_joint( world, cabinet, drawer, PrismaticJoint::new(JointAxisDirection::Z) .with_local_anchor1(Vec3::new(0.0, 0.0, 0.0)) .with_local_anchor2(Vec3::new(0.0, 0.0, -0.5)) .with_limits(JointLimits::new(0.0, 0.8)), ); }
The limits are in meters. 0 is fully closed, 0.8 is fully open.
Rope Joint
A rope joint enforces a maximum distance between two bodies. Within that distance the bodies move freely. At the limit they stop separating.
#![allow(unused)] fn main() { let ceiling = spawn_cube_at(world, Vec3::zeros()); let weight = spawn_sphere_at(world, Vec3::new(0.0, 0.0, 0.0)); create_rope_joint( world, ceiling, weight, RopeJoint::new(2.0) .with_local_anchor1(Vec3::new(0.0, -0.15, 0.0)) .with_local_anchor2(Vec3::new(0.0, 0.0, 0.0)), ); }
Use rope joints in series for a chain. Each link is a small body connected to the next by a rope joint with a short max distance.
Spring Joint
A spring joint applies a Hooke's-law restoring force toward a rest length. The body oscillates around that length and damping bleeds off the oscillation over time.
#![allow(unused)] fn main() { let anchor = spawn_cube_at(world, Vec3::zeros()); let bob = spawn_sphere_at(world, Vec3::new(0.0, -2.0, 0.0)); create_spring_joint( world, anchor, bob, SpringJoint::new(1.5, 50.0, 2.0) .with_local_anchor1(Vec3::new(0.0, -0.15, 0.0)) .with_local_anchor2(Vec3::new(0.0, 0.2, 0.0)), ); }
Arguments are rest length, stiffness, damping. Higher stiffness gives a stiffer spring. Higher damping settles the bounce faster.
Joint Limits
Limits clamp the free degree of freedom to a range.
#![allow(unused)] fn main() { RevoluteJoint::new(JointAxisDirection::Z) .with_limits(JointLimits::new(-1.57, 1.57)) PrismaticJoint::new(JointAxisDirection::X) .with_limits(JointLimits::new(-2.0, 2.0)) }
Revolute limits are radians. Prismatic limits are meters. Without limits the joint runs to its full mechanical range, which for a revolute is unbounded rotation and for a prismatic is unbounded translation.
Breaking Joints
Joints can break under excessive force. Set a maximum impulse and Rapier will dissolve the joint when contact forces push past it.
#![allow(unused)] fn main() { if let Some(joint) = world.resources.physics.get_joint_mut(joint_handle) { joint.set_max_force(1000.0); } }
Chain Example
A chain is a string of spherical joints. Each link's bottom anchor is the next link's top anchor.
#![allow(unused)] fn main() { fn create_chain(world: &mut World, start: Vec3, links: usize) { let mut previous = spawn_cube_at(world, start); for index in 0..links { let position = start - Vec3::new(0.0, (index + 1) as f32 * 0.5, 0.0); let link = spawn_sphere_at(world, position); create_spherical_joint( world, previous, link, SphericalJoint::new() .with_local_anchor1(Vec3::new(0.0, -0.2, 0.0)) .with_local_anchor2(Vec3::new(0.0, 0.2, 0.0)), ); previous = link; } } }
The first link anchors to the spawn cube. Each subsequent link anchors below the previous one.
Interactive Door Example
A real door is a revolute joint with locked rotations on every axis except the hinge axis. Locking rotations on the body itself, in addition to the joint constraint, makes the door behave like a door instead of a wobbling flap.
#![allow(unused)] fn main() { fn spawn_interactive_door(world: &mut World, position: Vec3) -> Entity { let frame = spawn_cube_at(world, position); let door = spawn_cube_at(world, position + Vec3::new(0.5, 0.0, 0.0)); if let Some(rb) = world.core.get_rigid_body(door) { if let Some(handle) = rb.handle { if let Some(body) = world.resources.physics.rigid_body_set.get_mut(handle.into()) { body.lock_rotations(true, true); } } } create_revolute_joint( world, frame, door, RevoluteJoint::new(JointAxisDirection::Y) .with_local_anchor1(Vec3::new(0.05, 0.0, 0.0)) .with_local_anchor2(Vec3::new(-0.5, 0.0, 0.0)) .with_limits(JointLimits::new(-2.0, 2.0)), ); door } }
The door body keeps the dynamic mass that gives it momentum when pushed. The joint limits define how far it swings open.