Prototype editor
This commit is contained in:
parent
b7e15bf4cd
commit
72bd683900
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal 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
BIN
assets/toyota_corolla_ae86_trueno.glb
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -35,7 +35,7 @@ esac
|
|||||||
|
|
||||||
# Build the game.
|
# Build the game.
|
||||||
echo "Building game$DLL_EXT"
|
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.
|
# 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
|
mv game_tmp$DLL_EXT game$DLL_EXT
|
||||||
|
81
common/container/freelist/freelist.odin
Normal file
81
common/container/freelist/freelist.odin
Normal 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
83
game/assets/assets.odin
Normal 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)
|
||||||
|
}
|
361
game/game.odin
361
game/game.odin
@ -14,17 +14,49 @@
|
|||||||
|
|
||||||
package game
|
package game
|
||||||
|
|
||||||
import "core:fmt"
|
import "assets"
|
||||||
import "core:math/linalg"
|
import "core:math/linalg"
|
||||||
import rl "vendor:raylib"
|
import rl "vendor:raylib"
|
||||||
|
import "vendor:raylib/rlgl"
|
||||||
|
|
||||||
PIXEL_WINDOW_HEIGHT :: 360
|
PIXEL_WINDOW_HEIGHT :: 360
|
||||||
|
|
||||||
|
Track :: struct {
|
||||||
|
points: [dynamic]rl.Vector3,
|
||||||
|
}
|
||||||
Game_Memory :: struct {
|
Game_Memory :: struct {
|
||||||
|
assetman: assets.Asset_Manager,
|
||||||
player_pos: rl.Vector3,
|
player_pos: rl.Vector3,
|
||||||
camera_yaw_pitch: rl.Vector2,
|
camera_yaw_pitch: rl.Vector2,
|
||||||
camera_speed: f32,
|
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
|
g_mem: ^Game_Memory
|
||||||
@ -60,6 +92,8 @@ ui_camera :: proc() -> rl.Camera2D {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update_free_look_camera :: proc() {
|
update_free_look_camera :: proc() {
|
||||||
|
es := &g_mem.es
|
||||||
|
|
||||||
input: rl.Vector2
|
input: rl.Vector2
|
||||||
|
|
||||||
if rl.IsKeyDown(.UP) || rl.IsKeyDown(.W) {
|
if rl.IsKeyDown(.UP) || rl.IsKeyDown(.W) {
|
||||||
@ -75,14 +109,17 @@ update_free_look_camera :: proc() {
|
|||||||
input.x += 1
|
input.x += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if rl.IsKeyPressed(.ESCAPE) {
|
should_capture_mouse := rl.IsMouseButtonDown(.RIGHT)
|
||||||
if g_mem.mouse_captured {
|
if es.mouse_captured != should_capture_mouse {
|
||||||
g_mem.mouse_captured = false
|
if should_capture_mouse {
|
||||||
|
rl.DisableCursor()
|
||||||
|
} else {
|
||||||
rl.EnableCursor()
|
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
|
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
|
g_mem.player_pos += (input.x * right + input.y * forward) * g_mem.camera_speed
|
||||||
}
|
}
|
||||||
|
|
||||||
update :: proc() {
|
add_track_spline_point :: proc() {
|
||||||
if rl.IsMouseButtonPressed(.LEFT) {
|
forward := camera_rotation_matrix()[2]
|
||||||
g_mem.mouse_captured = true
|
|
||||||
rl.DisableCursor()
|
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() {
|
draw :: proc() {
|
||||||
rl.BeginDrawing()
|
rl.BeginDrawing()
|
||||||
|
defer rl.EndDrawing()
|
||||||
rl.ClearBackground(rl.BLACK)
|
rl.ClearBackground(rl.BLACK)
|
||||||
|
|
||||||
|
camera := game_camera_3d()
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
rl.BeginMode3D(game_camera_3d())
|
rl.BeginMode3D(camera)
|
||||||
defer rl.EndMode3D()
|
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.BeginMode2D(ui_camera())
|
||||||
rl.DrawText(fmt.ctprintf("player_pos: %v", g_mem.player_pos), 5, 5, 8, rl.WHITE)
|
defer rl.EndMode2D()
|
||||||
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)
|
@(export)
|
||||||
@ -147,11 +468,15 @@ game_init :: proc() {
|
|||||||
|
|
||||||
g_mem^ = Game_Memory{}
|
g_mem^ = Game_Memory{}
|
||||||
|
|
||||||
|
g_mem.es.selected_track_point = -1
|
||||||
|
|
||||||
game_hot_reloaded(g_mem)
|
game_hot_reloaded(g_mem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@(export)
|
@(export)
|
||||||
game_shutdown :: proc() {
|
game_shutdown :: proc() {
|
||||||
|
assets.shutdown(&g_mem.assetman)
|
||||||
|
delete(g_mem.track.points)
|
||||||
free(g_mem)
|
free(g_mem)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
673
game/physics/collision/collision.odin
Normal file
673
game/physics/collision/collision.odin
Normal 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 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
|
||||||
|
}
|
57
game/physics/collision/octree.odin
Normal file
57
game/physics/collision/octree.odin
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
1
game/physics/physics.odin
Normal file
1
game/physics/physics.odin
Normal file
@ -0,0 +1 @@
|
|||||||
|
package physics
|
52
game/raylib_helpers.odin
Normal file
52
game/raylib_helpers.odin
Normal 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
10
ols.json
Normal 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,
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user