Change how interpolation works, add mouse movement override to follow camera

This commit is contained in:
sergeypdev 2025-08-02 20:41:22 +04:00
parent 7f928a6f2c
commit 5181b9b813
3 changed files with 153 additions and 54 deletions

View File

@ -597,8 +597,8 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
wheel_extent_x_back := f32(2) / 2
wheel_y := f32(0.5)
rest := f32(0.7)
natural_frequency := f32(1)
damping := f32(0.2)
natural_frequency := f32(1.2)
damping := f32(0.3)
radius := f32(0.737649) / 2
wheel_front_z := f32(1.6)
wheel_back_z := f32(-1.63)
@ -864,9 +864,15 @@ Car_Follow_Camera :: struct {
body: physics.Body_Handle,
com_offset: rl.Vector3,
distance: f32,
yaw, pitch: f32,
// if > 0 manual rotation is enabled, ticked down by dt
manual_rotation_time: f32,
has_yaw_pitch: bool,
// State
pos: rl.Vector3,
follow_target: rl.Vector3,
target: rl.Vector3,
up: rl.Vector3,
}
@ -879,7 +885,7 @@ Game_Camera :: union {
GAMEPAD_DEADZONE :: f32(0.07)
collect_camera_input :: proc() -> (delta: rl.Vector2, sense: f32) {
collect_camera_input :: proc() -> (delta: rl.Vector2, sense: f32, is_mouse: bool) {
gamepad_delta := rl.Vector2 {
rl.GetGamepadAxisMovement(0, .RIGHT_X),
rl.GetGamepadAxisMovement(0, .RIGHT_Y),
@ -903,15 +909,25 @@ collect_camera_input :: proc() -> (delta: rl.Vector2, sense: f32) {
mouse_delta := g_mem.mouse_captured ? rl.GetMouseDelta() : 0
@(static) first_delta := true
if mouse_delta != 0 && first_delta {
first_delta = false
mouse_delta = 0
} else if mouse_delta == 0 {
first_delta = true
}
MOUSE_SENSE :: 0.005
GAMEPAD_SENSE :: 1
if linalg.length2(mouse_delta) > linalg.length2(gamepad_delta) {
sense = MOUSE_SENSE
delta = mouse_delta
is_mouse = true
} else {
sense = GAMEPAD_SENSE * rl.GetFrameTime()
delta = gamepad_delta
is_mouse = false
}
return
@ -922,7 +938,7 @@ orbit_camera_update :: proc(camera: ^Orbit_Camera) {
camera.target =
physics.get_body(physics.get_sim_state(&world.physics_scene), world.car_handle).x
delta, sense := collect_camera_input()
delta, sense, _ := collect_camera_input()
camera.yaw += delta.x * sense
camera.pitch += delta.y * sense
@ -950,23 +966,61 @@ orbit_camera_to_rl :: proc(camera: Orbit_Camera) -> rl.Camera3D {
return result
}
dir_to_yaw_pitch :: proc "contextless" (dir: rl.Vector3) -> (yaw: f32, pitch: f32) {
pitch = math.asin(dir.y)
yaw = -math.atan2(dir.x, dir.z) + math.PI
return
}
yaw_pitch_to_rotation :: proc "contextless" (yaw, pitch: f32) -> linalg.Matrix3f32 {
return(
linalg.matrix3_rotate(-yaw, rl.Vector3{0, 1, 0}) *
linalg.matrix3_rotate(-pitch, rl.Vector3{1, 0, 0}) \
)
}
follow_camera_update :: proc(world: ^World, camera: ^Car_Follow_Camera, dt: f32) {
body := physics.get_interpolated_body(&world.physics_scene, SOLVER_CONFIG, camera.body)
camera.follow_target = body.x + physics.Vec3{0, 4, 0}
camera.target = body.x + physics.Vec3{0, 2.5, 0}
// forward := physics.body_local_to_world_vec(body, physics.Vec3{0, 0, 1})
// up := physics.body_local_to_world_vec(body, physics.Vec3{0, 1, 0})
delta, sense, _ := collect_camera_input()
dir := linalg.normalize0(camera.pos - camera.target)
distance := f32(7)
if delta != 0 {
camera.manual_rotation_time = 2 // 3 seconds we keep rotation
}
if camera.manual_rotation_time > 0 {
camera.manual_rotation_time -= dt
if !camera.has_yaw_pitch {
camera.has_yaw_pitch = true
camera.yaw, camera.pitch = dir_to_yaw_pitch(
linalg.normalize0(camera.follow_target - camera.pos),
)
}
camera.yaw += delta.x * sense
camera.pitch += delta.y * sense
camera.pitch = math.clamp(camera.pitch, -math.PI / 2.0 + 0.0001, math.PI / 2.0 - 0.0001)
camera.pos =
camera.follow_target +
yaw_pitch_to_rotation(camera.yaw, camera.pitch) * rl.Vector3{0, 0, 1} * distance
} else {
dir := linalg.normalize0(camera.pos - camera.follow_target)
if dir == 0 {
dir = {0, 0, -1}
}
distance := f32(8)
pos_target := camera.target + dir * distance
log.debugf("pos_target: {}", pos_target)
camera.pos = emath.exp_smooth(pos_target, camera.pos, 40, dt)
camera.has_yaw_pitch = false
pos_target := camera.follow_target + dir * distance
camera.pos = pos_target
}
}
follow_camera_to_rl :: proc(camera: Car_Follow_Camera) -> rl.Camera3D {
@ -974,7 +1028,7 @@ follow_camera_to_rl :: proc(camera: Car_Follow_Camera) -> rl.Camera3D {
position = camera.pos,
target = camera.target,
up = rl.Vector3{0, 1, 0},
fovy = 50,
fovy = 60,
projection = .PERSPECTIVE,
}
}
@ -1014,7 +1068,7 @@ free_camera_rotation :: proc(camera: Free_Camera) -> linalg.Matrix3f32 {
}
free_camera_update :: proc(camera: ^Free_Camera) {
delta, sense := collect_camera_input()
delta, sense, _ := collect_camera_input()
camera.yaw -= delta.x * sense
camera.pitch -= delta.y * sense

View File

@ -155,7 +155,7 @@ sim_state_interp_cache_destroy :: proc(cache: ^Sim_State_Interp_Cache) {
}
DEV_BUILD :: #config(DEV, false)
NUM_SIM_STATES :: 2
NUM_SIM_STATES :: 2 when DEV_BUILD else 1
Scene :: struct {
assetman: ^assets.Asset_Manager,
@ -352,6 +352,17 @@ Suspension_Constraint :: struct {
brake_impulse: f32,
applied_impulse: Vec3,
// Prev state for interp
prev_q: f32,
prev_w: f32,
prev_hit: bool,
prev_hit_normal: Vec3,
prev_hit_body: Body_Handle,
prev_hit_point: Vec3,
prev_hit_t: f32,
prev_turn_angle: f32,
prev_turn_assist: f32,
// Convenience for debug visualization to avoid recomputing
slip_angle: f32,
slip_ratio: f32,
@ -591,6 +602,16 @@ allocate_interpolated_body :: proc(
return idx, false
}
calc_interpolate_alpha :: proc "contextless" (
scene: ^Scene,
solver_config: Solver_Config,
) -> (
t: f32,
) {
t = scene.solver_state.accumulated_time / solver_config.timestep
return
}
get_interpolated_body :: proc(
scene: ^Scene,
solver_config: Solver_Config,
@ -604,27 +625,18 @@ get_interpolated_body :: proc(
return result
}
prev_sim_state := get_prev_sim_state(scene)
sim_state := get_sim_state(scene)
prev_body := get_body(prev_sim_state, handle)
body := get_body(sim_state, handle)
result^ = Body {
q = lg.QUATERNIONF32_IDENTITY,
}
if prev_body.alive && body.alive {
if body.alive {
// interpolate
t := scene.solver_state.accumulated_time / solver_config.timestep
t := calc_interpolate_alpha(scene, solver_config)
result^ = body^
result.x = lg.lerp(prev_body.x, body.x, t)
result.q = lg.quaternion_slerp_f32(prev_body.q, body.q, t)
result.v = lg.lerp(prev_body.v, body.v, t)
result.x = lg.lerp(body.prev_x, body.x, t)
result.q = lg.quaternion_slerp_f32(body.prev_q, body.q, t)
result.v = lg.lerp(body.prev_v, body.v, t)
// I don't think that's right, but not going to be used anyway probably
result.w = lg.lerp(prev_body.w, body.w, t)
} else if body.alive {
result^ = body^
result.w = lg.lerp(body.prev_w, body.w, t)
}
return result
@ -665,27 +677,21 @@ get_interpolated_wheel :: proc(
return result
}
prev_sim_state := get_prev_sim_state(scene)
sim_state := get_sim_state(scene)
prev_wheel := get_suspension_constraint(prev_sim_state, handle)
wheel := get_suspension_constraint(sim_state, handle)
if prev_wheel.alive && wheel.alive {
// interpolate
t := scene.solver_state.accumulated_time / solver_config.timestep
result^ = prev_wheel^
result.hit_t = lg.lerp(prev_wheel.hit_t, wheel.hit_t, t)
result.hit_point = lg.lerp(prev_wheel.hit_point, wheel.hit_point, t)
result.hit_normal = lg.normalize0(lg.lerp(prev_wheel.hit_normal, wheel.hit_normal, t))
result.turn_angle = lg.lerp(prev_wheel.turn_angle, wheel.turn_angle, t)
result.turn_assist = lg.lerp(prev_wheel.turn_assist, wheel.turn_assist, t)
result.q = lg.lerp(prev_wheel.q, wheel.q, t)
result.w = lg.lerp(prev_wheel.w, wheel.w, t)
} else if prev_wheel.alive {
result^ = prev_wheel^
} else if wheel.alive {
result^ = wheel^
if wheel.alive {
// interpolate
t := calc_interpolate_alpha(scene, solver_config)
result.hit_t = lg.lerp(wheel.prev_hit_t, wheel.hit_t, t)
result.hit_point = lg.lerp(wheel.prev_hit_point, wheel.hit_point, t)
result.hit_normal = lg.normalize0(lg.lerp(wheel.prev_hit_normal, wheel.hit_normal, t))
result.turn_angle = lg.lerp(wheel.prev_turn_angle, wheel.turn_angle, t)
result.turn_assist = lg.lerp(wheel.prev_turn_assist, wheel.turn_assist, t)
result.q = lg.lerp(wheel.prev_q, wheel.q, t)
result.w = lg.lerp(wheel.prev_w, wheel.w, t)
}
return result
@ -883,6 +889,11 @@ initialize_body_from_config :: proc(sim_state: ^Sim_State, body: ^Body, config:
body.v = config.initial_vel
body.w = config.initial_ang_vel
body.prev_x = body.x
body.prev_q = body.q
body.prev_v = body.v
body.prev_w = body.w
body.shape = add_shape(sim_state, config.shapes)
body.shape_offset = -combined_center_of_mass(config.shapes) + config.com_shift
body.shape_aabb = input_shape_get_aabb(config.shapes)

View File

@ -1344,7 +1344,11 @@ pgs_solve_suspension :: proc(
) *
math.sign(lateral_to_longitudinal_relation)
v.turn_assist = drift_amount * math.PI * 0.15
v.turn_assist =
math.smoothstep(f32(0), f32(1), lg.dot(body_forward_vec, body.v)) *
drift_amount *
math.PI *
0.15
}
@ -1781,6 +1785,36 @@ simulate_step :: proc(
) {
tracy.Zone()
// Save prev state
{
for i in 0 ..< len(sim_state.bodies) {
body := &sim_state.bodies_slice[i]
if body.alive {
body.prev_x = body.x
body.prev_v = body.v
body.prev_q = body.q
body.prev_w = body.w
}
}
for i in 0 ..< len(sim_state.suspension_constraints) {
wheel := &sim_state.suspension_constraints_slice[i]
if wheel.alive {
wheel.prev_q = wheel.q
wheel.prev_w = wheel.w
wheel.prev_hit = wheel.hit
wheel.prev_hit_normal = wheel.hit_normal
wheel.prev_hit_body = wheel.hit_body
wheel.prev_hit_point = wheel.hit_point
wheel.prev_hit_t = wheel.hit_t
wheel.prev_turn_angle = wheel.turn_angle
wheel.prev_turn_assist = wheel.turn_assist
}
}
}
substeps := config.substreps_minus_one + 1
dt_64 := f64(config.timestep) / f64(substeps)