From 3d985ba803ceb8ab12e608b999b2fe607ac2f6d6 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 25 Mar 2026 18:28:44 +0900 Subject: [PATCH] feat(physics): add Coulomb friction to collision response Add friction coefficient to RigidBody (default 0.5) and implement tangential impulse clamping in resolve_collisions using Coulomb's law, including resting-contact friction for sliding bodies. Co-Authored-By: Claude Sonnet 4.6 --- crates/voltex_physics/src/rigid_body.rs | 3 ++ crates/voltex_physics/src/solver.rs | 65 ++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/crates/voltex_physics/src/rigid_body.rs b/crates/voltex_physics/src/rigid_body.rs index d77c885..1f7ac6c 100644 --- a/crates/voltex_physics/src/rigid_body.rs +++ b/crates/voltex_physics/src/rigid_body.rs @@ -7,6 +7,7 @@ pub struct RigidBody { pub mass: f32, pub restitution: f32, pub gravity_scale: f32, + pub friction: f32, // Coulomb friction coefficient, default 0.5 } impl RigidBody { @@ -17,6 +18,7 @@ impl RigidBody { mass, restitution: 0.3, gravity_scale: 1.0, + friction: 0.5, } } @@ -27,6 +29,7 @@ impl RigidBody { mass: 0.0, restitution: 0.3, gravity_scale: 0.0, + friction: 0.5, } } diff --git a/crates/voltex_physics/src/solver.rs b/crates/voltex_physics/src/solver.rs index fd7319f..e4e19dd 100644 --- a/crates/voltex_physics/src/solver.rs +++ b/crates/voltex_physics/src/solver.rs @@ -35,12 +35,42 @@ pub fn resolve_collisions(world: &mut World, contacts: &[ContactPoint]) { let v_rel_n = v_rel.dot(contact.normal); // normal points A→B; v_rel_n > 0 means A approaches B → apply impulse - if v_rel_n > 0.0 { + let j = if v_rel_n > 0.0 { let e = rb_a.restitution.min(rb_b.restitution); let j = (1.0 + e) * v_rel_n / inv_mass_sum; velocity_changes.push((contact.entity_a, contact.normal * (-j * inv_mass_a))); velocity_changes.push((contact.entity_b, contact.normal * (j * inv_mass_b))); + + j + } else { + // No separating impulse needed, but use contact depth to derive a + // representative normal force magnitude for friction clamping. + // A simple proxy: treat the penetration as providing a static normal force. + contact.depth / inv_mass_sum + }; + + // Coulomb friction: tangential impulse clamped to mu * normal impulse + let v_rel_n_scalar = v_rel.dot(contact.normal); + let v_rel_tangent = v_rel - contact.normal * v_rel_n_scalar; + let tangent_len = v_rel_tangent.length(); + + if tangent_len > 1e-6 { + let tangent = v_rel_tangent * (1.0 / tangent_len); + + // Friction coefficient: average of both bodies + let mu = (rb_a.friction + rb_b.friction) * 0.5; + + // Coulomb's law: friction impulse <= mu * normal impulse + let jt = -v_rel_tangent.dot(tangent) / inv_mass_sum; + let friction_j = if jt.abs() <= j * mu { + jt // static friction + } else { + j * mu * jt.signum() // dynamic friction (sliding), clamped magnitude + }; + + velocity_changes.push((contact.entity_a, tangent * (friction_j * inv_mass_a))); + velocity_changes.push((contact.entity_b, tangent * (-friction_j * inv_mass_b))); } let correction_mag = (contact.depth - POSITION_SLOP).max(0.0) * POSITION_PERCENT / inv_mass_sum; @@ -202,6 +232,39 @@ mod tests { assert!(t.position.y > -1.0); } + #[test] + fn test_friction_slows_sliding() { + // Ball sliding on static floor with friction + let mut world = World::new(); + + let ball = world.spawn(); + world.add(ball, Transform::from_position(Vec3::new(0.0, 0.4, 0.0))); + world.add(ball, Collider::Sphere { radius: 0.5 }); + let mut rb = RigidBody::dynamic(1.0); + rb.velocity = Vec3::new(5.0, 0.0, 0.0); // sliding horizontally + rb.gravity_scale = 0.0; + rb.friction = 0.5; + world.add(ball, rb); + + let floor = world.spawn(); + world.add(floor, Transform::from_position(Vec3::new(0.0, -0.5, 0.0))); + world.add(floor, Collider::Box { half_extents: Vec3::new(10.0, 0.5, 10.0) }); + let mut floor_rb = RigidBody::statik(); + floor_rb.friction = 0.5; + world.add(floor, floor_rb); + + // Ball center at 0.4, radius 0.5, floor top at 0.0 → overlap 0.1 + let contacts = detect_collisions(&world); + if !contacts.is_empty() { + resolve_collisions(&mut world, &contacts); + } + + let ball_v = world.get::(ball).unwrap().velocity; + // X velocity should be reduced by friction + assert!(ball_v.x < 5.0, "friction should slow horizontal velocity: {}", ball_v.x); + assert!(ball_v.x > 0.0, "should still be moving: {}", ball_v.x); + } + #[test] fn test_both_static_no_response() { let mut world = World::new();