Refactor rewind to include the whole world, not just physics sceen

This commit is contained in:
sergeypdev 2025-05-08 14:32:31 +04:00
parent a4ed430efe
commit df0fe56368
15 changed files with 739 additions and 425 deletions

View File

@ -36,7 +36,7 @@ esac
# Build the game.
echo "Building game$DLL_EXT"
odin build game -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -define:RAYLIB_SHARED=true -define:PHYSFS_SHARED=true -define:TRACY_ENABLE=true -collection:libs=./libs -collection:common=./common -collection:game=./game -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 -define:PHYSFS_SHARED=true -define:TRACY_ENABLE=true -collection:libs=./libs -collection:common=./common -collection:game=./game -build-mode:dll -out:game_tmp$DLL_EXT -strict-style -vet -debug -o:speed
# 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

@ -28,9 +28,9 @@ destroy_spanpool :: proc(s: ^$T/Span_Pool($E)) {
delete(s.free_spans)
}
resolve_slice :: proc(s: ^$T/Span_Pool($E), handle: Handle) -> []E {
assert(int(handle.first + handle.len) <= len(s.elems))
assert(s.generations[handle.first] == handle.gen)
resolve_slice :: proc(s: ^$T/Span_Pool($E), handle: Handle, loc := #caller_location) -> []E {
assert(int(handle.first + handle.len) <= len(s.elems), "invalid spanpool handle", loc)
assert(s.generations[handle.first] == handle.gen, "invalid spanpool handle", loc)
return s.elems[handle.first:handle.first + handle.len]
}

View File

@ -45,7 +45,8 @@ update_free_look_camera :: proc(es: ^Editor_State) {
(input.x * right + input.y * forward) * get_runtime_world().camera_speed
}
update_editor :: proc(es: ^Editor_State) {
update_editor :: proc(es: ^Editor_State, dt: f32) {
update_world(&es.world, dt, false)
update_free_look_camera(es)
switch es.track_edit_state {
@ -150,4 +151,3 @@ update_editor :: proc(es: ^Editor_State) {
}
}
}

View File

@ -0,0 +1,92 @@
package lbp
import "base:intrinsics"
import "core:io"
import "core:mem"
import "core:strings"
Error :: union #shared_nil {
io.Error,
mem.Allocator_Error,
}
Coder :: struct {
stream: io.Stream,
version: i32,
allocator: mem.Allocator,
write: bool,
}
init_read :: proc(reader: io.Reader, allocator: mem.Allocator) -> (coder: Coder, err: Error) {
_, ok := io.to_reader(reader)
assert(ok, "init_read expected a reader")
coder = Coder {
stream = reader,
allocator = allocator,
write = false,
}
err = serialize_number(&coder, &coder.version)
return coder, err
}
init_write :: proc(writer: io.Writer, version: i32) -> (coder: Coder, err: Error) {
_, ok := io.to_writer(writer)
assert(ok, "init_write expected a writer")
coder = Coder {
stream = writer,
allocator = mem.panic_allocator(),
write = true,
version = version,
}
return coder, nil
}
serialize_bytes :: proc(e: ^Coder, val: []byte) -> io.Error {
if e.write {
_, err := io.write(e.stream, val)
return err
} else {
_, err := io.read(e.stream, val)
return err
}
}
serialize_number :: proc(
c: ^Coder,
val: ^$T,
) -> Error where intrinsics.type_is_integer(T) ||
intrinsics.type_is_float(T) {
num_bytes := size_of(val)
return serialize_bytes(c, mem.byte_slice(rawptr(val), size_of(T)))
}
serialize_vector :: proc(
c: ^Coder,
val: $T/^[$C]$V,
) -> Error where intrinsics.type_is_integer(V) ||
intrinsics.type_is_float(T) {
for i in 0 ..< len(val) {
serialize_number(c, &val[i])
}
}
serialize_string :: proc(c: ^Coder, val: ^string) -> Error {
if c.write {
bytes := transmute([]byte)(val^)
length := i32(len(bytes))
serialize_number(c, &length) or_return
serialize_bytes(c, bytes) or_return
} else {
length: i32
serialize_number(c, &length) or_return
bytes := make([]byte, length, c.allocator) or_return
serialize_bytes(c, bytes) or_return
val^ = string(bytes)
}
return nil
}

View File

@ -0,0 +1,56 @@
package toml
import "core:text/scanner"
import "core:unicode/utf8"
Tokenizer :: struct {
scanner: scanner.Scanner,
}
EOF :: scanner.EOF
Ident :: scanner.Ident
Int :: scanner.Int
Float :: scanner.Float
String :: scanner.String
Comment :: scanner.Comment
New_Line :: -9
tokenizer_init :: proc(tokenizer: ^Tokenizer, src: string) {
tokenizer^ = {}
scanner.init(&tokenizer.scanner, src)
tokenizer.scanner.flags = {.Scan_Ints, .Scan_Floats, .Scan_Strings, .Scan_Idents}
tokenizer.scanner.whitespace = {' ', '\t'}
}
is_ident :: #force_inline proc(r: rune) -> bool {
switch r {
case 'a' ..= 'z', 'A' ..= 'Z', '0' ..= '9', '-', '_':
return true
case:
return false
}
}
next_token :: proc(tokenizer: ^Tokenizer) -> rune {
ch := scanner.scan(&tokenizer.scanner)
tok := ch
switch ch {
case EOF, Int, Float, String:
case '\r':
if scanner.peek(&tokenizer.scanner) != '\n' {
scanner.error(&tokenizer.scanner, "invalid line ending, expected \n or \r\n")
} else {
scanner.next(&tokenizer.scanner)
}
tok = New_Line
case '\n':
tok = New_Line
case:
if is_ident(tok) {
ch = scanner.next(&tokenizer.scanner)
}
}
return tok
}

View File

@ -21,10 +21,8 @@ import "core:log"
import "core:math"
import "core:math/linalg"
import "core:slice"
import "game:halfedge"
import "game:physics"
import "game:physics/bvh"
import "game:physics/collision"
import "game:render"
import rl "libs:raylib"
import "libs:raylib/rlgl"
@ -37,24 +35,48 @@ Track :: struct {
points: [dynamic]rl.Vector3,
}
copy_track :: proc(dst, src: ^Track) {
resize(&dst.points, len(src.points))
copy(dst.points[:], src.points[:])
}
destroy_track :: proc(track: ^Track) {
delete(track.points)
}
Debug_Draw_State :: struct {
show_menu: bool,
draw_physics_scene: bool,
}
World :: struct {
player_pos: rl.Vector3,
track: Track,
physics_scene: physics.Scene,
}
destroy_world :: proc(world: ^World) {
delete(world.track.points)
physics.destroy_physics_scene(&world.physics_scene)
}
Runtime_World :: struct {
world: World,
pause: bool,
solver_state: physics.Solver_State,
car_com: rl.Vector3,
car_handle: physics.Body_Handle,
engine_handle: physics.Engine_Handle,
debug_state: Debug_Draw_State,
}
copy_world :: proc(dst, src: ^World) {
copy_track(&dst.track, &src.track)
physics.copy_physics_scene(&dst.physics_scene, &src.physics_scene)
dst.player_pos = src.player_pos
dst.pause = src.pause
dst.car_com = src.car_com
dst.car_handle = src.car_handle
dst.engine_handle = src.engine_handle
dst.debug_state = src.debug_state
}
destroy_world :: proc(world: ^World) {
destroy_track(&world.track)
physics.destroy_physics_scene(&world.physics_scene)
}
Runtime_World :: struct {
world_snapshots: []World,
current_world_index: int,
camera_yaw_pitch: rl.Vector2,
camera_speed: f32,
camera: rl.Camera3D,
@ -62,13 +84,31 @@ Runtime_World :: struct {
camera_mode: Camera_Mode,
dt: f32,
rewind_simulation: bool,
step_simulation: bool,
commit_simulation: bool,
single_step_simulation: bool,
}
destroy_runtime_world :: proc(runtime_world: ^Runtime_World) {
destroy_world(&runtime_world.world)
physics.destroy_solver_state(&runtime_world.solver_state)
runtime_world_init :: proc(runtime_world: ^Runtime_World, num_snapshots: int = 2) {
runtime_world.world_snapshots = make([]World, num_snapshots)
world := runtime_world_current_world(runtime_world)
world.debug_state.show_menu = true
world.debug_state.draw_physics_scene = true
}
runtime_world_current_world :: proc(runtime_world: ^Runtime_World) -> ^World {
return &runtime_world.world_snapshots[runtime_world.current_world_index]
}
runtime_world_next_world :: proc(runtime_world: ^Runtime_World) -> ^World {
next_world_index :=
(runtime_world.current_world_index + 1) %% len(runtime_world.world_snapshots)
return &runtime_world.world_snapshots[next_world_index]
}
runtime_world_destroy :: proc(runtime_world: ^Runtime_World) {
for &world in runtime_world.world_snapshots {
destroy_world(&world)
}
delete(runtime_world.world_snapshots)
}
Car :: struct {
@ -78,7 +118,7 @@ Car :: struct {
SOLVER_CONFIG :: physics.Solver_Config {
timestep = 1.0 / 60,
gravity = rl.Vector3{0, -9.8, 0},
substreps_minus_one = 4 - 1,
substreps_minus_one = 8 - 1,
}
Game_Memory :: struct {
@ -91,6 +131,7 @@ Game_Memory :: struct {
preview_bvh: int,
preview_node: int,
physics_pause: bool,
mouse_captured: bool,
free_cam: bool,
}
@ -133,7 +174,9 @@ get_runtime_world :: proc() -> ^Runtime_World {
}
get_world :: proc() -> ^World {
return g_mem.editor ? &g_mem.es.world : &g_mem.runtime_world.world
return(
g_mem.editor ? &g_mem.es.world : &g_mem.runtime_world.world_snapshots[g_mem.runtime_world.current_world_index] \
)
}
get_editor_state :: proc() -> ^Editor_State {
@ -178,7 +221,7 @@ game_camera_3d :: proc() -> rl.Camera3D {
}
ui_camera :: proc() -> rl.Camera2D {
return {zoom = f32(rl.GetScreenHeight()) / PIXEL_WINDOW_HEIGHT}
return {zoom = 1}
}
select_track_point :: proc(index: int) {
@ -243,17 +286,14 @@ get_movement_axes :: proc(
return out_axes[0:0], out_colors[0:0]
}
update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
world := &runtime_world.world
if !runtime_world.pause {
update_world :: proc(world: ^World, dt: f32, single_step_physics: bool) {
if !world.pause {
car_model := assets.get_model(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb")
car_bounds := rl.GetModelBoundingBox(car_model)
runtime_world.car_com = (car_bounds.min + car_bounds.max) / 2
world.car_com = (car_bounds.min + car_bounds.max) / 2
physics.immediate_body(
&world.physics_scene,
&runtime_world.solver_state,
#hash("floor", "fnv32a"),
physics.Body_Config {
initial_pos = {0, -0.5, 0},
@ -264,7 +304,6 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
physics.immediate_body(
&world.physics_scene,
&runtime_world.solver_state,
#hash("ramp", "fnv32a"),
physics.Body_Config {
initial_pos = {0, 0, 0},
@ -275,9 +314,8 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
car_convex := assets.get_convex(&g_mem.assetman, "assets/car_convex.obj")
runtime_world.car_handle = physics.immediate_body(
world.car_handle = physics.immediate_body(
&world.physics_scene,
&runtime_world.solver_state,
#hash("car", "fnv32a"),
physics.Body_Config {
initial_pos = {0, 4, -10},
@ -297,15 +335,14 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
},
)
if false {
if true {
for x in 0 ..< 10 {
for y in 0 ..< 10 {
physics.immediate_body(
&world.physics_scene,
&runtime_world.solver_state,
hash.fnv32a(slice.to_bytes([]int{(x | y << 8)})),
physics.Body_Config {
initial_pos = {0, 0.5 + f32(y) * 1.1, f32(x) * 3 + 10},
initial_pos = {5, 0.5 + f32(y) * 1.1, f32(x) * 3 + 10},
initial_rot = linalg.QUATERNIONF32_IDENTITY,
shape = physics.Shape_Box{size = 1},
mass = 10,
@ -357,7 +394,6 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
wheel_fl := physics.immediate_suspension_constraint(
&world.physics_scene,
&runtime_world.solver_state,
#hash("FL", "fnv32a"),
{
rel_pos = {-wheel_extent_x_front, wheel_y, wheel_front_z},
@ -366,7 +402,7 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
rest = rest,
natural_frequency = natural_frequency,
damping = damping,
body = runtime_world.car_handle,
body = world.car_handle,
mass = wheel_mass,
pacejka_lat = pacejka_lat,
pacejka_long = pacejka_long,
@ -374,7 +410,6 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
)
wheel_fr := physics.immediate_suspension_constraint(
&world.physics_scene,
&runtime_world.solver_state,
#hash("FR", "fnv32a"),
{
rel_pos = {wheel_extent_x_front, wheel_y, wheel_front_z},
@ -383,7 +418,7 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
rest = rest,
natural_frequency = natural_frequency,
damping = damping,
body = runtime_world.car_handle,
body = world.car_handle,
mass = wheel_mass,
pacejka_lat = pacejka_lat,
pacejka_long = pacejka_long,
@ -391,7 +426,6 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
)
wheel_rl := physics.immediate_suspension_constraint(
&world.physics_scene,
&runtime_world.solver_state,
#hash("RL", "fnv32a"),
{
rel_pos = {-wheel_extent_x_back, wheel_y, wheel_back_z},
@ -400,7 +434,7 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
rest = rest,
natural_frequency = natural_frequency,
damping = damping,
body = runtime_world.car_handle,
body = world.car_handle,
mass = wheel_mass,
pacejka_lat = pacejka_lat,
pacejka_long = pacejka_long,
@ -408,7 +442,6 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
)
wheel_rr := physics.immediate_suspension_constraint(
&world.physics_scene,
&runtime_world.solver_state,
#hash("RR", "fnv32a"),
{
rel_pos = {wheel_extent_x_back, wheel_y, wheel_back_z},
@ -417,16 +450,15 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
rest = rest,
natural_frequency = natural_frequency,
damping = damping,
body = runtime_world.car_handle,
body = world.car_handle,
mass = wheel_mass,
pacejka_lat = pacejka_lat,
pacejka_long = pacejka_long,
},
)
runtime_world.engine_handle = physics.immediate_engine(
world.engine_handle = physics.immediate_engine(
&world.physics_scene,
&runtime_world.solver_state,
#hash("engine", "fnv32a"),
physics.Engine_Config {
rpm_torque_curve = assets.get_curve_2d(
@ -435,14 +467,14 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
),
lowest_rpm = 1200,
rev_limit_rpm = 7800,
rev_limit_interval = 0.025,
rev_limit_interval = 0.01,
inertia = 0.264 * 0.5,
internal_friction = 0.005,
gear_ratios = []f32{3.48, 3.587, 2.022, 1.384, 1, 0.861},
axle = physics.Drive_Axle_Config {
wheels = {wheel_rl, wheel_rr},
wheel_count = 2,
diff_type = .Fixed,
diff_type = .Open,
final_drive_ratio = 4.1,
},
},
@ -480,7 +512,7 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
wheel.brake_impulse = brake_input * (1.0 - BRAKE_BIAS) * BRAKE_IMPULSE
}
engine := physics.get_engine(sim_state, runtime_world.engine_handle)
engine := physics.get_engine(sim_state, world.engine_handle)
engine.throttle = gas_input
if rl.IsKeyPressed(.LEFT_SHIFT) || rl.IsGamepadButtonPressed(0, .RIGHT_FACE_DOWN) {
@ -490,8 +522,8 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
engine.gear -= 1
}
car_body := physics.get_body(sim_state, runtime_world.car_handle)
turn_vel_correction := clamp(30.0 / linalg.length(car_body.v), 0, 1)
car_body := physics.get_body(sim_state, world.car_handle)
turn_vel_correction := clamp(10.0 / linalg.length(car_body.v), 0, 1)
turn_input := rl.GetGamepadAxisMovement(0, .LEFT_X)
if abs(turn_input) < GAMEPAD_DEADZONE {
@ -510,11 +542,37 @@ update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
wheel.turn_angle = TURN_ANGLE * turn_vel_correction * turn_input
}
physics.simulate(
&world.physics_scene,
SOLVER_CONFIG,
dt,
commit = true,
step_mode = single_step_physics ? physics.Step_Mode.Single : physics.Step_Mode.Accumulated_Time,
)
}
}
update_runtime_world :: proc(runtime_world: ^Runtime_World, dt: f32) {
cur_world := runtime_world_current_world(runtime_world)
runtime_world.dt = dt
should_single_step := rl.IsKeyPressed(.PERIOD)
runtime_world.rewind_simulation = rl.IsKeyPressed(.COMMA)
runtime_world.step_simulation = !g_mem.physics_pause || should_single_step
runtime_world.commit_simulation = !g_mem.physics_pause || should_single_step
runtime_world.single_step_simulation = should_single_step
if !runtime_world.rewind_simulation {
next_world := runtime_world_next_world(runtime_world)
copy_world(next_world, cur_world)
update_world(next_world, dt, should_single_step)
if runtime_world.commit_simulation {
runtime_world.current_world_index =
(runtime_world.current_world_index + 1) %% len(runtime_world.world_snapshots)
}
} else {
runtime_world.current_world_index =
(runtime_world.current_world_index - 1) %% len(runtime_world.world_snapshots)
}
}
@ -527,8 +585,9 @@ Orbit_Camera :: struct {
GAMEPAD_DEADZONE :: f32(0.07)
orbit_camera_update :: proc(camera: ^Orbit_Camera) {
world := runtime_world_current_world(get_runtime_world())
camera.target =
physics.get_body(physics.get_sim_state(&get_runtime_world().world.physics_scene), get_runtime_world().car_handle).x
physics.get_body(physics.get_sim_state(&world.physics_scene), world.car_handle).x
gamepad_delta := rl.Vector2 {
rl.GetGamepadAxisMovement(0, .RIGHT_X),
@ -541,7 +600,17 @@ orbit_camera_update :: proc(camera: ^Orbit_Camera) {
gamepad_delta.y = 0
}
mouse_delta := rl.GetMouseDelta()
should_capture_mouse := rl.IsMouseButtonDown(.RIGHT)
if g_mem.mouse_captured != should_capture_mouse {
if should_capture_mouse {
rl.DisableCursor()
} else {
rl.EnableCursor()
}
g_mem.mouse_captured = should_capture_mouse
}
mouse_delta := g_mem.mouse_captured ? rl.GetMouseDelta() : 0
MOUSE_SENSE :: 0.01
GAMEPAD_SENSE :: 1
@ -557,8 +626,6 @@ orbit_camera_update :: proc(camera: ^Orbit_Camera) {
delta = gamepad_delta
}
rl.HideCursor()
camera.yaw += delta.x * final_sense
camera.pitch += delta.y * final_sense
camera.pitch = math.clamp(camera.pitch, -math.PI / 2.0 + 0.0001, math.PI / 2.0 - 0.0001)
@ -588,6 +655,8 @@ orbit_camera_to_rl :: proc(camera: Orbit_Camera) -> rl.Camera3D {
update :: proc() {
tracy.Zone()
ui.rl_update_inputs(&g_mem.ui_context)
ui.begin(&g_mem.ui_context)
if rl.IsKeyPressed(.TAB) {
@ -646,8 +715,11 @@ update :: proc() {
}
if g_mem.editor {
update_editor(get_editor_state())
update_editor(get_editor_state(), dt)
} else {
update_runtime_world(get_runtime_world(), dt)
world := runtime_world_current_world(get_runtime_world())
if g_mem.free_cam {
update_free_look_camera(get_editor_state())
} else {
@ -657,8 +729,8 @@ update :: proc() {
get_runtime_world().camera = orbit_camera_to_rl(get_runtime_world().orbit_camera)
case .Hood:
car := physics.get_body(
physics.get_sim_state(&get_runtime_world().world.physics_scene),
get_runtime_world().car_handle,
physics.get_sim_state(&world.physics_scene),
world.car_handle,
)
cam: rl.Camera3D
@ -673,8 +745,6 @@ update :: proc() {
get_runtime_world().camera = cam
}
}
update_runtime_world(get_runtime_world(), dt)
}
}
@ -706,99 +776,72 @@ catmull_rom :: proc(a, b, c, d: rl.Vector3, t: f32) -> rl.Vector3 {
return a * t3 + b * t2 + c * t + d
}
draw :: proc() {
draw_world :: proc(world: ^World) {
tracy.Zone()
rl.BeginDrawing()
defer rl.EndDrawing()
rl.ClearBackground(rl.GRAY)
render.clear_stencil()
runtime_world := get_runtime_world()
world := get_world()
dt := runtime_world.dt
camera := game_camera_3d()
points := &world.track.points
interpolated_points := calculate_spline_interpolated_points(points[:], context.temp_allocator)
// collision, segment_idx := raycast_spline_tube(
// interpolated_points,
// rl.GetScreenToWorldRay(rl.GetMousePosition(), camera),
// )
if world.debug_state.draw_physics_scene {
physics.draw_debug_scene(&world.physics_scene)
}
sim_state := physics.get_sim_state(&world.physics_scene)
car_body := physics.get_body(sim_state, runtime_world.car_handle)
car_body := physics.get_body(sim_state, world.car_handle)
car_model := assets.get_model(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb")
_ = car_model
mesh_col: bvh.Collision
hit_mesh_idx := -1
rl_ray := rl.GetScreenToWorldRay(rl.GetMousePosition(), camera)
ray := bvh.Ray {
origin = rl_ray.position,
dir = rl_ray.direction,
}
_ = ray
engine := physics.get_engine(sim_state, world.engine_handle)
{
rl.BeginMode3D(camera)
defer rl.EndMode3D()
if rl.IsKeyPressed(.F8) {
world.debug_state.show_menu = !world.debug_state.show_menu
}
if world.debug_state.show_menu {
ui_ctx := &g_mem.ui_context
// rl.DrawGrid(100, 1)
ui.push_font_size_style(ui_ctx, 20)
defer ui.pop_style(ui_ctx)
physics.draw_debug_scene(&world.physics_scene)
physics.draw_debug_ui(&g_mem.ui_context, &world.physics_scene, SOLVER_CONFIG)
if ui.window(ui_ctx, "Debug Menu", {x = 0, y = 0, w = 200, h = 300}) {
cnt := ui.get_current_container(ui_ctx)
cnt.rect.x = max(cnt.rect.x, 0)
cnt.rect.y = max(cnt.rect.y, 0)
cnt.rect.w = max(cnt.rect.w, 200)
cnt.rect.h = max(cnt.rect.h, 300)
box1_mat := linalg.Matrix4f32(1)
box1_mat = linalg.matrix4_rotate(45 * math.RAD_PER_DEG, rl.Vector3{0, 1, 0}) * box1_mat
box2_mat := linalg.Matrix4f32(1)
box2_mat = linalg.matrix4_translate(rl.Vector3{0.0, 0.2, 0}) * box2_mat
box2_mat = linalg.matrix4_rotate(45 * math.RAD_PER_DEG, rl.Vector3{0, 0, 1}) * box2_mat
// box2_mat = linalg.matrix4_rotate(f32(rl.GetTime()), rl.Vector3{0, -1, 0}) * box2_mat
box2_mat = linalg.matrix4_translate(rl.Vector3{0.0, 0, 0}) * box2_mat
box2_mat = linalg.matrix4_rotate(f32(rl.GetTime()) * 0.1, rl.Vector3{0, 1, 0}) * box2_mat
box1, box2 := collision.Box {
pos = 0,
rad = 0.5,
}, collision.Box {
pos = 0,
rad = 0.5,
if .ACTIVE in ui.header(ui_ctx, "General", {.EXPANDED}) {
ui.checkbox(
ui_ctx,
"Show Physics Scene",
&world.debug_state.draw_physics_scene,
)
}
box1_convex := collision.box_to_convex(box1, context.temp_allocator)
box2_convex := collision.box_to_convex(box2, context.temp_allocator)
if .ACTIVE in ui.header(ui_ctx, "Car", {.EXPANDED}) &&
car_body.alive &&
engine.alive {
ui_keyval :: proc(ctx: ^ui.Context, key: string, val: any) {
ui.layout_row(ctx, {100, -1}, 0)
ui.label(ctx, key)
ui.label(ctx, fmt.tprintf("%v", val))
}
halfedge.transform_mesh(&box1_convex, box1_mat)
halfedge.transform_mesh(&box2_convex, box2_mat)
gear_ratios := physics.get_gear_ratios(sim_state, engine.gear_ratios)
ui.layout_row(ui_ctx, {100, -1}, 0)
ui.layout_row(ui_ctx, {100, -1}, 0)
ui_keyval(ui_ctx, "p", car_body.x)
ui_keyval(ui_ctx, "v", car_body.v)
ui_keyval(ui_ctx, "gear", engine.gear)
ui_keyval(ui_ctx, "ratio", physics.lookup_gear_ratio(gear_ratios, engine.gear))
ui_keyval(ui_ctx, "rpm", physics.angular_velocity_to_rpm(engine.w))
ui_keyval(ui_ctx, "clutch", engine.clutch)
ui_keyval(ui_ctx, "speed", linalg.length(car_body.v) * 3.6)
}
}
}
}
if !g_mem.editor {
car_matrix := rl.QuaternionToMatrix(car_body.q)
car_matrix =
(auto_cast linalg.matrix4_translate_f32(physics.body_get_shape_pos(car_body))) *
car_matrix
if !runtime_world.pause {
if runtime_world.rewind_simulation {
world.physics_scene.simulation_state_index = physics.get_prev_sim_state_index(
&world.physics_scene,
)
} else {
physics.simulate(
&world.physics_scene,
&runtime_world.solver_state,
SOLVER_CONFIG,
dt,
commit = runtime_world.step_simulation,
step_mode = g_mem.physics_pause ? physics.Step_Mode.Single : physics.Step_Mode.Accumulated_Time,
)
}
}
(auto_cast linalg.matrix4_translate_f32(physics.body_get_shape_pos(car_body))) * car_matrix
basic_shader := assets.get_shader(
&g_mem.assetman,
@ -809,18 +852,8 @@ draw :: proc() {
light_dir := linalg.normalize(rl.Vector3{1, -1, 0})
ambient := rl.Vector3{0.1, 0.1, 0.1}
light_color := rl.Vector3{0.816, 0.855, 0.89}
rl.SetShaderValue(
basic_shader.shader,
basic_shader.locations[.LightDir],
&light_dir,
.VEC3,
)
rl.SetShaderValue(
basic_shader.shader,
basic_shader.locations[.Ambient],
&ambient,
.VEC3,
)
rl.SetShaderValue(basic_shader.shader, basic_shader.locations[.LightDir], &light_dir, .VEC3)
rl.SetShaderValue(basic_shader.shader, basic_shader.locations[.Ambient], &ambient, .VEC3)
rl.SetShaderValue(
basic_shader.shader,
basic_shader.locations[.LightColor],
@ -835,7 +868,34 @@ draw :: proc() {
car_matrix,
rl.Color{255, 255, 255, 100},
)
}
}
draw :: proc() {
tracy.Zone()
rl.BeginDrawing()
defer rl.EndDrawing()
rl.ClearBackground(rl.GRAY)
render.clear_stencil()
world := get_world()
camera := game_camera_3d()
points := &world.track.points
interpolated_points := calculate_spline_interpolated_points(points[:], context.temp_allocator)
// collision, segment_idx := raycast_spline_tube(
// interpolated_points,
// rl.GetScreenToWorldRay(rl.GetMousePosition(), camera),
// )
{
rl.BeginMode3D(camera)
defer rl.EndMode3D()
draw_world(world)
{
// Debug draw spline road
@ -875,36 +935,6 @@ draw :: proc() {
}
}
}
// if mesh_col.hit {
// mesh := car_model.meshes[hit_mesh_idx]
// vertices := (cast([^]rl.Vector3)mesh.vertices)[:mesh.vertexCount]
// indices := mesh.indices[:mesh.triangleCount * 3]
// car_halfedge := halfedge.mesh_from_vertex_index_list(vertices, indices, 3, context.temp_allocator)
//
// face_idx := halfedge.Face_Index(mesh_col.prim)
// face := car_halfedge.faces[face_idx]
// first_edge_idx := face.edge
//
// first := true
// cur_edge_idx := first_edge_idx
// for first || cur_edge_idx != first_edge_idx {
// first = false
// edge := car_halfedge.edges[cur_edge_idx]
// cur_edge_idx = edge.next
//
// if edge.twin >= 0 {
// twin_edge := car_halfedge.edges[edge.twin]
// face := twin_edge.face
//
// i1, i2, i3 := indices[face * 3 + 0], indices[face * 3 + 1], indices[face * 3 + 2]
// v1, v2, v3 := vertices[i1], vertices[i2], vertices[i3]
//
// rl.DrawTriangle3D(v1, v2, v3, rl.RED)
// }
// }
// }
//
}
}
@ -920,29 +950,6 @@ draw :: proc() {
if g_mem.editor {
rl.DrawText("Editor", 5, 5, 8, rl.ORANGE)
rl.DrawText(
fmt.ctprintf(
"mesh: %v, aabb tests: %v, tri tests: %v",
hit_mesh_idx,
mesh_col.aabb_tests,
mesh_col.triangle_tests,
),
5,
32,
8,
rl.ORANGE,
)
rl.DrawText(
fmt.ctprintf("bvh: %v, node: %v", g_mem.preview_bvh, g_mem.preview_node),
5,
48,
8,
rl.ORANGE,
)
switch g_mem.es.track_edit_state {
case .Select:
case .Move:
@ -954,26 +961,6 @@ draw :: proc() {
rl.ORANGE,
)
}
} else {
car := physics.get_body(sim_state, runtime_world.car_handle)
engine := physics.get_engine(sim_state, runtime_world.engine_handle)
gear_ratios := physics.get_gear_ratios(sim_state, engine.gear_ratios)
rl.DrawText(
fmt.ctprintf(
"p: %v\nv: %v\ngear: %v\nratio: %v\nrpm: %v\nclutch: %v\nspeed: %v km/h",
car.x,
car.v,
engine.gear,
physics.lookup_gear_ratio(gear_ratios, engine.gear),
physics.angular_velocity_to_rpm(engine.w),
engine.clutch,
linalg.length(car.v) * 3.6,
),
5,
32,
8,
rl.ORANGE,
)
}
ui.rl_draw(&g_mem.ui_context)
@ -1117,7 +1104,7 @@ game_init_window :: proc(args: []string) {
init_physfs(args)
rl.SetConfigFlags({.WINDOW_RESIZABLE, .VSYNC_HINT})
rl.InitWindow(1280, 720, "Odin + Raylib + Hot Reload template!")
rl.InitWindow(1280, 720, "Gutter Runner")
rl.SetExitKey(.KEY_NULL)
rl.SetWindowPosition(200, 200)
rl.SetTargetFPS(120)
@ -1132,12 +1119,13 @@ game_init :: proc() {
init_physifs_raylib_callbacks()
assets.assetman_init(&g_mem.assetman)
physics.init_physics_scene(&g_mem.runtime_world.world.physics_scene, 100)
runtime_world_init(&g_mem.runtime_world, 100)
g_mem.default_font = rl.GetFontDefault()
ui.init(&g_mem.ui_context)
g_mem.ui_context.style.font = ui.Font(&g_mem.default_font)
g_mem.ui_context.default_style.font = ui.Font(&g_mem.default_font)
g_mem.ui_context.default_style.font_size = 32
g_mem.ui_context.text_width = ui.rl_measure_text_width
g_mem.ui_context.text_height = ui.rl_measure_text_height
@ -1149,7 +1137,7 @@ game_shutdown :: proc() {
assets.shutdown(&g_mem.assetman)
destroy_world(&g_mem.es.world)
delete(g_mem.es.point_selection)
destroy_runtime_world(&g_mem.runtime_world)
runtime_world_destroy(&g_mem.runtime_world)
free(g_mem)
}
@ -1176,6 +1164,7 @@ game_hot_reloaded :: proc(mem: rawptr) {
g_mem = (^Game_Memory)(mem)
render.init(&g_mem.assetman)
ui.rl_init()
g_mem.runtime_world.orbit_camera.distance = 4
}

View File

@ -85,8 +85,8 @@ convex_vs_convex_sat :: proc(a, b: Convex) -> (manifold: Contact_Manifold, colli
return
}
is_face_a_contact := face_query_a.separation > edge_separation
is_face_b_contact := face_query_b.separation > edge_separation
is_face_a_contact := (face_query_a.separation + 0.2) > edge_separation
is_face_b_contact := (face_query_b.separation + 0.2) > edge_separation
if is_face_a_contact || is_face_b_contact {
manifold = create_face_contact_manifold(face_query_a, a, face_query_b, b)

View File

@ -152,9 +152,9 @@ draw_debug_ui :: proc(ctx: ^ui.Context, scene: ^Scene, config: Solver_Config) {
sim_state := get_sim_state(scene)
active_wheels := []int{}
active_wheels := []int{0, 1}
w, h: i32 = 200, 200
w, h: i32 = 500, 500
window_x: i32 = 0
@ -168,7 +168,7 @@ draw_debug_ui :: proc(ctx: ^ui.Context, scene: ^Scene, config: Solver_Config) {
ctx,
fmt.tprintf("Wheel %v", i),
ui.Rect{x = window_x, y = 0, w = w, h = h},
ui.Options{.AUTO_SIZE},
ui.Options{},
) {
NUM_SAMPLES :: 100
@ -176,7 +176,7 @@ draw_debug_ui :: proc(ctx: ^ui.Context, scene: ^Scene, config: Solver_Config) {
inv_dt := 1.0 / dt
{
// ui.layout_row(ctx, {0}, 200)
ui.layout_row(ctx, {-1}, 300)
{
ui.begin_line(ctx, ui.Color{255, 0, 0, 255})
defer ui.end_line(ctx)
@ -228,8 +228,7 @@ draw_debug_ui :: proc(ctx: ^ui.Context, scene: ^Scene, config: Solver_Config) {
}
{
// ui.layout_row(ctx, {0}, 200)
ui.layout_row(ctx, {-1}, 300)
ui.begin_line(ctx, ui.Color{0, 255, 0, 255})
defer ui.end_line(ctx)

View File

@ -10,14 +10,8 @@ Body_Config_Inertia_Mode :: enum {
Explicit,
}
immediate_body :: proc(
scene: ^Scene,
state: ^Solver_State,
id: u32,
config: Body_Config,
) -> (
handle: Body_Handle,
) {
immediate_body :: proc(scene: ^Scene, id: u32, config: Body_Config) -> (handle: Body_Handle) {
state := &scene.solver_state
sim_state := get_sim_state(scene)
if id in state.immedate_bodies {
body := &state.immedate_bodies[id]
@ -41,12 +35,12 @@ immediate_body :: proc(
immediate_suspension_constraint :: proc(
scene: ^Scene,
state: ^Solver_State,
id: u32,
config: Suspension_Constraint_Config,
) -> (
handle: Suspension_Constraint_Handle,
) {
state := &scene.solver_state
if id in state.immediate_suspension_constraints {
constraint := &state.immediate_suspension_constraints[id]
if constraint.last_ref != state.simulation_frame {
@ -73,12 +67,12 @@ immediate_suspension_constraint :: proc(
immediate_engine :: proc(
scene: ^Scene,
state: ^Solver_State,
id: u32,
config: Engine_Config,
) -> (
handle: Engine_Handle,
) {
state := &scene.solver_state
sim_state := get_sim_state(scene)
if id in state.immediate_engines {
engine := &state.immediate_engines[id]
@ -100,15 +94,16 @@ immediate_engine :: proc(
return
}
prune_immediate :: proc(scene: ^Scene, state: ^Solver_State) {
prune_immediate :: proc(scene: ^Scene) {
tracy.Zone()
prune_immediate_bodies(scene, state)
prune_immediate_suspension_constraints(scene, state)
prune_immediate_engines(scene, state)
prune_immediate_bodies(scene)
prune_immediate_suspension_constraints(scene)
prune_immediate_engines(scene)
}
// TODO: Generic version
prune_immediate_bodies :: proc(scene: ^Scene, state: ^Solver_State) {
prune_immediate_bodies :: proc(scene: ^Scene) {
state := &scene.solver_state
if int(state.num_referenced_bodies) == len(state.immedate_bodies) {
return
}
@ -135,7 +130,8 @@ prune_immediate_bodies :: proc(scene: ^Scene, state: ^Solver_State) {
}
}
prune_immediate_suspension_constraints :: proc(scene: ^Scene, state: ^Solver_State) {
prune_immediate_suspension_constraints :: proc(scene: ^Scene) {
state := &scene.solver_state
if int(state.num_referenced_suspension_constraints) ==
len(state.immediate_suspension_constraints) {
return
@ -165,7 +161,8 @@ prune_immediate_suspension_constraints :: proc(scene: ^Scene, state: ^Solver_Sta
}
}
prune_immediate_engines :: proc(scene: ^Scene, state: ^Solver_State) {
prune_immediate_engines :: proc(scene: ^Scene) {
state := &scene.solver_state
if int(state.num_referenced_engines) == len(state.immediate_engines) {
return
}

View File

@ -3,6 +3,7 @@ package physics
import "collision"
import lg "core:math/linalg"
import "game:container/spanpool"
import "libs:tracy"
MAX_CONTACTS :: 1024 * 16
@ -67,14 +68,52 @@ Sim_State :: struct {
}
Scene :: struct {
simulation_states: []Sim_State,
// Speculative prediction state to find collisions
scratch_sim_state: Sim_State,
simulation_states: [2]Sim_State,
simulation_state_index: i32,
solver_state: Solver_State,
}
init_physics_scene :: proc(scene: ^Scene, max_history := int(2)) {
scene.simulation_states = make([]Sim_State, max(max_history, 2))
// Copy current state to next
copy_sim_state :: proc(dst: ^Sim_State, src: ^Sim_State) {
tracy.Zone()
convex_container_reconcile(&src.convex_container)
dst.num_bodies = src.num_bodies
dst.first_free_body_plus_one = src.first_free_body_plus_one
dst.first_free_suspension_constraint_plus_one = src.first_free_suspension_constraint_plus_one
dst.first_free_engine_plus_one = src.first_free_engine_plus_one
resize(&dst.bodies, len(src.bodies))
resize(&dst.suspension_constraints, len(src.suspension_constraints))
resize(&dst.engines, len(src.engines))
dst.bodies_slice = dst.bodies[:]
dst.suspension_constraints_slice = dst.suspension_constraints[:]
for i in 0 ..< len(dst.bodies) {
dst.bodies[i] = src.bodies[i]
}
for i in 0 ..< len(dst.suspension_constraints) {
dst.suspension_constraints[i] = src.suspension_constraints[i]
}
copy(dst.engines[:], src.engines[:])
contact_container_copy(&dst.contact_container, src.contact_container)
convex_container_copy(&dst.convex_container, src.convex_container)
spanpool.copy(&dst.rpm_torque_curves_pool, src.rpm_torque_curves_pool)
spanpool.copy(&dst.gear_ratios_pool, src.gear_ratios_pool)
}
copy_physics_scene :: proc(dst, src: ^Scene) {
tracy.Zone()
dst.simulation_state_index = src.simulation_state_index
src_sim_state := get_sim_state(src)
dst_sim_state := get_sim_state(dst)
copy_sim_state(dst_sim_state, src_sim_state)
copy_solver_state(&dst.solver_state, &src.solver_state)
}
Body :: struct {
@ -494,8 +533,12 @@ remove_gear_ratios :: proc(sim_state: ^Sim_State, handle: Gear_Ratios_Handle) {
spanpool.free(&sim_state.gear_ratios_pool, spanpool.Handle(handle))
}
get_gear_ratios :: proc(sim_state: ^Sim_State, handle: Gear_Ratios_Handle) -> []f32 {
return spanpool.resolve_slice(&sim_state.gear_ratios_pool, spanpool.Handle(handle))
get_gear_ratios :: proc(
sim_state: ^Sim_State,
handle: Gear_Ratios_Handle,
loc := #caller_location,
) -> []f32 {
return spanpool.resolve_slice(&sim_state.gear_ratios_pool, spanpool.Handle(handle), loc)
}
update_engine_from_config :: proc(
@ -678,6 +721,5 @@ destroy_physics_scene :: proc(scene: ^Scene) {
for &sim_state in scene.simulation_states {
destry_sim_state(&sim_state)
}
destry_sim_state(&scene.scratch_sim_state)
delete(scene.simulation_states)
destroy_solver_state(&scene.solver_state)
}

View File

@ -10,10 +10,8 @@ import "core:math"
import lg "core:math/linalg"
import "core:math/rand"
import "core:slice"
import "game:container/spanpool"
import "game:debug"
import he "game:halfedge"
import rl "libs:raylib"
import "libs:tracy"
_ :: log
@ -45,6 +43,28 @@ Solver_State :: struct {
immediate_engines: map[u32]Immedate_State(Engine_Handle),
}
copy_map :: proc(dst, src: $T/^map[$K]$V) {
clear(dst)
reserve_map(dst, len(src))
for k, v in src {
dst[k] = v
}
}
copy_solver_state :: proc(dst, src: ^Solver_State) {
dst.accumulated_time = src.accumulated_time
dst.simulation_frame = src.simulation_frame
dst.num_referenced_bodies = src.num_referenced_bodies
dst.num_referenced_suspension_constraints = src.num_referenced_suspension_constraints
dst.num_referenced_engines = src.num_referenced_engines
copy_map(&dst.immedate_bodies, &src.immedate_bodies)
copy_map(&dst.immediate_suspension_constraints, &src.immediate_suspension_constraints)
copy_map(&dst.immediate_engines, &src.immediate_engines)
}
destroy_solver_state :: proc(state: ^Solver_State) {
delete(state.immedate_bodies)
delete(state.immediate_suspension_constraints)
@ -59,38 +79,6 @@ Immedate_State :: struct($T: typeid) {
MAX_STEPS :: 10
// TODO: move into scene.odin
// Copy current state to next
sim_state_copy :: proc(dst: ^Sim_State, src: ^Sim_State) {
tracy.Zone()
convex_container_reconcile(&src.convex_container)
dst.num_bodies = src.num_bodies
dst.first_free_body_plus_one = src.first_free_body_plus_one
dst.first_free_suspension_constraint_plus_one = src.first_free_suspension_constraint_plus_one
dst.first_free_engine_plus_one = src.first_free_engine_plus_one
resize(&dst.bodies, len(src.bodies))
resize(&dst.suspension_constraints, len(src.suspension_constraints))
resize(&dst.engines, len(src.engines))
dst.bodies_slice = dst.bodies[:]
dst.suspension_constraints_slice = dst.suspension_constraints[:]
for i in 0 ..< len(dst.bodies) {
dst.bodies[i] = src.bodies[i]
}
for i in 0 ..< len(dst.suspension_constraints) {
dst.suspension_constraints[i] = src.suspension_constraints[i]
}
copy(dst.engines[:], src.engines[:])
contact_container_copy(&dst.contact_container, src.contact_container)
convex_container_copy(&dst.convex_container, src.convex_container)
spanpool.copy(&dst.rpm_torque_curves_pool, src.rpm_torque_curves_pool)
spanpool.copy(&dst.gear_ratios_pool, src.gear_ratios_pool)
}
Step_Mode :: enum {
Accumulated_Time,
Single,
@ -203,7 +191,7 @@ find_new_contacts :: proc(sim_state: ^Sim_State, tlas: ^TLAS) {
pair := make_contact_pair(i32(body_idx), i32(other_body_idx))
if body_idx != other_body_idx &&
(true || bvh.test_aabb_vs_aabb(body_aabb, prim_aabb)) &&
(bvh.test_aabb_vs_aabb(body_aabb, prim_aabb)) &&
!(pair in sim_state.contact_container.lookup) {
new_contact_idx := len(sim_state.contact_container.contacts)
@ -226,7 +214,6 @@ find_new_contacts :: proc(sim_state: ^Sim_State, tlas: ^TLAS) {
// Outer simulation loop for fixed timestepping
simulate :: proc(
scene: ^Scene,
state: ^Solver_State,
config: Solver_Config,
dt: f32,
commit := true, // commit = false is a special mode for debugging physics stepping to allow rerunning the same step each frame
@ -235,9 +222,11 @@ simulate :: proc(
tracy.Zone()
assert(config.timestep > 0)
prune_immediate(scene, state)
state := &scene.solver_state
sim_state_copy(get_next_sim_state(scene), get_sim_state(scene))
prune_immediate(scene)
copy_sim_state(get_next_sim_state(scene), get_sim_state(scene))
sim_state := get_next_sim_state(scene)
@ -346,11 +335,6 @@ update_contacts :: proc(sim_state: ^Sim_State) {
body_get_convex_shape_world(sim_state, body),
body_get_convex_shape_world(sim_state, body2)
if contact_idx == 2 {
he.debug_draw_mesh_wires(m1, rl.RED)
he.debug_draw_mesh_wires(m2, rl.BLUE)
}
// Raw manifold has contact points in world space
raw_manifold, collision := collision.convex_vs_convex_sat(m1, m2)
@ -450,7 +434,7 @@ pgs_solve_contacts :: proc(
body_local_to_world(body2, manifold.points_b[point_idx])
p_diff_normal := lg.dot(p2 - p1, manifold.normal)
separation := p_diff_normal
separation := p_diff_normal + 0.01
w1 := get_body_inverse_mass(body1, manifold.normal, p1)
w2 := get_body_inverse_mass(body2, manifold.normal, p2)
@ -536,10 +520,10 @@ pgs_solve_contacts :: proc(
applied_impulse_vec :=
applied_impulse.x * manifold.tangent + applied_impulse.y * manifold.bitangent
rl.DrawSphereWires(p1, 0.05, 8, 8, rl.RED)
rl.DrawLine3D(p1, p1 + v1, rl.RED)
rl.DrawSphereWires(p2, 0.05, 8, 8, rl.BLUE)
rl.DrawLine3D(p2, p2 + v2, rl.BLUE)
// rl.DrawSphereWires(p1, 0.05, 8, 8, rl.RED)
// rl.DrawLine3D(p1, p1 + v1, rl.RED)
// rl.DrawSphereWires(p2, 0.05, 8, 8, rl.BLUE)
// rl.DrawLine3D(p2, p2 + v2, rl.BLUE)
apply_velocity_correction(body1, -applied_impulse_vec, p1)
apply_velocity_correction(body2, applied_impulse_vec, p2)
@ -849,7 +833,7 @@ pgs_solve_suspension :: proc(
v.slip_ratio = slip_ratio
v.slip_angle = slip_angle
MAX_SLIP_LEN :: f32(1.5)
MAX_SLIP_LEN :: f32(1)
slip_vec := Vec2 {
slip_angle / PACEJKA94_LATERAL_PEAK_X / MAX_SLIP_LEN,
@ -1121,10 +1105,19 @@ simulate_step :: proc(scene: ^Scene, sim_state: ^Sim_State, config: Solver_Confi
for i < len(sim_state.contact_container.contacts) {
contact := sim_state.contact_container.contacts[i]
should_remove := false
should_remove |= !get_body(sim_state, contact.a).alive
should_remove |= !get_body(sim_state, contact.b).alive
if !should_remove {
aabb_a := tlas.body_aabbs[int(contact.a) - 1]
aabb_b := tlas.body_aabbs[int(contact.b) - 1]
if false && !bvh.test_aabb_vs_aabb(aabb_a, aabb_b) {
should_remove |= !bvh.test_aabb_vs_aabb(aabb_a, aabb_b)
}
if should_remove {
removed_pair := make_contact_pair(i32(contact.a) - 1, i32(contact.b) - 1)
delete_key(&sim_state.contact_container.lookup, removed_pair)

View File

@ -40,6 +40,7 @@ CLIP_STACK_SIZE :: #config(MICROUI_CLIP_STACK_SIZE, 32)
ID_STACK_SIZE :: #config(MICROUI_ID_STACK_SIZE, 32)
LAYOUT_STACK_SIZE :: #config(MICROUI_LAYOUT_STACK_SIZE, 16)
LINE_STACK_SIZE :: #config(MICROUI_LINE_STACK_SIZE, 16)
STYLE_STACK_SIZE :: #config(MICROUI_STYLE_STACK_SIZE, 16)
CONTAINER_POOL_SIZE :: #config(MICROUI_CONTAINER_POOL_SIZE, 48)
TREENODE_POOL_SIZE :: #config(MICROUI_TREENODE_POOL_SIZE, 48)
MAX_WIDTHS :: #config(MICROUI_MAX_WIDTHS, 16)
@ -178,6 +179,7 @@ Command_Text :: struct {
pos: Vec2,
color: Color,
str: string, /* + string data (VLA) */
font_size: i32,
}
Command_Icon :: struct {
using command: Command,
@ -225,6 +227,7 @@ Container :: struct {
Style :: struct {
font: Font,
font_size: i32,
size: Vec2,
padding: i32,
spacing: i32,
@ -238,12 +241,11 @@ Style :: struct {
Context :: struct {
/* callbacks */
text_width: proc(font: Font, str: string) -> i32,
text_height: proc(font: Font) -> i32,
text_width: proc(font: Font, font_size: i32, str: string) -> i32,
text_height: proc(font: Font, font_size: i32) -> i32,
draw_frame: proc(ctx: ^Context, rect: Rect, colorid: Color_Type),
/* core state */
_style: Style,
style: ^Style,
default_style: Style,
hover_id, focus_id, last_id: Id,
last_rect: Rect,
last_zindex: i32,
@ -262,6 +264,7 @@ Context :: struct {
id_stack: Stack(Id, ID_STACK_SIZE),
layout_stack: Stack(Layout, LAYOUT_STACK_SIZE),
lines_stack: Stack(Line, LINE_STACK_SIZE),
style_stack: Stack(Style, STYLE_STACK_SIZE),
/* retained state pools */
container_pool: [CONTAINER_POOL_SIZE]Pool_Item,
containers: [CONTAINER_POOL_SIZE]Container,
@ -349,14 +352,36 @@ rect_from_point_extent :: proc(p, e: Vec2) -> Rect {
return Rect{x = p.x - e.x, y = p.y - e.y, w = e.x * 2, h = e.y * 2}
}
get_style :: proc(ctx: ^Context) -> ^Style {
if ctx.style_stack.idx > 0 {
return &ctx.style_stack.items[ctx.style_stack.idx - 1]
}
return &ctx.default_style
}
push_style :: proc(ctx: ^Context, style: Style) {
push(&ctx.style_stack, style)
}
pop_style :: proc(ctx: ^Context) {
pop(&ctx.style_stack)
}
push_font_size_style :: proc(ctx: ^Context, font_size: i32) {
style := get_style(ctx)^
style.font_size = font_size
push_style(ctx, style)
}
@(private)
default_draw_frame :: proc(ctx: ^Context, rect: Rect, colorid: Color_Type) {
draw_rect(ctx, rect, ctx.style.colors[colorid])
draw_rect(ctx, rect, get_style(ctx).colors[colorid])
if colorid == .SCROLL_BASE || colorid == .SCROLL_THUMB || colorid == .TITLE_BG {
return
}
if ctx.style.colors[.BORDER].a != 0 { /* draw border */
draw_box(ctx, expand_rect(rect, 1), ctx.style.colors[.BORDER])
if get_style(ctx).colors[.BORDER].a != 0 { /* draw border */
draw_box(ctx, expand_rect(rect, 1), get_style(ctx).colors[.BORDER])
}
}
@ -368,8 +393,7 @@ init :: proc(
) {
ctx^ = {} // zero memory
ctx.draw_frame = default_draw_frame
ctx._style = default_style
ctx.style = &ctx._style
ctx.default_style = default_style
ctx.text_input = strings.builder_from_bytes(ctx._text_store[:])
ctx.textbox_state.set_clipboard = set_clipboard
@ -398,6 +422,7 @@ end :: proc(ctx: ^Context) {
assert(ctx.id_stack.idx == 0)
assert(ctx.layout_stack.idx == 0)
assert(ctx.lines_stack.idx == 0)
assert(ctx.style_stack.idx == 0)
/* handle scroll input */
if ctx.scroll_target != nil {
@ -759,8 +784,20 @@ draw_box :: proc(ctx: ^Context, rect: Rect, color: Color) {
draw_rect(ctx, Rect{rect.x + rect.w - 1, rect.y, 1, rect.h}, color)
}
draw_text :: proc(ctx: ^Context, font: Font, str: string, pos: Vec2, color: Color) {
rect := Rect{pos.x, pos.y, ctx.text_width(font, str), ctx.text_height(font)}
draw_text :: proc(
ctx: ^Context,
font: Font,
font_size: i32,
str: string,
pos: Vec2,
color: Color,
) {
rect := Rect {
pos.x,
pos.y,
ctx.text_width(font, font_size, str),
ctx.text_height(font, font_size),
}
clipped := check_clip(ctx, rect)
switch clipped {
case .NONE: // okay
@ -774,6 +811,7 @@ draw_text :: proc(ctx: ^Context, font: Font, str: string, pos: Vec2, color: Colo
text_cmd.pos = pos
text_cmd.color = color
text_cmd.font = font
text_cmd.font_size = font_size
/* copy string */
dst_str := ([^]byte)(text_cmd)[size_of(Command_Text):][:len(str)]
copy(dst_str, str)
@ -825,10 +863,23 @@ end_line :: proc(ctx: ^Context) {
first_segment := line.first_segment
num_segments := ctx.line_segments_num - first_segment
clipped := check_clip(ctx, line.rect)
switch clipped {
case .NONE:
case .ALL:
return
case .PART:
set_clip(ctx, get_clip_rect(ctx))
}
cmd := push_command(ctx, Command_Line)
cmd.first_segment = first_segment
cmd.num_segments = num_segments
cmd.color = line.color
if clipped != .NONE {
set_clip(ctx, unclipped_rect)
}
}
push_line_point :: proc(ctx: ^Context, p: Vec2f) {
@ -905,7 +956,7 @@ layout_set_next :: proc(ctx: ^Context, r: Rect, relative: bool) {
layout_next :: proc(ctx: ^Context) -> (res: Rect) {
layout := get_layout(ctx)
style := ctx.style
style := get_style(ctx)
defer ctx.last_rect = res
if layout.next_type != .NONE {
@ -996,18 +1047,19 @@ draw_control_text :: proc(
opt := Options{},
) {
pos: Vec2
font := ctx.style.font
tw := ctx.text_width(font, str)
font := get_style(ctx).font
font_size := get_style(ctx).font_size
tw := ctx.text_width(font, font_size, str)
push_clip_rect(ctx, rect)
pos.y = rect.y + (rect.h - ctx.text_height(font)) / 2
pos.y = rect.y + (rect.h - ctx.text_height(font, font_size)) / 2
if .ALIGN_CENTER in opt {
pos.x = rect.x + (rect.w - tw) / 2
} else if .ALIGN_RIGHT in opt {
pos.x = rect.x + rect.w - tw - ctx.style.padding
pos.x = rect.x + rect.w - tw - get_style(ctx).padding
} else {
pos.x = rect.x + ctx.style.padding
pos.x = rect.x + get_style(ctx).padding
}
draw_text(ctx, font, str, pos, ctx.style.colors[colorid])
draw_text(ctx, font, font_size, str, pos, get_style(ctx).colors[colorid])
pop_clip_rect(ctx)
}
@ -1052,10 +1104,12 @@ update_control :: proc(ctx: ^Context, id: Id, rect: Rect, opt := Options{}) {
text :: proc(ctx: ^Context, text: string) {
text := text
font := ctx.style.font
color := ctx.style.colors[.TEXT]
style := get_style(ctx)
font := style.font
font_size := style.font_size
color := style.colors[.TEXT]
layout_begin_column(ctx)
layout_row(ctx, {-1}, ctx.text_height(font))
layout_row(ctx, {-1}, ctx.text_height(font, font_size))
for len(text) > 0 {
w: i32
start: int
@ -1064,12 +1118,12 @@ text :: proc(ctx: ^Context, text: string) {
for ch, i in text {
if ch == ' ' || ch == '\n' {
word := text[start:i]
w += ctx.text_width(font, word)
w += ctx.text_width(font, font_size, word)
if w > r.w && start != 0 {
end = start
break
}
w += ctx.text_width(font, text[i:i + 1])
w += ctx.text_width(font, font_size, text[i:i + 1])
if ch == '\n' {
end = i + 1
break
@ -1077,14 +1131,14 @@ text :: proc(ctx: ^Context, text: string) {
start = i + 1
}
}
draw_text(ctx, font, text[:end], Vec2{r.x, r.y}, color)
draw_text(ctx, font, font_size, text[:end], Vec2{r.x, r.y}, color)
text = text[end:]
}
layout_end_column(ctx)
}
label :: proc(ctx: ^Context, text: string) {
draw_control_text(ctx, text, layout_next(ctx), .TEXT)
label :: proc(ctx: ^Context, text: string, opt := Options{}) {
draw_control_text(ctx, text, layout_next(ctx), .TEXT, opt)
}
button :: proc(
@ -1108,7 +1162,7 @@ button :: proc(
draw_control_text(ctx, label, r, .TEXT, opt)
}
if icon != .NONE {
draw_icon(ctx, icon, r, ctx.style.colors[.TEXT])
draw_icon(ctx, icon, r, get_style(ctx).colors[.TEXT])
}
return
}
@ -1126,7 +1180,7 @@ checkbox :: proc(ctx: ^Context, label: string, state: ^bool) -> (res: Result_Set
/* draw */
draw_control_frame(ctx, id, box, .BASE, {})
if state^ {
draw_icon(ctx, .CHECK, box, ctx.style.colors[.TEXT])
draw_icon(ctx, .CHECK, box, get_style(ctx).colors[.TEXT])
}
r = Rect{r.x + box.w, r.y, r.w - box.w, r.h}
draw_control_text(ctx, label, r, .TEXT)
@ -1145,7 +1199,9 @@ textbox_raw :: proc(
) {
update_control(ctx, id, r, opt | {.HOLD_FOCUS})
font := ctx.style.font
style := get_style(ctx)
font := style.font
font_size := style.font_size
if ctx.focus_id == id {
/* create a builder backed by the user's buffer */
@ -1259,7 +1315,9 @@ textbox_raw :: proc(
continue
}
if ctx.mouse_pos.x <
r.x + ctx.textbox_offset + ctx.text_width(font, string(textbuf[:i])) {
r.x +
ctx.textbox_offset +
ctx.text_width(font, font_size, string(textbuf[:i])) {
idx = i
break
}
@ -1276,14 +1334,14 @@ textbox_raw :: proc(
/* draw */
draw_control_frame(ctx, id, r, .BASE, opt)
if ctx.focus_id == id {
text_color := ctx.style.colors[.TEXT]
sel_color := ctx.style.colors[.SELECTION_BG]
textw := ctx.text_width(font, textstr)
texth := ctx.text_height(font)
headx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[0]])
tailx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[1]])
ofmin := max(ctx.style.padding - headx, r.w - textw - ctx.style.padding)
ofmax := min(r.w - headx - ctx.style.padding, ctx.style.padding)
text_color := style.colors[.TEXT]
sel_color := style.colors[.SELECTION_BG]
textw := ctx.text_width(font, font_size, textstr)
texth := ctx.text_height(font, font_size)
headx := ctx.text_width(font, font_size, textstr[:ctx.textbox_state.selection[0]])
tailx := ctx.text_width(font, font_size, textstr[:ctx.textbox_state.selection[1]])
ofmin := max(get_style(ctx).padding - headx, r.w - textw - get_style(ctx).padding)
ofmax := min(r.w - headx - get_style(ctx).padding, get_style(ctx).padding)
ctx.textbox_offset = clamp(ctx.textbox_offset, ofmin, ofmax)
textx := r.x + ctx.textbox_offset
texty := r.y + (r.h - texth) / 2
@ -1293,7 +1351,7 @@ textbox_raw :: proc(
Rect{textx + min(headx, tailx), texty, abs(headx - tailx), texth},
sel_color,
)
draw_text(ctx, font, textstr, Vec2{textx, texty}, text_color)
draw_text(ctx, font, font_size, textstr, Vec2{textx, texty}, text_color)
draw_rect(ctx, Rect{textx + headx, texty, 1, texth}, text_color)
pop_clip_rect(ctx)
} else {
@ -1372,7 +1430,7 @@ slider :: proc(
/* draw base */
draw_control_frame(ctx, id, base, .BASE, opt)
/* draw thumb */
w := ctx.style.thumb_size
w := get_style(ctx).thumb_size
x := i32((v - low) * Real(base.w - w) / (high - low))
thumb := Rect{base.x + x, base.y, w, base.h}
draw_control_frame(ctx, id, thumb, .BUTTON, opt)
@ -1452,14 +1510,15 @@ _header :: proc(ctx: ^Context, label: string, is_treenode: bool, opt := Options{
} else {
draw_control_frame(ctx, id, r, .BUTTON)
}
style := get_style(ctx)
draw_icon(
ctx,
expanded ? .EXPANDED : .COLLAPSED,
Rect{r.x, r.y, r.h, r.h},
ctx.style.colors[.TEXT],
style.colors[.TEXT],
)
r.x += r.h - ctx.style.padding
r.w -= r.h - ctx.style.padding
r.x += r.h - style.padding
r.w -= r.h - style.padding
draw_control_text(ctx, label, r, .TEXT)
return expanded ? {.ACTIVE} : {}
}
@ -1471,14 +1530,14 @@ header :: proc(ctx: ^Context, label: string, opt := Options{}) -> Result_Set {
begin_treenode :: proc(ctx: ^Context, label: string, opt := Options{}) -> Result_Set {
res := _header(ctx, label, true, opt)
if .ACTIVE in res {
get_layout(ctx).indent += ctx.style.indent
get_layout(ctx).indent += get_style(ctx).indent
push(&ctx.id_stack, ctx.last_id)
}
return res
}
end_treenode :: proc(ctx: ^Context) {
get_layout(ctx).indent -= ctx.style.indent
get_layout(ctx).indent -= get_style(ctx).indent
pop_id(ctx)
}
@ -1512,7 +1571,7 @@ scrollbar :: proc(ctx: ^Context, cnt: ^Container, _b: ^Rect, cs: Vec2, id_string
/* get sizing / positioning */
base := b^
base.pos[1 - i] = b.pos[1 - i] + b.size[1 - i]
base.size[1 - i] = ctx.style.scrollbar_size
base.size[1 - i] = get_style(ctx).scrollbar_size
/* handle input */
update_control(ctx, id, transmute(Rect)base)
@ -1525,7 +1584,7 @@ scrollbar :: proc(ctx: ^Context, cnt: ^Container, _b: ^Rect, cs: Vec2, id_string
/* draw base and thumb */
ctx.draw_frame(ctx, transmute(Rect)base, .SCROLL_BASE)
thumb := base
thumb.size[i] = max(ctx.style.thumb_size, base.size[i] * b.size[i] / cs[i])
thumb.size[i] = max(get_style(ctx).thumb_size, base.size[i] * b.size[i] / cs[i])
thumb.pos[i] += cnt.scroll[i] * (base.size[i] - thumb.size[i]) / maxscroll
ctx.draw_frame(ctx, transmute(Rect)thumb, .SCROLL_THUMB)
@ -1541,10 +1600,10 @@ scrollbar :: proc(ctx: ^Context, cnt: ^Container, _b: ^Rect, cs: Vec2, id_string
@(private)
scrollbars :: proc(ctx: ^Context, cnt: ^Container, body: ^Rect) {
sz := ctx.style.scrollbar_size
sz := get_style(ctx).scrollbar_size
cs := cnt.content_size
cs.x += ctx.style.padding * 2
cs.y += ctx.style.padding * 2
cs.x += get_style(ctx).padding * 2
cs.y += get_style(ctx).padding * 2
push_clip_rect(ctx, body^)
/* resize body to make room for scrollbars */
if cs.y > cnt.body.h {body.w -= sz}
@ -1562,7 +1621,7 @@ push_container_body :: proc(ctx: ^Context, cnt: ^Container, body: Rect, opt := O
if .NO_SCROLL not_in opt {
scrollbars(ctx, cnt, &body)
}
push_layout(ctx, expand_rect(body, -ctx.style.padding), cnt.scroll)
push_layout(ctx, expand_rect(body, -get_style(ctx).padding), cnt.scroll)
cnt.body = body
}
@ -1621,7 +1680,7 @@ begin_window :: proc(ctx: ^Context, title: string, rect: Rect, opt := Options{})
/* do title bar */
if .NO_TITLE not_in opt {
tr := rect
tr.h = ctx.style.title_height
tr.h = get_style(ctx).title_height
ctx.draw_frame(ctx, tr, .TITLE_BG)
/* do title text */
@ -1642,7 +1701,7 @@ begin_window :: proc(ctx: ^Context, title: string, rect: Rect, opt := Options{})
cid := get_id(ctx, "!close")
r := Rect{tr.x + tr.w - tr.h, tr.y, tr.h, tr.h}
tr.w -= r.w
draw_icon(ctx, .CLOSE, r, ctx.style.colors[.TITLE_TEXT])
draw_icon(ctx, .CLOSE, r, get_style(ctx).colors[.TITLE_TEXT])
update_control(ctx, cid, r, opt)
if .LEFT in ctx.mouse_released_bits && cid == ctx.hover_id {
cnt.open = false
@ -1652,10 +1711,10 @@ begin_window :: proc(ctx: ^Context, title: string, rect: Rect, opt := Options{})
/* do `resize` handle */
if .NO_RESIZE not_in opt {
sz := ctx.style.footer_height
sz := get_style(ctx).footer_height
rid := get_id(ctx, "!resize")
r := Rect{rect.x + rect.w - sz, rect.y + rect.h - sz, sz, sz}
draw_icon(ctx, .RESIZE, r, ctx.style.colors[.TEXT])
draw_icon(ctx, .RESIZE, r, get_style(ctx).colors[.TEXT])
update_control(ctx, rid, r, opt)
if rid == ctx.focus_id && .LEFT in ctx.mouse_down_bits {
cnt.rect.w = max(96, cnt.rect.w + ctx.mouse_delta.x)

View File

@ -6,51 +6,78 @@ import "core:log"
import "core:strings"
import rl "libs:raylib"
import "libs:raylib/rlgl"
import gl "vendor:OpenGL"
_ :: log
default_atlas_texture: rl.Texture2D
rl_init :: proc() {
rl.UnloadTexture(default_atlas_texture)
default_atlas_texture = {}
image := rl.Image{
data = &default_atlas_alpha,
width = DEFAULT_ATLAS_WIDTH,
height = DEFAULT_ATLAS_HEIGHT,
mipmaps = 1,
format = .UNCOMPRESSED_GRAYSCALE,
}
default_atlas_texture = rl.LoadTextureFromImage(image)
rl.SetTextureFilter(default_atlas_texture, .POINT)
gl.BindTexture(gl.TEXTURE_2D, default_atlas_texture.id)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_SWIZZLE_A, gl.RED)
gl.BindTexture(gl.TEXTURE_2D, 0)
}
to_rl_color :: proc(c: Color) -> rl.Color {
return rl.Color{c.r, c.g, c.b, c.a}
}
rl_get_font_size :: proc(font: Font) -> i32 {
font := cast(^rl.Font)font
return font.baseSize if font != nil else 16
to_rl_rect :: proc(r: Rect) -> rl.Rectangle {
return rl.Rectangle{x = f32(r.x), y = f32(r.y), width = f32(r.w), height = f32(r.h)}
}
rl_measure_text_2d :: #force_inline proc(font: Font, text: string) -> rl.Vector2 {
font_size := rl_get_font_size(font)
font := (cast(^rl.Font)font)^ if font != nil else rl.GetFontDefault()
rl_measure_text_2d :: #force_inline proc(font: Font, font_size: i32, text: string) -> rl.Vector2 {
font := (cast(^rl.Font)font)
size := rl.MeasureTextEx(
font,
font^ if font != nil else rl.GetFontDefault(),
strings.clone_to_cstring(text, context.temp_allocator),
f32(font_size),
0,
f32(font_size / (font.baseSize if font != nil else 10)),
)
return size
}
rl_measure_text_width :: proc(font: Font, text: string) -> i32 {
return i32(rl_measure_text_2d(font, text).x)
rl_measure_text_width :: proc(font: Font, font_size: i32, text: string) -> i32 {
return i32(rl_measure_text_2d(font, font_size, text).x)
}
rl_measure_text_height :: proc(font: Font) -> i32 {
return i32(rl_measure_text_2d(font, "A").y)
rl_measure_text_height :: proc(font: Font, font_size: i32) -> i32 {
return i32(rl_measure_text_2d(font, font_size, "A").y)
}
rl_draw :: proc(ctx: ^Context) {
tmp_cmd: ^Command
for cmd in next_command_iterator(ctx, &tmp_cmd) {
log.debugf("ui cmd: %v", cmd)
switch c in cmd {
case ^Command_Clip:
rlgl.Scissor(c.rect.x, c.rect.y, c.rect.w, c.rect.h)
if c.rect == unclipped_rect {
rl.EndScissorMode()
} else {
rl.BeginScissorMode(c.rect.x, c.rect.y, c.rect.w, c.rect.h)
}
case ^Command_Text:
font := cast(^rl.Font)c.font
rl.DrawText(
rl.DrawTextEx(
font^ if font != nil else rl.GetFontDefault(),
strings.clone_to_cstring(c.str, context.temp_allocator),
c.pos.x,
c.pos.y,
font.baseSize if font != nil else 16,
rl.Vector2{f32(c.pos.x), f32(c.pos.y)},
f32(c.font_size),
f32(c.font_size / (font.baseSize if font != nil else 10)),
to_rl_color(c.color),
)
case ^Command_Rect:
@ -58,14 +85,74 @@ rl_draw :: proc(ctx: ^Context) {
case ^Command_Line:
segments := get_line_segments(ctx, c.first_segment, c.num_segments)
for i in 1 ..< len(segments) {
p1 := segments[i - 1]
p2 := segments[i]
rl.DrawLineV(p1, p2, to_rl_color(c.color))
}
rl.DrawLineStrip(&segments[0], i32(len(segments)), to_rl_color(c.color))
case ^Command_Jump:
case ^Command_Icon:
src_rect := default_atlas[int(c.id)]
x := f32(c.rect.x + (c.rect.w - src_rect.w) / 2)
y := f32(c.rect.y + (c.rect.h - src_rect.h) / 2)
rl.DrawTextureRec(
default_atlas_texture,
to_rl_rect(src_rect),
{x, y},
to_rl_color(c.color),
)
}
}
rlgl.DrawRenderBatchActive()
rlgl.DisableScissorTest()
}
RL_MOUSE_BUTTON_MAPPING :: [Mouse]rl.MouseButton {
.LEFT = .LEFT,
.RIGHT = .RIGHT,
.MIDDLE = .MIDDLE,
}
RL_KEY_MAPPING :: [Key]rl.KeyboardKey {
.SHIFT = .LEFT_SHIFT,
.CTRL = .LEFT_CONTROL,
.ALT = .LEFT_ALT,
.BACKSPACE = .BACKSPACE,
.DELETE = .DELETE,
.RETURN = .ENTER,
.LEFT = .LEFT,
.RIGHT = .RIGHT,
.HOME = .HOME,
.END = .END,
.A = .A,
.X = .X,
.C = .C,
.V = .V,
}
rl_update_inputs :: proc(ctx: ^Context) {
ctx.mouse_pos.x = rl.GetMouseX()
ctx.mouse_pos.y = rl.GetMouseY()
for rl_btn, ui_btn in RL_MOUSE_BUTTON_MAPPING {
if rl.IsMouseButtonPressed(rl_btn) {
input_mouse_down(ctx, ctx.mouse_pos.x, ctx.mouse_pos.y, ui_btn)
}
if rl.IsMouseButtonReleased(rl_btn) {
input_mouse_up(ctx, ctx.mouse_pos.x, ctx.mouse_pos.y, ui_btn)
}
}
wheel_move := rl.GetMouseWheelMoveV() * -50
input_scroll(ctx, i32(wheel_move.x), i32(wheel_move.y))
for rl_key, ui_key in RL_KEY_MAPPING {
if rl.IsKeyPressed(rl_key) {
input_key_down(ctx, ui_key)
}
if rl.IsKeyReleased(rl_key) {
input_key_up(ctx, ui_key)
}
}
for char := rl.GetCharPressed(); char != 0; char = rl.GetCharPressed() {
strings.write_rune(&ctx.text_input, char)
}
}

BIN
src_assets/car_convex.blend (Stored with Git LFS)

Binary file not shown.

BIN
src_assets/car_convex.blend1 (Stored with Git LFS)

Binary file not shown.