This commit is contained in:
2026-08-14 00:15:10 +03:00
commit 41ec89a732
17 changed files with 1720 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
src.bin
.gdbf/
.gdbinit
build/
+40
View File
@@ -0,0 +1,40 @@
require("custom.runner").setup({
commands = {
run = function()
local command = require("custom.runner").command
command:add("odin run ./src/")
command.target = { position = "bottom" }
command:run()
end,
build_debug = function()
local command = require("custom.runner").command
command:add("odin build ./src/ -debug -out:build/debug")
command.target = { position = "bottom" }
command:run()
end,
debug = function()
local command = require("custom.runner").command
local current_file = vim.api.nvim_buf_get_name(0)
command:add("odin build ./src/ -debug -out:build/debug")
if current_file ~= "" then
command:add(string.format("raddbg ./build/debug --open %q", current_file))
else
command:add("raddbg ./build/debug")
end
command.target = { position = "bottom" }
command:run()
end,
},
exclude = {
"build",
"Tiled",
"assets",
".gdbf",
".gdbinit",
},
})
Executable
+571
View File
@@ -0,0 +1,571 @@
// raddbg 0.9.28 user file
recent_project:
{
path: debug
name: ""
}
recent_project:
{
path: "../debug"
name: ""
}
window:
{
size: 1780 1390
panels:
{
0.273086:
{
0.333333:
{
watch:
{
selected
expression: ""
watch_expression: expression: state
}
watch: expression: "query:types"
watch: expression: "query:locals"
}
0.333333:
{
watch: selected expression: "query:threads"
watch:
{
expression: "query:call_stack"
call_stack_frame: 0.050000 0.654968 0.295032
}
watch: expression: "query:breakpoints"
watch: expression: "query:watch_pins"
}
0.333333:
{
text:
{
expression: "query:output"
query: input: ""
cursor: 0x0
mark: 0x0
}
watch: selected expression: "query:targets"
}
}
0.726914:
{
selected
text:
{
selected
expression: "file:\"/home/alexey/projects/odin/racing/src/main.odin\".data"
project: ""
query: input: ""
cursor: 0x113
mark: 0x113
}
disasm: query: input: ""
}
}
split_x
pos: 775 45
monitor
}
keybindings:
{
{ kill_all f5 shift }
{ step_into_inst f11 alt }
{ step_over_inst f10 alt }
{ step_out f11 shift }
{ halt x ctrl shift }
{ halt pause }
{ run f5 }
{ restart f5 ctrl shift }
{ step_into f11 }
{ step_over f10 }
{ run_to_line f10 ctrl }
{ set_next_statement f10 ctrl shift }
{ inc_window_font_size equal alt }
{ dec_window_font_size minus alt }
{ toggle_fullscreen return ctrl }
{ new_panel_right p ctrl }
{ new_panel_down minus ctrl }
{ rotate_panel_columns 2 ctrl }
{ next_panel comma ctrl }
{ prev_panel comma ctrl shift }
{ focus_panel_right right ctrl alt }
{ focus_panel_left left ctrl alt }
{ focus_panel_up up ctrl alt }
{ focus_panel_down down ctrl alt }
{ undo z ctrl }
{ redo y ctrl }
{ go_back left alt }
{ go_forward right alt }
{ close_panel p ctrl shift alt }
{ next_tab page_down ctrl }
{ prev_tab page_up ctrl }
{ next_tab tab ctrl }
{ prev_tab tab ctrl shift }
{ move_tab_right page_down ctrl shift }
{ move_tab_left page_up ctrl shift }
{ close_tab w ctrl }
{ tab_bar_top up ctrl shift alt }
{ tab_bar_bottom down ctrl shift alt }
{ open_tab t ctrl }
{ tab_settings t ctrl alt }
{ open o ctrl }
{ open_source_file_from_debug_info i ctrl }
{ switch_to_partner_file o alt }
{ new_project n ctrl shift }
{ open_project o ctrl shift }
{ save_project s ctrl shift }
{ edit f2 }
{ accept return }
{ accept space }
{ cancel esc }
{ focus_menu d alt }
{ toggle_lock l ctrl }
{ move_left left }
{ move_right right }
{ move_up up }
{ move_down down }
{ move_left_select left shift }
{ move_right_select right shift }
{ move_up_select up shift }
{ move_down_select down shift }
{ move_left_chunk left ctrl }
{ move_right_chunk right ctrl }
{ move_up_chunk up ctrl }
{ move_down_chunk down ctrl }
{ move_up_page page_up }
{ move_down_page page_down }
{ move_up_whole home ctrl }
{ move_down_whole end ctrl }
{ move_left_chunk_select left ctrl shift }
{ move_right_chunk_select right ctrl shift }
{ move_up_chunk_select up ctrl shift }
{ move_down_chunk_select down ctrl shift }
{ move_up_page_select page_up shift }
{ move_down_page_select page_down shift }
{ move_up_whole_select home ctrl shift }
{ move_down_whole_select end ctrl shift }
{ move_up_reorder up alt }
{ move_down_reorder down alt }
{ move_home home }
{ move_end end }
{ move_home_select home shift }
{ move_end_select end shift }
{ select_all a ctrl }
{ delete_single delete }
{ delete_chunk delete ctrl }
{ backspace_single backspace }
{ backspace_chunk backspace ctrl }
{ copy c ctrl }
{ copy insert ctrl }
{ cut x ctrl }
{ paste v ctrl }
{ paste insert shift }
{ insert_text null }
{ move_next tab }
{ move_prev tab shift }
{ goto_line g ctrl }
{ goto_address g alt }
{ search f ctrl }
{ search slash }
{ search_backwards r ctrl }
{ search_backwards slash ctrl }
{ find_next f3 }
{ find_prev f3 ctrl }
{ find_selected_thread f4 }
{ goto_name j ctrl }
{ goto_name_at_cursor f12 }
{ toggle_watch_expr_at_cursor w alt }
{ toggle_watch_expr_at_mouse d ctrl }
{ toggle_watch_pin f9 ctrl }
{ toggle_breakpoint f9 }
{ add_address_breakpoint f9 shift }
{ add_function_breakpoint f9 ctrl shift }
{ attach f6 shift }
{ open_palette f1 }
{ open_palette p ctrl shift }
{ log_marker m ctrl shift alt }
{ toggle_dev_menu d ctrl shift alt }
}
breakpoint:
{
project: ""
source_location: "../../hello/main.odin:56:1"
hit_count: 0
}
current_path: "/home/alexey/projects/odin/racing/.raddbg"
debug_info:
{
path: "/home/alexey/projects/odin/racing/build/debug"
timestamp: 66222355938101
project: ""
}
debug_info:
{
path: "/usr/lib/libX11.so.6"
timestamp: 66206040958000
project: ""
}
debug_info:
{
path: "/usr/lib/libm.so.6"
timestamp: 66221135897000
project: ""
}
debug_info:
{
path: "/usr/lib/libc.so.6"
timestamp: 66221135897000
project: ""
}
debug_info:
{
path: "/lib64/ld-linux-x86-64.so.2"
timestamp: 66221135897000
project: ""
}
debug_info:
{
path: "/usr/lib/libxcb.so.1"
timestamp: 66184635927000
project: ""
}
debug_info:
{
path: "/usr/lib/libXau.so.6"
timestamp: 66168334589000
project: ""
}
debug_info:
{
path: "/usr/lib/libXdmcp.so.6"
timestamp: 66211487652000
project: ""
}
debug_info:
{
path: "/usr/lib/libXxf86vm.so.1"
timestamp: 66204799833000
project: ""
}
debug_info:
{
path: "/usr/lib/libXext.so.6"
timestamp: 66204798407000
project: ""
}
debug_info:
{
path: "/usr/lib/libXi.so.6"
timestamp: 66214983870000
project: ""
}
debug_info:
{
path: "/usr/lib/libXrandr.so.2"
timestamp: 66204799447000
project: ""
}
debug_info:
{
path: "/usr/lib/libXrender.so.1"
timestamp: 66168333224000
project: ""
}
debug_info:
{
path: "/usr/lib/libXcursor.so.1"
timestamp: 66164897222000
project: ""
}
debug_info:
{
path: "/usr/lib/libXfixes.so.3"
timestamp: 66191816105000
project: ""
}
debug_info:
{
path: "/usr/lib/libXinerama.so.1"
timestamp: 66204798889000
project: ""
}
debug_info:
{
path: "/usr/lib/libX11-xcb.so.1"
timestamp: 66206040958000
project: ""
}
debug_info:
{
path: "/usr/lib/libGLX.so.0"
timestamp: 66184693464000
project: ""
}
debug_info:
{
path: "/usr/lib/libGLdispatch.so.0"
timestamp: 66184693464000
project: ""
}
debug_info:
{
path: "/usr/lib/libGLX_mesa.so.0"
timestamp: 66221471789000
project: ""
}
debug_info:
{
path: "/usr/lib/libgallium-26.1.6-arch3.1.so"
timestamp: 66221471789000
project: ""
}
debug_info:
{
path: "/usr/lib/libdrm.so.2"
timestamp: 66216328784000
project: ""
}
debug_info:
{
path: "/usr/lib/libxcb-glx.so.0"
timestamp: 66184635927000
project: ""
}
debug_info:
{
path: "/usr/lib/libxcb-shm.so.0"
timestamp: 66184635927000
project: ""
}
debug_info:
{
path: "/usr/lib/libexpat.so.1"
timestamp: 66218340126000
project: ""
}
debug_info:
{
path: "/usr/lib/libxcb-dri3.so.0"
timestamp: 66184635927000
project: ""
}
debug_info:
{
path: "/usr/lib/libxcb-present.so.0"
timestamp: 66184635927000
project: ""
}
debug_info:
{
path: "/usr/lib/libxcb-xfixes.so.0"
timestamp: 66184635927000
project: ""
}
debug_info:
{
path: "/usr/lib/libgcc_s.so.1"
timestamp: 66221675792000
project: ""
}
debug_info:
{
path: "/usr/lib/libLLVM.so.22.1"
timestamp: 66219390635000
project: ""
}
debug_info:
{
path: "/usr/lib/libz.so.1"
timestamp: 66205596487000
project: ""
}
debug_info:
{
path: "/usr/lib/libzstd.so.1"
timestamp: 66174387757000
project: ""
}
debug_info:
{
path: "/usr/lib/libSPIRV-Tools.so"
timestamp: 66221645000000
project: ""
}
debug_info:
{
path: "/usr/lib/libsensors.so.5"
timestamp: 66181926213000
project: ""
}
debug_info:
{
path: "/usr/lib/libxcb-randr.so.0"
timestamp: 66184635927000
project: ""
}
debug_info:
{
path: "/usr/lib/libxcb-sync.so.1"
timestamp: 66184635927000
project: ""
}
debug_info:
{
path: "/usr/lib/libxshmfence.so.1"
timestamp: 66168333263000
project: ""
}
debug_info:
{
path: "/usr/lib/libelf.so.1"
timestamp: 66212257553000
project: ""
}
debug_info:
{
path: "/usr/lib/libdrm_amdgpu.so.1"
timestamp: 66216328784000
project: ""
}
debug_info:
{
path: "/usr/lib/libdrm_intel.so.1"
timestamp: 66216328784000
project: ""
}
debug_info:
{
path: "/usr/lib/libstdc++.so.6"
timestamp: 66221675792000
project: ""
}
debug_info:
{
path: "/usr/lib/libffi.so.8"
timestamp: 66219792944000
project: ""
}
debug_info:
{
path: "/usr/lib/libedit.so.0"
timestamp: 66215003270000
project: ""
}
debug_info:
{
path: "/usr/lib/libxml2.so.16"
timestamp: 66212388766000
project: ""
}
debug_info:
{
path: "/usr/lib/libpciaccess.so.0"
timestamp: 66209386711000
project: ""
}
debug_info:
{
path: "/usr/lib/libncursesw.so.6"
timestamp: 66209896673000
project: ""
}
debug_info:
{
path: "/usr/lib/libicuuc.so.78"
timestamp: 66209517782000
project: ""
}
debug_info:
{
path: "/usr/lib/libicudata.so.78"
timestamp: 66209517782000
project: ""
}
debug_info:
{
path: "/usr/lib/libpulse.so"
timestamp: 66201445208000
project: ""
}
debug_info:
{
path: "/usr/lib/pulseaudio/libpulsecommon-17.0.so"
timestamp: 66201445208000
project: ""
}
debug_info:
{
path: "/usr/lib/libdbus-1.so.3"
timestamp: 66175513452000
project: ""
}
debug_info:
{
path: "/usr/lib/libsndfile.so.1"
timestamp: 66196036422000
project: ""
}
debug_info:
{
path: "/usr/lib/libsystemd.so.0"
timestamp: 66220884102000
project: ""
}
debug_info:
{
path: "/usr/lib/libasyncns.so.0"
timestamp: 66184643894000
project: ""
}
debug_info:
{
path: "/usr/lib/libogg.so.0"
timestamp: 66187436605000
project: ""
}
debug_info:
{
path: "/usr/lib/libvorbisenc.so.2"
timestamp: 66172240374000
project: ""
}
debug_info:
{
path: "/usr/lib/libFLAC.so.14"
timestamp: 66184660199000
project: ""
}
debug_info:
{
path: "/usr/lib/libopus.so.0"
timestamp: 66203885564000
project: ""
}
debug_info:
{
path: "/usr/lib/libmpg123.so.0"
timestamp: 66212931451000
project: ""
}
debug_info:
{
path: "/usr/lib/libmp3lame.so.0"
timestamp: 66220534211000
project: ""
}
debug_info:
{
path: "/usr/lib/libvorbis.so.0"
timestamp: 66172240374000
project: ""
}
debug_info:
{
path: "/usr/lib/libmvec.so.1"
timestamp: 66221135897000
project: ""
}
Executable
+8
View File
@@ -0,0 +1,8 @@
// raddbg 0.9.28 project file
target:
{
executable: "../build/debug"
working_directory: "../../racing"
enabled: 1
}
BIN
View File
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/odinfmt.schema.json",
"character_width": 80,
"tabs": true,
"spaces": 2,
"tabs_width": 4
}
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json",
"enable_semantic_tokens": true,
"enable_document_symbols": true,
"enable_hover": true,
"enable_snippets": true,
"enable_checker_only_saved": true,
"enable_inlay_hints_params": true,
"enable_inlay_hints_default_params": true,
"enable_inlay_hints_implicit_return": true,
"enable_inlay_hints_optional_result": true,
"enable_inlay_hints_struct_fields": true
}
+579
View File
@@ -0,0 +1,579 @@
package main
import "core:fmt"
import "core:math"
import "core:math/linalg"
import "core:math/rand"
import rl "vendor:raylib"
Car :: struct {
pos: rl.Vector3,
vel: rl.Vector3,
engine_state: EngineState,
drivetrain: Drivetrain,
speed: f32,
forward: rl.Vector3,
wheel_yaw: f32,
}
EngineState :: struct {
throttle: f32,
brake: f32,
rpm: f32,
torque: f32,
fuel_cut: bool,
engine: Engine,
graph: Graph,
}
MIN_RPM :: 1000
MAX_RPM :: 7000
RPM_STEP :: 20
Engine :: struct {
idle_rpm: f32,
max_rpm: f32,
bypass: f32,
fric: f32,
inertia: f32,
hyst: f32,
rpm_lut: []f32,
}
MASS :: 1200
DRAG_COEF :: 0.40
ROLL_FORCE :: 176
CLUTCH_K :: 0.15
CLUTCH_MAX :: 300 // предел трения сцепления, N·m
BRAKE_MAX :: 10500 // ~0.9g для 1200 кг
Drivetrain :: struct {
ratios: [5]f32,
final_ratio: f32,
wheel_radius: f32,
efficiency: f32,
gear: i32,
}
Graph :: struct {
points: []rl.Vector2,
pos: rl.Vector2,
opts: GraphOpts,
}
GRAPH_STEP :: 75
GRAPH_MIN_VAL_Y :: 0
GRAPH_MAX_VAL_Y :: 500
GRAPH_MIN_VAL_X :: 1000
GRAPH_MAX_VAL_X :: 7000
GRAPH_WIDTH :: 300
GRAPH_HEIGHT :: 200
GRAPH_PADDING :: 5
RPMSample :: struct {
rpm: f32,
torq: f32,
}
update_car :: proc(car: ^Car) {
update_gear(&car.drivetrain, car.speed)
update_engine_state(car)
update_turning(car)
car.pos += car.vel * car.speed * rl.GetFrameTime()
}
update_turning :: proc(car: ^Car) {
dt := rl.GetFrameTime()
// 1. Get input direction (-1 for left, 1 for right)
input: f32 = 0
if rl.IsKeyDown(.D) || rl.IsKeyDown(.RIGHT) {input -= 1}
if rl.IsKeyDown(.A) || rl.IsKeyDown(.LEFT) {input += 1}
// 2. Turn sensitivity scaling based on speed
speed_scale := math.clamp(car.speed / 200.0, 0.0, 1.0)
turn_speed := f32(6.5) // Adjust turn speed in radians per second
// 3. Update the car's orientation angle (yaw)
car.wheel_yaw += input * turn_speed * speed_scale * dt
// 4. Calculate forward vector directly from yaw
// (X = sin, Z = cos gives standard 3D forward direction where 0 rad points North/Forward)
car.forward = rl.Vector3 {
math.sin(car.wheel_yaw),
0,
math.cos(car.wheel_yaw),
}
// 5. Arcade Grip / Blend Velocity towards Forward Vector
// Lower grip value (e.g. 5.0) = loose / drifting turn
// Higher grip value (e.g. 20.0) = tight / glued to road
grip_factor: f32 = 5.0
car.vel = linalg.lerp(car.vel, car.forward, grip_factor * dt)
// Normalize velocity vector to prevent unintended scaling
if rl.Vector3Length(car.vel) > 0 {
car.vel = rl.Vector3Normalize(car.vel)
}
}
update_gear :: proc(drivetrain: ^Drivetrain, speed: f32) {
if rl.IsKeyPressed(.E) {
if try_shift(drivetrain, speed, drivetrain.gear + 1) do drivetrain.gear += 1
}
if rl.IsKeyPressed(.Q) {
if try_shift(drivetrain, speed, drivetrain.gear - 1) do drivetrain.gear -= 1
}
}
try_shift :: proc(drivetrain: ^Drivetrain, speed: f32, new_gear: i32) -> bool {
if new_gear < 0 || new_gear > 5 do return false
if new_gear == 0 do return true
G := drivetrain.ratios[new_gear - 1] * drivetrain.final_ratio
projected_rpm := speed * G * 9.549 / drivetrain.wheel_radius
return projected_rpm <= MAX_RPM
}
update_engine_state :: proc(car: ^Car) {
dt := rl.GetFrameTime()
if rl.IsKeyDown(.S) {
car.engine_state.brake = linalg.lerp(car.engine_state.brake, 1, 0.2)
} else {
if car.engine_state.brake >= 0.1 {
car.engine_state.brake = linalg.lerp(
car.engine_state.brake,
0,
0.2,
)
} else {
car.engine_state.brake = 0
}
}
if rl.IsKeyDown(.W) {
car.engine_state.throttle = linalg.lerp(
car.engine_state.throttle,
1,
0.2,
)
} else {
if car.engine_state.throttle >= 0.1 {
car.engine_state.throttle = linalg.lerp(
car.engine_state.throttle,
0,
0.2,
)
} else {
car.engine_state.throttle = 0
}
}
if car.engine_state.brake > 0 && car.drivetrain.gear != 0 do car.engine_state.throttle = 0
if car.engine_state.rpm > MAX_RPM do car.engine_state.fuel_cut = true
else if car.engine_state.rpm < MAX_RPM - car.engine_state.engine.hyst do car.engine_state.fuel_cut = false
eng_torq :=
car.engine_state.fuel_cut ? 0 : car.engine_state.throttle * torq_from_lut(car.engine_state.rpm, &car.engine_state.engine) + car.engine_state.engine.bypass
t_net := eng_torq - car.engine_state.engine.fric * car.engine_state.rpm
// сила двигателя на колёсах (0 на нейтрали) и эффективная масса
force: f32 = 0
mass_eff := f32(MASS)
if car.drivetrain.gear == 0 {
// нейтраль: свободный оборот, на колёса ничего не идёт
car.engine_state.rpm +=
t_net / car.engine_state.engine.inertia * 9.549 * dt
} else {
G :=
car.drivetrain.ratios[car.drivetrain.gear - 1] *
car.drivetrain.final_ratio
coupled_rpm := car.speed * G * 9.549 / car.drivetrain.wheel_radius
if coupled_rpm < car.engine_state.rpm {
clutch_tq := min(
CLUTCH_K * (car.engine_state.rpm - coupled_rpm),
CLUTCH_MAX,
)
clutch_tq = max(clutch_tq, 0.0)
car.engine_state.rpm +=
(t_net - clutch_tq) /
car.engine_state.engine.inertia *
9.549 *
dt
car.engine_state.rpm = max(
car.engine_state.rpm,
car.engine_state.engine.idle_rpm,
)
force =
clutch_tq *
G *
car.drivetrain.efficiency /
car.drivetrain.wheel_radius
} else {
force =
t_net *
G *
car.drivetrain.efficiency /
car.drivetrain.wheel_radius
mass_eff =
MASS +
car.engine_state.engine.inertia *
G *
G /
(car.drivetrain.wheel_radius * car.drivetrain.wheel_radius)
car.engine_state.rpm = max(
coupled_rpm,
car.engine_state.engine.idle_rpm,
)
}
}
// скорость — всегда, с тормозом (и на нейтрали тоже)
brake_force := car.engine_state.brake * BRAKE_MAX
car.speed +=
(force -
DRAG_COEF * car.speed * car.speed -
ROLL_FORCE -
brake_force) /
mass_eff *
dt
car.speed = max(car.speed, 0)
car.engine_state.torque = torq_from_lut(
car.engine_state.rpm,
&car.engine_state.engine,
)
// eng_torq :=
// state.fuel_cut ? 0 : state.throttle * torq_from_lut(state.rpm, &state.engine) + state.engine.bypass
//
// load_torq := state.engine.fric * state.rpm
//
// state.rpm += (eng_torq - load_torq) / state.engine.inertia * 9.549 * dt
//
// state.torque = torq_from_lut(state.rpm, &state.engine)
}
init_car :: proc() -> Car {
car: Car
car.engine_state.engine = init_engine()
car.pos = rl.Vector3{0, 0, 0}
car.engine_state.graph.pos = rl.Vector2{950, 50}
steps := (GRAPH_MAX_VAL_X - GRAPH_MIN_VAL_X) / GRAPH_STEP + 1
car.engine_state.graph.opts = GraphOpts {
min_x = i32(car.engine_state.graph.pos.x) + GRAPH_PADDING,
max_x = i32(
car.engine_state.graph.pos.x,
) + GRAPH_WIDTH - GRAPH_PADDING,
min_y = i32(car.engine_state.graph.pos.y) + GRAPH_PADDING,
max_y = i32(
car.engine_state.graph.pos.y,
) + GRAPH_HEIGHT - GRAPH_PADDING,
}
car.engine_state.graph.points = make([]rl.Vector2, steps)
for i in 0 ..< steps {
rpm := f32(GRAPH_MIN_VAL_X + i * GRAPH_STEP)
draw_coords := get_graph_coords(
rpm,
&car.engine_state.engine,
car.engine_state.graph.opts,
)
car.engine_state.graph.points[i] = draw_coords
}
car.engine_state.rpm = MIN_RPM
car.drivetrain = Drivetrain {
ratios = {3.4, 2.1, 1.5, 1.15, 0.9},
final_ratio = 3.3,
wheel_radius = 0.33,
efficiency = 0.85,
gear = 0,
}
car.forward = rl.Vector3{0, 0, 1}
return car
}
init_engine :: proc() -> Engine {
engine: Engine
dyno_points := []RPMSample {
RPMSample{rpm = 1000, torq = 190},
RPMSample{rpm = 2000, torq = 205},
RPMSample{rpm = 2700, torq = 220},
RPMSample{rpm = 2850, torq = 230},
RPMSample{rpm = 2950, torq = 240},
RPMSample{rpm = 3100, torq = 250},
RPMSample{rpm = 3300, torq = 260},
RPMSample{rpm = 4050, torq = 390},
RPMSample{rpm = 4500, torq = 400},
RPMSample{rpm = 5000, torq = 390},
RPMSample{rpm = 5500, torq = 361},
RPMSample{rpm = 6000, torq = 320},
RPMSample{rpm = 6500, torq = 280},
RPMSample{rpm = 7000, torq = 240},
}
engine.idle_rpm = 1000
engine.max_rpm = 7000
engine.bypass = 25
engine.fric = 0.025
engine.inertia = 0.4
engine.hyst = 150
lut := make([]f32, (MAX_RPM - MIN_RPM) / RPM_STEP + 1)
for i in 0 ..< len(lut) {
rpm := f32(MIN_RPM + i * RPM_STEP)
lut[i] = torq_at_rpm(dyno_points, rpm)
}
engine.rpm_lut = lut
return engine
}
GraphOpts :: struct {
min_x: i32,
min_y: i32,
max_x: i32,
max_y: i32,
}
draw_graph :: proc(state: ^EngineState) {
pos := state.graph.pos
rl.DrawRectangle(
i32(pos.x),
i32(pos.y),
GRAPH_WIDTH,
GRAPH_HEIGHT,
rl.WHITE,
)
rl.DrawRectangleLines(
i32(pos.x),
i32(pos.y),
GRAPH_WIDTH,
GRAPH_HEIGHT,
rl.BLACK,
)
rl.DrawLine(
state.graph.opts.min_x,
state.graph.opts.min_y,
state.graph.opts.min_x,
state.graph.opts.max_y,
rl.BLACK,
)
rl.DrawLine(
state.graph.opts.min_x,
state.graph.opts.max_y,
state.graph.opts.max_x,
state.graph.opts.max_y,
rl.BLACK,
)
for p_pos in state.graph.points {
rl.DrawCircle(i32(p_pos.x), i32(p_pos.y), 2, rl.BLUE)
}
{
rpm := state.rpm
draw_coords := get_graph_coords(rpm, &state.engine, state.graph.opts)
rl.DrawCircle(i32(draw_coords.x), i32(draw_coords.y), 2, rl.RED)
}
}
get_graph_coords :: proc(
rpm: f32,
engine: ^Engine,
opts: GraphOpts,
) -> rl.Vector2 {
torq := torq_from_lut(rpm, engine)
x := map_to_range(
f32(GRAPH_MIN_VAL_X),
f32(GRAPH_MAX_VAL_X),
f32(opts.min_x),
f32(opts.max_x),
rpm,
)
y := map_to_range(
f32(GRAPH_MIN_VAL_Y),
f32(GRAPH_MAX_VAL_Y),
f32(opts.max_y),
f32(opts.min_y),
torq,
)
return rl.Vector2{x, y}
}
map_to_range :: proc(
in_min, in_max: f32,
out_min, out_max: f32,
val: f32,
) -> f32 {
t := (val - in_min) / (in_max - in_min)
return out_min + t * (out_max - out_min)
}
torq_from_lut :: proc(rpm: f32, engine: ^Engine) -> f32 {
x := (rpm - f32(MIN_RPM)) / f32(RPM_STEP)
i := int(x)
t := x - f32(i)
if i < 0 {
return engine.rpm_lut[0]
}
if i >= len(engine.rpm_lut) - 1 {
return engine.rpm_lut[len(engine.rpm_lut) - 1]
}
return linalg.lerp(engine.rpm_lut[i], engine.rpm_lut[i + 1], t)
}
hermite :: proc(y0, y1: f32, m0, m1: f32, x0, x1: f32, x: f32) -> f32 {
h := x1 - x0
t := (x - x0) / h
t2 := t * t
t3 := t2 * t
h00 := 2 * t3 - 3 * t2 + 1
h10 := t3 - 2 * t2 + t
h01 := -2 * t3 + 3 * t2
h11 := t3 - t2
return h00 * y0 + h10 * h * m0 + h01 * y1 + h11 * h * m1
}
calc_slope :: proc(points: []RPMSample, i: int) -> f32 {
if i == 0 {
return(
(points[1].torq - points[0].torq) /
(points[1].rpm - points[0].rpm) \
)
}
if i == len(points) - 1 {
return(
(points[i].torq - points[i - 1].torq) /
(points[i].rpm - points[i - 1].rpm) \
)
}
return(
(points[i + 1].torq - points[i - 1].torq) /
(points[i + 1].rpm - points[i - 1].rpm) \
)
}
torq_at_rpm :: proc(points: []RPMSample, rpm: f32) -> f32 {
if rpm <= points[0].rpm {
return points[0].torq
}
if rpm >= points[len(points) - 1].rpm {
return points[len(points) - 1].torq
}
for i in 0 ..< len(points) - 1 {
p0 := points[i]
p1 := points[i + 1]
if rpm >= p0.rpm && rpm <= p1.rpm {
m0 := calc_slope(points, i)
m1 := calc_slope(points, i + 1)
return hermite(p0.torq, p1.torq, m0, m1, p0.rpm, p1.rpm, rpm)
}
}
return 0
}
draw_gauges :: proc(car: ^Car) {
base_pos := rl.Vector2{1050, 600}
offset := rl.Vector2{40, 0}
text_offset := rl.Vector2{10, 102}
current_pos := base_pos
gear_offset := rl.Vector2{0, 0}
speed_offset := rl.Vector2{40, 0}
throttle_offset := rl.Vector2{80, 0}
rpm_offset := rl.Vector2{120, 0}
torque_offset := rl.Vector2{160, 0}
draw_val(current_pos + text_offset, f32(car.drivetrain.gear))
current_pos += offset
draw_bar(current_pos, car.speed * 3.6 * 100 / 300, rl.GREEN)
draw_val(current_pos + text_offset, car.speed * 3.6)
current_pos += offset
draw_bar(current_pos, car.engine_state.throttle * 100, rl.GREEN)
draw_val(current_pos + text_offset, car.engine_state.throttle * 100)
current_pos += offset
draw_bar(current_pos, car.engine_state.rpm * 100 / 10000, rl.RED)
draw_val(current_pos + text_offset, car.engine_state.rpm)
current_pos += offset
draw_bar(current_pos, car.engine_state.torque * 100 / 600, rl.BLUE)
draw_val(current_pos + text_offset, car.engine_state.torque)
}
draw_bar :: proc(pos: rl.Vector2, percentage: f32, color: rl.Color) {
bar_size_x: i32 = 20
bar_size_y: i32 = 100
rl.DrawRectangle(i32(pos.x), i32(pos.y), bar_size_x, bar_size_y, rl.GRAY)
target_h := i32(percentage * f32(bar_size_y) / 100)
rl.DrawRectangle(
i32(pos.x),
i32(pos.y) + bar_size_y - target_h,
bar_size_x,
target_h,
color,
)
rl.DrawRectangleLines(
i32(pos.x),
i32(pos.y),
bar_size_x,
bar_size_y,
rl.BLACK,
)
}
draw_val :: proc(pos: rl.Vector2, val: f32) {
str := fmt.ctprintf("%v", i32(val))
str_len := rl.MeasureText(str, 15)
rl.DrawText(str, i32(pos.x) - str_len / 2, i32(pos.y), 15, rl.WHITE)
}
draw_stats :: proc(car: ^Car) {
draw_graph(&car.engine_state)
}
+156
View File
@@ -0,0 +1,156 @@
package main
import math "core:math"
import rl "vendor:raylib"
Dyno_Point :: struct {
rpm: f32,
hp: f32,
}
LUT_STEP :: 10
MAX_RPM :: 8000
LUT_SIZE :: (MAX_RPM / LUT_STEP) + 1
Engine :: struct {
hp_lut: [LUT_SIZE]f32,
}
EngineState :: struct {
throttle: f32,
rpm: f32,
torque: f32,
engine: Engine,
}
draw_gauges :: proc(engine: ^EngineState) {
base_pos := rl.Vector2{1150, 600}
throttle_offset := rl.Vector2{}
rpm_offset := rl.Vector2{40, 0}
torque_offset := rl.Vector2{80, 0}
draw_bar(base_pos + throttle_offset, engine.throttle, rl.GREEN)
draw_bar(base_pos + rpm_offset, engine.rpm * 100 / 10000, rl.RED)
draw_bar(base_pos + torque_offset, engine.torque * 100 / 10000, rl.BLUE)
}
draw_bar :: proc(pos: rl.Vector2, percentage: f32, color: rl.Color) {
bar_size_x: i32 = 20
bar_size_y: i32 = 100
rl.DrawRectangle(i32(pos.x), i32(pos.y), bar_size_x, bar_size_y, rl.GRAY)
target_h := i32(percentage * f32(bar_size_y) / 100)
rl.DrawRectangle(
i32(pos.x),
i32(pos.y) + bar_size_y - target_h,
bar_size_x,
target_h,
color,
)
rl.DrawRectangleLines(
i32(pos.x),
i32(pos.y),
bar_size_x,
bar_size_y,
rl.BLACK,
)
}
graph_data :: struct {
min_val: f32,
max_val: f32,
steps: f32,
}
graph_value :: distinct [2]f32
bake_engine_lut :: proc(points: []Dyno_Point) -> Engine {
data: Engine
for step_idx in 0 ..< LUT_SIZE {
current_rpm := f32(step_idx * LUT_STEP)
if current_rpm <= points[0].rpm {
data.hp_lut[step_idx] = points[0].hp
continue
}
if current_rpm >= points[len(points) - 1].rpm {
data.hp_lut[step_idx] = points[len(points) - 1].hp
continue
}
for i in 0 ..< len(points) - 1 {
p0 := points[i]
p1 := points[i + 1]
if current_rpm >= p0.rpm && current_rpm <= p1.rpm {
t := (current_rpm - p0.rpm) / (p1.rpm - p0.rpm)
t_smooth := t * t * (3.0 - 2.0 * t)
data.hp_lut[step_idx] = math.lerp(p0.hp, p1.hp, t_smooth)
break
}
}
}
return data
}
get_horsepower :: proc(engine: ^Engine, rpm: f32) -> f32 {
clamped_rpm := math.clamp(rpm, 0, f32(MAX_RPM))
lut_index_f := clamped_rpm / f32(LUT_STEP)
base_idx := int(lut_index_f)
next_idx := min(base_idx + 1, LUT_SIZE - 1)
t := lut_index_f - f32(base_idx)
return math.lerp(engine.hp_lut[base_idx], engine.hp_lut[next_idx], t)
}
compute_graph_data :: proc(
data: graph_data,
engine: ^Engine,
) -> [dynamic]graph_value {
step := (data.max_val - data.min_val) / data.steps
values: [dynamic]graph_value
for i in 0 ..< data.steps {
rpm := data.max_val + step * i
append(&values, graph_value{rpm, get_horsepower(engine, rpm)})
}
return values
}
draw_graph :: proc(pos: rl.Vector2, data: [dynamic]graph_value) {
rl.DrawRectangle(i32(pos.x), i32(pos.y), 400, 200, rl.WHITE)
values := len(data)
step := 380 / values
offset := rl.Vector2{20, 180}
for val in data {
point_val := map_value(0, 600, 160, val[1])
rl.DrawCircle(
i32(pos.x + offset.x) + i32(step),
i32(pos.y + offset.y) - i32(point_val),
5,
rl.BLUE,
)
}
}
map_value :: proc(min_val_in, max_val_in, max_val_out, val: f32) -> f32 {
range_in := max_val_in - min_val_in
rem := val - min_val_in
scale := max_val_out / range_in
return val * rem
}
+76
View File
@@ -0,0 +1,76 @@
package main
import "core:math"
import "core:math/rand"
import rl "vendor:raylib"
SAMPLE_RATE :: 44100
CYLINDERS :: 4
GAIN :: 0.3
SUB_FRAMES :: 1024
EngineSound :: struct {
stream: rl.AudioStream,
phase: f32,
exhaust_lp: f32,
body_lp: f32,
buf: [SUB_FRAMES]i16,
}
engine_sound_init :: proc() -> EngineSound {
es: EngineSound
rl.SetAudioStreamBufferSizeDefault(SUB_FRAMES)
es.stream = rl.LoadAudioStream(SAMPLE_RATE, 16, 1)
rl.PlayAudioStream(es.stream)
return es
}
engine_sound_update :: proc(es: ^EngineSound, rpm: f32) {
if !rl.IsAudioStreamProcessed(es.stream) do return
f := rpm / 60.0 * (f32(CYLINDERS) / 2.0)
phase_step := math.TAU * f / f32(SAMPLE_RATE)
for i in 0 ..< SUB_FRAMES {
// Exhaust
harm1 := math.sin_f32(es.phase) // cyllinder main
harm2 := math.sin_f32(es.phase * 2.0) * 0.5 // exhaust
harm_sub := math.sin_f32(es.phase * 0.5) * 0.3 // cyllinder pop
exhaust_raw := harm1 + harm2 + harm_sub
exhaust_clipped := math.clamp(exhaust_raw * 1.4, -1.0, 1.0)
alpha_exhaust: f32 = 0.35
es.exhaust_lp += alpha_exhaust * (exhaust_clipped - es.exhaust_lp)
exhaust_sound := es.exhaust_lp * 0.6
// Body
mechanical_noise := (rand.float32() * 2.0 - 1.0) * 0.1
body_raw := harm1 + mechanical_noise
alpha_body: f32 = 0.08
es.body_lp += alpha_body * (body_raw - es.body_lp)
body_sound := es.body_lp * 0.4
// Final
final_sample := (exhaust_sound + body_sound) * GAIN
final_clamped := math.clamp(final_sample, -1.0, 1.0)
es.buf[i] = i16(final_clamped * 32767.0)
es.phase += phase_step
if es.phase >= math.TAU {
es.phase -= math.TAU
}
}
rl.UpdateAudioStream(es.stream, &es.buf[0], i32(SUB_FRAMES))
}
engine_sound_unload :: proc(es: ^EngineSound) {
rl.UnloadAudioStream(es.stream)
}
+83
View File
@@ -0,0 +1,83 @@
package main
import rl "vendor:raylib"
import "core:fmt"
GameState :: struct {
camera: rl.Camera3D,
cam_offset: rl.Vector3,
gfx_state: GfxState,
car: Car,
sound: EngineSound,
}
init_game_state :: proc() -> GameState {
state: GameState
state.gfx_state = gfx_init()
state.cam_offset = rl.Vector3{0, 2, -6}
state.car = init_car()
state.sound = engine_sound_init()
state.camera = rl.Camera3D {
position = state.car.pos + state.cam_offset,
target = state.car.pos,
up = {0, 1, 0},
fovy = 45.0,
projection = .PERSPECTIVE,
}
return state
}
cleanup_game_state :: proc(state: ^GameState) {
gfx_unload(&state.gfx_state)
engine_sound_unload(&state.sound)
}
update_game_state :: proc(state: ^GameState) {
if rl.Vector3Length(state.car.vel) != 0 {
state.camera.position =
state.car.pos +
rl.Vector3Normalize(state.car.vel) * -7 +
rl.Vector3{0, 2, 0}
// } else {
// state.camera.position = state.car.pos + state.cam_offset
}
// state.camera.position = state.car.pos + state.cam_offset
state.camera.target = state.car.pos + rl.Vector3{0, 1, 0}
update_car(&state.car)
engine_sound_update(&state.sound, state.car.engine_state.rpm)
cam_pos := state.camera.position
gfx_update(&state.gfx_state, &cam_pos)
}
draw_game_state :: proc(state: ^GameState) {
rl.ClearBackground(rl.GetColor(0x181825FF)) // Dark background
rl.BeginMode3D(state.camera)
{
draw_3d(state)
}
rl.EndMode3D()
draw_ui(state)
}
draw_3d :: proc(state: ^GameState) {
rl.DrawGrid(200, 2.0)
gfx_draw_car(&state.gfx_state, state.car.pos, state.car.wheel_yaw)
}
draw_ui :: proc(state: ^GameState) {
draw_gauges(&state.car)
draw_stats(&state.car)
}
+58
View File
@@ -0,0 +1,58 @@
package main
import "core:math"
import rl "vendor:raylib"
GfxState :: struct {
shader: rl.Shader,
shader_loc: i32,
car: rl.Model,
}
gfx_init :: proc() -> GfxState {
state: GfxState
state.car = rl.LoadModel("./assets/3rdparty/car.glb")
state.shader = rl.LoadShader(
"src/shader/nfs_car.vs",
"src/shader/nfs_car.fs",
)
light_dir_loc := rl.GetShaderLocation(state.shader, "lightDir")
light_col_loc := rl.GetShaderLocation(state.shader, "lightColor")
ambient_col_loc := rl.GetShaderLocation(state.shader, "ambientColor")
state.shader_loc = rl.GetShaderLocation(state.shader, "viewPos")
// Set Static Lighting Values (Night/Arcade Theme)
light_dir := rl.Vector3{-0.5, -1.0, -0.5} // Angled overhead light
light_color := rl.Vector3{1.2, 1.1, 0.9} // Slightly warm sunlight/streetlight
ambient_col := rl.Vector3{0.15, 0.15, 0.25} // Cool dark blue ambient for shadows
rl.SetShaderValue(state.shader, light_dir_loc, &light_dir, .VEC3)
rl.SetShaderValue(state.shader, light_col_loc, &light_color, .VEC3)
rl.SetShaderValue(state.shader, ambient_col_loc, &ambient_col, .VEC3)
// Assign shader to model materials
for i in 0 ..< state.car.materialCount {
state.car.materials[i].shader = state.shader
}
return state
}
gfx_unload :: proc(state: ^GfxState) {
rl.UnloadModel(state.car)
rl.UnloadShader(state.shader)
}
gfx_draw_car :: proc(state: ^GfxState, pos: rl.Vector3, yaw: f32) {
angle_degrees := yaw * (180 / math.PI)
rotation_axis := rl.Vector3{0, 1, 0}
rl.DrawModelEx(state.car, pos, rotation_axis, angle_degrees, 1.0, rl.WHITE)
}
gfx_update :: proc(state: ^GfxState, cam_pos: ^rl.Vector3) {
rl.SetShaderValue(state.shader, state.shader_loc, cam_pos, .VEC3)
}
+29
View File
@@ -0,0 +1,29 @@
package main
import "core:fmt"
import math "core:math"
import rl "vendor:raylib"
main :: proc() {
rl.InitWindow(1280, 720, "Odin + Raylib - NFS Style Lighting")
defer rl.CloseWindow()
rl.SetTargetFPS(140)
rl.InitAudioDevice()
defer rl.CloseAudioDevice()
state := init_game_state()
defer cleanup_game_state(&state)
for !rl.WindowShouldClose() {
update_game_state(&state)
rl.BeginDrawing()
{
draw_game_state(&state)
}
rl.EndDrawing()
}
}
+40
View File
@@ -0,0 +1,40 @@
#version 330
in vec2 fragTexCoord;
in vec3 fragNormal;
in vec3 fragFragPos;
in vec4 fragColor;
out vec4 finalColor;
uniform sampler2D texture0;
uniform vec4 colDiffuse;
// Custom Uniforms for PS2-era arcade look
uniform vec3 lightDir; // Direction light comes from (e.g. overhead sun)
uniform vec3 lightColor; // Color of sun/streetlights (e.g. warm white/yellow)
uniform vec3 ambientColor; // Shadow tint (e.g. dark blue/purple night sky)
uniform vec3 viewPos; // Camera position for specular shine
void main() {
// 1. Sample base texture and material tint
vec4 texColor = texture(texture0, fragTexCoord) * colDiffuse * fragColor;
vec3 N = normalize(fragNormal);
vec3 L = normalize(-lightDir);
vec3 V = normalize(viewPos - fragFragPos);
// 2. Directional Diffuse (Lambert)
float diff = max(dot(N, L), 0.0);
vec3 diffuse = diff * lightColor;
// 3. Early-2000s Specular Highlight (Blinn-Phong)
vec3 H = normalize(L + V);
float spec = pow(max(dot(N, H), 0.0), 32.0); // 32.0 = medium glossiness
vec3 specular = lightColor * spec * 0.6; // 0.6 = gloss strength
// 4. Combine Ambient + Diffuse + Specular
vec3 lighting = ambientColor + diffuse + specular;
finalColor = vec4(texColor.rgb * lighting, texColor.a);
}
+26
View File
@@ -0,0 +1,26 @@
#version 330
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec3 vertexNormal;
in vec4 vertexColor;
out vec2 fragTexCoord;
out vec3 fragNormal;
out vec3 fragFragPos;
out vec4 fragColor;
uniform mat4 mvp;
uniform mat4 matModel;
uniform mat4 matNormal;
void main() {
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
// Transform normal and position to world space for lighting
fragNormal = normalize(vec3(matNormal * vec4(vertexNormal, 0.0)));
fragFragPos = vec3(matModel * vec4(vertexPosition, 1.0));
gl_Position = mvp * vec4(vertexPosition, 1.0);
}
+14
View File
@@ -0,0 +1,14 @@
#version 330
in vec2 fragTexCoord;
in vec4 fragColor;
out vec4 finalColor;
uniform sampler2D texture0;
uniform vec4 colDiffuse;
void main() {
vec4 texelColor = texture(texture0, fragTexCoord);
finalColor = texelColor * colDiffuse * fragColor;
}
+16
View File
@@ -0,0 +1,16 @@
#version 330
in vec3 vertexPosition;
in vec2 vertexTexCoord;
in vec4 vertexColor;
out vec2 fragTexCoord;
out vec4 fragColor;
uniform mat4 mvp;
void main() {
fragTexCoord = vertexTexCoord;
fragColor = vertexColor;
gl_Position = mvp * vec4(vertexPosition, 1.0);
}