// This file is compiled as part of the `odin.dll` file. It contains the // procs that `game_hot_reload.exe` will call, such as: // // game_init: Sets up the game state // game_update: Run once per frame // game_shutdown: Shuts down game and frees memory // game_memory: Run just before a hot reload, so game.exe has a pointer to the // game's memory. // game_hot_reloaded: Run after a hot reload so that the `g_mem` global variable // can be set to whatever pointer it was in the old DLL. // // Note: When compiled as part of the release executable this whole package is imported as a normal // odin package instead of a DLL. package game import "assets" import "core:c" import "core:fmt" import "core:hash" import "core:log" import "core:math" import "core:math/linalg" import "core:slice" import "game:halfedge" import "game:physics" import "game:physics/bvh" import "game:physics/collision" import "libs:tracy" import "ui" import rl "vendor:raylib" import "vendor:raylib/rlgl" PIXEL_WINDOW_HEIGHT :: 360 Track :: struct { points: [dynamic]rl.Vector3, } World :: struct { player_pos: rl.Vector3, track: Track, physics_scene: physics.Scene, } destroy_world :: proc(world: ^World) { delete(world.track.points) physics.destroy_physics_scene(&world.physics_scene) } Runtime_World :: struct { world: World, pause: bool, solver_state: physics.Solver_State, car_com: rl.Vector3, car_handle: physics.Body_Handle, engine_handle: physics.Engine_Handle, camera_yaw_pitch: rl.Vector2, camera_speed: f32, camera: rl.Camera3D, orbit_camera: Orbit_Camera, camera_mode: Camera_Mode, dt: f32, rewind_simulation: bool, step_simulation: bool, single_step_simulation: bool, } destroy_runtime_world :: proc(runtime_world: ^Runtime_World) { destroy_world(&runtime_world.world) physics.destroy_solver_state(&runtime_world.solver_state) } Car :: struct { pos: rl.Vector3, } SOLVER_CONFIG :: physics.Solver_Config { timestep = 1.0 / 60, gravity = rl.Vector3{0, -9.8, 0}, substreps_minus_one = 8 - 1, } Game_Memory :: struct { assetman: assets.Asset_Manager, runtime_world: Runtime_World, es: Editor_State, ui_context: ui.Context, default_font: rl.Font, editor: bool, preview_bvh: int, preview_node: int, physics_pause: bool, free_cam: bool, } Camera_Mode :: enum { Orbit, Hood, } Track_Edit_State :: enum { // Point selection Select, // Moving points Move, } Move_Axis :: enum { None, X, Y, Z, XZ, XY, YZ, } Editor_State :: struct { world: World, mouse_captured: bool, point_selection: map[int]bool, track_edit_state: Track_Edit_State, move_axis: Move_Axis, total_movement_world: rl.Vector3, initial_point_pos: rl.Vector3, } g_mem: ^Game_Memory get_runtime_world :: proc() -> ^Runtime_World { return &g_mem.runtime_world } get_world :: proc() -> ^World { return g_mem.editor ? &g_mem.es.world : &g_mem.runtime_world.world } get_editor_state :: proc() -> ^Editor_State { return &g_mem.es } game_camera :: proc() -> rl.Camera2D { w := f32(rl.GetScreenWidth()) h := f32(rl.GetScreenHeight()) return { zoom = h / PIXEL_WINDOW_HEIGHT, target = get_world().player_pos.xy, offset = {w / 2, h / 2}, } } camera_rotation_matrix :: proc() -> matrix[3, 3]f32 { return linalg.matrix3_from_euler_angles_xy( get_runtime_world().camera_yaw_pitch.x, get_runtime_world().camera_yaw_pitch.y, ) } camera_forward_vec :: proc() -> rl.Vector3 { rotation_matrix := camera_rotation_matrix() return rotation_matrix * rl.Vector3{0, 0, 1} } game_camera_3d :: proc() -> rl.Camera3D { if g_mem.editor || g_mem.free_cam { return { position = get_world().player_pos, up = {0, 1, 0}, fovy = 60, target = get_world().player_pos + camera_forward_vec(), projection = .PERSPECTIVE, } } return get_runtime_world().camera } ui_camera :: proc() -> rl.Camera2D { return {zoom = f32(rl.GetScreenHeight()) / PIXEL_WINDOW_HEIGHT} } select_track_point :: proc(index: int) { clear(&g_mem.es.point_selection) g_mem.es.point_selection[index] = true } is_point_selected :: proc() -> bool { return len(g_mem.es.point_selection) > 0 } add_track_spline_point :: proc() { forward := camera_rotation_matrix()[2] append(&get_world().track.points, get_world().player_pos + forward) select_track_point(len(&get_world().track.points) - 1) } get_movement_axes :: proc( axis: Move_Axis, out_axes: ^[2]rl.Vector3, out_colors: ^[2]rl.Color, ) -> ( axes: []rl.Vector3, colors: []rl.Color, ) { switch axis { case .None: return out_axes[0:0], {} case .X: out_axes[0] = {1, 0, 0} out_colors[0] = rl.RED return out_axes[0:1], out_colors[0:1] case .Y: out_axes[0] = {0, 1, 0} out_colors[0] = rl.GREEN return out_axes[0:1], out_colors[0:1] case .Z: out_axes[0] = {0, 0, 1} out_colors[0] = rl.BLUE return out_axes[0:1], out_colors[0:1] case .XZ: out_axes[0] = {1, 0, 0} out_axes[1] = {0, 0, 1} out_colors[0] = rl.RED out_colors[1] = rl.BLUE return out_axes[:], out_colors[:] case .XY: out_axes[0] = {1, 0, 0} out_axes[1] = {0, 1, 0} out_colors[0] = rl.RED out_colors[1] = rl.GREEN return out_axes[:], out_colors[:] case .YZ: out_axes[0] = {0, 1, 0} out_axes[1] = {0, 0, 1} out_colors[0] = rl.GREEN out_colors[1] = rl.BLUE return out_axes[:], out_colors[:] } return out_axes[0:0], out_colors[0:0] } update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) { world := &runtime_world.world if !runtime_world.pause { car_model := assets.get_model(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb") car_bounds := rl.GetModelBoundingBox(car_model) runtime_world.car_com = (car_bounds.min + car_bounds.max) / 2 physics.immediate_body( &world.physics_scene, &runtime_world.solver_state, #hash("floor", "fnv32a"), physics.Body_Config { initial_pos = {0, -0.5, 0}, initial_rot = linalg.QUATERNIONF32_IDENTITY, shape = physics.Shape_Box{size = {1000, 1, 1000}}, }, ) physics.immediate_body( &world.physics_scene, &runtime_world.solver_state, #hash("ramp", "fnv32a"), physics.Body_Config { initial_pos = {0, 0, 0}, initial_rot = linalg.quaternion_from_euler_angle_x_f32(-10 * math.RAD_PER_DEG), shape = physics.Shape_Box{size = {5, 1, 100}}, }, ) car_convex := assets.get_convex(&g_mem.assetman, "assets/car_convex.obj") runtime_world.car_handle = physics.immediate_body( &world.physics_scene, &runtime_world.solver_state, #hash("car", "fnv32a"), physics.Body_Config { initial_pos = {0, 4, -10}, initial_rot = linalg.QUATERNIONF32_IDENTITY, // initial_rot = linalg.quaternion_angle_axis( // math.RAD_PER_DEG * 180, // rl.Vector3{0, 0, 1}, // ) * // linalg.quaternion_angle_axis(math.RAD_PER_DEG * 30, rl.Vector3{1, 0, 0}), initial_ang_vel = {0, 0, 0}, shape = physics.Shape_Convex { mesh = car_convex.mesh, center_of_mass = car_convex.center_of_mass, inertia_tensor = auto_cast car_convex.inertia_tensor, }, mass = 1000, }, ) if false { for x in 0 ..< 10 { for y in 0 ..< 10 { physics.immediate_body( &world.physics_scene, &runtime_world.solver_state, hash.fnv32a(slice.to_bytes([]int{(x | y << 8)})), physics.Body_Config { initial_pos = {0, 0.5 + f32(y) * 1.1, f32(x) * 3 + 10}, initial_rot = linalg.QUATERNIONF32_IDENTITY, shape = physics.Shape_Box{size = 1}, mass = 10, }, ) } } } // car_body := physics.get_body(&world.physics_scene, runtime_world.car_handle) // camera := &runtime_world.camera // camera.up = rl.Vector3{0, 1, 0} // camera.fovy = 60 // camera.projection = .PERSPECTIVE // camera.position = physics.body_local_to_world( // car_body, // physics.body_world_to_local( // car_body, // physics.body_local_to_world(car_body, rl.Vector3{1, 0, -2}), // ), // ) // camera.target = physics.get_body(&world.physics_scene, runtime_world.car_handle).x // if runtime_world.camera.position == {} { // runtime_world.camera.position = runtime_world.camera.target - rl.Vector3{10, 0, 10} // } sim_state := physics.get_sim_state(&world.physics_scene) wheel_extent_x_front := f32(1.355) / 2 wheel_extent_x_back := f32(1.345) / 2 wheel_y := f32(-0.4) rest := f32(0.2) natural_frequency := f32(1.2) damping := f32(0.15) radius := f32(0.2888) wheel_front_z := f32(1.35) wheel_back_z := f32(-1.05) wheel_mass := f32(14) pacejka_long := physics.slice_to_pacejka94_long( assets.get_curve_1d(&g_mem.assetman, "assets/tyre_longitudinal.csv"), ) pacejka_lat := physics.slice_to_pacejka94_lat( assets.get_curve_1d(&g_mem.assetman, "assets/tyre_lateral.csv"), ) wheel_fl := physics.immediate_suspension_constraint( &world.physics_scene, &runtime_world.solver_state, #hash("FL", "fnv32a"), { rel_pos = {-wheel_extent_x_front, wheel_y, wheel_front_z}, rel_dir = {0, -1, 0}, radius = radius, rest = rest, natural_frequency = natural_frequency, damping = damping, body = runtime_world.car_handle, mass = wheel_mass, pacejka_lat = pacejka_lat, pacejka_long = pacejka_long, }, ) wheel_fr := physics.immediate_suspension_constraint( &world.physics_scene, &runtime_world.solver_state, #hash("FR", "fnv32a"), { rel_pos = {wheel_extent_x_front, wheel_y, wheel_front_z}, rel_dir = {0, -1, 0}, radius = radius, rest = rest, natural_frequency = natural_frequency, damping = damping, body = runtime_world.car_handle, mass = wheel_mass, pacejka_lat = pacejka_lat, pacejka_long = pacejka_long, }, ) wheel_rl := physics.immediate_suspension_constraint( &world.physics_scene, &runtime_world.solver_state, #hash("RL", "fnv32a"), { rel_pos = {-wheel_extent_x_back, wheel_y, wheel_back_z}, rel_dir = {0, -1, 0}, radius = radius, rest = rest, natural_frequency = natural_frequency, damping = damping, body = runtime_world.car_handle, mass = wheel_mass, pacejka_lat = pacejka_lat, pacejka_long = pacejka_long, }, ) wheel_rr := physics.immediate_suspension_constraint( &world.physics_scene, &runtime_world.solver_state, #hash("RR", "fnv32a"), { rel_pos = {wheel_extent_x_back, wheel_y, wheel_back_z}, rel_dir = {0, -1, 0}, radius = radius, rest = rest, natural_frequency = natural_frequency, damping = damping, body = runtime_world.car_handle, mass = wheel_mass, pacejka_lat = pacejka_lat, pacejka_long = pacejka_long, }, ) runtime_world.engine_handle = 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", ), lowest_rpm = 1100, rev_limit_rpm = 7800, rev_limit_interval = 0.025, inertia = 0.264 * 0.5, internal_friction = 0.01, 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.1, }, }, ) drive_wheels := []physics.Suspension_Constraint_Handle{wheel_rl, wheel_rr} turn_wheels := []physics.Suspension_Constraint_Handle{wheel_fl, wheel_fr} front_wheels := turn_wheels back_wheels := drive_wheels DRIVE_IMPULSE :: 3000 BRAKE_IMPULSE :: 10 TURN_ANGLE :: -f32(50) * math.RAD_PER_DEG // 68% front, 32% rear BRAKE_BIAS :: f32(0.68) gas_input := rl.GetGamepadAxisMovement(0, .RIGHT_TRIGGER) * 0.5 + 0.5 brake_input := rl.GetGamepadAxisMovement(0, .LEFT_TRIGGER) * 0.5 + 0.5 if rl.IsKeyDown(.S) && !g_mem.free_cam { brake_input = 1 } if rl.IsKeyDown(.W) && !g_mem.free_cam { gas_input = 1 } for wheel_handle in front_wheels { wheel := physics.get_suspension_constraint(sim_state, wheel_handle) wheel.brake_impulse = brake_input * BRAKE_BIAS * BRAKE_IMPULSE } for wheel_handle in back_wheels { wheel := physics.get_suspension_constraint(sim_state, wheel_handle) wheel.brake_impulse = brake_input * (1.0 - BRAKE_BIAS) * BRAKE_IMPULSE } engine := physics.get_engine(sim_state, runtime_world.engine_handle) engine.throttle = gas_input if rl.IsKeyPressed(.LEFT_SHIFT) || rl.IsGamepadButtonPressed(0, .RIGHT_FACE_DOWN) { engine.gear += 1 } if rl.IsKeyPressed(.LEFT_CONTROL) || rl.IsGamepadButtonPressed(0, .RIGHT_FACE_LEFT) { engine.gear -= 1 } car_body := physics.get_body(sim_state, runtime_world.car_handle) turn_vel_correction := clamp(30.0 / linalg.length(car_body.v), 0, 1) turn_input := rl.GetGamepadAxisMovement(0, .LEFT_X) if abs(turn_input) < GAMEPAD_DEADZONE { turn_input = 0 } if rl.IsKeyDown(.A) && !g_mem.free_cam { turn_input = -1 } if rl.IsKeyDown(.D) && !g_mem.free_cam { turn_input = 1 } for wheel_handle in turn_wheels { wheel := physics.get_suspension_constraint(sim_state, wheel_handle) wheel.turn_angle = TURN_ANGLE * turn_vel_correction * turn_input } runtime_world.dt = dt should_single_step := rl.IsKeyPressed(.PERIOD) runtime_world.rewind_simulation = rl.IsKeyPressed(.COMMA) runtime_world.step_simulation = !g_mem.physics_pause || should_single_step runtime_world.single_step_simulation = should_single_step } } Orbit_Camera :: struct { target: rl.Vector3, yaw, pitch: f32, distance: f32, } GAMEPAD_DEADZONE :: f32(0.07) orbit_camera_update :: proc(camera: ^Orbit_Camera) { camera.target = physics.get_body(physics.get_sim_state(&get_runtime_world().world.physics_scene), get_runtime_world().car_handle).x gamepad_delta := rl.Vector2 { rl.GetGamepadAxisMovement(0, .RIGHT_X), rl.GetGamepadAxisMovement(0, .RIGHT_Y), } if abs(gamepad_delta.x) < GAMEPAD_DEADZONE { gamepad_delta.x = 0 } if abs(gamepad_delta.y) < GAMEPAD_DEADZONE { gamepad_delta.y = 0 } mouse_delta := rl.GetMouseDelta() MOUSE_SENSE :: 0.01 GAMEPAD_SENSE :: 1 final_sense: f32 delta: rl.Vector2 if linalg.length2(mouse_delta) > linalg.length2(gamepad_delta) { final_sense = MOUSE_SENSE delta = mouse_delta } else { final_sense = GAMEPAD_SENSE * rl.GetFrameTime() delta = gamepad_delta } rl.HideCursor() camera.yaw += delta.x * final_sense camera.pitch += delta.y * final_sense camera.pitch = math.clamp(camera.pitch, -math.PI / 2.0 + 0.0001, math.PI / 2.0 - 0.0001) } orbit_camera_to_rl :: proc(camera: Orbit_Camera) -> rl.Camera3D { result: rl.Camera3D result.target = camera.target rotation := linalg.matrix3_rotate(-camera.yaw, rl.Vector3{0, 1, 0}) * linalg.matrix3_rotate(-camera.pitch, rl.Vector3{1, 0, 0}) // rotation = linalg.transpose(rotation) position := rotation * rl.Vector3{0, 0, 1} position *= camera.distance result.position = result.target + position result.up = rl.Vector3{0, 1, 0} result.fovy = 60 return result } update :: proc() { tracy.Zone() ui.begin(&g_mem.ui_context) if rl.IsKeyPressed(.TAB) { g_mem.editor = !g_mem.editor } if rl.IsKeyPressed(.F1) { g_mem.free_cam = !g_mem.free_cam g_mem.es.world.player_pos = g_mem.runtime_world.camera.position } if rl.IsKeyPressed(.F2) && !g_mem.free_cam { cam_mode := &get_runtime_world().camera_mode switch cam_mode^ { case .Orbit: cam_mode^ = .Hood case .Hood: cam_mode^ = .Orbit } } dt := rl.GetFrameTime() // Debug BVH traversal mesh_bvh := assets.get_bvh(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb") if rl.IsKeyDown(.LEFT_SHIFT) { if g_mem.preview_bvh >= 0 && g_mem.preview_bvh < len(mesh_bvh.bvhs) { b := mesh_bvh.bvhs[g_mem.preview_bvh] node := &b.nodes[g_mem.preview_node] if !bvh.is_leaf_node(node^) { if rl.IsKeyPressed(.LEFT_BRACKET) { g_mem.preview_node = int(node.child_or_prim_start) } else if rl.IsKeyPressed(.RIGHT_BRACKET) { g_mem.preview_node = int(node.child_or_prim_start + 1) } else if rl.IsKeyPressed(.P) { g_mem.preview_node = 0 } } } } else { if rl.IsKeyPressed(.LEFT_BRACKET) { g_mem.preview_bvh -= 1 g_mem.preview_node = 0 } if rl.IsKeyPressed(.RIGHT_BRACKET) { g_mem.preview_bvh += 1 g_mem.preview_node = 0 } } if rl.IsKeyPressed(.SPACE) { g_mem.physics_pause = !g_mem.physics_pause } if g_mem.editor { update_editor(get_editor_state()) } else { if g_mem.free_cam { update_free_look_camera(get_editor_state()) } else { switch get_runtime_world().camera_mode { case .Orbit: orbit_camera_update(&get_runtime_world().orbit_camera) get_runtime_world().camera = orbit_camera_to_rl(get_runtime_world().orbit_camera) case .Hood: car := physics.get_body( physics.get_sim_state(&get_runtime_world().world.physics_scene), get_runtime_world().car_handle, ) cam: rl.Camera3D cam.position = physics.body_local_to_world(car, physics.Vec3{0, 0.9, 2}) cam.target = cam.position + physics.body_local_to_world_vec(car, physics.Vec3{0, 0, 1}) cam.up = physics.body_local_to_world_vec(car, physics.Vec3{0, 1, 0}) cam.fovy = 60 cam.projection = .PERSPECTIVE get_runtime_world().camera = cam } } update_runtime_world(get_runtime_world(), dt) } } catmull_rom_coefs :: proc( v0, v1, v2, v3: rl.Vector3, alpha, tension: f32, ) -> ( a, b, c, d: rl.Vector3, ) { t01 := math.pow(linalg.distance(v0, v1), alpha) t12 := math.pow(linalg.distance(v1, v2), alpha) t23 := math.pow(linalg.distance(v2, v3), alpha) m1 := (1.0 - tension) * (v2 - v1 + t12 * ((v1 - v0) / t01 - (v2 - v0) / (t01 + t12))) m2 := (1.0 - tension) * (v2 - v1 + t12 * ((v3 - v2) / t23 - (v3 - v1) / (t12 + t23))) a = 2.0 * (v1 - v2) + m1 + m2 b = -3.0 * (v1 - v2) - m1 - m1 - m2 c = m1 d = v1 return } catmull_rom :: proc(a, b, c, d: rl.Vector3, t: f32) -> rl.Vector3 { t2 := t * t t3 := t2 * t return a * t3 + b * t2 + c * t + d } draw :: proc() { tracy.Zone() rl.BeginDrawing() defer rl.EndDrawing() rl.ClearBackground(rl.GRAY) runtime_world := get_runtime_world() world := get_world() dt := runtime_world.dt camera := game_camera_3d() points := &world.track.points interpolated_points := calculate_spline_interpolated_points(points[:], context.temp_allocator) // collision, segment_idx := raycast_spline_tube( // interpolated_points, // rl.GetScreenToWorldRay(rl.GetMousePosition(), camera), // ) sim_state := physics.get_sim_state(&world.physics_scene) car_body := physics.get_body(sim_state, runtime_world.car_handle) car_model := assets.get_model(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb") mesh_col: bvh.Collision hit_mesh_idx := -1 rl_ray := rl.GetScreenToWorldRay(rl.GetMousePosition(), camera) ray := bvh.Ray { origin = rl_ray.position, dir = rl_ray.direction, } _ = ray { rl.BeginMode3D(camera) defer rl.EndMode3D() // rl.DrawGrid(100, 1) physics.draw_debug_scene(&world.physics_scene) physics.draw_debug_ui(&g_mem.ui_context, &world.physics_scene, SOLVER_CONFIG) box1_mat := linalg.Matrix4f32(1) box1_mat = linalg.matrix4_rotate(45 * math.RAD_PER_DEG, rl.Vector3{0, 1, 0}) * box1_mat box2_mat := linalg.Matrix4f32(1) box2_mat = linalg.matrix4_translate(rl.Vector3{0.0, 0.2, 0}) * box2_mat box2_mat = linalg.matrix4_rotate(45 * math.RAD_PER_DEG, rl.Vector3{0, 0, 1}) * box2_mat // box2_mat = linalg.matrix4_rotate(f32(rl.GetTime()), rl.Vector3{0, -1, 0}) * box2_mat box2_mat = linalg.matrix4_translate(rl.Vector3{0.0, 0, 0}) * box2_mat box2_mat = linalg.matrix4_rotate(f32(rl.GetTime()) * 0.1, rl.Vector3{0, 1, 0}) * box2_mat box1, box2 := collision.Box { pos = 0, rad = 0.5, }, collision.Box { pos = 0, rad = 0.5, } box1_convex := collision.box_to_convex(box1, context.temp_allocator) box2_convex := collision.box_to_convex(box2, context.temp_allocator) halfedge.transform_mesh(&box1_convex, box1_mat) halfedge.transform_mesh(&box2_convex, box2_mat) if !g_mem.editor { car_matrix := rl.QuaternionToMatrix(car_body.q) car_model.transform = car_matrix if !runtime_world.pause { if runtime_world.rewind_simulation { world.physics_scene.simulation_state_index = physics.get_prev_sim_state_index( &world.physics_scene, ) } else { physics.simulate( &world.physics_scene, &runtime_world.solver_state, SOLVER_CONFIG, dt, commit = runtime_world.step_simulation, step_mode = g_mem.physics_pause ? physics.Step_Mode.Single : physics.Step_Mode.Accumulated_Time, ) } } rl.DrawModel(car_model, physics.body_get_shape_pos(car_body), 1, rl.WHITE) } { // Debug draw spline road { rlgl.EnableWireMode() defer rlgl.DisableWireMode() rlgl.Color3f(1, 0, 0) debug_draw_spline(interpolated_points) debug_draw_spline_mesh(interpolated_points) } if g_mem.editor { es := &g_mem.es switch es.track_edit_state { case .Select: case .Move: rlgl.Begin(rlgl.LINES) defer rlgl.End() axes_buf: [2]rl.Vector3 colors_buf: [2]rl.Color axes, colors := get_movement_axes(es.move_axis, &axes_buf, &colors_buf) for v in soa_zip(axis = axes, color = colors) { rlgl.Color4ub(v.color.r, v.color.g, v.color.b, v.color.a) start, end := es.initial_point_pos - v.axis * 100000, es.initial_point_pos + v.axis * 100000 rlgl.Vertex3f(start.x, start.y, start.z) rlgl.Vertex3f(end.x, end.y, end.z) } } } // if mesh_col.hit { // mesh := car_model.meshes[hit_mesh_idx] // vertices := (cast([^]rl.Vector3)mesh.vertices)[:mesh.vertexCount] // indices := mesh.indices[:mesh.triangleCount * 3] // car_halfedge := halfedge.mesh_from_vertex_index_list(vertices, indices, 3, context.temp_allocator) // // face_idx := halfedge.Face_Index(mesh_col.prim) // face := car_halfedge.faces[face_idx] // first_edge_idx := face.edge // // first := true // cur_edge_idx := first_edge_idx // for first || cur_edge_idx != first_edge_idx { // first = false // edge := car_halfedge.edges[cur_edge_idx] // cur_edge_idx = edge.next // // if edge.twin >= 0 { // twin_edge := car_halfedge.edges[edge.twin] // face := twin_edge.face // // i1, i2, i3 := indices[face * 3 + 0], indices[face * 3 + 1], indices[face * 3 + 2] // v1, v2, v3 := vertices[i1], vertices[i2], vertices[i3] // // rl.DrawTriangle3D(v1, v2, v3, rl.RED) // } // } // } // } } { ui.end(&g_mem.ui_context) rl.BeginMode2D(ui_camera()) defer rl.EndMode2D() rl.DrawFPS(0, 0) if g_mem.editor { rl.DrawText("Editor", 5, 5, 8, rl.ORANGE) rl.DrawText( fmt.ctprintf( "mesh: %v, aabb tests: %v, tri tests: %v", hit_mesh_idx, mesh_col.aabb_tests, mesh_col.triangle_tests, ), 5, 32, 8, rl.ORANGE, ) rl.DrawText( fmt.ctprintf("bvh: %v, node: %v", g_mem.preview_bvh, g_mem.preview_node), 5, 48, 8, rl.ORANGE, ) switch g_mem.es.track_edit_state { case .Select: case .Move: rl.DrawText( fmt.ctprintf("%v %v", g_mem.es.move_axis, g_mem.es.total_movement_world), 5, 16, 8, rl.ORANGE, ) } } else { car := physics.get_body(sim_state, runtime_world.car_handle) engine := physics.get_engine(sim_state, runtime_world.engine_handle) gear_ratios := physics.get_gear_ratios(sim_state, engine.gear_ratios) rl.DrawText( fmt.ctprintf( "p: %v\nv: %v\ngear: %v\nratio: %v\nrpm: %v\nspeed: %v km/h", car.x, car.v, engine.gear, physics.lookup_gear_ratio(gear_ratios, engine.gear), physics.angular_velocity_to_rpm(engine.w), linalg.length(car.v) * 3.6, ), 5, 32, 8, rl.ORANGE, ) } ui.rl_draw(&g_mem.ui_context) } if g_mem.editor { es := &g_mem.es points_len := len(points) if points_len > 0 { // Add point before first { tangent: rl.Vector3 if points_len > 1 { tangent = linalg.normalize0(points[1] - points[0]) } else { tangent = rl.Vector3{-1, 0, 0} } new_point_pos := points[0] - tangent * 4 if (spline_handle( new_point_pos, camera, false, rl.GuiIconName.ICON_TARGET_POINT, )) { inject_at(&world.track.points, 0, new_point_pos) log.debugf("add point before 0") } } // Add point after last { tangent: rl.Vector3 if points_len > 1 { tangent = linalg.normalize0(points[points_len - 1] - points[points_len - 2]) } else { tangent = rl.Vector3{-1, 0, 0} } new_point_pos := points[points_len - 1] + tangent * 4 if (spline_handle( new_point_pos, camera, false, rl.GuiIconName.ICON_TARGET_POINT, )) { inject_at(&world.track.points, points_len - 1 + 1, new_point_pos) log.debugf("add point before 0") } } } selected_point := false for i in 0 ..< points_len { if i < points_len - 1 { t := (f32(i) + 0.5) / f32(points_len) middle_pos := sample_spline(points[:], t) if (spline_handle(middle_pos, camera, false, rl.GuiIconName.ICON_TARGET_POINT)) { inject_at(&world.track.points, i + 1, middle_pos) log.debugf("add point after %d", i) } } if spline_handle(world.track.points[i], camera, es.point_selection[i]) { if !rl.IsKeyDown(.LEFT_CONTROL) { clear(&g_mem.es.point_selection) } g_mem.es.point_selection[i] = true selected_point = true } } if rl.IsMouseButtonPressed(.LEFT) && !selected_point { clear(&g_mem.es.point_selection) } } // axis lines if g_mem.editor { size := f32(100) pos := rl.Vector2{20, f32(rl.GetScreenHeight()) - 20 - size} view_rotation := linalg.transpose(rl.GetCameraMatrix(camera)) view_rotation[3].xyz = 0 view_proj := view_rotation * rl.MatrixOrtho(-1, 1, 1, -1, -1, 1) center := (rl.Vector4{0, 0, 0, 1} * view_proj).xy * 0.5 + 0.5 x_axis := (rl.Vector4{1, 0, 0, 1} * view_proj).xy * 0.5 + 0.5 y_axis := (rl.Vector4{0, 1, 0, 1} * view_proj).xy * 0.5 + 0.5 z_axis := (rl.Vector4{0, 0, 1, 1} * view_proj).xy * 0.5 + 0.5 old_width := rlgl.GetLineWidth() rlgl.SetLineWidth(4) defer rlgl.SetLineWidth(old_width) rl.DrawLineV(pos + center * size, pos + x_axis * size, rl.RED) rl.DrawLineV(pos + center * size, pos + y_axis * size, rl.GREEN) rl.DrawLineV(pos + center * size, pos + z_axis * size, rl.BLUE) } } spline_handle :: proc( world_pos: rl.Vector3, camera: rl.Camera, selected: bool, icon := rl.GuiIconName.ICON_NONE, size := f32(20), ) -> ( clicked: bool, ) { if linalg.dot(camera.target - camera.position, world_pos - camera.position) < 0 { return } pos := rl.GetWorldToScreen(world_pos, camera) min, max := pos - size, pos + size mouse_pos := rl.GetMousePosition() is_hover := (mouse_pos.x >= min.x && mouse_pos.y >= min.y && mouse_pos.x <= max.x && mouse_pos.y <= max.y) rl.DrawCircleV(pos, size / 2, selected ? rl.BLUE : (is_hover ? rl.ORANGE : rl.WHITE)) if icon != .ICON_NONE { rl.GuiDrawIcon( icon, c.int(pos.x) - 7, c.int(pos.y) - 7, 1, selected || is_hover ? rl.WHITE : rl.BLACK, ) } // rl.DrawRectangleV(pos, size, selected ? rl.BLUE : (is_hover ? rl.ORANGE : rl.WHITE)) return rl.IsMouseButtonPressed(.LEFT) && is_hover } @(export) game_update :: proc() -> bool { tracy.Zone() defer tracy.FrameMark() update() draw() return !rl.WindowShouldClose() } @(export) game_init_window :: proc(args: []string) { tracy.SetThreadName("Main") init_physfs(args) rl.SetConfigFlags({.WINDOW_RESIZABLE, .VSYNC_HINT}) rl.InitWindow(1280, 720, "Odin + Raylib + Hot Reload template!") rl.SetExitKey(.KEY_NULL) rl.SetWindowPosition(200, 200) rl.SetTargetFPS(120) } @(export) game_init :: proc() { g_mem = new(Game_Memory) g_mem^ = Game_Memory{} init_physifs_raylib_callbacks() assets.assetman_init(&g_mem.assetman) physics.init_physics_scene(&g_mem.runtime_world.world.physics_scene, 100) g_mem.default_font = rl.GetFontDefault() ui.init(&g_mem.ui_context) g_mem.ui_context.style.font = ui.Font(&g_mem.default_font) g_mem.ui_context.text_width = ui.rl_measure_text_width g_mem.ui_context.text_height = ui.rl_measure_text_height game_hot_reloaded(g_mem) } @(export) game_shutdown :: proc() { assets.shutdown(&g_mem.assetman) destroy_world(&g_mem.es.world) delete(g_mem.es.point_selection) destroy_runtime_world(&g_mem.runtime_world) free(g_mem) } @(export) game_shutdown_window :: proc() { rl.CloseWindow() deinit_physfs() } @(export) game_memory :: proc() -> rawptr { return g_mem } @(export) game_memory_size :: proc() -> int { return size_of(Game_Memory) } @(export) game_hot_reloaded :: proc(mem: rawptr) { g_mem = (^Game_Memory)(mem) g_mem.runtime_world.orbit_camera.distance = 4 } @(export) game_force_reload :: proc() -> bool { return rl.IsKeyPressed(.F5) } @(export) game_force_restart :: proc() -> bool { return rl.IsKeyPressed(.F6) }