Start implementing engine simulation
This commit is contained in:
parent
a9f007a90e
commit
78ab4eeae9
63
assets/ae86_rpm_torque.csv
Normal file
63
assets/ae86_rpm_torque.csv
Normal 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,9 +1,6 @@
|
||||
package assets
|
||||
|
||||
import "core:bytes"
|
||||
import "core:c"
|
||||
import "core:encoding/csv"
|
||||
import "core:io"
|
||||
import "core:log"
|
||||
import "core:math"
|
||||
import lg "core:math/linalg"
|
||||
@ -194,57 +191,12 @@ get_curve_2d :: proc(assetman: ^Asset_Manager, path: string) -> (curve: Loaded_C
|
||||
return
|
||||
}
|
||||
|
||||
bytes_reader: bytes.Reader
|
||||
bytes.reader_init(&bytes_reader, data)
|
||||
bytes_stream := bytes.reader_to_stream(&bytes_reader)
|
||||
err2: Curve_Parse_Error
|
||||
curve, err2 = parse_curve_2d(data, context.temp_allocator)
|
||||
|
||||
csv_reader: csv.Reader
|
||||
csv.reader_init(&csv_reader, bytes_stream, context.temp_allocator)
|
||||
|
||||
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)
|
||||
if err2 != nil {
|
||||
log.errorf("Failed to parse curve: %s, %v", path, err2)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
42
game/assets/assets_test.odin
Normal file
42
game/assets/assets_test.odin
Normal 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
79
game/assets/parsers.odin
Normal 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
|
||||
}
|
@ -15,7 +15,9 @@ init_physfs :: proc(args: []string) {
|
||||
|
||||
physfs.init(strings.clone_to_cstring(args[0], context.temp_allocator))
|
||||
physfs.setSaneConfig("serega", "gutter-runner", "zip", 0, 1)
|
||||
}
|
||||
|
||||
init_physifs_raylib_callbacks :: proc() {
|
||||
rl.SetLoadFileDataCallback(raylib_load_file_data_physfs)
|
||||
rl.SetSaveFileDataCallback(raylib_save_file_data_physfs)
|
||||
rl.SetLoadFileTextCallback(raylib_load_file_text_physfs)
|
||||
|
@ -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}
|
||||
turn_wheels := []physics.Suspension_Constraint_Handle{wheel_fl, wheel_fr}
|
||||
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
|
||||
}
|
||||
|
||||
for wheel_handle in drive_wheels {
|
||||
wheel := physics.get_suspension_constraint(sim_state, wheel_handle)
|
||||
|
||||
wheel.drive_impulse = gas_input * DRIVE_IMPULSE
|
||||
}
|
||||
physics.get_engine(sim_state, engine).throttle = gas_input
|
||||
|
||||
car_body := physics.get_body(sim_state, runtime_world.car_handle)
|
||||
turn_vel_correction := clamp(30.0 / linalg.length(car_body.v), 0, 1)
|
||||
@ -1040,6 +1055,8 @@ game_init :: proc() {
|
||||
|
||||
g_mem^ = Game_Memory{}
|
||||
|
||||
init_physifs_raylib_callbacks()
|
||||
|
||||
physics.init_physics_scene(&g_mem.runtime_world.world.physics_scene, 100)
|
||||
|
||||
game_hot_reloaded(g_mem)
|
||||
|
@ -1,6 +1,7 @@
|
||||
package physics
|
||||
|
||||
import "collision"
|
||||
import "core:math"
|
||||
import lg "core:math/linalg"
|
||||
import "game:halfedge"
|
||||
|
||||
@ -150,3 +151,11 @@ body_get_aabb :: proc(body: Body_Ptr) -> (aabb: 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
|
||||
}
|
||||
|
@ -71,10 +71,40 @@ immediate_suspension_constraint :: proc(
|
||||
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) {
|
||||
tracy.Zone()
|
||||
prune_immediate_bodies(scene, state)
|
||||
prune_immediate_suspension_constraints(scene, state)
|
||||
prune_immediate_engines(scene, state)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package physics
|
||||
|
||||
import "collision"
|
||||
import lg "core:math/linalg"
|
||||
import "game:container/spanpool"
|
||||
|
||||
MAX_CONTACTS :: 1024 * 16
|
||||
|
||||
@ -43,18 +44,26 @@ contact_container_copy :: proc(dst: ^Contact_Container, src: Contact_Container)
|
||||
Sim_State :: struct {
|
||||
bodies: #soa[dynamic]Body,
|
||||
suspension_constraints: #soa[dynamic]Suspension_Constraint,
|
||||
engines: [dynamic]Engine,
|
||||
// Number of alive bodies
|
||||
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
|
||||
bodies_slice: #soa[]Body,
|
||||
suspension_constraints_slice: #soa[]Suspension_Constraint,
|
||||
first_free_body_plus_one: i32,
|
||||
first_free_suspension_constraint_plus_one: i32,
|
||||
first_free_engine_plus_one: i32,
|
||||
|
||||
// Persistent stuff for simulation
|
||||
contact_container: Contact_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 {
|
||||
@ -167,12 +176,80 @@ Suspension_Constraint :: struct {
|
||||
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
|
||||
Body_Handle :: distinct i32
|
||||
Suspension_Constraint_Handle :: distinct i32
|
||||
Engine_Handle :: distinct i32
|
||||
|
||||
INVALID_BODY :: Body_Handle(0)
|
||||
INVALID_SUSPENSION_CONSTRAINT :: Suspension_Constraint_Handle(0)
|
||||
INVALID_ENGINE :: Engine_Handle(0)
|
||||
|
||||
is_body_handle_valid :: proc(handle: Body_Handle) -> bool {
|
||||
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 {
|
||||
return i32(handle) > 0
|
||||
}
|
||||
is_engine_handle_valid :: proc(handle: Engine_Handle) -> bool {
|
||||
return i32(handle) > 0
|
||||
}
|
||||
is_handle_valid :: proc {
|
||||
is_body_handle_valid,
|
||||
is_suspension_constraint_handle_valid,
|
||||
is_engine_handle_valid,
|
||||
}
|
||||
|
||||
Body_Ptr :: #soa^#soa[]Body
|
||||
Suspension_Constraint_Ptr :: #soa^#soa[]Suspension_Constraint
|
||||
Engine_Ptr :: ^Engine
|
||||
|
||||
_invalid_body: #soa[1]Body
|
||||
_invalid_body_slice := _invalid_body[:]
|
||||
@ -264,6 +346,25 @@ Suspension_Constraint_Config :: struct {
|
||||
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(
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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.contact_container.contacts)
|
||||
delete_map(sim_state.contact_container.lookup)
|
||||
delete(sim_state.engines)
|
||||
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) {
|
||||
|
@ -9,6 +9,7 @@ import "core:math"
|
||||
import lg "core:math/linalg"
|
||||
import "core:math/rand"
|
||||
import "core:slice"
|
||||
import "game:container/spanpool"
|
||||
import "libs:tracy"
|
||||
|
||||
_ :: rand
|
||||
@ -31,13 +32,16 @@ Solver_State :: struct {
|
||||
// Number of immediate bodies referenced this frame
|
||||
num_referenced_bodies: i32,
|
||||
num_referenced_suspension_constraints: i32,
|
||||
num_referenced_engines: i32,
|
||||
immedate_bodies: map[u32]Immedate_State(Body_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) {
|
||||
delete(state.immedate_bodies)
|
||||
delete(state.immediate_suspension_constraints)
|
||||
delete(state.immediate_engines)
|
||||
}
|
||||
|
||||
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.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_engine_plus_one = src.first_free_engine_plus_one
|
||||
|
||||
resize(&dst.bodies, len(src.bodies))
|
||||
resize(&dst.suspension_constraints, len(src.suspension_constraints))
|
||||
resize(&dst.engines, len(src.engines))
|
||||
|
||||
dst.bodies_slice = dst.bodies[:]
|
||||
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) {
|
||||
dst.suspension_constraints[i] = src.suspension_constraints[i]
|
||||
}
|
||||
copy(dst.engines[:], src.engines[:])
|
||||
|
||||
contact_container_copy(&dst.contact_container, src.contact_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 {
|
||||
@ -204,6 +213,7 @@ simulate :: proc(
|
||||
state.simulation_frame += 1
|
||||
state.num_referenced_bodies = 0
|
||||
state.num_referenced_suspension_constraints = 0
|
||||
state.num_referenced_engines = 0
|
||||
}
|
||||
|
||||
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) {
|
||||
// Solve suspension velocity
|
||||
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_vec *= slip_len
|
||||
|
||||
log.debugf("slip_vec: %v", slip_vec)
|
||||
// log.debugf("slip_vec: %v", slip_vec)
|
||||
|
||||
long_friction :=
|
||||
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) {
|
||||
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
|
||||
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)
|
||||
|
||||
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) {
|
||||
s := &sim_state.suspension_constraints_slice[i]
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
configure_cmake:
|
||||
cmake -B build ./physfs
|
||||
build_physfs: configure_cmake
|
||||
cmake --build build --config Release -j8
|
||||
cmake --build build --config Debug -j8
|
||||
libphysfs.a: build_physfs
|
||||
cp ./build/libphysfs.a .
|
||||
libphysfs.so: build_physfs
|
||||
|
98
research/engine_friction.ipynb
Normal file
98
research/engine_friction.ipynb
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user