commit 41ec89a732284fda314fbb414807b897e00995d3 Author: Alexey Bagno Date: Fri Aug 14 00:15:10 2026 +0300 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24be7ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +src.bin +.gdbf/ +.gdbinit +build/ diff --git a/.nvim.lua b/.nvim.lua new file mode 100644 index 0000000..adbb17a --- /dev/null +++ b/.nvim.lua @@ -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", + }, +}) diff --git a/.raddbg/alexey b/.raddbg/alexey new file mode 100755 index 0000000..1ed0cd5 --- /dev/null +++ b/.raddbg/alexey @@ -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: "" +} diff --git a/.raddbg/debug b/.raddbg/debug new file mode 100755 index 0000000..29658b9 --- /dev/null +++ b/.raddbg/debug @@ -0,0 +1,8 @@ +// raddbg 0.9.28 project file + +target: +{ + executable: "../build/debug" + working_directory: "../../racing" + enabled: 1 +} diff --git a/assets/3rdparty/car.glb b/assets/3rdparty/car.glb new file mode 100644 index 0000000..8fc4d47 Binary files /dev/null and b/assets/3rdparty/car.glb differ diff --git a/odinfmt.json b/odinfmt.json new file mode 100644 index 0000000..7ffb1ba --- /dev/null +++ b/odinfmt.json @@ -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 +} diff --git a/ols.json b/ols.json new file mode 100644 index 0000000..c488e1b --- /dev/null +++ b/ols.json @@ -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 +} diff --git a/src/car.odin b/src/car.odin new file mode 100644 index 0000000..078867c --- /dev/null +++ b/src/car.odin @@ -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) +} diff --git a/src/engine.odin.bak b/src/engine.odin.bak new file mode 100644 index 0000000..7ecdaf6 --- /dev/null +++ b/src/engine.odin.bak @@ -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 +} diff --git a/src/engine_sound.odin b/src/engine_sound.odin new file mode 100644 index 0000000..58d973b --- /dev/null +++ b/src/engine_sound.odin @@ -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) +} diff --git a/src/game.odin b/src/game.odin new file mode 100644 index 0000000..e83ee9a --- /dev/null +++ b/src/game.odin @@ -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) +} diff --git a/src/graphics.odin b/src/graphics.odin new file mode 100644 index 0000000..9c66c2f --- /dev/null +++ b/src/graphics.odin @@ -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) +} diff --git a/src/main.odin b/src/main.odin new file mode 100644 index 0000000..a5306a3 --- /dev/null +++ b/src/main.odin @@ -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() + } +} diff --git a/src/shader/nfs_car.fs b/src/shader/nfs_car.fs new file mode 100644 index 0000000..91a9e77 --- /dev/null +++ b/src/shader/nfs_car.fs @@ -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); +} diff --git a/src/shader/nfs_car.vs b/src/shader/nfs_car.vs new file mode 100644 index 0000000..7f2a656 --- /dev/null +++ b/src/shader/nfs_car.vs @@ -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); +} diff --git a/src/shader/unlit.fs b/src/shader/unlit.fs new file mode 100644 index 0000000..f1f1931 --- /dev/null +++ b/src/shader/unlit.fs @@ -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; +} diff --git a/src/shader/unlit.vs b/src/shader/unlit.vs new file mode 100644 index 0000000..b0fad17 --- /dev/null +++ b/src/shader/unlit.vs @@ -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); +}