Start implementing engine simulation

This commit is contained in:
sergeypdev 2025-04-26 20:29:58 +04:00
parent a9f007a90e
commit 78ab4eeae9
14 changed files with 740 additions and 70 deletions

View File

@ -0,0 +1,63 @@
rpm,torque
1000,63.6
1100,72.3
1200,79.5
1300,85.7
1400,90.9
1500,95.5
1600,99.4
1700,102.9
1800,106.1
1900,108.9
2000,111.4
2100,113.6
2200,115.7
2300,117.6
2400,119.3
2500,120.9
2600,122.4
2700,123.7
2800,125
2900,126.2
3000,127.3
3100,128.3
3200,129.3
3300,130.2
3400,131
3500,131.8
3600,132.6
3700,133.3
3800,134
3900,134.6
4000,135.2
4100,135.8
4200,136.4
4300,136.9
4400,137.4
4500,137.9
4600,138.3
4700,138.8
4800,139.2
4900,139.6
5000,140
5100,140
5200,139.8
5300,139.6
5400,139.3
5500,138.9
5600,138.4
5700,137.8
5800,137.2
5900,136.4
6000,135.6
6100,134.7
6200,133.7
6300,132.6
6400,131.4
6500,130.1
6600,128.8
6700,126.3
6800,123
6900,118.7
7000,113.6
7100,107.7
1 rpm torque
2 1000 63.6
3 1100 72.3
4 1200 79.5
5 1300 85.7
6 1400 90.9
7 1500 95.5
8 1600 99.4
9 1700 102.9
10 1800 106.1
11 1900 108.9
12 2000 111.4
13 2100 113.6
14 2200 115.7
15 2300 117.6
16 2400 119.3
17 2500 120.9
18 2600 122.4
19 2700 123.7
20 2800 125
21 2900 126.2
22 3000 127.3
23 3100 128.3
24 3200 129.3
25 3300 130.2
26 3400 131
27 3500 131.8
28 3600 132.6
29 3700 133.3
30 3800 134
31 3900 134.6
32 4000 135.2
33 4100 135.8
34 4200 136.4
35 4300 136.9
36 4400 137.4
37 4500 137.9
38 4600 138.3
39 4700 138.8
40 4800 139.2
41 4900 139.6
42 5000 140
43 5100 140
44 5200 139.8
45 5300 139.6
46 5400 139.3
47 5500 138.9
48 5600 138.4
49 5700 137.8
50 5800 137.2
51 5900 136.4
52 6000 135.6
53 6100 134.7
54 6200 133.7
55 6300 132.6
56 6400 131.4
57 6500 130.1
58 6600 128.8
59 6700 126.3
60 6800 123
61 6900 118.7
62 7000 113.6
63 7100 107.7

View File

@ -1,9 +1,6 @@
package assets package assets
import "core:bytes"
import "core:c" import "core:c"
import "core:encoding/csv"
import "core:io"
import "core:log" import "core:log"
import "core:math" import "core:math"
import lg "core:math/linalg" import lg "core:math/linalg"
@ -194,58 +191,13 @@ get_curve_2d :: proc(assetman: ^Asset_Manager, path: string) -> (curve: Loaded_C
return return
} }
bytes_reader: bytes.Reader err2: Curve_Parse_Error
bytes.reader_init(&bytes_reader, data) curve, err2 = parse_curve_2d(data, context.temp_allocator)
bytes_stream := bytes.reader_to_stream(&bytes_reader)
csv_reader: csv.Reader if err2 != nil {
csv.reader_init(&csv_reader, bytes_stream, context.temp_allocator) log.errorf("Failed to parse curve: %s, %v", path, err2)
tmp_result := make([dynamic][2]f32, context.temp_allocator)
skipped_header := false
for {
row, err := csv.read(&csv_reader, context.temp_allocator)
if err != nil {
if err != io.Error.EOF {
log.errorf("Failed to read curve %v", err)
}
break
}
if len(row) != 2 {
log.errorf("Curve expected 2 columns, got %v", len(row))
break
}
ok: bool
key: f64
val: f64
key, ok = strconv.parse_f64(row[0])
if !ok {
if skipped_header {
log.errorf("Curve expected numbers, got %s", row[0])
break
}
skipped_header = true
continue
}
val, ok = strconv.parse_f64(row[1])
if !ok {
if skipped_header {
log.errorf("Curve expected numbers, got %s", row[1])
break
}
skipped_header = true
continue
}
append(&tmp_result, [2]f32{f32(key), f32(val)})
} }
curve.points = make([][2]f32, len(tmp_result), context.temp_allocator)
copy(curve.points, tmp_result[:])
return return
} }

View File

@ -0,0 +1,42 @@
package assets
import "core:testing"
@(private = "file")
test_csv := `
rpm,torque
1000,100
2000,110.5
5000,354.123
`
@(private = "file")
test_invalid_csv := `
rpm,torque
rpm,torque
1000,100
2000,110.5
5000,354.123
`
@(test)
test_curve_parsing :: proc(t: ^testing.T) {
curve, err := parse_curve_2d(transmute([]u8)test_csv, context.temp_allocator)
testing.expect_value(t, err, nil)
testing.expect_value(t, len(curve.points), 3)
testing.expect_value(t, curve.points[0], [2]f32{1000, 100})
testing.expect_value(t, curve.points[1], [2]f32{2000, 110.5})
testing.expect_value(t, curve.points[2], [2]f32{5000, 354.123})
}
@(test)
test_curve_parsing_error :: proc(t: ^testing.T) {
curve, err := parse_curve_2d(transmute([]u8)test_invalid_csv, context.temp_allocator)
defer free_all(context.temp_allocator)
testing.expect_value(t, err, Curve_Parse_Error.ExpectedNumber)
testing.expect_value(t, len(curve.points), 0)
}

79
game/assets/parsers.odin Normal file
View File

@ -0,0 +1,79 @@
package assets
import "core:bytes"
import "core:encoding/csv"
import "core:io"
import "core:log"
import "core:strconv"
Curve_Parse_Error :: enum {
Ok,
TooManyColumns,
ExpectedNumber,
}
parse_curve_2d :: proc(
data: []byte,
allocator := context.allocator,
) -> (
curve: Loaded_Curve_2D,
error: Curve_Parse_Error,
) {
bytes_reader: bytes.Reader
bytes.reader_init(&bytes_reader, data)
bytes_stream := bytes.reader_to_stream(&bytes_reader)
csv_reader: csv.Reader
csv.reader_init(&csv_reader, bytes_stream, context.temp_allocator)
defer csv.reader_destroy(&csv_reader)
tmp_result := make([dynamic][2]f32, context.temp_allocator)
skipped_header := false
for {
row, err := csv.read(&csv_reader, context.temp_allocator)
if err != nil {
if err != io.Error.EOF {
log.warnf("Failed to read curve %v", err)
}
break
}
if len(row) != 2 {
log.warnf("Curve expected 2 columns, got %v", len(row))
error = .TooManyColumns
break
}
ok: bool
key: f64
val: f64
key, ok = strconv.parse_f64(row[0])
if !ok {
if skipped_header {
log.warnf("Curve expected numbers, got %s", row[0])
error = .ExpectedNumber
break
}
skipped_header = true
continue
}
val, ok = strconv.parse_f64(row[1])
if !ok {
if skipped_header {
log.warnf("Curve expected numbers, got %s", row[1])
error = .ExpectedNumber
break
}
skipped_header = true
continue
}
append(&tmp_result, [2]f32{f32(key), f32(val)})
}
curve.points = make([][2]f32, len(tmp_result), allocator)
copy(curve.points, tmp_result[:])
return
}

View File

@ -15,7 +15,9 @@ init_physfs :: proc(args: []string) {
physfs.init(strings.clone_to_cstring(args[0], context.temp_allocator)) physfs.init(strings.clone_to_cstring(args[0], context.temp_allocator))
physfs.setSaneConfig("serega", "gutter-runner", "zip", 0, 1) physfs.setSaneConfig("serega", "gutter-runner", "zip", 0, 1)
}
init_physifs_raylib_callbacks :: proc() {
rl.SetLoadFileDataCallback(raylib_load_file_data_physfs) rl.SetLoadFileDataCallback(raylib_load_file_data_physfs)
rl.SetSaveFileDataCallback(raylib_save_file_data_physfs) rl.SetSaveFileDataCallback(raylib_save_file_data_physfs)
rl.SetLoadFileTextCallback(raylib_load_file_text_physfs) rl.SetLoadFileTextCallback(raylib_load_file_text_physfs)

View File

@ -394,6 +394,25 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
}, },
) )
engine := physics.immediate_engine(
&world.physics_scene,
&runtime_world.solver_state,
#hash("engine", "fnv32a"),
physics.Engine_Config {
rpm_torque_curve = assets.get_curve_2d(&g_mem.assetman, "assets/ae86_rpm_torque.csv").points,
lowest_rpm = 1000,
inertia = 10,
internal_friction = 8,
gear_ratios = []f32{3.48, 3.587, 2.022, 1.384, 1, 0.861},
axle = physics.Drive_Axle_Config {
wheels = {wheel_rl, wheel_rr},
wheel_count = 2,
diff_type = .Fixed,
final_drive_ratio = 4.3,
},
},
)
drive_wheels := []physics.Suspension_Constraint_Handle{wheel_rl, wheel_rr} drive_wheels := []physics.Suspension_Constraint_Handle{wheel_rl, wheel_rr}
turn_wheels := []physics.Suspension_Constraint_Handle{wheel_fl, wheel_fr} turn_wheels := []physics.Suspension_Constraint_Handle{wheel_fl, wheel_fr}
front_wheels := turn_wheels front_wheels := turn_wheels
@ -426,11 +445,7 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
wheel.brake_impulse = brake_input * (1.0 - BRAKE_BIAS) * BRAKE_IMPULSE wheel.brake_impulse = brake_input * (1.0 - BRAKE_BIAS) * BRAKE_IMPULSE
} }
for wheel_handle in drive_wheels { physics.get_engine(sim_state, engine).throttle = gas_input
wheel := physics.get_suspension_constraint(sim_state, wheel_handle)
wheel.drive_impulse = gas_input * DRIVE_IMPULSE
}
car_body := physics.get_body(sim_state, runtime_world.car_handle) car_body := physics.get_body(sim_state, runtime_world.car_handle)
turn_vel_correction := clamp(30.0 / linalg.length(car_body.v), 0, 1) turn_vel_correction := clamp(30.0 / linalg.length(car_body.v), 0, 1)
@ -1040,6 +1055,8 @@ game_init :: proc() {
g_mem^ = Game_Memory{} g_mem^ = Game_Memory{}
init_physifs_raylib_callbacks()
physics.init_physics_scene(&g_mem.runtime_world.world.physics_scene, 100) physics.init_physics_scene(&g_mem.runtime_world.world.physics_scene, 100)
game_hot_reloaded(g_mem) game_hot_reloaded(g_mem)

View File

@ -1,6 +1,7 @@
package physics package physics
import "collision" import "collision"
import "core:math"
import lg "core:math/linalg" import lg "core:math/linalg"
import "game:halfedge" import "game:halfedge"
@ -150,3 +151,11 @@ body_get_aabb :: proc(body: Body_Ptr) -> (aabb: AABB) {
return aabb return aabb
} }
rpm_to_angular_velocity :: proc(rpm: f32) -> f32 {
return 2.0 * math.PI * (rpm / 60.0)
}
angular_velocity_to_rpm :: proc(w: f32) -> f32 {
return (w / (2.0 * math.PI)) * 60.0
}

View File

@ -71,10 +71,40 @@ immediate_suspension_constraint :: proc(
return return
} }
immediate_engine :: proc(
scene: ^Scene,
state: ^Solver_State,
id: u32,
config: Engine_Config,
) -> (
handle: Engine_Handle,
) {
sim_state := get_sim_state(scene)
if id in state.immediate_engines {
engine := &state.immediate_engines[id]
if engine.last_ref != state.simulation_frame {
engine.last_ref = state.simulation_frame
state.num_referenced_engines += 1
}
handle = engine.handle
update_engine_from_config(sim_state, get_engine(sim_state, handle), config)
} else {
state.num_referenced_engines += 1
handle = add_engine(sim_state, config)
state.immediate_engines[id] = {
handle = handle,
last_ref = state.simulation_frame,
}
}
return
}
prune_immediate :: proc(scene: ^Scene, state: ^Solver_State) { prune_immediate :: proc(scene: ^Scene, state: ^Solver_State) {
tracy.Zone() tracy.Zone()
prune_immediate_bodies(scene, state) prune_immediate_bodies(scene, state)
prune_immediate_suspension_constraints(scene, state) prune_immediate_suspension_constraints(scene, state)
prune_immediate_engines(scene, state)
} }
// TODO: Generic version // TODO: Generic version
@ -134,3 +164,30 @@ prune_immediate_suspension_constraints :: proc(scene: ^Scene, state: ^Solver_Sta
remove_suspension_constraint(get_sim_state(scene), handle) remove_suspension_constraint(get_sim_state(scene), handle)
} }
} }
prune_immediate_engines :: proc(scene: ^Scene, state: ^Solver_State) {
if int(state.num_referenced_engines) == len(state.immediate_engines) {
return
}
num_unreferenced_engines := len(state.immediate_engines) - int(state.num_referenced_engines)
assert(num_unreferenced_engines >= 0)
engines_to_remove := make([]u32, num_unreferenced_engines, context.temp_allocator)
i := 0
for k, &v in state.immediate_engines {
if v.last_ref != state.simulation_frame {
engines_to_remove[i] = k
i += 1
}
}
assert(i == len(engines_to_remove))
for k in engines_to_remove {
handle := state.immediate_engines[k].handle
delete_key(&state.immediate_engines, k)
remove_engine(get_sim_state(scene), handle)
}
}

View File

@ -2,6 +2,7 @@ package physics
import "collision" import "collision"
import lg "core:math/linalg" import lg "core:math/linalg"
import "game:container/spanpool"
MAX_CONTACTS :: 1024 * 16 MAX_CONTACTS :: 1024 * 16
@ -43,18 +44,26 @@ contact_container_copy :: proc(dst: ^Contact_Container, src: Contact_Container)
Sim_State :: struct { Sim_State :: struct {
bodies: #soa[dynamic]Body, bodies: #soa[dynamic]Body,
suspension_constraints: #soa[dynamic]Suspension_Constraint, suspension_constraints: #soa[dynamic]Suspension_Constraint,
engines: [dynamic]Engine,
// Number of alive bodies // Number of alive bodies
num_bodies: i32, num_bodies: i32,
num_engines: i32,
// Slices. When you call get_body or get_suspension_constraint you will get a pointer to an element in this slice // Slices. When you call get_body or get_suspension_constraint you will get a pointer to an element in this slice
bodies_slice: #soa[]Body, bodies_slice: #soa[]Body,
suspension_constraints_slice: #soa[]Suspension_Constraint, suspension_constraints_slice: #soa[]Suspension_Constraint,
first_free_body_plus_one: i32, first_free_body_plus_one: i32,
first_free_suspension_constraint_plus_one: i32, first_free_suspension_constraint_plus_one: i32,
first_free_engine_plus_one: i32,
// Persistent stuff for simulation // Persistent stuff for simulation
contact_container: Contact_Container, contact_container: Contact_Container,
convex_container: Convex_Container, convex_container: Convex_Container,
// NOTE: kinda overkill, but it simplifies copying sim states around a lot
// Engine array data
rpm_torque_curves_pool: spanpool.Span_Pool([2]f32),
gear_ratios_pool: spanpool.Span_Pool(f32),
} }
Scene :: struct { Scene :: struct {
@ -167,12 +176,80 @@ Suspension_Constraint :: struct {
next_plus_one: i32, next_plus_one: i32,
} }
Diff_Type :: enum {
Fixed,
// TODO: LSD
}
Drive_Wheel :: struct {
wheel: Suspension_Constraint_Handle,
impulse: f32,
}
Drive_Axle :: struct {
// Params
wheels: [2]Drive_Wheel,
wheel_count: i32,
diff_type: Diff_Type,
final_drive_ratio: f32,
// State
// Diff angular vel
w: f32,
// Impulse that constrains diff and engine motion
engine_impulse: f32,
// Impulse that constrains wheels motion relative to each other
diff_impulse: f32,
}
Engine_Curve_Handle :: distinct spanpool.Handle
Gear_Ratios_Handle :: distinct spanpool.Handle
// This actually handles everything, engine, transmission, differential, etc. It's easier to keep it in one place
Engine :: struct {
alive: bool,
// Engine Params
rpm_torque_curve: Engine_Curve_Handle,
lowest_rpm: f32,
inertia: f32,
internal_friction: f32,
// Transmission Params
// 0 - reverse, 1 - first, etc.
gear_ratios: Gear_Ratios_Handle,
axle: Drive_Axle,
// Engine State
// Angular velocity, omega
q: f32,
w: f32,
// Impulse applied when engine is stalling (rpm < lowest_rpm)
unstall_impulse: f32,
// Friction that makes rpm go down when you're not accelerating
friction_impulse: f32,
// Impulse applied from releasing throttle
throttle_impulse: f32,
// Transmission State
// -1 - reeverse, 0 - neutral, 1 - first, etc.
gear: i32,
// Controls
throttle: f32,
// Free list
next_plus_one: i32,
}
// Index plus one, so handle 0 maps to invalid body // Index plus one, so handle 0 maps to invalid body
Body_Handle :: distinct i32 Body_Handle :: distinct i32
Suspension_Constraint_Handle :: distinct i32 Suspension_Constraint_Handle :: distinct i32
Engine_Handle :: distinct i32
INVALID_BODY :: Body_Handle(0) INVALID_BODY :: Body_Handle(0)
INVALID_SUSPENSION_CONSTRAINT :: Suspension_Constraint_Handle(0) INVALID_SUSPENSION_CONSTRAINT :: Suspension_Constraint_Handle(0)
INVALID_ENGINE :: Engine_Handle(0)
is_body_handle_valid :: proc(handle: Body_Handle) -> bool { is_body_handle_valid :: proc(handle: Body_Handle) -> bool {
return i32(handle) > 0 return i32(handle) > 0
@ -180,13 +257,18 @@ is_body_handle_valid :: proc(handle: Body_Handle) -> bool {
is_suspension_constraint_handle_valid :: proc(handle: Suspension_Constraint_Handle) -> bool { is_suspension_constraint_handle_valid :: proc(handle: Suspension_Constraint_Handle) -> bool {
return i32(handle) > 0 return i32(handle) > 0
} }
is_engine_handle_valid :: proc(handle: Engine_Handle) -> bool {
return i32(handle) > 0
}
is_handle_valid :: proc { is_handle_valid :: proc {
is_body_handle_valid, is_body_handle_valid,
is_suspension_constraint_handle_valid, is_suspension_constraint_handle_valid,
is_engine_handle_valid,
} }
Body_Ptr :: #soa^#soa[]Body Body_Ptr :: #soa^#soa[]Body
Suspension_Constraint_Ptr :: #soa^#soa[]Suspension_Constraint Suspension_Constraint_Ptr :: #soa^#soa[]Suspension_Constraint
Engine_Ptr :: ^Engine
_invalid_body: #soa[1]Body _invalid_body: #soa[1]Body
_invalid_body_slice := _invalid_body[:] _invalid_body_slice := _invalid_body[:]
@ -264,6 +346,25 @@ Suspension_Constraint_Config :: struct {
mass: f32, mass: f32,
} }
Drive_Axle_Config :: struct {
wheels: [2]Suspension_Constraint_Handle,
wheel_count: i32,
diff_type: Diff_Type,
final_drive_ratio: f32,
}
Engine_Config :: struct {
rpm_torque_curve: [][2]f32,
lowest_rpm: f32,
inertia: f32,
internal_friction: f32,
// Transmission Params
// 0 - reverse, 1 - first, etc.
gear_ratios: []f32,
axle: Drive_Axle_Config,
}
calculate_body_params_from_config :: proc( calculate_body_params_from_config :: proc(
config: Body_Config, config: Body_Config,
) -> ( ) -> (
@ -335,6 +436,53 @@ update_suspension_constraint_from_config :: proc(
constraint.inv_inertia = 1.0 / (0.5 * config.mass * config.radius * config.radius) constraint.inv_inertia = 1.0 / (0.5 * config.mass * config.radius * config.radius)
} }
add_engine_curve :: proc(sim_state: ^Sim_State, curve: [][2]f32) -> Engine_Curve_Handle {
handle := spanpool.allocate_elems(&sim_state.rpm_torque_curves_pool, ..curve)
return Engine_Curve_Handle(handle)
}
remove_engine_curve :: proc(sim_state: ^Sim_State, handle: Engine_Curve_Handle) {
spanpool.free(&sim_state.rpm_torque_curves_pool, spanpool.Handle(handle))
}
get_engine_curve :: proc(sim_state: ^Sim_State, handle: Engine_Curve_Handle) -> [][2]f32 {
return spanpool.resolve_slice(&sim_state.rpm_torque_curves_pool, spanpool.Handle(handle))
}
add_gear_ratios :: proc(sim_state: ^Sim_State, gear_ratios: []f32) -> Gear_Ratios_Handle {
handle := spanpool.allocate_elems(&sim_state.gear_ratios_pool, ..gear_ratios)
return Gear_Ratios_Handle(handle)
}
remove_gear_ratios :: proc(sim_state: ^Sim_State, handle: Gear_Ratios_Handle) {
spanpool.free(&sim_state.gear_ratios_pool, spanpool.Handle(handle))
}
get_gear_ratios :: proc(sim_state: ^Sim_State, handle: Gear_Ratios_Handle) -> []f32 {
return spanpool.resolve_slice(&sim_state.gear_ratios_pool, spanpool.Handle(handle))
}
update_engine_from_config :: proc(
sim_state: ^Sim_State,
engine: Engine_Ptr,
config: Engine_Config,
) {
remove_engine_curve(sim_state, engine.rpm_torque_curve)
engine.rpm_torque_curve = add_engine_curve(sim_state, config.rpm_torque_curve)
remove_gear_ratios(sim_state, engine.gear_ratios)
engine.gear_ratios = add_gear_ratios(sim_state, config.gear_ratios)
engine.lowest_rpm = config.lowest_rpm
engine.inertia = config.inertia
engine.internal_friction = config.internal_friction
engine.axle.final_drive_ratio = config.axle.final_drive_ratio
engine.axle.wheels[0].wheel = config.axle.wheels[0]
engine.axle.wheels[1].wheel = config.axle.wheels[1]
engine.axle.wheel_count = config.axle.wheel_count
engine.axle.diff_type = config.axle.diff_type
}
add_body :: proc(sim_state: ^Sim_State, config: Body_Config) -> Body_Handle { add_body :: proc(sim_state: ^Sim_State, config: Body_Config) -> Body_Handle {
body: Body body: Body
@ -425,6 +573,54 @@ remove_suspension_constraint :: proc(sim_state: ^Sim_State, handle: Suspension_C
} }
} }
invalid_engine: Engine
get_engine :: proc(sim_state: ^Sim_State, handle: Engine_Handle) -> Engine_Ptr {
index := int(handle) - 1
if index < 0 || index >= len(sim_state.bodies_slice) {
return &invalid_engine
}
return &sim_state.engines[index]
}
add_engine :: proc(sim_state: ^Sim_State, config: Engine_Config) -> Engine_Handle {
sim_state.num_engines += 1
engine: Engine
update_engine_from_config(sim_state, &engine, config)
engine.alive = true
if sim_state.first_free_engine_plus_one > 0 {
index := sim_state.first_free_engine_plus_one
new_engine := get_engine(sim_state, Engine_Handle(index))
next_plus_one := new_engine.next_plus_one
new_engine^ = engine
sim_state.first_free_engine_plus_one = next_plus_one
return Engine_Handle(index)
}
append(&sim_state.engines, engine)
index := len(sim_state.engines)
return Engine_Handle(index)
}
remove_engine :: proc(sim_state: ^Sim_State, handle: Engine_Handle) {
if is_handle_valid(handle) {
engine := get_engine(sim_state, handle)
remove_engine_curve(sim_state, engine.rpm_torque_curve)
remove_gear_ratios(sim_state, engine.gear_ratios)
engine.alive = false
engine.next_plus_one = sim_state.first_free_engine_plus_one
sim_state.first_free_engine_plus_one = i32(handle)
sim_state.num_engines -= 1
}
}
_get_first_free_body :: proc(sim_state: ^Sim_State) -> i32 { _get_first_free_body :: proc(sim_state: ^Sim_State) -> i32 {
return sim_state.first_free_body_plus_one - 1 return sim_state.first_free_body_plus_one - 1
} }
@ -434,7 +630,10 @@ destry_sim_state :: proc(sim_state: ^Sim_State) {
delete_soa(sim_state.suspension_constraints) delete_soa(sim_state.suspension_constraints)
delete_soa(sim_state.contact_container.contacts) delete_soa(sim_state.contact_container.contacts)
delete_map(sim_state.contact_container.lookup) delete_map(sim_state.contact_container.lookup)
delete(sim_state.engines)
convex_container_destroy(&sim_state.convex_container) convex_container_destroy(&sim_state.convex_container)
spanpool.destroy_spanpool(&sim_state.rpm_torque_curves_pool)
spanpool.destroy_spanpool(&sim_state.gear_ratios_pool)
} }
destroy_physics_scene :: proc(scene: ^Scene) { destroy_physics_scene :: proc(scene: ^Scene) {

View File

@ -9,6 +9,7 @@ import "core:math"
import lg "core:math/linalg" import lg "core:math/linalg"
import "core:math/rand" import "core:math/rand"
import "core:slice" import "core:slice"
import "game:container/spanpool"
import "libs:tracy" import "libs:tracy"
_ :: rand _ :: rand
@ -31,13 +32,16 @@ Solver_State :: struct {
// Number of immediate bodies referenced this frame // Number of immediate bodies referenced this frame
num_referenced_bodies: i32, num_referenced_bodies: i32,
num_referenced_suspension_constraints: i32, num_referenced_suspension_constraints: i32,
num_referenced_engines: i32,
immedate_bodies: map[u32]Immedate_State(Body_Handle), immedate_bodies: map[u32]Immedate_State(Body_Handle),
immediate_suspension_constraints: map[u32]Immedate_State(Suspension_Constraint_Handle), immediate_suspension_constraints: map[u32]Immedate_State(Suspension_Constraint_Handle),
immediate_engines: map[u32]Immedate_State(Engine_Handle),
} }
destroy_solver_state :: proc(state: ^Solver_State) { destroy_solver_state :: proc(state: ^Solver_State) {
delete(state.immedate_bodies) delete(state.immedate_bodies)
delete(state.immediate_suspension_constraints) delete(state.immediate_suspension_constraints)
delete(state.immediate_engines)
} }
Immedate_State :: struct($T: typeid) { Immedate_State :: struct($T: typeid) {
@ -57,9 +61,11 @@ sim_state_copy :: proc(dst: ^Sim_State, src: ^Sim_State) {
dst.num_bodies = src.num_bodies dst.num_bodies = src.num_bodies
dst.first_free_body_plus_one = src.first_free_body_plus_one dst.first_free_body_plus_one = src.first_free_body_plus_one
dst.first_free_suspension_constraint_plus_one = src.first_free_suspension_constraint_plus_one dst.first_free_suspension_constraint_plus_one = src.first_free_suspension_constraint_plus_one
dst.first_free_engine_plus_one = src.first_free_engine_plus_one
resize(&dst.bodies, len(src.bodies)) resize(&dst.bodies, len(src.bodies))
resize(&dst.suspension_constraints, len(src.suspension_constraints)) resize(&dst.suspension_constraints, len(src.suspension_constraints))
resize(&dst.engines, len(src.engines))
dst.bodies_slice = dst.bodies[:] dst.bodies_slice = dst.bodies[:]
dst.suspension_constraints_slice = dst.suspension_constraints[:] dst.suspension_constraints_slice = dst.suspension_constraints[:]
@ -70,9 +76,12 @@ sim_state_copy :: proc(dst: ^Sim_State, src: ^Sim_State) {
for i in 0 ..< len(dst.suspension_constraints) { for i in 0 ..< len(dst.suspension_constraints) {
dst.suspension_constraints[i] = src.suspension_constraints[i] dst.suspension_constraints[i] = src.suspension_constraints[i]
} }
copy(dst.engines[:], src.engines[:])
contact_container_copy(&dst.contact_container, src.contact_container) contact_container_copy(&dst.contact_container, src.contact_container)
convex_container_copy(&dst.convex_container, src.convex_container) convex_container_copy(&dst.convex_container, src.convex_container)
spanpool.copy(&dst.rpm_torque_curves_pool, src.rpm_torque_curves_pool)
spanpool.copy(&dst.gear_ratios_pool, src.gear_ratios_pool)
} }
Step_Mode :: enum { Step_Mode :: enum {
@ -204,6 +213,7 @@ simulate :: proc(
state.simulation_frame += 1 state.simulation_frame += 1
state.num_referenced_bodies = 0 state.num_referenced_bodies = 0
state.num_referenced_suspension_constraints = 0 state.num_referenced_suspension_constraints = 0
state.num_referenced_engines = 0
} }
GLOBAL_PLANE :: collision.Plane { GLOBAL_PLANE :: collision.Plane {
@ -474,6 +484,119 @@ pgs_solve_contacts :: proc(
} }
} }
pgs_solve_engines :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32, inv_dt: f32) {
for &engine in sim_state.engines {
if engine.alive {
rpm_torque_curve := get_engine_curve(sim_state, engine.rpm_torque_curve)
gear_ratios := get_gear_ratios(sim_state, engine.gear_ratios)
// Unstall impulse
{
engine_lowest_velocity := rpm_to_angular_velocity(engine.lowest_rpm)
delta_omega := engine_lowest_velocity - engine.w
inv_w := engine.inertia
incremental_impulse := inv_w * delta_omega
new_total_impulse := max(engine.unstall_impulse + incremental_impulse, 0)
applied_impulse := new_total_impulse - engine.unstall_impulse
engine.unstall_impulse = new_total_impulse
engine.w += applied_impulse / engine.inertia
}
// Internal Friction
{
delta_omega := -engine.w
inv_w := engine.inertia
friction :=
math.pow(
max(engine.w - rpm_to_angular_velocity(engine.lowest_rpm), 0) * 0.001,
4,
) *
engine.internal_friction +
engine.internal_friction
incremental_impulse := inv_w * delta_omega
new_total_impulse := math.clamp(
engine.friction_impulse + incremental_impulse,
-friction,
friction,
)
applied_impulse := new_total_impulse - engine.friction_impulse
engine.friction_impulse = new_total_impulse
engine.w += applied_impulse / engine.inertia
}
// Throttle
{
rpm := angular_velocity_to_rpm(engine.w)
torque: f32
idx, _ := slice.binary_search_by(
rpm_torque_curve,
rpm,
proc(a: [2]f32, k: f32) -> slice.Ordering {
return slice.cmp(a[0], k)
},
)
if idx > 0 && idx < len(rpm_torque_curve) - 1 {
cur_point := rpm_torque_curve[idx]
next_point := rpm_torque_curve[idx + 1]
rpm_diff := next_point[0] - cur_point[0]
alpha := (rpm - cur_point[0]) / rpm_diff
torque = math.lerp(cur_point[1], next_point[1], alpha)
} else {
torque = rpm_torque_curve[math.clamp(idx, 0, len(rpm_torque_curve) - 1)][1]
}
torque *= engine.throttle
engine.w += torque / engine.inertia
}
// Transmission
{
// TODO: update from game
engine.gear = 1
power_split := 1.0 / f32(engine.axle.wheel_count)
for i in 0 ..< engine.axle.wheel_count {
drive_wheel := &engine.axle.wheels[i]
wheel := get_suspension_constraint(sim_state, drive_wheel.wheel)
ratio := gear_ratios[1] * engine.axle.final_drive_ratio
inv_ratio := f32(1.0 / ratio)
w1 := wheel.inv_inertia
w2 := f32(1.0 / (engine.inertia * ratio))
w := w1 + w2
inv_w := f32(1.0 / w)
delta_omega := -engine.w - wheel.w * ratio
incremental_impulse := -inv_w * delta_omega * power_split
drive_wheel.impulse += incremental_impulse
wheel.w += -incremental_impulse * wheel.inv_inertia * inv_ratio
engine.w += -(incremental_impulse / engine.inertia)
}
}
// tracy.Plot("rpm", f64(angular_velocity_to_rpm(engine.w)))
log.debugf("rpm: %v", angular_velocity_to_rpm(engine.w))
}
}
}
pgs_solve_suspension :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32, inv_dt: f32) { pgs_solve_suspension :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32, inv_dt: f32) {
// Solve suspension velocity // Solve suspension velocity
for _, i in sim_state.suspension_constraints { for _, i in sim_state.suspension_constraints {
@ -604,7 +727,7 @@ pgs_solve_suspension :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f
slip_len = slip_len == 0 ? 0 : min(slip_len, 1) / slip_len slip_len = slip_len == 0 ? 0 : min(slip_len, 1) / slip_len
slip_vec *= slip_len slip_vec *= slip_len
log.debugf("slip_vec: %v", slip_vec) // log.debugf("slip_vec: %v", slip_vec)
long_friction := long_friction :=
abs( abs(
@ -712,6 +835,28 @@ pgs_substep :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32, inv_d
} }
} }
for i in 0 ..< len(sim_state.engines) {
e := &sim_state.engines[i]
if e.alive {
gear_ratios := get_gear_ratios(sim_state, e.gear_ratios)
e.w += e.unstall_impulse / e.inertia
e.w += e.friction_impulse / e.inertia
for i in 0 ..< e.axle.wheel_count {
drive_wheel := &e.axle.wheels[i]
wheel := get_suspension_constraint(sim_state, drive_wheel.wheel)
ratio := gear_ratios[1] * e.axle.final_drive_ratio
inv_ratio := f32(1.0 / ratio)
wheel.w += -drive_wheel.impulse * wheel.inv_inertia * inv_ratio
e.w += -(drive_wheel.impulse / e.inertia)
}
}
}
for i in 0 ..< len(sim_state.suspension_constraints) { for i in 0 ..< len(sim_state.suspension_constraints) {
s := &sim_state.suspension_constraints_slice[i] s := &sim_state.suspension_constraints_slice[i]
@ -739,6 +884,7 @@ pgs_substep :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32, inv_d
apply_bias := true apply_bias := true
pgs_solve_contacts(sim_state, config, dt, inv_dt, apply_bias) pgs_solve_contacts(sim_state, config, dt, inv_dt, apply_bias)
pgs_solve_engines(sim_state, config, dt, inv_dt)
pgs_solve_suspension(sim_state, config, dt, inv_dt) pgs_solve_suspension(sim_state, config, dt, inv_dt)
for i in 0 ..< len(sim_state.bodies_slice) { for i in 0 ..< len(sim_state.bodies_slice) {
@ -763,6 +909,12 @@ pgs_substep :: proc(sim_state: ^Sim_State, config: Solver_Config, dt: f32, inv_d
} }
} }
for i in 0 ..< len(sim_state.engines) {
e := &sim_state.engines[i]
e.q = math.mod_f32(e.q + 0.5 * e.w * dt, math.PI * 2)
}
for i in 0 ..< len(sim_state.suspension_constraints_slice) { for i in 0 ..< len(sim_state.suspension_constraints_slice) {
s := &sim_state.suspension_constraints_slice[i] s := &sim_state.suspension_constraints_slice[i]

View File

@ -1,7 +1,7 @@
configure_cmake: configure_cmake:
cmake -B build ./physfs cmake -B build ./physfs
build_physfs: configure_cmake build_physfs: configure_cmake
cmake --build build --config Release -j8 cmake --build build --config Debug -j8
libphysfs.a: build_physfs libphysfs.a: build_physfs
cp ./build/libphysfs.a . cp ./build/libphysfs.a .
libphysfs.so: build_physfs libphysfs.so: build_physfs

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash #!/usr/bin/env bash
odin test game -collection:common=./common -collection:game=./game -collection:libs=./libs -strict-style -vet odin test game/assets -collection:common=./common -collection:game=./game -collection:libs=./libs -strict-style -vet -sanitize:memory