From 72bd683900c43d99ccca166946b799f07c2928a3 Mon Sep 17 00:00:00 2001 From: sergeypdev Date: Fri, 3 Jan 2025 00:42:38 +0400 Subject: [PATCH] Prototype editor --- .gitattributes | 4 + assets/toyota_corolla_ae86_trueno.glb | 3 + build_hot_reload.sh | 2 +- common/container/freelist/freelist.odin | 81 +++ game/assets/assets.odin | 83 +++ game/game.odin | 361 ++++++++++++- game/physics/collision/collision.odin | 673 ++++++++++++++++++++++++ game/physics/collision/octree.odin | 57 ++ game/physics/physics.odin | 1 + game/raylib_helpers.odin | 52 ++ ols.json | 10 + 11 files changed, 1308 insertions(+), 19 deletions(-) create mode 100644 .gitattributes create mode 100644 assets/toyota_corolla_ae86_trueno.glb create mode 100644 common/container/freelist/freelist.odin create mode 100644 game/assets/assets.odin create mode 100644 game/physics/collision/collision.odin create mode 100644 game/physics/collision/octree.odin create mode 100644 game/physics/physics.odin create mode 100644 game/raylib_helpers.odin create mode 100644 ols.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..660c855 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.png filter=lfs diff=lfs merge=lfs -text +*.glb filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text diff --git a/assets/toyota_corolla_ae86_trueno.glb b/assets/toyota_corolla_ae86_trueno.glb new file mode 100644 index 0000000..9f79738 --- /dev/null +++ b/assets/toyota_corolla_ae86_trueno.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cb8ec8e32fdb46d54b5bc7e4a8b2b0be57a68c06ef96c49ca004300fd1055d3c +size 1520588 diff --git a/build_hot_reload.sh b/build_hot_reload.sh index 478cf8c..5a64161 100755 --- a/build_hot_reload.sh +++ b/build_hot_reload.sh @@ -35,7 +35,7 @@ esac # Build the game. echo "Building game$DLL_EXT" -odin build game -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -define:RAYLIB_SHARED=true -build-mode:dll -out:game_tmp$DLL_EXT -strict-style -vet -debug +odin build game -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -define:RAYLIB_SHARED=true -collection:common=./common -build-mode:dll -out:game_tmp$DLL_EXT -strict-style -vet -debug # Need to use a temp file on Linux because it first writes an empty `game.so`, which the game will load before it is actually fully written. mv game_tmp$DLL_EXT game$DLL_EXT diff --git a/common/container/freelist/freelist.odin b/common/container/freelist/freelist.odin new file mode 100644 index 0000000..c175781 --- /dev/null +++ b/common/container/freelist/freelist.odin @@ -0,0 +1,81 @@ +package freelist + +import "base:builtin" +import "base:runtime" + +Element :: struct($T: typeid) { + value: T, + next_plus_one: int, +} + +Free_List :: struct($T: typeid) { + data: [dynamic]Element(T), + len: int, + first_free_plus_one: int, +} + +init :: proc(f: ^$T/Free_List($E), allocator := context.allocator) -> Free_List(T) { + if f.data.allocator.procedure == nil { + f.data.allocator = allocator + } + clear(f) +} + +insert :: proc( + f: ^$T/Free_List($E), + value: E, + loc := #caller_location, +) -> ( + int, + runtime.Allocator_Error, +) #optional_allocator_error { + if (f.first_free_plus_one > 0) { + index := f.first_free_plus_one - 1 + result := &f.data[index] + f.first_free_plus_one = result.next_plus_one + result.value = value + f.len += 1 + + return index, nil + } else { + _, err := builtin.append(&f.data, Element(E){value = value}, loc) + if err == nil { + f.len += 1 + } + + return builtin.len(f.data) - 1, err + } +} + +remove :: proc(f: ^$T/Free_List($E), #any_int index: int) { + elem := &f.data[index] + elem.next_plus_one = f.first_free_plus_one + f.first_free_plus_one = index + 1 + f.len -= 1 +} + +len :: proc(f: ^$T/Free_List($E)) -> int { + return f.len +} + +cap :: proc(f: ^$T/Free_List($E)) -> int { + return builtin.len(f.data) +} + +// Remaining space in the freelist (cap-len) +space :: proc(f: ^$T/Free_List($E)) -> int { + return builtin.len(f.data) - int(f.len) +} + +reserve :: proc(f: ^$T/Free_List($E), capacity: int) -> runtime.Allocator_Error { + return builtin.reserve(f.data, capacity) +} + +clear :: proc(f: ^$T/Free_List($E)) { + builtin.clear(f.data) + f.first_free_plus_one = 0 +} + +destroy :: proc(f: ^$T/Free_List) { + delete(f.data) +} diff --git a/game/assets/assets.odin b/game/assets/assets.odin new file mode 100644 index 0000000..42ab74a --- /dev/null +++ b/game/assets/assets.odin @@ -0,0 +1,83 @@ +package assets + +import "core:c" +import "core:log" +import rl "vendor:raylib" + +Loaded_Texture :: struct { + texture: rl.Texture2D, + modtime: c.long, +} + +Loaded_Model :: struct { + model: rl.Model, + modtime: c.long, +} + +Asset_Manager :: struct { + textures: map[cstring]Loaded_Texture, + models: map[cstring]Loaded_Model, +} + +get_texture :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Texture2D { + modtime := rl.GetFileModTime(path) + + existing, ok := assetman.textures[path] + if ok && existing.modtime == modtime { + return existing.texture + } + + if ok { + rl.UnloadTexture(existing.texture) + delete_key(&assetman.textures, path) + log.infof("deleted texture %s. New textures len: %d", path, len(assetman.textures)) + } + + loaded := rl.LoadTexture(path) + if rl.IsTextureValid(loaded) { + assetman.textures[path] = { + texture = loaded, + modtime = modtime, + } + return loaded + } else { + return rl.Texture2D{} + } +} + +get_model :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Model { + modtime := rl.GetFileModTime(path) + + existing, ok := assetman.models[path] + if ok && existing.modtime == modtime { + return existing.model + } + + if ok { + rl.UnloadModel(existing.model) + delete_key(&assetman.textures, path) + log.infof("deleted model %s. New models len: %d", path, len(assetman.textures)) + } + + loaded := rl.LoadModel(path) + if rl.IsModelValid(loaded) { + assetman.models[path] = { + model = loaded, + modtime = modtime, + } + return loaded + } else { + return rl.Model{} + } +} + +shutdown :: proc(assetman: ^Asset_Manager) { + for _, texture in assetman.textures { + rl.UnloadTexture(texture.texture) + } + for _, model in assetman.models { + rl.UnloadModel(model.model) + } + delete(assetman.textures) + delete(assetman.models) +} diff --git a/game/game.odin b/game/game.odin index 5e0b90b..ff4e259 100644 --- a/game/game.odin +++ b/game/game.odin @@ -14,17 +14,49 @@ package game -import "core:fmt" +import "assets" import "core:math/linalg" import rl "vendor:raylib" +import "vendor:raylib/rlgl" PIXEL_WINDOW_HEIGHT :: 360 +Track :: struct { + points: [dynamic]rl.Vector3, +} Game_Memory :: struct { + assetman: assets.Asset_Manager, player_pos: rl.Vector3, camera_yaw_pitch: rl.Vector2, camera_speed: f32, - mouse_captured: bool, + track: Track, + es: Editor_State, + editor: bool, +} + +Track_Edit_State :: enum { + // Point selection + Select, + // Moving points + Move, +} + +Move_Axis :: enum { + None, + X, + Y, + Z, + XZ, + XY, + YZ, +} + +Editor_State :: struct { + mouse_captured: bool, + selected_track_point: int, + track_edit_state: Track_Edit_State, + move_axis: Move_Axis, + initial_point_pos: rl.Vector3, } g_mem: ^Game_Memory @@ -60,6 +92,8 @@ ui_camera :: proc() -> rl.Camera2D { } update_free_look_camera :: proc() { + es := &g_mem.es + input: rl.Vector2 if rl.IsKeyDown(.UP) || rl.IsKeyDown(.W) { @@ -75,14 +109,17 @@ update_free_look_camera :: proc() { input.x += 1 } - if rl.IsKeyPressed(.ESCAPE) { - if g_mem.mouse_captured { - g_mem.mouse_captured = false + should_capture_mouse := rl.IsMouseButtonDown(.RIGHT) + if es.mouse_captured != should_capture_mouse { + if should_capture_mouse { + rl.DisableCursor() + } else { rl.EnableCursor() } } + es.mouse_captured = should_capture_mouse - if g_mem.mouse_captured { + if es.mouse_captured { g_mem.camera_yaw_pitch += rl.GetMouseDelta().yx * -1 * 0.001 } @@ -97,32 +134,316 @@ update_free_look_camera :: proc() { g_mem.player_pos += (input.x * right + input.y * forward) * g_mem.camera_speed } -update :: proc() { - if rl.IsMouseButtonPressed(.LEFT) { - g_mem.mouse_captured = true - rl.DisableCursor() +add_track_spline_point :: proc() { + forward := camera_rotation_matrix()[2] + + append(&g_mem.track.points, g_mem.player_pos + forward) + g_mem.es.selected_track_point = len(g_mem.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[:] } - update_free_look_camera() + return out_axes[0:0], out_colors[0:0] +} + +update_editor :: proc() { + es := &g_mem.es + + switch es.track_edit_state { + case .Select: + { + if rl.IsKeyPressed(.ENTER) { + add_track_spline_point() + } + + if rl.IsKeyPressed(.G) && es.selected_track_point >= 0 { + es.track_edit_state = .Move + es.move_axis = .None + es.initial_point_pos = g_mem.track.points[es.selected_track_point] + } + } + case .Move: + { + if rl.IsKeyPressed(.ESCAPE) { + es.track_edit_state = .Select + g_mem.track.points[es.selected_track_point] = es.initial_point_pos + break + } + + if (rl.IsMouseButtonPressed(.LEFT)) { + es.track_edit_state = .Select + break + } + + if !es.mouse_captured { + // Blender style movement + if rl.IsKeyDown(.LEFT_SHIFT) { + if rl.IsKeyPressed(.X) { + es.move_axis = .YZ + } + if rl.IsKeyPressed(.Y) { + es.move_axis = .XZ + } + if rl.IsKeyPressed(.Z) { + es.move_axis = .XY + } + } else { + if rl.IsKeyPressed(.X) { + es.move_axis = .X + } + if rl.IsKeyPressed(.Y) { + es.move_axis = .Y + } + if rl.IsKeyPressed(.Z) { + es.move_axis = .Z + } + } + + // log.debugf("Move axis %v", es.move_axis) + + camera := game_camera_3d() + + mouse_delta := rl.GetMouseDelta() * 0.05 + + view_rotation := linalg.transpose(rl.GetCameraMatrix(camera)) + view_rotation[3].xyz = 0 + view_proj := view_rotation * rl.MatrixOrtho(-1, 1, 1, -1, -1, 1) + + axes_buf: [2]rl.Vector3 + colors_buf: [2]rl.Color + axes, _ := get_movement_axes(es.move_axis, &axes_buf, &colors_buf) + + movement_world: rl.Vector3 + for axis in axes { + axis_screen := (rl.Vector4{axis.x, axis.y, axis.z, 1} * view_proj).xy + axis_screen = linalg.normalize0(axis_screen) + + movement_screen := linalg.dot(axis_screen, mouse_delta) * axis_screen + movement_world += + (rl.Vector4{movement_screen.x, movement_screen.y, 0, 1} * rl.MatrixInvert(view_proj)).xyz + } + + g_mem.track.points[es.selected_track_point] += movement_world + } + } + } + +} + +update :: proc() { + if rl.IsKeyPressed(.TAB) { + g_mem.editor = !g_mem.editor + + if g_mem.editor { + rl.EnableCursor() + } else { + rl.DisableCursor() + } + } + + if g_mem.editor { + update_free_look_camera() + + update_editor() + } } draw :: proc() { rl.BeginDrawing() + defer rl.EndDrawing() rl.ClearBackground(rl.BLACK) + camera := game_camera_3d() + + { - rl.BeginMode3D(game_camera_3d()) + rl.BeginMode3D(camera) defer rl.EndMode3D() - rl.DrawBoundingBox(rl.BoundingBox{min = -1, max = 1}, {255, 0, 0, 255}) + rl.DrawModel( + assets.get_model(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb"), + rl.Vector3{0, 0, 0}, + 1, + rl.WHITE, + ) + + points := &g_mem.track.points + points_len := len(points) + + { + rlgl.Begin(rlgl.LINES) + defer rlgl.End() + + rlgl.Color3f(1, 0, 0) + + SPLINE_SUBDIVS :: 8 + + for i in 0 ..< points_len { + if i >= 1 && i < points_len - 2 { + for j in 0 ..< SPLINE_SUBDIVS { + t := f32(j) / f32(SPLINE_SUBDIVS) + t2 := f32(j + 1) / f32(SPLINE_SUBDIVS) + point := linalg.catmull_rom( + points[i - 1], + points[i], + points[i + 1], + points[i + 2], + t, + ) + point2 := linalg.catmull_rom( + points[i - 1], + points[i], + points[i + 1], + points[i + 2], + t2, + ) + + rlgl.Vertex3f(point.x, point.y, point.z) + rlgl.Vertex3f(point2.x, point2.y, point2.z) + } + } + } + + + if g_mem.editor { + es := &g_mem.es + + switch es.track_edit_state { + case .Select: + case .Move: + 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) + } + } + } + } } - rl.BeginMode2D(ui_camera()) - // Note: main_hot_reload.odin clears the temp allocator at end of frame. - rl.DrawText(fmt.ctprintf("player_pos: %v", g_mem.player_pos), 5, 5, 8, rl.WHITE) - rl.EndMode2D() + { + rl.BeginMode2D(ui_camera()) + defer rl.EndMode2D() - rl.EndDrawing() + if g_mem.editor { + rl.DrawText("Editor", 5, 5, 8, rl.ORANGE) + } + } + + if g_mem.editor { + es := &g_mem.es + + points := &g_mem.track.points + points_len := len(points) + + for i in 0 ..< points_len { + + if spline_handle(g_mem.track.points[i], camera, es.selected_track_point == i) { + g_mem.es.selected_track_point = i + } + } + } + + // 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, +) -> ( + clicked: bool, +) { + if linalg.dot(camera.target - camera.position, world_pos - camera.position) < 0 { + return + } + pos := rl.GetWorldToScreen(world_pos, camera) + size := rl.Vector2{10, 10} + + 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.DrawRectangleV(pos, size, selected ? rl.BLUE : (is_hover ? rl.ORANGE : rl.WHITE)) + + return rl.IsMouseButtonPressed(.LEFT) && is_hover } @(export) @@ -147,11 +468,15 @@ game_init :: proc() { g_mem^ = Game_Memory{} + g_mem.es.selected_track_point = -1 + game_hot_reloaded(g_mem) } @(export) game_shutdown :: proc() { + assets.shutdown(&g_mem.assetman) + delete(g_mem.track.points) free(g_mem) } diff --git a/game/physics/collision/collision.odin b/game/physics/collision/collision.odin new file mode 100644 index 0000000..c5baa69 --- /dev/null +++ b/game/physics/collision/collision.odin @@ -0,0 +1,673 @@ +package collision +// +// from Real-Time Collision Detection by Christer Ericson, published by Morgan Kaufmann Publishers, © 2005 Elsevier Inc +// +// This should serve as an reference implementation for common collision queries for games. +// The goal is good numerical robustness, handling edge cases and optimized math equations. +// The code isn't necessarily very optimized. +// +// There are a few cases you don't want to use the procedures below directly, but instead manually inline the math and adapt it to your needs. +// In my experience this method is clearer when writing complex level queries where I need to handle edge cases differently etc. + +import "core:math" +import "core:math/linalg" + +Vec3 :: [3]f32 + +sqrt :: math.sqrt + +dot :: linalg.dot +cross :: linalg.cross +length2 :: linalg.vector_length2 + +Aabb :: struct { + min: Vec3, + max: Vec3, +} + +// Infinitely small +AABB_INVALID :: Aabb { + min = 1e20, + max = -1e20, +} + +Sphere :: struct { + pos: Vec3, + rad: f32, +} + +// Radius is the half size +Box :: struct { + pos: Vec3, + rad: Vec3, +} + +Plane :: struct { + normal: Vec3, + dist: f32, +} + +Capsule :: struct { + a: Vec3, + b: Vec3, + rad: f32, +} + +// Same layout, slightly different meaning +Cylinder :: distinct Capsule + + +aabb_center :: proc(a: Aabb) -> Vec3 { + return (a.min + a.max) * 0.5 +} + +aabb_half_size :: proc(a: Aabb) -> Vec3 { + return (a.max - a.min) * 0.5 +} + +aabb_to_box :: proc(a: Aabb) -> Box { + center := aabb_center(a) + return {pos = center, rad = a.max - center} +} + +box_to_aabb :: proc(a: Box) -> Aabb { + return {min = a.pos - a.rad, max = a.pos + a.rad} +} + +plane_from_point_normal :: proc(point: Vec3, normal: Vec3) -> Plane { + return {normal = normal, dist = dot(point, normal)} +} + + + +////////////////////////////////////////////////////////////////////////////////// +// Distance to closest point +// + +signed_distance_plane :: proc(point: Vec3, plane: Plane) -> f32 { + // If plane equation normalized (||p.n||==1) + return dot(point, plane.normal) - plane.dist + // If not normalized + // return (dot(plane.normal, point) - plane.dist) / Ddt(plane.normal, plane.normal); +} + +squared_distance_aabb :: proc(point: Vec3, aabb: Aabb) -> (dist: f32) { + for i in 0 ..< 3 { + // For each axis count any excess distance outside box extents + if point[i] < aabb.min[i] do dist += (aabb.min[i] - point[i]) * (aabb.min[i] - point[i]) + if point[i] > aabb.max[i] do dist += (point[i] - aabb.max[i]) * (point[i] - aabb.max[i]) + } + return dist +} + +// Returns the squared distance between point and segment ab +squared_distance_segment :: proc(point, a, b: Vec3) -> f32 { + ab := b - a + ac := point - a + bc := point - b + e := dot(ac, ab) + // Handle cases where c projects outside ab + if e <= 0.0 { + return dot(ac, ac) + } + f := dot(ab, ab) + if e >= f { + return dot(bc, bc) + } + // Handle cases where c projects onto ab + return dot(ac, ac) - e * e / f +} + + + +////////////////////////////////////////////////////////////////////////////////// +// Closest point +// + +closest_point_plane :: proc(point: Vec3, plane: Plane) -> Vec3 { + t := dot(plane.normal, point) - plane.dist + return point - t * plane.normal +} + +closest_point_aabb :: proc(point: Vec3, aabb: Aabb) -> Vec3 { + return { + clamp(point.x, aabb.min.x, aabb.max.x), + clamp(point.y, aabb.min.y, aabb.max.y), + clamp(point.z, aabb.min.z, aabb.max.z), + } +} + +// Given segment ab and point c, computes closest point d on ab. +// Also returns t for the position of d, d(t)=a+ t*(b - a) +closest_point_segment :: proc(pos, a, b: Vec3) -> (t: f32, point: Vec3) { + ab := b - a + // Project pos onto ab, computing parameterized position d(t)=a+ t*(b – a) + t = dot(pos - a, ab) / dot(ab, ab) + t = clamp(t, 0, 1) + // Compute projected position from the clamped t + point = a + t * ab + return t, point +} + +// Computes closest points C1 and C2 of S1(s)=P1+s*(Q1-P1) and +// S2(t)=P2+t*(Q2-P2), returning s and t. Function result is squared +// distance between between S1(s) and S2(t) +// TODO: [2]Vec3 +closest_point_between_segments :: proc(p1, q1, p2, q2: Vec3) -> (t: [2]f32, points: [2]Vec3) { + d1 := q1 - p1 // Direction vector of segment S1 + d2 := q2 - p2 // Direction vector of segment S2 + r := p1 - p2 + a := dot(d1, d1) // Squared length of segment S1, always nonnegative + e := dot(d2, d2) // Squared length of segment S2, always nonnegative + f := dot(d2, r) + + EPS :: 1e-6 + + // Check if either or both segments degenerate into points + if a <= EPS && e <= EPS { + // Both segments degenerate into points + t = 0 + points = {p1, p2} + return t, points + } + if a <= EPS { + // First segment degenerates into a point + t[0] = 0 + t[1] = clamp(f / e, 0, 1) // s = 0 => t = (b*s + f) / e = f / e + } else { + c := dot(d1, r) + if e <= EPS { + // Second segment degenerates into a point + t[1] = 0 + t[0] = clamp(-c / a, 0, 1) // t = 0 => s = (b*t - c) / a = -c / a + } else { + // The general nondegenerate case starts here + b := dot(d1, d2) + denom := a * e - b * b // Always nonnegative + + // If segments not parallel, compute closest point on L1 to L2 and + // clamp to segment S1. Else pick arbitrary s (here 0) + if denom != 0.0 { + t[0] = clamp((b * f - c * e) / denom, 0, 1) + } else { + t[0] = 0 + } + // Compute point on L2 closest to S1(s) using + // t = Dot((P1 + D1*s) - P2,D2) / Dot(D2,D2) = (b*s + f) / e + tnom := (b * t[0] + f) + + // If t in [0,1] done. Else clamp t, recompute s for the new value + // of t using s = Dot((P2 + D2*t) - P1,D1) / Dot(D1,D1)= (t*b - c) / a + // and clamp s to [0, 1] + if tnom < 0 { + t[1] = 0 + t[0] = clamp(-c / a, 0, 1) + } else if tnom > 1 { + t[1] = 1 + t[0] = clamp((b - c) / a, 0, 1) + } else { + t[1] = tnom / e + } + } + } + + points[0] = p1 + d1 * t[0] + points[1] = p2 + d2 * t[1] + return t, points +} + +closest_point_triangle :: proc(point, a, b, c: Vec3) -> Vec3 { + ab := b - a + ac := c - a + ap := point - a + d1 := dot(ab, ap) + d2 := dot(ac, ap) + if d1 <= 0 && d2 <= 0 do return a // barycentric coordinates (1,0,0) + + // Check if P in vertex region outside B + bp := point - b + d3 := dot(ab, bp) + d4 := dot(ac, bp) + if d3 >= 0 && d4 <= d3 do return b // barycentric coordinates (0,1,0) + + // Check if P in edge region of AB, if so return projection of P onto AB + vc := d1 * d4 - d3 * d2 + if vc < 0 && d1 >= 0 && d3 <= 0 { + v := d1 / (d1 - d3) + return a + v * ab // barycentric coordinates (1-v,v,0) + } + + // Check if P in vertex region outside C + cp := point - c + d5 := dot(ab, cp) + d6 := dot(ac, cp) + if d6 >= 0 && d5 <= d6 do return c // barycentric coordinates (0,0,1) + + // Check if P in edge region of AC, if so return projection of P onto AC + vb := d5 * d2 - d1 * d6 + if vb <= 0 && d2 >= 0 && d6 <= 0 { + w := d2 / (d2 - d6) + return a + w * ac // barycentric coordinates (1-w,0,w) + } + + // Check if P in edge region of BC, if so return projection of P onto BC + va := d3 * d6 - d5 * d4 + if va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0 { + w := (d4 - d3) / ((d4 - d3) + (d5 - d6)) + return b + w * (c - b) // barycentric coordinates (0,1-w,w) + } + + // P inside face region. Compute Q through its barycentric coordinates (u,v,w) + denom := 1.0 / (va + vb + vc) + v := vb * denom + w := vc * denom + return a + ab * v + ac * w // = u*a + v*b + w*c, u = va * denom = 1.0f-v-w +} + + + +////////////////////////////////////////////////////////////////////////////////// +// Tests +// + +test_aabb_vs_aabb :: proc(a, b: Aabb) -> bool { + // Exit with no intersection if separated along an axis + if a.max[0] < b.min[0] || a.min[0] > b.max[0] do return false + if a.max[1] < b.min[1] || a.min[1] > b.max[1] do return false + if a.max[2] < b.min[2] || a.min[2] > b.max[2] do return false + // Overlapping on all axes means AABBs are intersecting + return true +} + +test_sphere_vs_aabb :: proc(sphere: Sphere, aabb: Aabb) -> bool { + s := squared_distance_aabb(sphere.pos, aabb) + return s <= sphere.rad * sphere.rad +} + +test_sphere_vs_plane :: proc(sphere: Sphere, plane: Plane) -> bool { + dist := signed_distance_plane(sphere.pos, plane) + return abs(dist) <= sphere.rad +} + +test_point_vs_halfspace :: proc(pos: Vec3, plane: Plane) -> bool { + return signed_distance_plane(pos, plane) <= 0.0 +} + +test_sphere_vs_halfspace :: proc(sphere: Sphere, plane: Plane) -> bool { + dist := signed_distance_plane(sphere.pos, plane) + return dist <= sphere.rad +} + +test_box_vs_plane :: proc(box: Box, plane: Plane) -> bool { + // Compute the projection interval radius of b onto L(t) = b.c + t * p.n + r := box.rad.x * abs(plane.normal.x) + box.rad.y * abs(plane.normal.y) + box.rad.z * abs(plane.normal.z) + s := signed_distance_plane(box.pos, plane) + return abs(s) <= r +} + +test_capsule_vs_capsule :: proc(a, b: Capsule) -> bool { + // Compute (squared) distance between the inner structures of the capsules + _, points := closest_point_between_segments(a.a, a.b, b.a, b.b) + squared_dist := length2(points[1] - points[0]) + // If (squared) distance smaller than (squared) sum of radii, they collide + rad := a.rad + b.rad + return squared_dist <= rad * rad +} + +test_sphere_vs_capsule :: proc(sphere: Sphere, capsule: Capsule) -> bool { + // Compute (squared) distance between sphere center and capsule line segment + dist2 := squared_distance_segment(point = sphere.pos, a = capsule.a, b = capsule.b) + // If (squared) distance smaller than (squared) sum of radii, they collide + rad := sphere.rad + capsule.rad + return dist2 <= rad * rad +} + +test_capsule_vs_plane :: proc(capsule: Capsule, plane: Plane) -> bool { + adist := dot(capsule.a, plane.normal) - plane.dist + bdist := dot(capsule.b, plane.normal) - plane.dist + // Intersects if on different sides of plane (distances have different signs) + if adist * bdist < 0.0 do return true + // Intersects if start or end position within radius from plane + if abs(adist) <= capsule.rad || abs(bdist) <= capsule.rad do return true + return false +} + +test_capsule_vs_halfspace :: proc(capsule: Capsule, plane: Plane) -> bool { + adist := dot(capsule.a, plane.normal) - plane.dist + bdist := dot(capsule.b, plane.normal) - plane.dist + return min(adist, bdist) <= capsule.rad +} + +test_ray_sphere :: proc(pos, dir: Vec3, sphere: Sphere) -> bool { + m := pos - sphere.pos + c := dot(m, m) - sphere.rad * sphere.rad + // If there is definitely at least one real root, there must be an intersection + if c <= 0 do return true + b := dot(m, dir) + // Early exit if ray origin outside sphere and ray pointing away from sphere + if b > 0 do return false + discr := b * b - c + // A negative discriminant corresponds to ray missing sphere + return discr >= 0 +} + +test_point_polyhedron :: proc(pos: Vec3, planes: []Plane) -> bool { + for plane in planes { + if signed_distance_plane(pos, plane) > 0.0 { + return false + } + } + return true +} + + + +////////////////////////////////////////////////////////////////////////////////// +// Intersections +// + +// Given planes a and b, compute line L = p+t*d of their intersection. +intersect_planes :: proc(a, b: Plane) -> (point, dir: Vec3, ok: bool) { + // Compute direction of intersection line + dir = cross(a.normal, b.normal) + // If d is (near) zero, the planes are parallel (and separated) + // or coincident, so they’re not considered intersecting + denom := dot(dir, dir) + EPS :: 1e-6 + if denom < EPS do return {}, dir, false + // Compute point on intersection line + point = cross(a.dist * b.normal - b.dist * a.normal, dir) / denom + return point, dir, true +} + +// TODO: moving vs static +intersect_moving_spheres :: proc(a, b: Sphere, vel_a, vel_b: Vec3) -> (t: f32, ok: bool) { + s := b.pos - a.pos + v := vel_b - vel_a // Relative motion of s1 with respect to stationary s0 + r := a.rad + b.rad + c := dot(s, s) - r * r + if c < 0 { + // Spheres initially overlapping so exit directly + return 0, true + } + a := dot(v, v) + EPS :: 1e-6 + if a < EPS { + return 1, false // Spheres not moving relative each other + } + b := dot(v, s) + if b >= 0 { + return 1, false // Spheres not moving towards each other + } + d := b * b - a * c + if d < 0 { + return 1, false // No real-valued root, spheres do not intersect + } + t = (-b - sqrt(d)) / a + return t, true +} + +intersect_moving_aabbs :: proc(a, b: Aabb, vel_a, vel_b: Vec3) -> (t: [2]f32, ok: bool) { + // Use relative velocity; effectively treating ’a’ as stationary + return intersect_static_aabb_vs_moving_aabb(a, b, vel_relative = vel_b - vel_a) +} + +// 'a' is static, 'b' is moving +intersect_static_aabb_vs_moving_aabb :: proc(a, b: Aabb, vel_relative: Vec3) -> (t: [2]f32, ok: bool) { + // Exit early if ‘a’ and ‘b’ initially overlapping + if test_aabb_vs_aabb(a, b) { + return 0, true + } + + // Initialize ts of first and last contact + t = {0, 1} + + // For each axis, determine ts of first and last contact, if any + for i in 0 ..< 3 { + if vel_relative[i] < 0.0 { + if b.max[i] < a.min[i] do return 1, false // Nonintersecting and moving apart + if a.max[i] < b.min[i] do t[0] = max(t[0], (a.max[i] - b.min[i]) / vel_relative[i]) + if b.max[i] > a.min[i] do t[1] = min(t[1], (a.min[i] - b.max[i]) / vel_relative[i]) + } + + if vel_relative[i] > 0.0 { + if b.min[i] > a.max[i] do return 1, false // Nonintersecting and moving apart + if b.max[i] < a.min[i] do t[0] = max(t[0], (a.min[i] - b.max[i]) / vel_relative[i]) + if a.max[i] > b.min[i] do t[1] = min(t[1], (a.max[i] - b.min[i]) / vel_relative[i]) + } + + // No overlap possible if t of first contact occurs after t of last contact + if t[0] > t[1] do return 1, false + } + + return t, true +} + +// Intersect sphere s with movement vector v with plane p. If intersecting +// return t t of collision and point at which sphere hits plane +intersect_moving_sphere_vs_plane :: proc(sphere: Sphere, vel: Vec3, plane: Plane) -> (t: f32, point: Vec3, ok: bool) { + // Compute distance of sphere center to plane + dist := dot(plane.normal, sphere.pos) - plane.dist + if abs(dist) <= sphere.rad { + // The sphere is already overlapping the plane. Set t of + // intersection to zero and q to sphere center + return 0.0, sphere.pos, true + } + + denom := dot(plane.normal, vel) + if (denom * dist >= 0.0) { + // No intersection as sphere moving parallel to or away from plane + return 1.0, sphere.pos, false + } + + // Sphere is moving towards the plane + + // Use +r in computations if sphere in front of plane, else -r + r := dist > 0.0 ? sphere.rad : -sphere.rad + t = (r - dist) / denom + point = sphere.pos + vel * t - r * plane.normal + return t, point, t <= 1.0 +} + +intersect_ray_sphere :: proc(pos: Vec3, dir: Vec3, sphere: Sphere) -> (t: f32, ok: bool) { + m := pos - sphere.pos + b := dot(m, dir) + c := dot(m, m) - sphere.rad * sphere.rad + // Exit if r’s origin outside s (c > 0) and r pointing away from s (b > 0) + if c > 0 && b > 0 { + return 0, false + } + discr := b * b - c + // A negative discriminant corresponds to ray missing sphere + if discr < 0 do return 0, false + // Ray now found to intersect sphere, compute smallest t value of intersection + t = -b - sqrt(discr) + // If t is negative, ray started inside sphere so clamp t to zero + t = max(0, t) + return t, true +} + +intersect_ray_aabb :: proc(pos: Vec3, dir: Vec3, aabb: Aabb, range: f32 = max(f32)) -> (t: [2]f32, ok: bool) { + // https://tavianator.com/cgit/dimension.git/tree/libdimension/bvh/bvh.c#n196 + + // This is actually correct, even though it appears not to handle edge cases + // (dir.{x,y,z} == 0). It works because the infinities that result from + // dividing by zero will still behave correctly in the comparisons. Rays + // which are parallel to an axis and outside the box will have tmin == inf + // or tmax == -inf, while rays inside the box will have tmin and tmax + // unchanged. + + inv_dir := 1.0 / dir + + t1 := (aabb.min - pos) * inv_dir + t2 := (aabb.max - pos) * inv_dir + + t = {max(min(t1.x, t2.x), min(t1.y, t2.y), min(t1.z, t2.z)), min(max(t1.x, t2.x), max(t1.y, t2.y), max(t1.z, t2.z))} + + return t, t[1] >= max(0.0, t[0]) && t[0] < range +} + +intersect_ray_polyhedron :: proc(pos, dir: Vec3, planes: []Plane, segment: [2]f32 = {0.0, max(f32)}) -> (t: [2]f32, ok: bool) { + t = segment + for plane in planes { + denom := dot(plane.normal, dir) + dist := plane.dist - dot(plane.normal, pos) + // Test if segment runs parallel to the plane + if denom == 0.0 { + // If so, return “no intersection” if segment lies outside plane + if dist > 0.0 { + return 0, false + } + } else { + // Compute parameterized t value for intersection with current plane + tplane := dist / denom + if denom < 0.0 { + // When entering halfspace, update tfirst if t is larger + t[0] = max(t[0], tplane) + } else { + // When exiting halfspace, update tlast if t is smaller + t[1] = min(t[1], tplane) + } + if t[0] > t[1] { + return 0, false + } + } + } + return t, true +} + +intersect_segment_triangle :: proc( + segment: [2]Vec3, + triangle: [3]Vec3, +) -> ( + t: f32, + normal: Vec3, + barycentric: [3]f32, + ok: bool, +) { + ab := triangle[1] - triangle[0] + ac := triangle[2] - triangle[0] + qp := segment[0] - segment[1] + + normal = cross(ab, ac) + + denom := dot(qp, normal) + // If denom <= 0, segment is parallel to or points away from triangle + if denom <= 0 { + return 0, normal, 0, false + } + + // Compute intersection t value of pq with plane of triangle. A ray + // intersects if 0 <= t. Segment intersects iff 0 <= t <= 1. Delay + // dividing by d until intersection has been found to pierce triangle + ap := segment[0] - triangle[0] + t = dot(ap, normal) + if t < 0 { + return + } + if t > denom { + // For segment; exclude this code line for a ray test + return + } + + // Compute barycentric coordinate components and test if within bounds + e := cross(qp, ap) + barycentric.y = dot(ac, e) + if barycentric.y < 0 || barycentric.y > denom { + return + } + barycentric.z = -dot(ab, e) + if barycentric.z < 0 || barycentric.y + barycentric.z > denom { + return + } + + // Segment/ray intersects triangle. Perform delayed division and + // compute the last barycentric coordinate component + ood := 1.0 / denom + t *= ood + barycentric.yz *= ood + barycentric.x = 1.0 - barycentric.y - barycentric.z + return t, normal, barycentric, true +} + +intersect_segment_plane :: proc(segment: [2]Vec3, plane: Plane) -> (t: f32, point: Vec3, ok: bool) { + ab := segment[1] - segment[0] + t = (plane.dist - dot(plane.normal, segment[0])) / dot(plane.normal, ab) + + if t >= 0 && t <= 1 { + point = segment[0] + t * ab + return t, point, true + } + + return t, segment[0], false +} + +// TODO: alternative with capsule endcaps +intersect_segment_cylinder :: proc(segment: [2]Vec3, cylinder: Cylinder) -> (t: f32, ok: bool) { + d := cylinder.b - cylinder.a + m := segment[0] - cylinder.a + n := segment[1] - segment[0] + md := dot(m, d) + nd := dot(n, d) + dd := dot(d, d) + // Test if segment fully outside either endcap of cylinder + if md < 0 && md + nd < 0 { + return 0, false // Segment outside ’a’ side of cylinder + } + if md > dd && md + nd > dd { + return 0, false // Segment outside ’b’ side of cylinder + } + nn := dot(n, n) + mn := dot(m, n) + a := dd * nn - nd * nd + k := dot(m, m) - cylinder.rad * cylinder.rad + c := dd * k - md * md + EPS :: 1e-6 + if abs(a) < EPS { + // Segment runs parallel to cylinder axis + if c > 0 { + return 0, false + } + // Now known that segment intersects cylinder; figure out how it intersects + if md < 0 { + // Intersect segment against ’a’ endcap + t = -mn / nn + } else if md > dd { + // Intersect segment against ’b’ endcap + t = (nd - mn) / nn + } else { + // ’a’ lies inside cylinder + t = 0 + } + return t, true + } + b := dd * mn - nd * md + discr := b * b - a * c + if discr < 0 { + return 0, false // no real roots + } + t = (-b - sqrt(discr)) / a + if t < 0 || t > 1 { + return 0, false // intersection outside segment + } + if md + t * nd < 0 { + // Intersection outside cylinder on ’a’ side + if nd <= 0 { + // Segment pointing away from endcap + return 0, false + } + t = -md / nd + ok = k + 2 * t * (mn + t * nn) <= 0 + return t, ok + } else if md + t * nd > dd { + // Intersection outside cylinder on ’b’ side + if nd >= 0 { + // Segment pointing away from endcap + return 0, false + } + t = (dd - md) / nd + ok = k + dd - 2 * md + t * (2 * (mn - nd) + t * nn) <= 0 + return t, ok + } + return t, true +} diff --git a/game/physics/collision/octree.odin b/game/physics/collision/octree.odin new file mode 100644 index 0000000..df4982a --- /dev/null +++ b/game/physics/collision/octree.odin @@ -0,0 +1,57 @@ +package collision + +import fl "common:container/freelist" + +Octree :: struct { + elements: fl.Free_List(Element), + element_nodes: fl.Free_List(Element_Node), + root: Octree_Node, + nodes: fl.Free_List(Octree_Node_Children), + free_node_plus_one: int, + root_aabb: Aabb, + max_depth: int, +} + +// You insert elements +Element :: struct { + id: i32, + aabb: Aabb, +} + +// Elements may spawn multiple element nodes +Element_Node :: struct { + next: i32, + element: i32, +} + +Octree_Node_Children :: struct { + children: [8]Octree_Node, +} + +Octree_Node :: struct { + // Index of children if this node is a branch or first element if this node is a leaf + children_or_first_element: i32, + + // Number of elements in a leaf or -1 if not a leaf + len: i32, +} + +is_leaf :: proc(node: Octree_Node) -> bool { + return node.len != -1 +} + +add_element_to_leaf :: #force_inline proc(octree: ^Octree, node: ^Octree_Node, el_index: i32) { + new_elem_node_idx := fl.insert( + &octree.element_nodes, + Element_Node{element = el_index, next = node.children_or_first_element}, + ) + node.children_or_first_element = i32(new_elem_node_idx) +} + +insert :: proc(octree: ^Octree, el: Element) { + el_index := i32(fl.insert(&octree.elements, el)) + + if is_leaf(octree.root) { + add_element_to_leaf(octree, &octree.root, el_index) + } +} diff --git a/game/physics/physics.odin b/game/physics/physics.odin new file mode 100644 index 0000000..e055fc2 --- /dev/null +++ b/game/physics/physics.odin @@ -0,0 +1 @@ +package physics diff --git a/game/raylib_helpers.odin b/game/raylib_helpers.odin new file mode 100644 index 0000000..3ccf421 --- /dev/null +++ b/game/raylib_helpers.odin @@ -0,0 +1,52 @@ +package game + +import "base:runtime" +import "core:c/libc" +import "core:log" +import "core:mem" +import rl "vendor:raylib" + +logger: log.Logger = { + lowest_level = .Debug, +} +rl_log_buf: []byte +rl_log :: proc "c" (logLevel: rl.TraceLogLevel, text: cstring, args: ^libc.va_list) { + context = runtime.default_context() + context.logger = logger + + level: log.Level + switch logLevel { + case .TRACE, .DEBUG: + level = .Debug + case .ALL, .NONE, .INFO: + level = .Info + case .WARNING: + level = .Warning + case .ERROR: + level = .Error + case .FATAL: + level = .Fatal + } + + if level < logger.lowest_level { + return + } + + if rl_log_buf == nil { + rl_log_buf = make([]byte, 1024) + } + + defer mem.zero_slice(rl_log_buf) + + n: int + for { + va := args + n = int(libc.vsnprintf(raw_data(rl_log_buf), len(rl_log_buf), text, va)) + if n < len(rl_log_buf) do break + log.infof("Resizing raylib log buffer from %m to %m", len(rl_log_buf), len(rl_log_buf) * 2) + rl_log_buf, _ = mem.resize_bytes(rl_log_buf, len(rl_log_buf) * 2) + } + + formatted := string(rl_log_buf[:n]) + log.log(level, formatted) +} diff --git a/ols.json b/ols.json new file mode 100644 index 0000000..d49db1f --- /dev/null +++ b/ols.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", + "collections": [ + { "name": "common", "path": "./common" } + ], + "enable_semantic_tokens": false, + "enable_document_symbols": true, + "enable_hover": true, + "enable_snippets": true, +}