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
1.1
-200
1.5
-20
1700
2000
10
0.5
0
0
-0.5
0
0
-1
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
1.2
1.5
-200
1400
20
1
0
0
0
-2
-10
300
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 {
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,
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"),
)
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(
&world.physics_scene,
#hash("FL", "fnv32a"),
{
name = name.from_string("FL"),
turn_wheel = true,
steer_lock = TURN_ANGLE_AT_LOW_SPEED,
rel_pos = {-wheel_extent_x_front, wheel_y, wheel_front_z},
rel_dir = {0, -1, 0},
radius = radius,
@ -632,7 +637,9 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
&world.physics_scene,
#hash("FR", "fnv32a"),
{
name = name.from_string("FR"),
turn_wheel = true,
steer_lock = TURN_ANGLE_AT_LOW_SPEED,
rel_pos = {wheel_extent_x_front, wheel_y, wheel_front_z},
rel_dir = {0, -1, 0},
radius = radius,
@ -649,6 +656,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
&world.physics_scene,
#hash("RL", "fnv32a"),
{
name = name.from_string("RL"),
rel_pos = {-wheel_extent_x_back, wheel_y, wheel_back_z},
rel_dir = {0, -1, 0},
radius = radius,
@ -665,6 +673,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
&world.physics_scene,
#hash("RR", "fnv32a"),
{
name = name.from_string("RR"),
rel_pos = {wheel_extent_x_back, wheel_y, wheel_back_z},
rel_dir = {0, -1, 0},
radius = radius,
@ -706,10 +715,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
front_wheels := turn_wheels
back_wheels := drive_wheels
DRIVE_IMPULSE :: 3000
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
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)
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)
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) {
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}
camera.target = body.x + physics.Vec3{0, 2.5, 0}
// COM can be a lot lower than the center of visual mesh, offset by that first
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()
@ -995,31 +1009,28 @@ follow_camera_update :: proc(world: ^World, camera: ^Car_Follow_Camera, dt: f32)
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}
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.has_yaw_pitch = false
pos_target := camera.follow_target + dir * distance
camera.pos = pos_target
}
camera.pos =
camera.follow_target +
yaw_pitch_to_rotation(camera.yaw, camera.pitch) * rl.Vector3{0, 0, 1} * distance
}
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) {
// s := &sim_state.suspension_constraints_slice[i]
NUM_SAMPLES :: 100
// if s.alive {
// for idx in active_wheels {
// 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)
inv_dt := 1.0 / dt
// dt := f32(config.timestep) / f32(config.substreps_minus_one + 1)
// inv_dt := 1.0 / dt
if .ACTIVE in ui.treenode(ctx, "Longitudinal") {
ui.layout_row(ctx, {-1}, 200)
{
ui.begin_line(ctx, ui.Color{255, 0, 0, 255})
defer ui.end_line(ctx)
// {
// ui.layout_row(ctx, {-1}, 300)
// {
// ui.begin_line(ctx, ui.Color{255, 0, 0, 255})
// defer ui.end_line(ctx)
for j in 0 ..< NUM_SAMPLES {
alpha := f32(j) / f32(NUM_SAMPLES - 1)
x := alpha * 200.0 - 100.0
// for j in 0 ..< NUM_SAMPLES {
// alpha := f32(j) / f32(NUM_SAMPLES - 1)
// x := alpha * 200.0 - 100.0
long_friction := abs(
pacejka_94_longitudinal(
s.pacejka_long,
x,
max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
),
)
// long_friction := abs(
// pacejka_94_longitudinal(
// s.pacejka_long,
// x,
// max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
// ),
// )
ui.push_line_point(ctx, ui.Vec2f{alpha, long_friction * -0.5 + 1})
}
// ui.push_line_point(
// ctx,
// ui.Vec2f{alpha, long_friction * -0.5 + 1},
// )
// }
long_friction := abs(
pacejka_94_longitudinal(
s.pacejka_long,
s.slip_ratio,
max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
),
)
// long_friction := abs(
// 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
// 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 :=
// 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},
// )
// }
// }
if .ACTIVE in ui.treenode(ctx, "Lateral") {
ui.layout_row(ctx, {-1}, 200)
ui.begin_line(ctx, ui.Color{0, 255, 0, 255})
defer ui.end_line(ctx)
// {
// ui.layout_row(ctx, {-1}, 300)
// ui.begin_line(ctx, ui.Color{0, 255, 0, 255})
// defer ui.end_line(ctx)
for j in 0 ..< NUM_SAMPLES {
alpha := f32(j) / f32(NUM_SAMPLES - 1)
x := alpha * 180.0 - 90.0
// for j in 0 ..< NUM_SAMPLES {
// alpha := f32(j) / f32(NUM_SAMPLES - 1)
// x := alpha * 180.0 - 90.0
lat_friction := abs(
pacejka_94_lateral(
s.pacejka_lat,
x,
max(abs(s.spring_impulse), 0.001) * inv_dt * 0.001,
0.0,
),
)
// lat_friction := abs(
// 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})
}
// 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(
// 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
// rect := ui.get_line(ctx).rect
// cur_point :=
// Vec2{(s.slip_angle + 100.0) / 200.0, lat_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},
// )
// }
// window_x += w
// }
// }
// }
// }
// }
cur_point :=
Vec2{(s.slip_angle + 100.0) / 200.0, lat_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},
)
}
}
}
}
}
}
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,
s_v: f32 = 0,
) -> f32 {
if f_z < 0.001 {
return 0
}
f_z_sq := f_z * f_z
C := b[0]
@ -77,6 +80,9 @@ pacejka_94_lateral :: proc(
f_z: f32,
camber_angle: f32,
) -> f32 {
if f_z < 0.001 {
return 0
}
camber_angle_sq := camber_angle * camber_angle
C := a[0]

View File

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

View File

@ -975,7 +975,7 @@ pgs_solve_contacts :: proc(
inv_dt: f32,
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 {
mass_coef = 1
bias_rate = 0
@ -1032,8 +1032,7 @@ pgs_solve_contacts :: proc(
bias := f32(0.0)
MAX_BAUMGARTE_VELOCITY :: 4.0
MAX_BAUMGARTE_VELOCITY :: 1000
if separation > 0 {
bias = separation * inv_dt
} else if apply_bias {
@ -1332,6 +1331,8 @@ pgs_solve_suspension :: proc(
if body.alive {
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_right_vec := body_local_to_world_vec(body, {1, 0, 0})
lateral_to_longitudinal_relation :=
@ -1349,6 +1350,11 @@ pgs_solve_suspension :: proc(
drift_amount *
math.PI *
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)
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 :=
abs(
pacejka_94_longitudinal(
v.pacejka_long,
slip_ratio,
max(abs(v.spring_impulse), 0.001) * inv_dt * 0.001,
v.spring_impulse * inv_dt * 0.001,
),
) *
abs(slip_vec.y)
@ -1513,8 +1523,8 @@ pgs_solve_suspension :: proc(
pacejka_94_lateral(
v.pacejka_lat,
slip_angle,
max(abs(v.spring_impulse), 0.001) * inv_dt * 0.001,
0.0,
v.spring_impulse * inv_dt * 0.001,
v.camber,
),
) *
abs(slip_vec.x)