Skip to content

Commit 68f492b

Browse files
committed
[update] 2d rotations to be locked so body rotation no longer changes from collision torque
1 parent fdec0d5 commit 68f492b

3 files changed

Lines changed: 63 additions & 11 deletions

File tree

crates/lambda-rs-platform/src/physics/rapier2d.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,8 +1192,10 @@ impl PhysicsBackend2D {
11921192

11931193
/// Builds a Rapier rigid body builder with `lambda-rs` invariants applied.
11941194
///
1195-
/// Bodies created by this backend do not lock the 2D rotation axis. Dynamic
1196-
/// bodies are expected to rotate in response to collisions.
1195+
/// Bodies created by this backend lock 2D rotation so `RigidBody2D` rotation
1196+
/// changes only through explicit `set_rotation()` calls. This preserves the
1197+
/// current public 2D rigid-body contract, which excludes angular dynamics from
1198+
/// simulation stepping.
11971199
///
11981200
/// # Arguments
11991201
/// - `body_type`: The integration mode for the rigid body.
@@ -1219,18 +1221,24 @@ fn build_rapier_rigid_body(
12191221
return RigidBodyBuilder::fixed()
12201222
.translation(translation)
12211223
.rotation(rotation)
1224+
.angvel(0.0)
1225+
.lock_rotations()
12221226
.linvel(linear_velocity);
12231227
}
12241228
RigidBodyType2D::Kinematic => {
12251229
return RigidBodyBuilder::kinematic_velocity_based()
12261230
.translation(translation)
12271231
.rotation(rotation)
1232+
.angvel(0.0)
1233+
.lock_rotations()
12281234
.linvel(linear_velocity);
12291235
}
12301236
RigidBodyType2D::Dynamic => {
12311237
return RigidBodyBuilder::dynamic()
12321238
.translation(translation)
12331239
.rotation(rotation)
1240+
.angvel(0.0)
1241+
.lock_rotations()
12341242
.linvel(linear_velocity)
12351243
.additional_mass(additional_mass_kg);
12361244
}

crates/lambda-rs/tests/physics_2d/colliders.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,49 @@ fn physics_2d_rectangle_collider_local_rotation_changes_motion() {
188188
return;
189189
}
190190

191+
/// Ensures collision response does not rotate dynamic bodies during stepping.
192+
///
193+
/// This test uses an off-center collider attachment so ground contact would
194+
/// introduce torque if the backend allowed angular dynamics. The body's
195+
/// rotation MUST remain unchanged after collision resolution.
196+
#[test]
197+
fn physics_2d_collision_response_does_not_change_body_rotation() {
198+
let mut world = PhysicsWorld2DBuilder::new().build().unwrap();
199+
200+
let ground = RigidBody2DBuilder::new(RigidBodyType::Static)
201+
.with_position(0.0, -1.0)
202+
.build(&mut world)
203+
.unwrap();
204+
205+
Collider2DBuilder::rectangle(20.0, 0.5)
206+
.build(&mut world, ground)
207+
.unwrap();
208+
209+
let body = RigidBody2DBuilder::new(RigidBodyType::Dynamic)
210+
.with_position(0.0, 4.0)
211+
.build(&mut world)
212+
.unwrap();
213+
214+
Collider2DBuilder::circle(0.5)
215+
.with_offset(0.75, 0.0)
216+
.build(&mut world, body)
217+
.unwrap();
218+
219+
step_world(&mut world, DEFAULT_STEP_COUNT);
220+
221+
let rotation = body.rotation(&world).unwrap();
222+
let position = body.position(&world).unwrap();
223+
224+
assert_in_range("rotation", rotation, -0.001, 0.001);
225+
assert!(
226+
position[1] > -0.25,
227+
"body did not appear to collide with the ground: y={}",
228+
position[1],
229+
);
230+
231+
return;
232+
}
233+
191234
/// Simulates a circle falling onto a platform with a rotated rectangle collider.
192235
///
193236
/// # Arguments

docs/specs/physics/colliders-2d.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ title: "2D Colliders"
33
document_id: "colliders-2d-2026-02-17"
44
status: "living"
55
created: "2026-02-17T23:08:44Z"
6-
last_updated: "2026-03-14T22:54:24Z"
7-
version: "0.1.3"
6+
last_updated: "2026-03-14T23:51:46Z"
7+
version: "0.1.4"
88
engine_workspace_version: "2023.1.30"
99
wgpu_version: "26.0.1"
1010
shader_backend_default: "naga"
@@ -276,11 +276,10 @@ Collision detection
276276
Collision response (normative)
277277
- Contact resolution MUST update dynamic-body motion such that penetrations are
278278
resolved and restitution and friction affect motion.
279-
- Collision response MAY change `RigidBody2D` rotation for dynamic bodies when
280-
contacts introduce torque through collider shape or local offset.
281-
- The public API does not currently expose explicit angular-velocity controls,
282-
but dynamic-body rotation observed through `RigidBody2D` state MUST remain
283-
backend-consistent.
279+
- Collision response MUST NOT change `RigidBody2D` rotation during
280+
`PhysicsWorld2D::step()`.
281+
- Collider shape and local offset MAY change linear contact response, but MUST
282+
NOT introduce angular dynamics through public 2D rigid-body stepping.
284283

285284
Material properties
286285
- `density` MUST affect mass properties for dynamic bodies when the body mass
@@ -441,5 +440,7 @@ Manual verification
441440
- 2026-02-17 0.1.0: Define 2D collider shapes and attachment APIs.
442441
- 2026-02-17 0.1.1: Specify defaults and mass recomputation rules.
443442
- 2026-02-17 0.1.2: Add local rotation, material struct, and polygon limits.
444-
- 2026-03-14 0.1.3: Align the specification with the implemented rotation,
445-
testing, and demo behavior.
443+
- 2026-03-14 0.1.3: Align the specification with the implemented testing and
444+
demo behavior.
445+
- 2026-03-14 0.1.4: Restore the no-angular-dynamics collision contract to
446+
match `RigidBody2D`.

0 commit comments

Comments
 (0)