gutter_runner/game/physics/simulation.odin

204 lines
4.8 KiB
Odin

package physics
import "collision"
import "core:math"
import lg "core:math/linalg"
import rl "vendor:raylib"
_ :: math
Solver_Config :: struct {
// Will automatically do fixed timestep
timestep: f32,
gravity: rl.Vector3,
}
Solver_State :: struct {
accumulated_time: f32,
// Incremented when simulate is called (not simulate_step)
simulation_frame: u32,
// Number of immediate bodies referenced this frame
num_referenced_bodies: i32,
num_referenced_suspension_constraints: i32,
immedate_bodies: map[u32]Immedate_State(Body_Handle),
immediate_suspension_constraints: map[u32]Immedate_State(Suspension_Constraint_Handle),
}
Immedate_State :: struct($T: typeid) {
handle: T,
// When was this referenced last time (frame number)
last_ref: u32,
}
destroy_solver_state :: proc(state: ^Solver_State) {
delete(state.immedate_bodies)
}
// Outer simulation loop for fixed timestepping
simulate :: proc(scene: ^Scene, state: ^Solver_State, config: Solver_Config, dt: f32) {
assert(config.timestep > 0)
prune_immediate(scene, state)
state.accumulated_time += dt
num_steps := 0
for state.accumulated_time >= config.timestep {
num_steps += 1
state.accumulated_time -= config.timestep
simulate_step(scene, config)
}
state.simulation_frame += 1
state.num_referenced_bodies = 0
state.num_referenced_suspension_constraints = 0
}
Body_Sim_State :: struct {
prev_x: rl.Vector3,
prev_q: rl.Quaternion,
}
simulate_step :: proc(scene: ^Scene, config: Solver_Config) {
body_states := make_soa(#soa[]Body_Sim_State, len(scene.bodies), context.temp_allocator)
dt := config.timestep
inv_dt := 1.0 / dt
// Integrate positions and rotations
for &body, i in scene.bodies {
if body.alive {
body_states[i].prev_x = body.x
body.v += dt * config.gravity
body.x += dt * body.v
body_states[i].prev_q = body.q
// TODO: Probably can do it using built in quaternion math but I have no idea how it works
// NOTE: figure out how this works https://fgiesen.wordpress.com/2012/08/24/quaternion-differentiation/
q := body.q
delta_rot := quaternion(x = body.w.x, y = body.w.y, z = body.w.z, w = 0)
delta_rot = delta_rot * q
q.x += 0.5 * dt * delta_rot.x
q.y += 0.5 * dt * delta_rot.y
q.z += 0.5 * dt * delta_rot.z
q.w += 0.5 * dt * delta_rot.w
q = lg.normalize0(q)
body.q = q
}
}
for &v in scene.suspension_constraints {
if v.alive {
body := get_body(scene, v.body)
q := body.q
pos := body_local_to_world(body, v.rel_pos)
dir := lg.quaternion_mul_vector3(q, v.rel_dir)
pos2 := pos + dir * v.rest
v.hit_t, v.hit_point, v.hit = collision.intersect_segment_plane(
{pos, pos2},
collision.plane_from_point_normal({}, collision.Vec3{0, 1, 0}),
)
if v.hit {
corr := v.hit_point - pos
distance := lg.length(corr)
corr = corr / distance if distance > 0 else 0
apply_constraint_correction_unilateral(
dt,
body,
v.compliance,
error = distance - v.rest,
error_gradient = corr,
pos = pos,
other_combined_inv_mass = 0,
)
}
}
}
// Compute new linear and angular velocities
for &body, i in scene.bodies {
if body.alive {
body.v = (body.x - body_states[i].prev_x) * inv_dt
delta_q := body.q * lg.quaternion_inverse(body_states[i].prev_q)
body.w = rl.Vector3{delta_q.x, delta_q.y, delta_q.z} * 2.0 * inv_dt
if delta_q.w < 0 {
body.w = -body.w
}
}
}
}
apply_constraint_correction_unilateral :: proc(
dt: f32,
body: Body_Ptr,
compliance: f32,
error: f32,
error_gradient: rl.Vector3,
pos: rl.Vector3,
other_combined_inv_mass: f32 = 0,
) {
if error == 0 {
return
}
w := get_body_inverse_mass(body, error_gradient, pos)
w += other_combined_inv_mass
if w == 0 {
return
}
alpha := compliance / dt / dt
lambda := -error / (w + alpha)
delta_pos := error_gradient * -lambda
apply_correction(body, delta_pos, pos)
}
apply_correction :: proc(body: Body_Ptr, corr: rl.Vector3, pos: rl.Vector3) {
body.x += corr * body.inv_mass
q := body.q
inv_q := lg.quaternion_inverse(q)
delta_omega := pos - body.x
delta_omega = lg.cross(delta_omega, corr)
delta_omega = lg.quaternion_mul_vector3(inv_q, delta_omega)
delta_omega *= body.inv_inertia_tensor
delta_omega = lg.quaternion_mul_vector3(q, delta_omega)
delta_rot := quaternion(x = delta_omega.x, y = delta_omega.y, z = delta_omega.z, w = 0)
delta_rot *= q
q.x += 0.5 * delta_rot.x
q.y += 0.5 * delta_rot.y
q.z += 0.5 * delta_rot.z
q.w += 0.5 * delta_rot.w
q = lg.normalize0(q)
body.q = q
}
get_body_inverse_mass :: proc(body: Body_Ptr, normal, pos: rl.Vector3) -> f32 {
q := body.q
inv_q := lg.quaternion_inverse(q)
rn := pos - body.x
rn = lg.cross(rn, normal)
rn = lg.quaternion_mul_vector3(inv_q, rn)
rn *= rn
w := lg.dot(rn, body.inv_inertia_tensor)
w += body.inv_mass
return w
}