A bunch of tweaks

- Add max steering lock and limit drift assist to never exceed it
- Bring back tyre curve debug
- Show relevant wheel debug values
- Calculate camber for pacejka
This commit is contained in:
sergeypdev 2025-08-04 00:35:46 +04:00
parent 6e600e9e6d
commit 0226e83010
8 changed files with 179 additions and 152 deletions

View File

@ -1,13 +1,13 @@
a a
1.1 1.5
-200 -20
1700 1700
2000 2000
10 0.5
0 0
0 0
-0.5 0
0 -1
0 0
0 0
0 0

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

View File

@ -1,13 +1,13 @@
b b
1.2 1.5
-200 -200
1400 1400
20 -10
1 300
0 0
0 0
0 0
-2 0
0 0
0 0
0 0

1 b
2 1.2 1.5
3 -200
4 1400
5 20 -10
6 1 300
7 0
8 0
9 0
10 -2 0
11 0
12 0
13 0

View File

@ -8,3 +8,7 @@ _ :: math
exp_smooth :: proc "contextless" (target: $T, pos: T, speed: f32, dt: f32) -> T { exp_smooth :: proc "contextless" (target: $T, pos: T, speed: f32, dt: f32) -> T {
return pos + ((target - pos) * (1.0 - math.exp_f32(-speed * dt))) return pos + ((target - pos) * (1.0 - math.exp_f32(-speed * dt)))
} }
exp_smooth_angle :: proc "contextless" (target: $T, pos: T, speed: f32, dt: f32) -> T {
return pos + (math.angle_diff(pos, target) * (1.0 - math.exp_f32(-speed * dt)))
}

View File

@ -543,7 +543,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
}, },
}, },
mass = 1000, mass = 1000,
com_shift = physics.Vec3{0, 0.8, -0.5}, com_shift = physics.Vec3{0, 1, -0.5},
}, },
) )
@ -611,11 +611,16 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
assets.get_curve_1d(&g_mem.assetman, "assets/tyre_lateral.csv"), assets.get_curve_1d(&g_mem.assetman, "assets/tyre_lateral.csv"),
) )
TURN_ANGLE_AT_HIGH_SPEED :: f32(10) * math.RAD_PER_DEG
TURN_ANGLE_AT_LOW_SPEED :: f32(30) * math.RAD_PER_DEG
wheel_fl := physics.immediate_suspension_constraint( wheel_fl := physics.immediate_suspension_constraint(
&world.physics_scene, &world.physics_scene,
#hash("FL", "fnv32a"), #hash("FL", "fnv32a"),
{ {
name = name.from_string("FL"),
turn_wheel = true, turn_wheel = true,
steer_lock = TURN_ANGLE_AT_LOW_SPEED,
rel_pos = {-wheel_extent_x_front, wheel_y, wheel_front_z}, rel_pos = {-wheel_extent_x_front, wheel_y, wheel_front_z},
rel_dir = {0, -1, 0}, rel_dir = {0, -1, 0},
radius = radius, radius = radius,
@ -632,7 +637,9 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
&world.physics_scene, &world.physics_scene,
#hash("FR", "fnv32a"), #hash("FR", "fnv32a"),
{ {
name = name.from_string("FR"),
turn_wheel = true, turn_wheel = true,
steer_lock = TURN_ANGLE_AT_LOW_SPEED,
rel_pos = {wheel_extent_x_front, wheel_y, wheel_front_z}, rel_pos = {wheel_extent_x_front, wheel_y, wheel_front_z},
rel_dir = {0, -1, 0}, rel_dir = {0, -1, 0},
radius = radius, radius = radius,
@ -649,6 +656,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
&world.physics_scene, &world.physics_scene,
#hash("RL", "fnv32a"), #hash("RL", "fnv32a"),
{ {
name = name.from_string("RL"),
rel_pos = {-wheel_extent_x_back, wheel_y, wheel_back_z}, rel_pos = {-wheel_extent_x_back, wheel_y, wheel_back_z},
rel_dir = {0, -1, 0}, rel_dir = {0, -1, 0},
radius = radius, radius = radius,
@ -665,6 +673,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
&world.physics_scene, &world.physics_scene,
#hash("RR", "fnv32a"), #hash("RR", "fnv32a"),
{ {
name = name.from_string("RR"),
rel_pos = {wheel_extent_x_back, wheel_y, wheel_back_z}, rel_pos = {wheel_extent_x_back, wheel_y, wheel_back_z},
rel_dir = {0, -1, 0}, rel_dir = {0, -1, 0},
radius = radius, radius = radius,
@ -706,10 +715,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
front_wheels := turn_wheels front_wheels := turn_wheels
back_wheels := drive_wheels back_wheels := drive_wheels
DRIVE_IMPULSE :: 3000
BRAKE_IMPULSE :: 10 BRAKE_IMPULSE :: 10
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 +751,10 @@ 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 := math.smoothstep(f32(90), f32(10), linalg.length(car_body.v) * 3.6) car_vel_kmh := linalg.length(car_body.v) * 3.6
LOW_SPEED :: f32(10)
HIGH_SPEED :: f32(90)
turn_vel_correction := 1.0 - math.saturate((car_vel_kmh - 10) / (HIGH_SPEED - LOW_SPEED))
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 {
@ -980,9 +989,14 @@ yaw_pitch_to_rotation :: proc "contextless" (yaw, pitch: f32) -> linalg.Matrix3f
follow_camera_update :: proc(world: ^World, camera: ^Car_Follow_Camera, dt: f32) { follow_camera_update :: proc(world: ^World, camera: ^Car_Follow_Camera, dt: f32) {
body := physics.get_interpolated_body(&world.physics_scene, SOLVER_CONFIG, camera.body) body := physics.get_interpolated_body(&world.physics_scene, SOLVER_CONFIG, camera.body)
body_up := physics.body_local_to_world_vec(body, {0, 1, 0})
camera.follow_target = body.x + physics.Vec3{0, 4, 0} // COM can be a lot lower than the center of visual mesh, offset by that first
camera.target = body.x + physics.Vec3{0, 2.5, 0} car_center := body.x + body_up * 1
// When car flips it's noticeable that camera is offset from COM, remove the offset when it sways
camera.follow_target = car_center + physics.Vec3{0, 3, 0}
camera.target = car_center + physics.Vec3{0, 1.5, 0}
delta, sense, _ := collect_camera_input() delta, sense, _ := collect_camera_input()
@ -995,31 +1009,28 @@ follow_camera_update :: proc(world: ^World, camera: ^Car_Follow_Camera, dt: f32)
if camera.manual_rotation_time > 0 { if camera.manual_rotation_time > 0 {
camera.manual_rotation_time -= dt 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.yaw += delta.x * sense
camera.pitch += delta.y * 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.pitch = math.clamp(camera.pitch, -math.PI / 2.0 + 0.0001, math.PI / 2.0 - 0.0001)
} else {
v_len := linalg.length(body.v)
if v_len >= 0.0001 {
dir := body.v / v_len
speed := math.smoothstep(f32(0.2), f32(20), v_len) * 6
// speed *= math.smoothstep(f32(6), f32(20), v_len) * 3
target_yaw, target_pitch := dir_to_yaw_pitch(dir)
camera.yaw = emath.exp_smooth_angle(target_yaw, camera.yaw, speed, dt)
camera.pitch = emath.exp_smooth_angle(target_pitch, camera.pitch, speed, dt)
}
}
camera.pos = camera.pos =
camera.follow_target + camera.follow_target +
yaw_pitch_to_rotation(camera.yaw, camera.pitch) * rl.Vector3{0, 0, 1} * distance 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}
}
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 { follow_camera_to_rl :: proc(camera: Car_Follow_Camera) -> rl.Camera3D {

View File

@ -366,133 +366,121 @@ draw_debug_ui :: proc(
} }
} }
// active_wheels := []int{0, 1}
// w, h: i32 = 500, 500 if .ACTIVE in ui.treenode(ctx, "Wheels") {
for i in 0 ..< len(sim_state.suspension_constraints_slice) {
s := get_interpolated_wheel(scene, config, Suspension_Constraint_Handle(i + 1))
if s.alive {
// window_x: i32 = 0 if .ACTIVE in ui.treenode(ctx, fmt.tprintf("Wheel {}", name.to_string(s.name))) {
ui.keyval(ctx, "Spring Impulse", s.spring_impulse)
ui.keyval(ctx, "Turn Angle", s.turn_angle * math.DEG_PER_RAD)
ui.keyval(ctx, "Turn Assist", s.turn_assist * math.DEG_PER_RAD)
ui.keyval(ctx, "Total Turn", (s.turn_angle + s.turn_assist) * math.DEG_PER_RAD)
ui.keyval(ctx, "Long Slip", s.slip_ratio)
ui.keyval(ctx, "Lat Slip", s.slip_angle)
ui.keyval(ctx, "Camber", s.camber)
// for i in 0 ..< len(sim_state.suspension_constraints_slice) { NUM_SAMPLES :: 100
// s := &sim_state.suspension_constraints_slice[i]
// if s.alive { dt := f32(config.timestep) / f32(config.substreps_minus_one + 1)
// for idx in active_wheels { inv_dt := 1.0 / dt
// if i == idx {
// if ui.window(
// ctx,
// fmt.tprintf("Wheel %v", i),
// ui.Rect{x = window_x, y = 0, w = w, h = h},
// ui.Options{},
// ) {
// NUM_SAMPLES :: 100
// dt := f32(config.timestep) / f32(config.substreps_minus_one + 1) if .ACTIVE in ui.treenode(ctx, "Longitudinal") {
// inv_dt := 1.0 / dt ui.layout_row(ctx, {-1}, 200)
{
ui.begin_line(ctx, ui.Color{255, 0, 0, 255})
defer ui.end_line(ctx)
// { for j in 0 ..< NUM_SAMPLES {
// ui.layout_row(ctx, {-1}, 300) alpha := f32(j) / f32(NUM_SAMPLES - 1)
// { x := alpha * 200.0 - 100.0
// ui.begin_line(ctx, ui.Color{255, 0, 0, 255})
// defer ui.end_line(ctx)
// for j in 0 ..< NUM_SAMPLES { long_friction := abs(
// alpha := f32(j) / f32(NUM_SAMPLES - 1) pacejka_94_longitudinal(
// x := alpha * 200.0 - 100.0 s.pacejka_long,
x,
max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
),
)
// long_friction := abs( ui.push_line_point(ctx, ui.Vec2f{alpha, long_friction * -0.5 + 1})
// pacejka_94_longitudinal( }
// s.pacejka_long,
// x,
// max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
// ),
// )
// ui.push_line_point( long_friction := abs(
// ctx, pacejka_94_longitudinal(
// ui.Vec2f{alpha, long_friction * -0.5 + 1}, s.pacejka_long,
// ) s.slip_ratio,
// } max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
),
)
// long_friction := abs( rect := ui.get_line(ctx).rect
// pacejka_94_longitudinal(
// s.pacejka_long,
// s.slip_ratio,
// max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
// ),
// )
// rect := ui.get_line(ctx).rect cur_point :=
Vec2{(s.slip_ratio + 100.0) / 200.0, long_friction * -0.5 + 1} *
Vec2{f32(rect.w), f32(rect.h)} +
Vec2{f32(rect.x), f32(rect.y)}
ui.draw_rect(
ctx,
ui.rect_from_point_extent(
ui.Vec2{i32(cur_point.x), i32(cur_point.y)},
2,
),
ui.Color{255, 255, 0, 255},
)
}
}
// cur_point := if .ACTIVE in ui.treenode(ctx, "Lateral") {
// Vec2 { ui.layout_row(ctx, {-1}, 200)
// (s.slip_ratio + 100.0) / 200.0, ui.begin_line(ctx, ui.Color{0, 255, 0, 255})
// long_friction * -0.5 + 1, defer ui.end_line(ctx)
// } *
// Vec2{f32(rect.w), f32(rect.h)} +
// Vec2{f32(rect.x), f32(rect.y)}
// ui.draw_rect(
// ctx,
// ui.rect_from_point_extent(
// ui.Vec2{i32(cur_point.x), i32(cur_point.y)},
// 2,
// ),
// ui.Color{255, 255, 0, 255},
// )
// }
// }
// { for j in 0 ..< NUM_SAMPLES {
// ui.layout_row(ctx, {-1}, 300) alpha := f32(j) / f32(NUM_SAMPLES - 1)
// ui.begin_line(ctx, ui.Color{0, 255, 0, 255}) x := alpha * 180.0 - 90.0
// defer ui.end_line(ctx)
// for j in 0 ..< NUM_SAMPLES { lat_friction := abs(
// alpha := f32(j) / f32(NUM_SAMPLES - 1) pacejka_94_lateral(
// x := alpha * 180.0 - 90.0 s.pacejka_lat,
x,
max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
0.0,
),
)
// lat_friction := abs( ui.push_line_point(ctx, ui.Vec2f{alpha, lat_friction * -0.5 + 1})
// pacejka_94_lateral( }
// s.pacejka_lat,
// x,
// max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
// 0.0,
// ),
// )
// ui.push_line_point(ctx, ui.Vec2f{alpha, lat_friction * -0.5 + 1}) lat_friction := abs(
// } pacejka_94_lateral(
s.pacejka_lat,
s.slip_angle,
max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
0.0,
),
)
// lat_friction := abs( rect := ui.get_line(ctx).rect
// pacejka_94_lateral(
// s.pacejka_lat,
// s.slip_angle,
// max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
// 0.0,
// ),
// )
// rect := ui.get_line(ctx).rect cur_point :=
Vec2{(s.slip_angle + 100.0) / 200.0, lat_friction * -0.5 + 1} *
// cur_point := Vec2{f32(rect.w), f32(rect.h)} +
// Vec2{(s.slip_angle + 100.0) / 200.0, lat_friction * -0.5 + 1} * Vec2{f32(rect.x), f32(rect.y)}
// Vec2{f32(rect.w), f32(rect.h)} + ui.draw_rect(
// Vec2{f32(rect.x), f32(rect.y)} ctx,
// ui.draw_rect( ui.rect_from_point_extent(
// ctx, ui.Vec2{i32(cur_point.x), i32(cur_point.y)},
// ui.rect_from_point_extent( 2,
// ui.Vec2{i32(cur_point.x), i32(cur_point.y)}, ),
// 2, ui.Color{255, 255, 0, 255},
// ), )
// ui.Color{255, 255, 0, 255}, }
// ) }
// } }
}
// window_x += w }
// }
// }
// }
// }
// }
} }
debug_transform_points_local_to_world :: proc(body: Body_Ptr, points: []Vec3) { debug_transform_points_local_to_world :: proc(body: Body_Ptr, points: []Vec3) {

View File

@ -55,6 +55,9 @@ pacejka_94_longitudinal :: proc(
f_z: f32, f_z: f32,
s_v: f32 = 0, s_v: f32 = 0,
) -> f32 { ) -> f32 {
if f_z < 0.001 {
return 0
}
f_z_sq := f_z * f_z f_z_sq := f_z * f_z
C := b[0] C := b[0]
@ -77,6 +80,9 @@ pacejka_94_lateral :: proc(
f_z: f32, f_z: f32,
camber_angle: f32, camber_angle: f32,
) -> f32 { ) -> f32 {
if f_z < 0.001 {
return 0
}
camber_angle_sq := camber_angle * camber_angle camber_angle_sq := camber_angle * camber_angle
C := a[0] C := a[0]

View File

@ -297,7 +297,9 @@ Collision_Shape :: struct {
Suspension_Constraint :: struct { Suspension_Constraint :: struct {
alive: bool, alive: bool,
name: name.Name,
turn_wheel: bool, turn_wheel: bool,
steer_lock: f32,
// Pos relative to the body // Pos relative to the body
rel_pos: Vec3, rel_pos: Vec3,
// Dir relative to the body // Dir relative to the body
@ -366,6 +368,7 @@ Suspension_Constraint :: struct {
// Convenience for debug visualization to avoid recomputing // Convenience for debug visualization to avoid recomputing
slip_angle: f32, slip_angle: f32,
slip_ratio: f32, slip_ratio: f32,
camber: f32,
// Multipliers for combined friction // Multipliers for combined friction
slip_vec: Vec2, slip_vec: Vec2,
@ -746,7 +749,10 @@ Body_Config :: struct {
// TODO: rename to wheel // TODO: rename to wheel
Suspension_Constraint_Config :: struct { Suspension_Constraint_Config :: struct {
name: name.Name,
turn_wheel: bool, turn_wheel: bool,
// Max turn angle either left or right
steer_lock: f32,
rel_pos: Vec3, rel_pos: Vec3,
rel_dir: Vec3, rel_dir: Vec3,
body: Body_Handle, body: Body_Handle,
@ -917,7 +923,9 @@ update_suspension_constraint_from_config :: proc(
constraint: Suspension_Constraint_Ptr, constraint: Suspension_Constraint_Ptr,
config: Suspension_Constraint_Config, config: Suspension_Constraint_Config,
) { ) {
constraint.name = config.name
constraint.turn_wheel = config.turn_wheel constraint.turn_wheel = config.turn_wheel
constraint.steer_lock = config.steer_lock
constraint.rel_pos = config.rel_pos constraint.rel_pos = config.rel_pos
constraint.rel_dir = config.rel_dir constraint.rel_dir = config.rel_dir
constraint.body = config.body constraint.body = config.body

View File

@ -975,7 +975,7 @@ 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(240 / 8, 1, f64(dt)) bias_rate, mass_coef, impulse_coef := calculate_soft_constraint_params(30, 8, f64(dt))
if !apply_bias { if !apply_bias {
mass_coef = 1 mass_coef = 1
bias_rate = 0 bias_rate = 0
@ -1032,8 +1032,7 @@ pgs_solve_contacts :: proc(
bias := f32(0.0) bias := f32(0.0)
MAX_BAUMGARTE_VELOCITY :: 4.0 MAX_BAUMGARTE_VELOCITY :: 1000
if separation > 0 { if separation > 0 {
bias = separation * inv_dt bias = separation * inv_dt
} else if apply_bias { } else if apply_bias {
@ -1332,6 +1331,8 @@ pgs_solve_suspension :: proc(
if body.alive { if body.alive {
if v.turn_wheel { if v.turn_wheel {
v.turn_angle = math.clamp(v.turn_angle, -v.steer_lock, v.steer_lock)
body_forward_vec := body_local_to_world_vec(body, {0, 0, 1}) body_forward_vec := body_local_to_world_vec(body, {0, 0, 1})
body_right_vec := body_local_to_world_vec(body, {1, 0, 0}) body_right_vec := body_local_to_world_vec(body, {1, 0, 0})
lateral_to_longitudinal_relation := lateral_to_longitudinal_relation :=
@ -1349,6 +1350,11 @@ pgs_solve_suspension :: proc(
drift_amount * drift_amount *
math.PI * math.PI *
0.15 0.15
// Clamp the assist to not go outside steer lock
v.turn_assist =
math.clamp(v.turn_angle + v.turn_assist, -v.steer_lock, v.steer_lock) -
v.turn_angle
} }
@ -1499,12 +1505,16 @@ pgs_solve_suspension :: proc(
// log.debugf("slip_vec: %v", slip_vec) // log.debugf("slip_vec: %v", slip_vec)
camber_cos := -lg.dot(wheel_get_right_vec(body, v), v.right)
camber_angle_rad := camber_cos < 0.999999 ? math.acos(camber_cos) : 0
v.camber = camber_angle_rad * math.DEG_PER_RAD * math.sign(v.rel_pos.x)
long_friction := long_friction :=
abs( abs(
pacejka_94_longitudinal( pacejka_94_longitudinal(
v.pacejka_long, v.pacejka_long,
slip_ratio, slip_ratio,
max(abs(v.spring_impulse), 0.001) * inv_dt * 0.001, v.spring_impulse * inv_dt * 0.001,
), ),
) * ) *
abs(slip_vec.y) abs(slip_vec.y)
@ -1513,8 +1523,8 @@ pgs_solve_suspension :: proc(
pacejka_94_lateral( pacejka_94_lateral(
v.pacejka_lat, v.pacejka_lat,
slip_angle, slip_angle,
max(abs(v.spring_impulse), 0.001) * inv_dt * 0.001, v.spring_impulse * inv_dt * 0.001,
0.0, v.camber,
), ),
) * ) *
abs(slip_vec.x) abs(slip_vec.x)