Prototype editor

This commit is contained in:
sergeypdev 2025-01-03 00:42:38 +04:00
parent b7e15bf4cd
commit 72bd683900
11 changed files with 1308 additions and 19 deletions

4
.gitattributes vendored Normal file
View File

@ -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

BIN
assets/toyota_corolla_ae86_trueno.glb (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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

View File

@ -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)
}

83
game/assets/assets.odin Normal file
View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 theyre 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 rs 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
}

View File

@ -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)
}
}

View File

@ -0,0 +1 @@
package physics

52
game/raylib_helpers.odin Normal file
View File

@ -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)
}

10
ols.json Normal file
View File

@ -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,
}