More tyre tweaks, chase camera like in GTA 5, basic rigid body interpolation so rendering can run faster than physics and still see everything smoothly

This commit is contained in:
sergeypdev 2025-08-02 14:01:32 +04:00
parent 82e9022c73
commit a6cbfaf88c
6 changed files with 186 additions and 64 deletions

View File

@ -1,12 +1,12 @@
a a
1.1 1.1
-180 -200
1500 1700
2000 2000
10 10
0 0
0 0
-1 -0.5
0 0
0 0
0 0

1 a
2 1.1
3 -180 -200
4 1500 1700
5 2000
6 10
7 0
8 0
9 -1 -0.5
10 0
11 0
12 0

View File

@ -1,9 +1,9 @@
b b
1.4 1.2
-120 -200
1700 1400
0 20
300 1
0 0
0 0
0 0

1 b
2 1.4 1.2
3 -120 -200
4 1700 1400
5 0 20
6 300 1
7 0
8 0
9 0

10
common/emath/math.odin Normal file
View File

@ -0,0 +1,10 @@
// "Engine" math, to avoid aliasing with core:math
package emath
import "core:math"
_ :: math
exp_smooth :: proc "contextless" (target: $T, pos: T, speed: f32, dt: f32) -> T {
return pos + ((target - pos) * (1.0 - math.exp_f32(-speed * dt)))
}

View File

@ -15,6 +15,7 @@
package game package game
import "assets" import "assets"
import "common:emath"
import "common:name" import "common:name"
import "core:fmt" import "core:fmt"
import "core:log" import "core:log"
@ -27,6 +28,8 @@ import "libs:raylib/rlgl"
import "libs:tracy" import "libs:tracy"
import "ui" import "ui"
_ :: emath
PIXEL_WINDOW_HEIGHT :: 360 PIXEL_WINDOW_HEIGHT :: 360
Track :: struct { Track :: struct {
@ -115,6 +118,8 @@ World :: struct {
player_pos: rl.Vector3, player_pos: rl.Vector3,
track: Track, track: Track,
physics_scene: physics.Scene, physics_scene: physics.Scene,
// How much time passed in physics scene, can be 0 when paused
physics_dt: f32,
pause: bool, pause: bool,
car_handle: physics.Body_Handle, car_handle: physics.Body_Handle,
engine_handle: physics.Engine_Handle, engine_handle: physics.Engine_Handle,
@ -149,8 +154,7 @@ Runtime_World :: struct {
camera_yaw_pitch: rl.Vector2, camera_yaw_pitch: rl.Vector2,
camera_speed: f32, camera_speed: f32,
camera: rl.Camera3D, camera: rl.Camera3D,
orbit_camera: Orbit_Camera, game_camera: Game_Camera,
camera_mode: Camera_Mode,
dt: f32, dt: f32,
rewind_simulation: bool, rewind_simulation: bool,
commit_simulation: bool, commit_simulation: bool,
@ -206,11 +210,6 @@ Game_Memory :: struct {
free_cam: bool, free_cam: bool,
} }
Camera_Mode :: enum {
Orbit,
Hood,
}
Track_Edit_State :: enum { Track_Edit_State :: enum {
// Point selection // Point selection
Select, Select,
@ -354,7 +353,7 @@ camera_forward_vec :: proc() -> rl.Vector3 {
game_camera_3d :: proc() -> rl.Camera3D { game_camera_3d :: proc() -> rl.Camera3D {
if g_mem.editor || g_mem.free_cam { if g_mem.editor || g_mem.free_cam {
return free_camera_to_rl(&g_mem.es.camera) return free_camera_to_rl(g_mem.es.camera)
} }
return get_runtime_world().camera return get_runtime_world().camera
@ -557,7 +556,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 + f32(y) * 1.01, 1, f32(x) * 1.01 + -11.5}, 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 = {
{ {
@ -709,7 +708,8 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
DRIVE_IMPULSE :: 3000 DRIVE_IMPULSE :: 3000
BRAKE_IMPULSE :: 10 BRAKE_IMPULSE :: 10
TURN_ANGLE :: -f32(50) * math.RAD_PER_DEG TURN_ANGLE_AT_HIGH_SPEED :: f32(10) * math.RAD_PER_DEG
TURN_ANGLE_AT_LOW_SPEED :: f32(30) * math.RAD_PER_DEG
// 68% front, 32% rear // 68% front, 32% rear
BRAKE_BIAS :: f32(0.68) BRAKE_BIAS :: f32(0.68)
@ -745,7 +745,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
} }
car_body := physics.get_body(sim_state, world.car_handle) car_body := physics.get_body(sim_state, world.car_handle)
turn_vel_correction := clamp(4.0 / linalg.length(car_body.v), 0, 1) turn_vel_correction := math.smoothstep(f32(90), f32(10), linalg.length(car_body.v) * 3.6)
turn_input := rl.GetGamepadAxisMovement(0, .LEFT_X) turn_input := rl.GetGamepadAxisMovement(0, .LEFT_X)
if abs(turn_input) < GAMEPAD_DEADZONE { if abs(turn_input) < GAMEPAD_DEADZONE {
@ -761,7 +761,10 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
for wheel_handle in turn_wheels { for wheel_handle in turn_wheels {
wheel := physics.get_suspension_constraint(sim_state, wheel_handle) wheel := physics.get_suspension_constraint(sim_state, wheel_handle)
wheel.turn_angle = TURN_ANGLE * turn_vel_correction * turn_input wheel.turn_angle =
TURN_ANGLE_AT_HIGH_SPEED +
(TURN_ANGLE_AT_LOW_SPEED - TURN_ANGLE_AT_HIGH_SPEED) * turn_vel_correction
wheel.turn_angle *= -turn_input
} }
immediate_scene(world, &world.main_scene, "assets/blender/test_level_blend/Scene.scn") immediate_scene(world, &world.main_scene, "assets/blender/test_level_blend/Scene.scn")
@ -788,7 +791,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
) )
} }
physics.simulate( world.physics_dt = physics.simulate(
&world.physics_scene, &world.physics_scene,
SOLVER_CONFIG, SOLVER_CONFIG,
dt, dt,
@ -816,7 +819,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
} }
} }
update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) { runtime_world_update :: 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
@ -856,6 +859,24 @@ Orbit_Camera :: struct {
distance: f32, distance: f32,
} }
Car_Follow_Camera :: struct {
// Params
body: physics.Body_Handle,
com_offset: rl.Vector3,
distance: f32,
// State
pos: rl.Vector3,
target: rl.Vector3,
up: rl.Vector3,
}
Game_Camera :: union {
Free_Camera,
Orbit_Camera,
Car_Follow_Camera,
}
GAMEPAD_DEADZONE :: f32(0.07) GAMEPAD_DEADZONE :: f32(0.07)
collect_camera_input :: proc() -> (delta: rl.Vector2, sense: f32) { collect_camera_input :: proc() -> (delta: rl.Vector2, sense: f32) {
@ -929,6 +950,60 @@ orbit_camera_to_rl :: proc(camera: Orbit_Camera) -> rl.Camera3D {
return result return result
} }
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.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})
dir := linalg.normalize0(camera.pos - camera.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)
}
follow_camera_to_rl :: proc(camera: Car_Follow_Camera) -> rl.Camera3D {
return rl.Camera3D {
position = camera.pos,
target = camera.target,
up = rl.Vector3{0, 1, 0},
fovy = 50,
projection = .PERSPECTIVE,
}
}
game_camera_update :: proc(world: ^World, camera: ^Game_Camera, dt: f32) {
switch &c in camera {
case Free_Camera:
free_camera_update(&c)
case Orbit_Camera:
orbit_camera_update(&c)
case Car_Follow_Camera:
follow_camera_update(world, &c, dt)
}
}
game_camera_to_rl :: proc(camera: Game_Camera) -> rl.Camera3D {
result: rl.Camera3D
switch c in camera {
case Free_Camera:
result = free_camera_to_rl(c)
case Orbit_Camera:
result = orbit_camera_to_rl(c)
case Car_Follow_Camera:
result = follow_camera_to_rl(c)
}
return result
}
Matrix3 :: # row_major matrix[3, 3]f32 Matrix3 :: # row_major matrix[3, 3]f32
free_camera_rotation :: proc(camera: Free_Camera) -> linalg.Matrix3f32 { free_camera_rotation :: proc(camera: Free_Camera) -> linalg.Matrix3f32 {
@ -971,8 +1046,8 @@ free_camera_update :: proc(camera: ^Free_Camera) {
camera.pos += (input.x * right + input.y * forward) * camera.speed camera.pos += (input.x * right + input.y * forward) * camera.speed
} }
free_camera_to_rl :: proc(camera: ^Free_Camera) -> (result: rl.Camera3D) { free_camera_to_rl :: proc(camera: Free_Camera) -> (result: rl.Camera3D) {
rotation := free_camera_rotation(camera^) rotation := free_camera_rotation(camera)
forward := -rotation[2] forward := -rotation[2]
result.position = camera.pos result.position = camera.pos
@ -1008,16 +1083,6 @@ update :: proc() {
// g_mem.es.world.player_pos = g_mem.runtime_world.camera.position // g_mem.es.world.player_pos = g_mem.runtime_world.camera.position
} }
if rl.IsKeyPressed(.F2) && !g_mem.free_cam {
cam_mode := &get_runtime_world().camera_mode
switch cam_mode^ {
case .Orbit:
cam_mode^ = .Hood
case .Hood:
cam_mode^ = .Orbit
}
}
dt := rl.GetFrameTime() dt := rl.GetFrameTime()
// Debug BVH traversal // Debug BVH traversal
@ -1056,33 +1121,25 @@ update :: proc() {
if g_mem.editor { if g_mem.editor {
update_editor(get_editor_state(), dt) update_editor(get_editor_state(), dt)
} else { } else {
update_runtime_world(get_runtime_world(), dt) runtime_world_update(get_runtime_world(), dt)
world := runtime_world_current_world(get_runtime_world()) world := runtime_world_current_world(get_runtime_world())
if g_mem.free_cam { if g_mem.free_cam {
free_camera_update(&g_mem.es.camera) free_camera_update(&g_mem.es.camera)
} else { } else {
switch get_runtime_world().camera_mode { game_cam := &get_runtime_world().game_camera
case .Orbit: if _, ok := game_cam.(Car_Follow_Camera); !ok {
orbit_camera_update(&get_runtime_world().orbit_camera) game_cam^ = Car_Follow_Camera{}
get_runtime_world().camera = orbit_camera_to_rl(get_runtime_world().orbit_camera) log.debugf("overwriting cam")
case .Hood:
car := physics.get_body(
physics.get_sim_state(&world.physics_scene),
world.car_handle,
)
cam: rl.Camera3D
cam.position = physics.body_local_to_world(car, physics.Vec3{0, 0.9, 2})
cam.target =
cam.position + physics.body_local_to_world_vec(car, physics.Vec3{0, 0, 1})
cam.up = physics.body_local_to_world_vec(car, physics.Vec3{0, 1, 0})
cam.fovy = 60
cam.projection = .PERSPECTIVE
get_runtime_world().camera = cam
} }
car_follow_cam := &game_cam.(Car_Follow_Camera)
car_follow_cam.body = world.car_handle
if world.physics_dt == 0 {
log.debugf("phys dt == 0")
}
game_camera_update(world, &get_runtime_world().game_camera, dt)
get_runtime_world().camera = game_camera_to_rl(get_runtime_world().game_camera)
} }
} }
} }
@ -1125,7 +1182,11 @@ draw_world :: proc(world: ^World) {
phys_debug_state: physics.Debug_State phys_debug_state: physics.Debug_State
physics.init_debug_state(&phys_debug_state) physics.init_debug_state(&phys_debug_state)
car_body := physics.get_body(sim_state, world.car_handle) car_body := physics.get_interpolated_body(
&world.physics_scene,
SOLVER_CONFIG,
world.car_handle,
)
engine := physics.get_engine(sim_state, world.engine_handle) engine := physics.get_engine(sim_state, world.engine_handle)
{ {
@ -1187,8 +1248,9 @@ draw_world :: proc(world: ^World) {
car_matrix := rl.QuaternionToMatrix(car_body.q) car_matrix := rl.QuaternionToMatrix(car_body.q)
car_matrix = // TODO: figure out how to use helper funcs that take Body_Ptr for interpolated body which is just Body
(auto_cast linalg.matrix4_translate_f32(physics.body_get_shape_pos(car_body))) * car_matrix car_pos := linalg.quaternion_mul_vector3(car_body.q, car_body.shape_offset) + car_body.x
car_matrix = (auto_cast linalg.matrix4_translate_f32(car_pos)) * car_matrix
// basic_shader := assets.get_shader( // basic_shader := assets.get_shader(
// &g_mem.assetman, // &g_mem.assetman,
@ -1532,7 +1594,10 @@ game_hot_reloaded :: proc(mem: rawptr) {
render.init(&g_mem.assetman) render.init(&g_mem.assetman)
ui.rl_init() ui.rl_init()
g_mem.runtime_world.orbit_camera.distance = 6 orbit, ok := &g_mem.runtime_world.game_camera.(Orbit_Camera)
if ok {
orbit.distance = 6
}
log.debugf("hot reloaded") log.debugf("hot reloaded")
} }

View File

@ -3,11 +3,14 @@ package physics
import "bvh" import "bvh"
import "collision" import "collision"
import "common:name" import "common:name"
import "core:log"
import lg "core:math/linalg" import lg "core:math/linalg"
import "game:assets" import "game:assets"
import "game:container/spanpool" import "game:container/spanpool"
import "libs:tracy" import "libs:tracy"
_ :: log
MAX_CONTACTS :: 1024 * 16 MAX_CONTACTS :: 1024 * 16
Vec3 :: [3]f32 Vec3 :: [3]f32
@ -122,7 +125,7 @@ Sim_State :: struct {
} }
DEV_BUILD :: #config(DEV, false) DEV_BUILD :: #config(DEV, false)
NUM_SIM_STATES :: 2 when DEV_BUILD else 1 NUM_SIM_STATES :: 2
Scene :: struct { Scene :: struct {
assetman: ^assets.Asset_Manager, assetman: ^assets.Asset_Manager,
@ -537,6 +540,40 @@ get_body :: proc(sim_state: ^Sim_State, handle: Body_Handle) -> Body_Ptr {
return &sim_state.bodies_slice[index] return &sim_state.bodies_slice[index]
} }
get_interpolated_body :: proc(
scene: ^Scene,
solver_config: Solver_Config,
handle: Body_Handle,
) -> Body {
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 = Body {
q = lg.QUATERNIONF32_IDENTITY,
}
if prev_body.alive && body.alive {
// interpolate
t := scene.solver_state.accumulated_time / solver_config.timestep
log.debugf("t = {}", t)
result = prev_body^
result.x = body.x * t + (1.0 - t) * prev_body.x
result.q = lg.quaternion_slerp_f32(prev_body.q, body.q, t)
result.v = lg.lerp(prev_body.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 prev_body.alive {
result = prev_body^
} else if body.alive {
result = body^
}
return result
}
remove_shape :: proc(sim_state: ^Sim_State, idx: i32) { remove_shape :: proc(sim_state: ^Sim_State, idx: i32) {
cur_idx := idx cur_idx := idx
for { for {

View File

@ -696,6 +696,8 @@ simulate :: proc(
dt: f32, dt: f32,
commit := true, // commit = false is a special mode for debugging physics stepping to allow rerunning the same step each frame commit := true, // commit = false is a special mode for debugging physics stepping to allow rerunning the same step each frame
step_mode := Step_Mode.Accumulated_Time, step_mode := Step_Mode.Accumulated_Time,
) -> (
accumulated_dt: f32,
) { ) {
tracy.Zone() tracy.Zone()
assert(config.timestep > 0) assert(config.timestep > 0)
@ -704,31 +706,37 @@ simulate :: proc(
prune_immediate(scene) prune_immediate(scene)
copy_sim_state(get_next_sim_state(scene), get_sim_state(scene)) did_copy := false
sim_state := get_next_sim_state(scene) sim_state := get_next_sim_state(scene)
// runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD()
sim_cache: Sim_Cache sim_cache: Sim_Cache
sim_cache.level_geom_asset_bvh = make_map( sim_cache.level_geom_asset_bvh = make_map(
map[Level_Geom_Handle]assets.Loaded_BVH, map[Level_Geom_Handle]assets.Loaded_BVH,
context.temp_allocator, context.temp_allocator,
) )
simulated_dt := f32(0)
num_steps := 0 num_steps := 0
switch step_mode { switch step_mode {
case .Accumulated_Time: case .Accumulated_Time:
state.accumulated_time += dt state.accumulated_time += dt
for state.accumulated_time >= config.timestep { for state.accumulated_time >= config.timestep {
if !did_copy {
did_copy = true
copy_sim_state(get_next_sim_state(scene), get_sim_state(scene))
}
num_steps += 1 num_steps += 1
state.accumulated_time -= config.timestep state.accumulated_time -= config.timestep
simulated_dt += config.timestep
if num_steps < MAX_STEPS { if num_steps < MAX_STEPS {
simulate_step(scene, sim_state, &sim_cache, config) simulate_step(scene, sim_state, &sim_cache, config)
} }
} }
case .Single: case .Single:
copy_sim_state(get_next_sim_state(scene), get_sim_state(scene))
simulate_step(scene, get_next_sim_state(scene), &sim_cache, config) simulate_step(scene, get_next_sim_state(scene), &sim_cache, config)
num_steps += 1 num_steps += 1
} }
@ -742,6 +750,8 @@ simulate :: proc(
state.immediate_suspension_constraints.num_items = 0 state.immediate_suspension_constraints.num_items = 0
state.immediate_engines.num_items = 0 state.immediate_engines.num_items = 0
state.immediate_level_geoms.num_items = 0 state.immediate_level_geoms.num_items = 0
return simulated_dt
} }
GLOBAL_PLANE :: collision.Plane { GLOBAL_PLANE :: collision.Plane {
@ -1332,7 +1342,7 @@ pgs_solve_suspension :: proc(
) * ) *
math.sign(lateral_to_longitudinal_relation) math.sign(lateral_to_longitudinal_relation)
v.turn_assist = drift_amount * math.PI * 0.1 v.turn_assist = drift_amount * math.PI * 0.15
} }