Implement friction in PGS

This commit is contained in:
sergeypdev 2025-03-15 20:53:45 +04:00
parent d1feaa0602
commit 50d0401479
5 changed files with 179 additions and 46 deletions

View File

@ -35,7 +35,7 @@ esac
# Build the game. # Build the game.
echo "Building game$DLL_EXT" echo "Building game$DLL_EXT"
odin build game -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -define:RAYLIB_SHARED=true -define:TRACY_ENABLE=true -collection:libs=./libs -collection:common=./common -collection:game=./game -build-mode:dll -out:game_tmp$DLL_EXT -strict-style -vet -debug odin build game -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -define:RAYLIB_SHARED=true -define:TRACY_ENABLE=true -collection:libs=./libs -collection:common=./common -collection:game=./game -build-mode:dll -out:game_tmp$DLL_EXT -strict-style -vet -debug -o:speed
# Need to use a temp file on Linux because it first writes an empty `game.so`, which the game will load before it is actually fully written. # Need to use a temp file on Linux because it first writes an empty `game.so`, which the game will load before it is actually fully written.
mv game_tmp$DLL_EXT game$DLL_EXT mv game_tmp$DLL_EXT game$DLL_EXT

View File

@ -59,6 +59,8 @@ Contact_Type :: union {
Contact_Manifold :: struct { Contact_Manifold :: struct {
type: Contact_Type, type: Contact_Type,
normal: Vec3, normal: Vec3,
tangent: Vec3,
bitangent: Vec3,
separation: f32, separation: f32,
points_a: [4]Vec3, points_a: [4]Vec3,
points_b: [4]Vec3, points_b: [4]Vec3,
@ -89,7 +91,6 @@ convex_vs_convex_sat :: proc(a, b: Convex) -> (manifold: Contact_Manifold, colli
is_face_a_contact := biased_face_a_separation >= biased_edge_separation is_face_a_contact := biased_face_a_separation >= biased_edge_separation
is_face_b_contact := biased_face_b_separation >= biased_edge_separation is_face_b_contact := biased_face_b_separation >= biased_edge_separation
collision = true
if is_face_a_contact || is_face_b_contact { if is_face_a_contact || is_face_b_contact {
manifold = create_face_contact_manifold(face_query_a, a, face_query_b, b) manifold = create_face_contact_manifold(face_query_a, a, face_query_b, b)
} else { } else {
@ -102,6 +103,10 @@ convex_vs_convex_sat :: proc(a, b: Convex) -> (manifold: Contact_Manifold, colli
edge_b, edge_b,
) )
} }
collision = manifold.points_len > 0
if collision {
manifold.bitangent = lg.normalize0(lg.cross(manifold.tangent, manifold.normal))
}
return return
} }
@ -423,8 +428,12 @@ create_face_contact_manifold :: proc(
} }
} }
ref_face_vert := ref_convex.vertices[ref_convex.edges[ref_face.edge].origin].pos ref_face_vert, ref_face_vert2 := halfedge.get_edge_points(
ref_convex,
ref_convex.edges[ref_face.edge],
)
ref_plane := plane_from_point_normal(ref_face_vert, ref_face.normal) ref_plane := plane_from_point_normal(ref_face_vert, ref_face.normal)
ref_tangent := lg.normalize0(ref_face_vert2 - ref_face_vert)
// Final clipping, remove verts above ref face and project those left onto the reference plane // Final clipping, remove verts above ref face and project those left onto the reference plane
{ {
@ -527,6 +536,7 @@ create_face_contact_manifold :: proc(
manifold.separation = ref_face_query.separation manifold.separation = ref_face_query.separation
// Normal is always pointing from a to b // Normal is always pointing from a to b
manifold.normal = is_ref_a ? ref_face.normal : -ref_face.normal manifold.normal = is_ref_a ? ref_face.normal : -ref_face.normal
manifold.tangent = ref_tangent
return return
} }
@ -560,6 +570,7 @@ create_edge_contact_manifold :: proc(
manifold.points_a[0] = ps[0] manifold.points_a[0] = ps[0]
manifold.points_b[0] = ps[1] manifold.points_b[0] = ps[1]
manifold.points_len = 1 manifold.points_len = 1
manifold.tangent = lg.normalize0(lg.cross(a2 - a1, manifold.normal))
return return
} }

View File

@ -117,12 +117,14 @@ draw_debug_scene :: proc(scene: ^Scene) {
debug_transform_points_local_to_world(get_body(sim_state, contact.a), points_a_slice) debug_transform_points_local_to_world(get_body(sim_state, contact.a), points_a_slice)
debug_transform_points_local_to_world(get_body(sim_state, contact.b), points_b_slice) debug_transform_points_local_to_world(get_body(sim_state, contact.b), points_b_slice)
debug_draw_manifold_points( debug_draw_manifold_points(
-contact.manifold.normal, contact,
-1,
points_a_slice, points_a_slice,
color = debug.int_to_color(i32(contact_idx * 2 + 0)), color = debug.int_to_color(i32(contact_idx * 2 + 0)),
) )
debug_draw_manifold_points( debug_draw_manifold_points(
contact.manifold.normal, contact,
1,
points_b_slice, points_b_slice,
color = debug.int_to_color(i32(contact_idx * 2 + 1)), color = debug.int_to_color(i32(contact_idx * 2 + 1)),
) )
@ -136,7 +138,12 @@ debug_transform_points_local_to_world :: proc(body: Body_Ptr, points: []Vec3) {
} }
} }
debug_draw_manifold_points :: proc(normal: Vec3, points: []Vec3, color: rl.Color) { debug_draw_manifold_points :: proc(
contact: Contact,
impulse_sign: f32,
points: []Vec3,
color: rl.Color,
) {
if len(points) >= 3 { if len(points) >= 3 {
// Triangle or quad // Triangle or quad
v1 := points[0] v1 := points[0]
@ -151,9 +158,15 @@ debug_draw_manifold_points :: proc(normal: Vec3, points: []Vec3, color: rl.Color
rl.DrawLine3D(points[0], points[1], color) rl.DrawLine3D(points[0], points[1], color)
} }
for p in points {
rl.DrawLine3D(p, p + normal, color)
rl.DrawSphereWires(p, 0.1, 4, 4, color) for point_idx in 0 ..< len(points) {
p := points[point_idx]
total_impulse :=
contact.total_normal_impulse[point_idx] * contact.manifold.normal +
contact.total_friction_impulse[point_idx].x * contact.manifold.tangent +
contact.total_friction_impulse[point_idx].y * contact.manifold.bitangent
rl.DrawLine3D(p, p + total_impulse * impulse_sign, color)
// rl.DrawSphereWires(p, 0.1, 4, 4, color)
} }
} }

View File

@ -6,6 +6,7 @@ import lg "core:math/linalg"
MAX_CONTACTS :: 1024 * 16 MAX_CONTACTS :: 1024 * 16
Vec3 :: [3]f32 Vec3 :: [3]f32
Vec2 :: [2]f32
Quat :: quaternion128 Quat :: quaternion128
Matrix3 :: # row_major matrix[3, 3]f32 Matrix3 :: # row_major matrix[3, 3]f32
AABB :: struct { AABB :: struct {

View File

@ -219,7 +219,8 @@ Contact :: struct {
// For PGS // For PGS
total_normal_impulse: [4]f32, total_normal_impulse: [4]f32,
total_tangent_impulse: [4]f32, // xy - tangent and bitangent (linear friction), z - twist friction around normal (rotational impulse only)
total_friction_impulse: [4]Vec2,
// XPBD stuff // XPBD stuff
lambda_normal: [4]f32, lambda_normal: [4]f32,
@ -253,7 +254,7 @@ update_contacts :: proc(sim_state: ^Sim_State) {
old_manifold := contact.manifold old_manifold := contact.manifold
old_total_normal_impulse := contact.total_normal_impulse old_total_normal_impulse := contact.total_normal_impulse
old_total_tangent_impulse := contact.total_tangent_impulse old_total_friction_impulse := contact.total_friction_impulse
contact.prev_x_a = body.x contact.prev_x_a = body.x
contact.prev_x_b = body2.x contact.prev_x_b = body2.x
@ -261,7 +262,7 @@ update_contacts :: proc(sim_state: ^Sim_State) {
contact.prev_q_b = body2.q contact.prev_q_b = body2.q
contact.manifold = {} contact.manifold = {}
contact.total_normal_impulse = 0 contact.total_normal_impulse = 0
contact.total_tangent_impulse = 0 contact.total_friction_impulse = 0
contact.lambda_normal = 0 contact.lambda_normal = 0
contact.lambda_tangent = 0 contact.lambda_tangent = 0
contact.applied_static_friction = false contact.applied_static_friction = false
@ -285,18 +286,11 @@ update_contacts :: proc(sim_state: ^Sim_State) {
raw_manifold, collision := collision.convex_vs_convex_sat(m1, m2) raw_manifold, collision := collision.convex_vs_convex_sat(m1, m2)
if collision { if collision {
assert(raw_manifold.points_len > 0)
manifold := &contact.manifold manifold := &contact.manifold
manifold^ = raw_manifold manifold^ = raw_manifold
preserve_impulse :=
old_manifold.points_len == raw_manifold.points_len &&
old_manifold.type == raw_manifold.type
if preserve_impulse {
contact.total_normal_impulse = old_total_normal_impulse
contact.total_tangent_impulse = old_total_tangent_impulse
}
// Convert manifold contact from world to local space // Convert manifold contact from world to local space
for point_idx in 0 ..< manifold.points_len { for point_idx in 0 ..< manifold.points_len {
manifold.points_a[point_idx] = body_world_to_local( manifold.points_a[point_idx] = body_world_to_local(
@ -308,6 +302,25 @@ update_contacts :: proc(sim_state: ^Sim_State) {
manifold.points_b[point_idx], manifold.points_b[point_idx],
) )
} }
preserve_impulse :=
old_manifold.points_len == raw_manifold.points_len &&
old_manifold.type == raw_manifold.type
if preserve_impulse {
contact.total_normal_impulse = old_total_normal_impulse
for point_idx in 0 ..< manifold.points_len {
contact.total_friction_impulse[point_idx].x = lg.dot(
old_total_friction_impulse[point_idx].x * old_manifold.tangent,
manifold.tangent,
)
contact.total_friction_impulse[point_idx].y = lg.dot(
old_total_friction_impulse[point_idx].y * old_manifold.bitangent,
manifold.bitangent,
)
}
}
} }
} }
} }
@ -822,13 +835,27 @@ pgs_solve_contacts :: proc(
inv_dt: f32, inv_dt: f32,
apply_bias: bool, apply_bias: bool,
) { ) {
bias_rate, mass_coef, impulse_coef := calculate_soft_constraint_params(100, 1, dt) bias_rate, mass_coef, impulse_coef := calculate_soft_constraint_params(30, 0.8, dt)
if !apply_bias { if !apply_bias {
mass_coef = 1
bias_rate = 0 bias_rate = 0
impulse_coef = 0
}
random_order := make([]i32, len(sim_state.contact_container.contacts))
{
for i in 0 ..< len(random_order) {
random_order[i] = i32(i)
}
for i in 0 ..< len(random_order) - 1 {
j := rand.int_max(len(random_order))
slice.swap(random_order, i, j)
}
} }
for i in 0 ..< len(sim_state.contact_container.contacts) { for i in 0 ..< len(sim_state.contact_container.contacts) {
contact := &sim_state.contact_container.contacts[i] contact := &sim_state.contact_container.contacts[random_order[i]]
manifold := &contact.manifold manifold := &contact.manifold
body1, body2 := get_body(sim_state, contact.a), get_body(sim_state, contact.b) body1, body2 := get_body(sim_state, contact.a), get_body(sim_state, contact.b)
@ -857,29 +884,71 @@ pgs_solve_contacts :: proc(
inv_w := 1.0 / w inv_w := 1.0 / w
delta_v := lg.dot(v2 - v1, manifold.normal) delta_v := v2 - v1
{
delta_v_norm := lg.dot(delta_v, manifold.normal)
incremental_impulse := bias := f32(0.0)
-inv_w * mass_coef * (delta_v + bias_rate * separation) -
impulse_coef * contact.total_normal_impulse[point_idx]
new_total_impulse := max( MAX_BAUMGARTE_VELOCITY :: 4.0
contact.total_normal_impulse[point_idx] + incremental_impulse,
0,
)
applied_impulse := new_total_impulse - contact.total_normal_impulse[point_idx]
contact.total_normal_impulse[point_idx] = new_total_impulse
applied_impulse_vec := applied_impulse * manifold.normal if separation > 0 {
bias = separation * inv_dt
} else if apply_bias {
bias = lg.max(bias_rate * separation, -MAX_BAUMGARTE_VELOCITY)
}
apply_velocity_correction(body1, -applied_impulse_vec, p1) incremental_impulse :=
apply_velocity_correction(body2, applied_impulse_vec, p2) -inv_w * mass_coef * (delta_v_norm + bias) -
impulse_coef * contact.total_normal_impulse[point_idx]
new_total_impulse := max(
contact.total_normal_impulse[point_idx] + incremental_impulse,
0,
)
applied_impulse := new_total_impulse - contact.total_normal_impulse[point_idx]
contact.total_normal_impulse[point_idx] = new_total_impulse
applied_impulse_vec := applied_impulse * manifold.normal
apply_velocity_correction(body1, -applied_impulse_vec, p1)
apply_velocity_correction(body2, applied_impulse_vec, p2)
}
{
FRICTION :: 0.6
friction_clamp := contact.total_normal_impulse[point_idx] * FRICTION
delta_v_tang := Vec2 {
lg.dot(delta_v, manifold.tangent),
lg.dot(delta_v, manifold.bitangent),
}
incremental_impulse := -inv_w * delta_v_tang
new_total_impulse: Vec2 = lg.clamp(
contact.total_friction_impulse[point_idx] + incremental_impulse,
Vec2(-friction_clamp),
Vec2(friction_clamp),
)
applied_impulse :=
new_total_impulse - contact.total_friction_impulse[point_idx]
contact.total_friction_impulse[point_idx] = new_total_impulse
applied_impulse_vec :=
applied_impulse.x * manifold.tangent +
applied_impulse.y * manifold.bitangent
apply_velocity_correction(body1, -applied_impulse_vec, p1)
apply_velocity_correction(body2, applied_impulse_vec, p2)
}
} else { } else {
contact.total_normal_impulse[point_idx] = 0 contact.total_normal_impulse[point_idx] = 0
contact.total_friction_impulse[point_idx] = 0
} }
} }
} }
} }
pgs_substep :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32, inv_dt: f32) { pgs_substep :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32, inv_dt: f32) {
@ -907,6 +976,13 @@ pgs_substep :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32, inv_d
apply_velocity_correction(body1, -total_normal_impulse, p1) apply_velocity_correction(body1, -total_normal_impulse, p1)
apply_velocity_correction(body2, total_normal_impulse, p2) apply_velocity_correction(body2, total_normal_impulse, p2)
total_tangent_impulse :=
contact.total_friction_impulse[point_idx].x * manifold.tangent +
contact.total_friction_impulse[point_idx].y * manifold.bitangent
apply_velocity_correction(body1, -total_tangent_impulse, p1)
apply_velocity_correction(body2, total_tangent_impulse, p2)
} }
} }
} }
@ -950,7 +1026,6 @@ simulate_step :: proc(scene: ^Scene, sim_state: ^Sim_State, config: Solver_Confi
tlas := build_tlas(sim_state, config) tlas := build_tlas(sim_state, config)
find_new_contacts(sim_state, &tlas)
{ {
tracy.ZoneN("simulate_step::remove_invalid_contacts") tracy.ZoneN("simulate_step::remove_invalid_contacts")
@ -981,6 +1056,8 @@ simulate_step :: proc(scene: ^Scene, sim_state: ^Sim_State, config: Solver_Confi
} }
} }
find_new_contacts(sim_state, &tlas)
update_contacts(sim_state) update_contacts(sim_state)
Solver :: enum { Solver :: enum {
@ -1131,14 +1208,32 @@ multiply_inv_intertia :: proc(body: Body_Ptr, vec: Vec3) -> (result: Vec3) {
return result return result
} }
apply_velocity_correction :: proc(body: Body_Ptr, impulse: Vec3, pos: Vec3) { apply_velocity_correction :: #force_inline proc "contextless" (
body.v += impulse * body.inv_mass body: Body_Ptr,
impulse: Vec3,
pos: Vec3,
) {
apply_velocity_correction_linear(body, impulse)
angular_impulse := lg.cross(pos - body.x, impulse)
apply_velocity_correction_angular(body, angular_impulse)
}
apply_velocity_correction_linear :: #force_inline proc "contextless" (
body: Body_Ptr,
impulse: Vec3,
) {
body.v += impulse * body.inv_mass
}
apply_velocity_correction_angular :: #force_inline proc "contextless" (
body: Body_Ptr,
angular_impulse: Vec3,
) {
q := body.q q := body.q
inv_q := lg.quaternion_normalize0(lg.quaternion_inverse(q)) inv_q := lg.quaternion_normalize0(lg.quaternion_inverse(q))
delta_omega := pos - body.x delta_omega := lg.quaternion_mul_vector3(inv_q, angular_impulse)
delta_omega = lg.cross(delta_omega, impulse)
delta_omega = lg.quaternion_mul_vector3(inv_q, delta_omega)
delta_omega *= body.inv_inertia_tensor delta_omega *= body.inv_inertia_tensor
delta_omega = lg.quaternion_mul_vector3(q, delta_omega) delta_omega = lg.quaternion_mul_vector3(q, delta_omega)
@ -1167,7 +1262,20 @@ apply_position_correction :: proc(body: Body_Ptr, corr: Vec3, pos: Vec3) {
body.q = q body.q = q
} }
// Total inverse mass (linear + angular)
get_body_inverse_mass :: proc(body: Body_Ptr, normal, pos: Vec3) -> f32 { get_body_inverse_mass :: proc(body: Body_Ptr, normal, pos: Vec3) -> f32 {
linear, angular := get_body_inverse_mass_separate(body, normal, pos)
return linear + angular
}
get_body_inverse_mass_separate :: proc(
body: Body_Ptr,
normal, pos: Vec3,
) -> (
linear: f32,
angular: f32,
) {
q := body.q q := body.q
inv_q := lg.quaternion_normalize0(lg.quaternion_inverse(q)) inv_q := lg.quaternion_normalize0(lg.quaternion_inverse(q))
@ -1175,10 +1283,10 @@ get_body_inverse_mass :: proc(body: Body_Ptr, normal, pos: Vec3) -> f32 {
rn = lg.cross(rn, normal) rn = lg.cross(rn, normal)
rn = lg.quaternion_mul_vector3(inv_q, rn) rn = lg.quaternion_mul_vector3(inv_q, rn)
w := lg.dot(rn, rn * body.inv_inertia_tensor) linear = body.inv_mass
w += body.inv_mass angular = lg.dot(rn, rn * body.inv_inertia_tensor)
return w return
} }
get_body_angular_inverse_mass :: proc(body: Body_Ptr, normal: Vec3) -> f32 { get_body_angular_inverse_mass :: proc(body: Body_Ptr, normal: Vec3) -> f32 {