Fix suspension when driving on other rigid bodies, super solid now

The key was to limit the total spring impulse similar to how contact impulse is limited, so that spring always pushes and never pulls
This commit is contained in:
sergeypdev 2025-07-29 01:57:47 +04:00
parent 44a401344f
commit 88fac29a6c
3 changed files with 122 additions and 37 deletions

View File

@ -557,7 +557,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
u32(box_name), u32(box_name),
physics.Body_Config { physics.Body_Config {
name = box_name, name = box_name,
initial_pos = {-5, 0.5 + f32(y) * 1.1, f32(x) * 3 + 10}, initial_pos = {-5 + f32(y) * 1.01, 1, f32(x) * 1.01 + -11.5},
initial_rot = linalg.QUATERNIONF32_IDENTITY, initial_rot = linalg.QUATERNIONF32_IDENTITY,
shapes = { shapes = {
{ {
@ -598,8 +598,8 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
wheel_extent_x_back := f32(2) / 2 wheel_extent_x_back := f32(2) / 2
wheel_y := f32(0.5) wheel_y := f32(0.5)
rest := f32(0.7) rest := f32(0.7)
natural_frequency := f32(0.4) natural_frequency := f32(1.2)
damping := f32(0.06) damping := f32(0.4)
radius := f32(0.737649) / 2 radius := f32(0.737649) / 2
wheel_front_z := f32(1.6) wheel_front_z := f32(1.6)
wheel_back_z := f32(-1.63) wheel_back_z := f32(-1.63)
@ -818,9 +818,9 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
cur_world := runtime_world_current_world(runtime_world) cur_world := runtime_world_current_world(runtime_world)
runtime_world.dt = dt runtime_world.dt = dt
should_single_step := rl.IsKeyPressed(.PERIOD) should_single_step := g_mem.physics_pause // rl.IsKeyPressed(.PERIOD)
runtime_world.rewind_simulation = rl.IsKeyPressed(.COMMA) runtime_world.rewind_simulation = rl.IsKeyPressed(.COMMA)
runtime_world.commit_simulation = !g_mem.physics_pause || should_single_step runtime_world.commit_simulation = !g_mem.physics_pause || rl.IsKeyPressed(.PERIOD)
runtime_world.single_step_simulation = should_single_step runtime_world.single_step_simulation = should_single_step
if !runtime_world.rewind_simulation { if !runtime_world.rewind_simulation {

View File

@ -288,16 +288,21 @@ Suspension_Constraint :: struct {
lateral_impulse: f32, lateral_impulse: f32,
longitudinal_impulse: f32, longitudinal_impulse: f32,
brake_friction_impulse: f32, brake_friction_impulse: f32,
inv_mass: f32,
// Inverse inertia along wheel rotation axis // Inverse inertia along wheel rotation axis
inv_inertia: f32, inv_inertia: f32,
// Wheel pos along suspension dir
x: f32,
// Rotation // Rotation
q: f32, q: f32,
// Linear velocity along suspension dir
v: f32,
// Angular velocity // Angular velocity
w: f32, w: f32,
hit: bool, hit: bool,
hit_normal: Vec3, hit_normal: Vec3,
hit_body: Body_Handle,
hit_point: Vec3, hit_point: Vec3,
// rel_hit_point = rel_pos + rel_dir * hit_t // rel_hit_point = rel_pos + rel_dir * hit_t
hit_t: f32, hit_t: f32,

View File

@ -247,6 +247,7 @@ raycast_bodies :: proc(
) -> ( ) -> (
t: f32, t: f32,
normal: Vec3, normal: Vec3,
body_handle: Body_Handle,
hit: bool, hit: bool,
) { ) {
temp := runtime.default_temp_allocator_temp_begin() temp := runtime.default_temp_allocator_temp_begin()
@ -263,8 +264,9 @@ raycast_bodies :: proc(
for leaf_node in bvh.iterator_intersect_leaf_next(&it) { for leaf_node in bvh.iterator_intersect_leaf_next(&it) {
for j in 0 ..< leaf_node.prim_len { for j in 0 ..< leaf_node.prim_len {
body_idx := tlas.bvh_tree.primitives[leaf_node.child_or_prim_start + j] body_idx := tlas.bvh_tree.primitives[leaf_node.child_or_prim_start + j]
tmp_body_handle := index_to_body_handle(int(body_idx))
body := get_body(sim_state, index_to_body_handle(int(body_idx))) body := get_body(sim_state, tmp_body_handle)
it := shapes_iterator(sim_state, body.shape) it := shapes_iterator(sim_state, body.shape)
for shape in shapes_iterator_next(&it) { for shape in shapes_iterator_next(&it) {
@ -285,12 +287,17 @@ raycast_bodies :: proc(
if ok && (!hit || hit_t < t) { if ok && (!hit || hit_t < t) {
t = hit_t t = hit_t
normal = tmp_normal normal = tmp_normal
body_handle = tmp_body_handle
hit = true hit = true
} }
} }
} }
} }
if hit {
normal = lg.normalize0(normal)
}
return return
} }
@ -304,12 +311,13 @@ raycast :: proc(
) -> ( ) -> (
t: f32, t: f32,
normal: Vec3, normal: Vec3,
hit_body: Body_Handle,
hit: bool, hit: bool,
) { ) {
t = distance t = distance
t1, normal1, hit1 := raycasts_level(sim_state, sim_cache, static_tlas, origin, dir, t) t1, normal1, hit1 := raycasts_level(sim_state, sim_cache, static_tlas, origin, dir, t)
t2, normal2, hit2 := raycast_bodies(sim_state, dyn_tlas, origin, dir, t) t2, normal2, body, hit2 := raycast_bodies(sim_state, dyn_tlas, origin, dir, t)
hit = hit1 || hit2 hit = hit1 || hit2
@ -320,6 +328,7 @@ raycast :: proc(
} else { } else {
t = t2 t = t2
normal = normal2 normal = normal2
hit_body = body
} }
} else if hit1 { } else if hit1 {
t = t1 t = t1
@ -327,11 +336,9 @@ raycast :: proc(
} else if hit2 { } else if hit2 {
t = t2 t = t2
normal = normal2 normal = normal2
hit_body = body
} }
// TODO: raycast_level and raycast_bodies should return a normalized vec
normal = lg.normalize0(normal)
return return
} }
@ -1267,18 +1274,31 @@ pgs_solve_engines :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32,
calculate_ground_vel :: proc( calculate_ground_vel :: proc(
body: Body_Ptr, body: Body_Ptr,
hit_body: Body_Ptr,
p: Vec3, p: Vec3,
right, forward: Vec3, right, forward: Vec3,
) -> ( ) -> (
body_vel_at_contact_patch: Vec3, body_vel_at_contact_patch: Vec3,
ground_vel: Vec2, ground_vel: Vec2,
) { ) {
body_vel_at_contact_patch = body_velocity_at_point(body, p) body_vel_at_contact_patch =
body_velocity_at_point(body, p) - body_velocity_at_point(hit_body, p)
ground_vel.x = lg.dot(body_vel_at_contact_patch, right) ground_vel.x = lg.dot(body_vel_at_contact_patch, right)
ground_vel.y = lg.dot(body_vel_at_contact_patch, forward) ground_vel.y = lg.dot(body_vel_at_contact_patch, forward)
return return
} }
combine_inv_mass :: proc(w1, w2: f32) -> f32 {
if w1 > 0 && w2 > 0 {
return (w1 * w2) / (w1 + w2)
}
if w1 > 0 {
return w1
}
return w2
}
pgs_solve_suspension :: proc( pgs_solve_suspension :: proc(
sim_state: ^Sim_State, sim_state: ^Sim_State,
sim_cache: ^Sim_Cache, sim_cache: ^Sim_Cache,
@ -1287,6 +1307,7 @@ pgs_solve_suspension :: proc(
config: Solver_Config, config: Solver_Config,
dt: f32, dt: f32,
inv_dt: f32, inv_dt: f32,
apply_bias: bool,
) { ) {
// Solve suspension velocity // Solve suspension velocity
for _, i in sim_state.suspension_constraints { for _, i in sim_state.suspension_constraints {
@ -1297,7 +1318,8 @@ pgs_solve_suspension :: proc(
if body.alive { if body.alive {
wheel_world_pos := body_local_to_world(body, v.rel_pos + body.shape_offset) wheel_world_pos := body_local_to_world(body, v.rel_pos + body.shape_offset)
dir := body_local_to_world_vec(body, v.rel_dir) dir := body_local_to_world_vec(body, v.rel_dir)
v.hit_t, v.hit_normal, v.hit = raycast( prev_hit_body := v.hit_body
v.hit_t, v.hit_normal, v.hit_body, v.hit = raycast(
sim_state, sim_state,
sim_cache, sim_cache,
static_tlas, static_tlas,
@ -1306,13 +1328,12 @@ pgs_solve_suspension :: proc(
dir, dir,
v.rest, v.rest,
) )
// log.debugf("hit_t: %v, hit: %v", v.hit_t, v.hit) hit_body := get_body(sim_state, v.hit_body)
v.hit_point = wheel_world_pos + dir * v.hit_t v.hit_point = wheel_world_pos + dir * v.hit_t
forward := wheel_get_forward_vec(body, v) w_normal1 := get_body_inverse_mass(body, -dir, wheel_world_pos)
right := wheel_get_right_vec(body, v) w_normal2 := get_body_inverse_mass(hit_body, dir, v.hit_point)
w_normal := combine_inv_mass(w_normal1, w_normal2)
w_normal := get_body_angular_inverse_mass(body, -v.hit_normal)
inv_w_normal := 1.0 / w_normal inv_w_normal := 1.0 / w_normal
// Drive force // Drive force
@ -1347,7 +1368,17 @@ pgs_solve_suspension :: proc(
} }
} }
if !v.hit || v.hit_body != prev_hit_body {
v.lateral_impulse = 0
v.spring_impulse = 0
v.longitudinal_impulse = 0
}
if v.hit { if v.hit {
forward := wheel_get_forward_vec(body, v)
right := lg.normalize0(lg.cross(forward, v.hit_normal))
forward = lg.normalize0(lg.cross(v.hit_normal, right))
// Spring force // Spring force
{ {
@ -1356,21 +1387,30 @@ pgs_solve_suspension :: proc(
f64(v.damping), f64(v.damping),
f64(dt), f64(dt),
) )
if !apply_bias {
mass_coef = 1
bias_coef = 0
impulse_coef = 0
}
vel := lg.dot(body_velocity_at_point(body, v.hit_point), -v.hit_normal) vel := lg.dot(
body_velocity_at_point(body, v.hit_point) -
body_velocity_at_point(hit_body, v.hit_point),
dir,
)
x := v.hit_t x := v.hit_t
separation := v.rest - x separation := v.rest - x
// spring_dot_normal := abs(lg.dot(dir, v.hit_normal))
incremental_impulse := incremental_impulse :=
-inv_w_normal * mass_coef * (vel + separation * bias_coef) - -inv_w_normal * mass_coef * (vel + separation * bias_coef) -
impulse_coef * v.spring_impulse impulse_coef * v.spring_impulse
v.spring_impulse += incremental_impulse new_spring_impulse := min(0, v.spring_impulse + incremental_impulse)
applied_impulse := new_spring_impulse - v.spring_impulse
v.spring_impulse = new_spring_impulse
apply_velocity_correction( apply_velocity_correction(body, applied_impulse * dir, wheel_world_pos)
body, apply_velocity_correction(hit_body, applied_impulse * -dir, v.hit_point)
incremental_impulse * v.hit_normal,
v.hit_point,
)
} }
// Positive means spinning forward // Positive means spinning forward
@ -1378,6 +1418,7 @@ pgs_solve_suspension :: proc(
body_vel_at_contact_patch, ground_vel := calculate_ground_vel( body_vel_at_contact_patch, ground_vel := calculate_ground_vel(
body, body,
hit_body,
v.hit_point, v.hit_point,
right, right,
forward, forward,
@ -1432,6 +1473,7 @@ pgs_solve_suspension :: proc(
if true { if true {
body_vel_at_contact_patch, ground_vel = calculate_ground_vel( body_vel_at_contact_patch, ground_vel = calculate_ground_vel(
body, body,
hit_body,
v.hit_point, v.hit_point,
right, right,
forward, forward,
@ -1442,9 +1484,11 @@ pgs_solve_suspension :: proc(
friction_clamp := abs(v.spring_impulse) * long_friction friction_clamp := abs(v.spring_impulse) * long_friction
w_body := get_body_inverse_mass(body, forward, v.hit_point) w_body1 := get_body_inverse_mass(body, forward, v.hit_point)
w_body2 := get_body_inverse_mass(hit_body, forward, v.hit_point)
w_body := w_body1 + v.inv_inertia + w_body2
w_long := w_body + v.inv_inertia w_long := w_body
inv_w_long := 1.0 / w_long inv_w_long := 1.0 / w_long
incremental_impulse := -inv_w_long * relative_vel incremental_impulse := -inv_w_long * relative_vel
@ -1457,12 +1501,18 @@ pgs_solve_suspension :: proc(
v.longitudinal_impulse = new_total_impulse v.longitudinal_impulse = new_total_impulse
apply_velocity_correction(body, applied_impulse * forward, v.hit_point) apply_velocity_correction(body, applied_impulse * forward, v.hit_point)
apply_velocity_correction(
hit_body,
-applied_impulse * forward,
v.hit_point,
)
v.w += v.inv_inertia * applied_impulse v.w += v.inv_inertia * applied_impulse
} }
// Lateral friction // Lateral friction
if true { if true {
body_vel_at_contact_patch, ground_vel = calculate_ground_vel( body_vel_at_contact_patch, ground_vel = calculate_ground_vel(
body, body,
hit_body,
v.hit_point, v.hit_point,
right, right,
forward, forward,
@ -1470,7 +1520,14 @@ pgs_solve_suspension :: proc(
lateral_vel := ground_vel.x lateral_vel := ground_vel.x
friction_clamp := abs(v.spring_impulse) * lat_friction friction_clamp := abs(v.spring_impulse) * lat_friction
incremental_impulse := -inv_w_normal * lateral_vel w_body1 := get_body_inverse_mass(body, right, v.hit_point)
w_body2 := get_body_inverse_mass(hit_body, right, v.hit_point)
w_body := w_body1 + w_body2
w_lat := w_body
inv_w_lat := 1.0 / w_lat
incremental_impulse := -inv_w_lat * lateral_vel
new_total_impulse := clamp( new_total_impulse := clamp(
v.lateral_impulse + incremental_impulse, v.lateral_impulse + incremental_impulse,
-friction_clamp, -friction_clamp,
@ -1480,11 +1537,8 @@ pgs_solve_suspension :: proc(
v.lateral_impulse = new_total_impulse v.lateral_impulse = new_total_impulse
apply_velocity_correction(body, applied_impulse * right, v.hit_point) apply_velocity_correction(body, applied_impulse * right, v.hit_point)
apply_velocity_correction(hit_body, -applied_impulse * right, v.hit_point)
} }
} else {
v.lateral_impulse = 0
v.spring_impulse = 0
v.longitudinal_impulse = 0
} }
} }
} }
@ -1589,18 +1643,25 @@ pgs_substep :: proc(
if s.hit { if s.hit {
body := get_body(sim_state, s.body) body := get_body(sim_state, s.body)
hit_body := get_body(sim_state, s.hit_body)
p := body_local_to_world(body, s.rel_pos + body.shape_offset) p := body_local_to_world(body, s.rel_pos + body.shape_offset)
dir := body_local_to_world_vec(body, s.rel_dir)
hit_p := body_local_to_world( hit_p := body_local_to_world(
body, body,
s.rel_pos + body.shape_offset + s.rel_dir * s.hit_t, s.rel_pos + body.shape_offset + s.rel_dir * s.hit_t,
) )
forward := wheel_get_forward_vec(body, s) forward := wheel_get_forward_vec(body, s)
right := wheel_get_right_vec(body, s) right := lg.normalize0(lg.cross(forward, s.hit_normal))
forward = lg.normalize0(lg.cross(s.hit_normal, right))
apply_velocity_correction(body, s.spring_impulse * -s.hit_normal, hit_p) apply_velocity_correction(body, s.spring_impulse * dir, p)
apply_velocity_correction(body, s.lateral_impulse * right, p) apply_velocity_correction(hit_body, s.spring_impulse * -dir, hit_p)
apply_velocity_correction(body, s.lateral_impulse * right, hit_p)
apply_velocity_correction(hit_body, -s.lateral_impulse * right, hit_p)
apply_velocity_correction(body, s.longitudinal_impulse * forward, hit_p) apply_velocity_correction(body, s.longitudinal_impulse * forward, hit_p)
apply_velocity_correction(hit_body, -s.longitudinal_impulse * forward, hit_p)
s.w += s.inv_inertia * s.longitudinal_impulse s.w += s.inv_inertia * s.longitudinal_impulse
} }
@ -1611,7 +1672,16 @@ pgs_substep :: proc(
apply_bias := true apply_bias := true
pgs_solve_contacts(sim_state, config, dt, inv_dt, apply_bias) pgs_solve_contacts(sim_state, config, dt, inv_dt, apply_bias)
pgs_solve_engines(sim_state, config, dt, inv_dt) pgs_solve_engines(sim_state, config, dt, inv_dt)
pgs_solve_suspension(sim_state, sim_cache, static_tlas, dyn_tlas, config, dt, inv_dt) pgs_solve_suspension(
sim_state,
sim_cache,
static_tlas,
dyn_tlas,
config,
dt,
inv_dt,
apply_bias,
)
for i in 0 ..< len(sim_state.bodies_slice) { for i in 0 ..< len(sim_state.bodies_slice) {
body := &sim_state.bodies_slice[i] body := &sim_state.bodies_slice[i]
@ -1644,12 +1714,22 @@ pgs_substep :: proc(
for i in 0 ..< len(sim_state.suspension_constraints_slice) { for i in 0 ..< len(sim_state.suspension_constraints_slice) {
s := &sim_state.suspension_constraints_slice[i] s := &sim_state.suspension_constraints_slice[i]
s.x += s.v * dt
s.q = math.mod_f32(s.q + 0.5 * s.w * dt, math.PI * 2) s.q = math.mod_f32(s.q + 0.5 * s.w * dt, math.PI * 2)
} }
apply_bias = false apply_bias = false
pgs_solve_contacts(sim_state, config, dt, inv_dt, apply_bias) pgs_solve_contacts(sim_state, config, dt, inv_dt, apply_bias)
// pgs_solve_suspension(sim_state, config, dt, inv_dt, apply_bias) // pgs_solve_suspension(
// sim_state,
// sim_cache,
// static_tlas,
// dyn_tlas,
// config,
// dt,
// inv_dt,
// apply_bias,
// )
} }
simulate_step :: proc( simulate_step :: proc(