From a6cbfaf88c43c8812d2c90f4c1658c4dc5ab5580 Mon Sep 17 00:00:00 2001 From: sergeypdev Date: Sat, 2 Aug 2025 14:01:32 +0400 Subject: [PATCH] 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 --- assets/tyre_lateral.csv | 6 +- assets/tyre_longitudinal.csv | 10 +-- common/emath/math.odin | 10 +++ game/game.odin | 167 ++++++++++++++++++++++++----------- game/physics/scene.odin | 39 +++++++- game/physics/simulation.odin | 18 +++- 6 files changed, 186 insertions(+), 64 deletions(-) create mode 100644 common/emath/math.odin diff --git a/assets/tyre_lateral.csv b/assets/tyre_lateral.csv index edf51bf..10f376e 100644 --- a/assets/tyre_lateral.csv +++ b/assets/tyre_lateral.csv @@ -1,12 +1,12 @@ a 1.1 --180 -1500 +-200 +1700 2000 10 0 0 --1 +-0.5 0 0 0 diff --git a/assets/tyre_longitudinal.csv b/assets/tyre_longitudinal.csv index 691d00a..6e0999a 100644 --- a/assets/tyre_longitudinal.csv +++ b/assets/tyre_longitudinal.csv @@ -1,9 +1,9 @@ b -1.4 --120 -1700 -0 -300 +1.2 +-200 +1400 +20 +1 0 0 0 diff --git a/common/emath/math.odin b/common/emath/math.odin new file mode 100644 index 0000000..b93059f --- /dev/null +++ b/common/emath/math.odin @@ -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))) +} diff --git a/game/game.odin b/game/game.odin index 7a17872..793423b 100644 --- a/game/game.odin +++ b/game/game.odin @@ -15,6 +15,7 @@ package game import "assets" +import "common:emath" import "common:name" import "core:fmt" import "core:log" @@ -27,6 +28,8 @@ import "libs:raylib/rlgl" import "libs:tracy" import "ui" +_ :: emath + PIXEL_WINDOW_HEIGHT :: 360 Track :: struct { @@ -115,6 +118,8 @@ World :: struct { player_pos: rl.Vector3, track: Track, physics_scene: physics.Scene, + // How much time passed in physics scene, can be 0 when paused + physics_dt: f32, pause: bool, car_handle: physics.Body_Handle, engine_handle: physics.Engine_Handle, @@ -149,8 +154,7 @@ Runtime_World :: struct { camera_yaw_pitch: rl.Vector2, camera_speed: f32, camera: rl.Camera3D, - orbit_camera: Orbit_Camera, - camera_mode: Camera_Mode, + game_camera: Game_Camera, dt: f32, rewind_simulation: bool, commit_simulation: bool, @@ -206,11 +210,6 @@ Game_Memory :: struct { free_cam: bool, } -Camera_Mode :: enum { - Orbit, - Hood, -} - Track_Edit_State :: enum { // Point selection Select, @@ -354,7 +353,7 @@ camera_forward_vec :: proc() -> rl.Vector3 { game_camera_3d :: proc() -> rl.Camera3D { 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 @@ -557,7 +556,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) { u32(box_name), physics.Body_Config { 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, shapes = { { @@ -709,7 +708,8 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) { DRIVE_IMPULSE :: 3000 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 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) - 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) 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 { 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") @@ -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, SOLVER_CONFIG, 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) runtime_world.dt = dt @@ -856,6 +859,24 @@ Orbit_Camera :: struct { 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) 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 } +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 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 } -free_camera_to_rl :: proc(camera: ^Free_Camera) -> (result: rl.Camera3D) { - rotation := free_camera_rotation(camera^) +free_camera_to_rl :: proc(camera: Free_Camera) -> (result: rl.Camera3D) { + rotation := free_camera_rotation(camera) forward := -rotation[2] result.position = camera.pos @@ -1008,16 +1083,6 @@ update :: proc() { // 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() // Debug BVH traversal @@ -1056,33 +1121,25 @@ update :: proc() { if g_mem.editor { update_editor(get_editor_state(), dt) } else { - update_runtime_world(get_runtime_world(), dt) + runtime_world_update(get_runtime_world(), dt) world := runtime_world_current_world(get_runtime_world()) if g_mem.free_cam { free_camera_update(&g_mem.es.camera) } else { - switch get_runtime_world().camera_mode { - case .Orbit: - orbit_camera_update(&get_runtime_world().orbit_camera) - get_runtime_world().camera = orbit_camera_to_rl(get_runtime_world().orbit_camera) - 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 + game_cam := &get_runtime_world().game_camera + if _, ok := game_cam.(Car_Follow_Camera); !ok { + game_cam^ = Car_Follow_Camera{} + log.debugf("overwriting 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 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) { @@ -1187,8 +1248,9 @@ draw_world :: proc(world: ^World) { car_matrix := rl.QuaternionToMatrix(car_body.q) - car_matrix = - (auto_cast linalg.matrix4_translate_f32(physics.body_get_shape_pos(car_body))) * car_matrix + // TODO: figure out how to use helper funcs that take Body_Ptr for interpolated body which is just Body + 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( // &g_mem.assetman, @@ -1532,7 +1594,10 @@ game_hot_reloaded :: proc(mem: rawptr) { render.init(&g_mem.assetman) 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") } diff --git a/game/physics/scene.odin b/game/physics/scene.odin index 8b06e99..09dc777 100644 --- a/game/physics/scene.odin +++ b/game/physics/scene.odin @@ -3,11 +3,14 @@ package physics import "bvh" import "collision" import "common:name" +import "core:log" import lg "core:math/linalg" import "game:assets" import "game:container/spanpool" import "libs:tracy" +_ :: log + MAX_CONTACTS :: 1024 * 16 Vec3 :: [3]f32 @@ -122,7 +125,7 @@ Sim_State :: struct { } DEV_BUILD :: #config(DEV, false) -NUM_SIM_STATES :: 2 when DEV_BUILD else 1 +NUM_SIM_STATES :: 2 Scene :: struct { 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] } +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) { cur_idx := idx for { diff --git a/game/physics/simulation.odin b/game/physics/simulation.odin index 2cf8c86..1f2ef4e 100644 --- a/game/physics/simulation.odin +++ b/game/physics/simulation.odin @@ -696,6 +696,8 @@ simulate :: proc( dt: f32, 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, +) -> ( + accumulated_dt: f32, ) { tracy.Zone() assert(config.timestep > 0) @@ -704,31 +706,37 @@ simulate :: proc( 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) - // runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() - sim_cache: Sim_Cache sim_cache.level_geom_asset_bvh = make_map( map[Level_Geom_Handle]assets.Loaded_BVH, context.temp_allocator, ) + simulated_dt := f32(0) + num_steps := 0 switch step_mode { case .Accumulated_Time: state.accumulated_time += dt 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 state.accumulated_time -= config.timestep + simulated_dt += config.timestep if num_steps < MAX_STEPS { simulate_step(scene, sim_state, &sim_cache, config) } } 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) num_steps += 1 } @@ -742,6 +750,8 @@ simulate :: proc( state.immediate_suspension_constraints.num_items = 0 state.immediate_engines.num_items = 0 state.immediate_level_geoms.num_items = 0 + + return simulated_dt } GLOBAL_PLANE :: collision.Plane { @@ -1332,7 +1342,7 @@ pgs_solve_suspension :: proc( ) * math.sign(lateral_to_longitudinal_relation) - v.turn_assist = drift_amount * math.PI * 0.1 + v.turn_assist = drift_amount * math.PI * 0.15 }