Compare commits

...

2 Commits

30 changed files with 741 additions and 167 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
layout python3

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ game_web/
.venv
build/
bin/
.direnv/

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
python 3.11.13

BIN
assets/blender/ice_cream_truck_blend/Split.glb (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/blender/test_level_blend/NurbsPath.glb (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/blender/test_level_blend/Plane.glb (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,45 @@
(inst
:model "assets/blender/testblend_blend/Bakery.glb"
:pos (0.000000 0.000000 12.000000)
:rot (0.000000 0.000000 0.000000 1.000000)
:scale (1.000000 1.000000 1.000000))
(inst
:model "assets/blender/test_level_blend/Plane.glb"
:pos (0.000000 -1.000000 0.000000)
:rot (0.000000 0.000000 0.000000 1.000000)
:scale (22.469545 22.469545 22.469545))
(inst
:model "assets/blender/testblend_blend/Fire_Station.glb"
:pos (9.000000 0.000000 13.000000)
:rot (0.000000 0.000000 0.000000 1.000000)
:scale (1.000000 1.000000 1.000000))
(inst
:model "assets/blender/testblend_blend/Gas_Station_Shop.glb"
:pos (-8.000000 0.000000 11.000000)
:rot (0.000000 0.000000 0.000000 1.000000)
:scale (1.000000 1.000000 1.000000))
(inst
:model "assets/blender/testblend_blend/Gas_Station_Shop.glb"
:pos (-16.000000 0.000000 11.000000)
:rot (0.000000 0.000000 0.000000 1.000000)
:scale (1.000000 1.000000 1.000000))
(inst
:model "assets/blender/testblend_blend/Gas_Station_Shop.glb"
:pos (-15.559284 1.609015 0.259853)
:rot (0.000000 0.000000 0.000000 1.000000)
:scale (1.000000 1.000000 1.000000))
(inst
:model "assets/blender/testblend_blend/Green_House.glb"
:pos (14.000000 0.000000 -7.000000)
:rot (0.000000 0.000000 0.000000 1.000000)
:scale (1.000000 1.000000 1.000000))
(inst
:model "assets/blender/testblend_blend/Hotel.glb"
:pos (8.000000 0.000000 -18.000000)
:rot (0.000000 0.000000 0.000000 1.000000)
:scale (1.000000 1.000000 1.000000))
(inst
:model "assets/blender/test_level_blend/NurbsPath.glb"
:pos (0.000000 0.000000 0.000000)
:rot (0.000000 0.000000 0.000000 1.000000)
:scale (1.000000 1.000000 1.000000))

BIN
assets/blender/testblend_blend/Bakery.glb (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/blender/testblend_blend/Fire_Station.glb (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/blender/testblend_blend/Gas_Station_Shop.glb (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/blender/testblend_blend/Green_House.glb (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/blender/testblend_blend/Hotel.glb (Stored with Git LFS) Normal file

Binary file not shown.

87
blender/batch_export.py Normal file
View File

@ -0,0 +1,87 @@
import bpy
import os
import pathlib
# Ensure blend file is saved
blend_filepath = bpy.data.filepath
if not blend_filepath:
raise Exception("Blend file is not saved")
assert bpy.context.scene is not None
def clean_blend_path(blend_path: str) -> str:
return os.path.join(os.path.dirname(blend_path), bpy.path.clean_name(os.path.basename(blend_path)))
# Directory setup
abs_filepath = bpy.path.abspath(blend_filepath)
src_assets_path = os.path.dirname(abs_filepath)
project_path = os.path.dirname(src_assets_path)
assets_path = os.path.join(project_path, "assets")
rel_blend_path = os.path.relpath(abs_filepath, src_assets_path)
clean_rel_path = clean_blend_path(rel_blend_path)
scene_name = bpy.context.scene.name
instance_output_path = os.path.join(assets_path, "blender", clean_rel_path, f"{scene_name}.scn")
# Utility: returns path to store the glb
def get_model_export_path(obj: bpy.types.Object) -> str:
if obj.instance_type == 'COLLECTION' and obj.instance_collection:
lib_path = obj.instance_collection.library.filepath
collection_name = bpy.path.clean_name(obj.instance_collection.name)
rel_lib_path = os.path.relpath(bpy.path.abspath(lib_path), src_assets_path)
rel_lib_path = clean_blend_path(rel_lib_path)
return os.path.join("blender", rel_lib_path, f"{collection_name}.glb")
else:
obj_name = bpy.path.clean_name(obj.name)
return os.path.join("blender", clean_rel_path, f"{obj_name}.glb")
# Utility: writes an object to glb
def export_object_as_glb(obj: bpy.types.Object, export_path: str):
full_export_path = os.path.join(assets_path, export_path)
pathlib.Path(os.path.dirname(full_export_path)).mkdir(parents=True, exist_ok=True)
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
assert bpy.context.view_layer is not None
bpy.context.view_layer.objects.active = obj
bpy.ops.export_scene.gltf(filepath=full_export_path, use_selection=True, export_apply=True)
print(f"Exported {obj.name} -> {export_path}")
# Collect all visible, non-hidden objects in the scene
visible_objects = [
obj for obj in bpy.context.scene.objects
if obj.visible_get()
]
exported = set()
instances = []
for obj in visible_objects:
model_path = get_model_export_path(obj)
if model_path not in exported:
export_object_as_glb(obj, model_path)
exported.add(model_path)
loc = obj.location
rot = obj.rotation_quaternion
scale = obj.scale
instance_sexpr = (
'(inst'
f'\n\t:model "assets/{model_path}"'
f'\n\t:pos ({loc.x:.6f} {loc.z:.6f} {loc.y:.6f})'
f'\n\t:rot ({rot.x:.6f} {rot.y:.6f} {rot.z:.6f} {rot.w:.6f})'
f'\n\t:scale ({scale.x:.6f} {scale.y:.6f} {scale.z:.6f}))'
)
instances.append(instance_sexpr)
# Save instances to scene file
instance_output_abs = os.path.join(project_path, instance_output_path)
pathlib.Path(os.path.dirname(instance_output_abs)).mkdir(parents=True, exist_ok=True)
with open(instance_output_abs, "w", encoding="utf-8") as f:
for line in instances:
f.write(line + "\n")
print(f"Written instances to {instance_output_abs}")

View File

@ -4,6 +4,7 @@ import "common:name"
import "core:container/intrusive/list"
import "core:fmt"
import "core:io"
import "core:strconv"
import "core:strings"
import "core:testing"
import "core:unicode"
@ -151,7 +152,13 @@ parse_atom :: proc(ctx: ^SEXP_Parser) -> (atom: Atom, error: Error) {
result := ctx.data[start:ctx.pos]
ctx.pos = next
return result, nil
case '0' ..= '9', '-', '+':
value, n, ok := strconv.parse_f64_prefix(ctx.data[ctx.pos:])
if !ok {
return nil, make_error(ctx, "failed to parse number")
}
ctx.pos += n
return value, nil
}
return nil, make_error(ctx, fmt.tprintf("unknown atom {}", c))
@ -233,6 +240,43 @@ iterator_next_checked :: proc(it: ^List_Iterator) -> Sexp {
return node.expr
}
iterator_has_more :: proc(it: List_Iterator) -> bool {
return it.curr != nil
}
iterator_expect_list :: proc(it: ^List_Iterator) -> (Sexp_List, bool) {
next, ok := iterator_next(it)
if !ok {
return {}, false
}
list_expr: Sexp_List
list_expr, ok = next.expr.(Sexp_List)
return list_expr, ok
}
iterator_expect_atom :: proc(it: ^List_Iterator, $T: typeid) -> (T, bool) {
next, ok := iterator_next(it)
if !ok {
return {}, false
}
atom_expr: Atom
atom_expr, ok = next.expr.(Atom)
if !ok {
return {}, false
}
result: T
result, ok = atom_expr.(T)
return result, ok
}
print_list :: proc(it: List_Iterator, w: io.Writer) -> io.Error {
it := it
io.write_byte(w, '(') or_return
@ -402,6 +446,16 @@ print_pretty_error :: proc(w: io.Writer, data: string, error: Error) -> io.Error
return nil
}
// Prints pretty error to temp string
temp_pretty_error :: proc(data: string, error: Error) -> string {
builder: strings.Builder
strings.builder_init(&builder, context.temp_allocator)
writer := strings.to_writer(&builder)
print_pretty_error(writer, data, error)
return strings.to_string(builder)
}
@(test)
test_parse :: proc(t: ^testing.T) {
ctx: SEXP_Parser

View File

@ -81,3 +81,7 @@ to_string :: proc(name: Name) -> string {
sync.atomic_rw_mutex_shared_guard(&global_container.lock)
return global_container.names_array[name]
}
to_cstring :: proc(name: Name) -> cstring {
return strings.unsafe_string_to_cstring(to_string(name))
}

View File

@ -1,5 +1,7 @@
package assets
import "common:encoding/sexp"
import "common:name"
import "core:log"
import "core:math"
import lg "core:math/linalg"
@ -15,11 +17,24 @@ import "libs:tracy"
_ :: math
@(private = "file")
g_assetman_instance: ^Asset_Manager
init :: proc(assetman: ^Asset_Manager) {
g_assetman_instance = assetman
}
manager :: #force_inline proc() -> ^Asset_Manager {
return g_assetman_instance
}
Loaded_BVH :: struct {
// AABB of all bvhs
aabb: bvh.AABB,
// BVH for each mesh in a model
bvhs: []bvh.BVH,
bvh: bvh.BVH,
vertices: []rl.Vector3,
indices: []u16,
modtime: physfs.sint64,
}
@ -34,17 +49,28 @@ Loaded_Curve_2D :: struct {
points: [][2]f32,
}
destroy_loaded_bvh :: proc(loaded_bvh: Loaded_BVH) {
destroy_loaded_bvh :: proc(loaded_bvh: ^Loaded_BVH) {
tracy.Zone()
for &mesh_bvh in loaded_bvh.bvhs {
bvh.destroy_bvh(&mesh_bvh)
}
delete(loaded_bvh.bvhs)
bvh.destroy_bvh(&loaded_bvh.bvh)
delete(loaded_bvh.vertices)
delete(loaded_bvh.indices)
}
Curve_2D :: [][2]f32
Scene_Instance :: struct {
model: name.Name,
// TODO: common format definitions
pos: rl.Vector3,
rot: rl.Quaternion,
scale: rl.Vector3,
}
Scene_Desc :: struct {
instances: []Scene_Instance,
}
Asset_Manager :: struct {
textures: Asset_Cache(rl.Texture2D),
models: Asset_Cache(rl.Model),
@ -53,6 +79,7 @@ Asset_Manager :: struct {
music: Asset_Cache(rl.Music),
curves_1d: Asset_Cache([]f32),
curves_2d: Asset_Cache(Curve_2D),
scenes: Asset_Cache(Scene_Desc),
bvhs: map[cstring]Loaded_BVH,
curves: map[cstring]Loaded_Curve_2D,
}
@ -191,6 +218,104 @@ CURVE_2D_CSV_LOADER :: Asset_Cache_Loader(Curve_2D) {
},
}
SCENE_DESC_LOADER :: Asset_Cache_Loader(Scene_Desc) {
load = proc(path: cstring, payload: Asset_Cache_Loader_Payload) -> (Scene_Desc, bool) {
data, err := physfs.read_entire_file(string(path), context.temp_allocator)
if err != nil {
log.errorf("Failed to read curve: %s, %v", path, err)
return {}, false
}
parser: sexp.SEXP_Parser
parser.data = string(data)
parsed, err2 := sexp.parse(&parser, context.temp_allocator)
if err2 != nil {
log.errorf(
"Failed to parse curve: %s\n%s",
path,
sexp.temp_pretty_error(parser.data, err2),
)
}
inst_identifier := sexp.Ident(name.from_string("inst"))
num_instances: int
top_it := sexp.iterator_list(parsed)
for top_level_expr in sexp.iterator_next(&top_it) {
list_expr := top_level_expr.expr.(sexp.Sexp_List) or_continue
it := sexp.iterator_list(list_expr)
ident := sexp.iterator_expect_atom(&it, sexp.Ident) or_continue
if ident != inst_identifier {
continue
}
num_instances += 1
}
instances := make([]Scene_Instance, num_instances)
num_instances = 0
top_it = sexp.iterator_list(parsed)
for top_level_expr in sexp.iterator_next(&top_it) {
list_expr := top_level_expr.expr.(sexp.Sexp_List) or_continue
it := sexp.iterator_list(list_expr)
ident := sexp.iterator_expect_atom(&it, sexp.Ident) or_continue
if ident != inst_identifier {
continue
}
inst: Scene_Instance
for sexp.iterator_has_more(it) {
key := sexp.iterator_expect_atom(&it, sexp.Tag) or_continue
switch name.Name(key) {
case name.from_string("model"):
model_path := sexp.iterator_expect_atom(&it, string) or_continue
inst.model = name.from_string(model_path)
case name.from_string("pos"):
pos_list := sexp.iterator_expect_list(&it) or_continue
pos_it := sexp.iterator_list(pos_list)
x := sexp.iterator_expect_atom(&pos_it, f64) or_continue
y := sexp.iterator_expect_atom(&pos_it, f64) or_continue
z := sexp.iterator_expect_atom(&pos_it, f64) or_continue
inst.pos = {f32(x), f32(y), f32(z)}
case name.from_string("rot"):
rot_list := sexp.iterator_expect_list(&it) or_continue
rot_it := sexp.iterator_list(rot_list)
inst.rot.x = f32(sexp.iterator_expect_atom(&rot_it, f64) or_continue)
inst.rot.y = f32(sexp.iterator_expect_atom(&rot_it, f64) or_continue)
inst.rot.z = f32(sexp.iterator_expect_atom(&rot_it, f64) or_continue)
inst.rot.w = f32(sexp.iterator_expect_atom(&rot_it, f64) or_continue)
case name.from_string("scale"):
scale_list := sexp.iterator_expect_list(&it) or_continue
scale_it := sexp.iterator_list(scale_list)
x := sexp.iterator_expect_atom(&scale_it, f64) or_continue
y := sexp.iterator_expect_atom(&scale_it, f64) or_continue
z := sexp.iterator_expect_atom(&scale_it, f64) or_continue
inst.scale = {f32(x), f32(y), f32(z)}
}
}
instances[num_instances] = inst
num_instances += 1
}
return Scene_Desc{instances = instances[:num_instances]}, true
},
unload = proc(scene_desc: Scene_Desc) {
delete(scene_desc.instances)
},
}
assetman_init :: proc(assetman: ^Asset_Manager) {
assetman.models = {
loader = MODEL_LOADER,
@ -213,6 +338,9 @@ assetman_init :: proc(assetman: ^Asset_Manager) {
assetman.curves_2d = {
loader = CURVE_2D_CSV_LOADER,
}
assetman.scenes = {
loader = SCENE_DESC_LOADER,
}
}
Asset_Cache_Result :: enum {
@ -226,6 +354,7 @@ assetcache_fetch_or_load :: proc(
ac: ^$T/Asset_Cache($E),
path: cstring,
payload: Asset_Cache_Loader_Payload = nil,
force_no_reload := false,
) -> (
value: E,
modtime: physfs.sint64,
@ -238,7 +367,7 @@ assetcache_fetch_or_load :: proc(
if has_existing {
new_modtime := physfs.getLastModTime(path)
if existing.modtime == new_modtime {
if force_no_reload || existing.modtime == new_modtime {
result = .Cached
return existing.value, new_modtime, result
} else {
@ -361,7 +490,7 @@ get_music :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Music {
null_bvhs: []bvh.BVH
get_bvh :: proc(assetman: ^Asset_Manager, path: cstring) -> Loaded_BVH {
get_bvh :: proc(assetman: ^Asset_Manager, path: cstring) -> (Loaded_BVH, bool) {
tracy.Zone()
loaded_bvh, ok := assetman.bvhs[path]
@ -370,22 +499,41 @@ get_bvh :: proc(assetman: ^Asset_Manager, path: cstring) -> Loaded_BVH {
should_recreate := reloaded || !ok
if ok && should_recreate {
destroy_loaded_bvh(loaded_bvh)
destroy_loaded_bvh(&loaded_bvh)
delete_key(&assetman.bvhs, path)
}
if should_recreate {
new_bvhs := make([]bvh.BVH, model.meshCount)
outer_aabb := bvh.AABB {
min = max(f32),
max = min(f32),
}
vert_count := 0
indices_count := 0
for i in 0 ..< model.meshCount {
mesh := model.meshes[i]
vertices := (cast([^]rl.Vector3)mesh.vertices)[:mesh.vertexCount]
indices := mesh.indices[:mesh.triangleCount * 3]
vert_count += int(mesh.vertexCount)
indices_count += int(mesh.triangleCount * 3)
}
vertices := make([]bvh.Vec3, vert_count)
indices := make([]u16, indices_count)
vert_count = 0
indices_count = 0
for i in 0 ..< model.meshCount {
mesh := model.meshes[i]
mesh_vertices := (cast([^]rl.Vector3)mesh.vertices)[:mesh.vertexCount]
mesh_indices := mesh.indices[:mesh.triangleCount * 3]
copy(vertices[vert_count:], mesh_vertices)
for j in 0 ..< len(mesh_indices) {
index := vert_count + int(mesh_indices[j])
assert(index <= int(max(u16)))
indices[indices_count + j] = u16(index)
}
vert_count += len(mesh_vertices)
indices_count += len(mesh_indices)
}
mesh_bvh := bvh.build_bvh_from_mesh(
{vertices = vertices, indices = indices},
@ -393,20 +541,17 @@ get_bvh :: proc(assetman: ^Asset_Manager, path: cstring) -> Loaded_BVH {
)
root_aabb := mesh_bvh.bvh.nodes[0].aabb
outer_aabb.min = lg.min(outer_aabb.min, root_aabb.min)
outer_aabb.max = lg.max(outer_aabb.max, root_aabb.max)
new_bvhs[i] = mesh_bvh.bvh
}
assetman.bvhs[path] = Loaded_BVH {
aabb = outer_aabb,
bvhs = new_bvhs,
aabb = root_aabb,
bvh = mesh_bvh.bvh,
vertices = vertices,
indices = indices,
modtime = modtime,
}
}
return assetman.bvhs[path]
return assetman.bvhs[path], should_recreate
}
get_curve_1d :: proc(assetman: ^Asset_Manager, path: cstring) -> (curve: []f32) {
@ -420,6 +565,20 @@ get_curve_2d :: proc(assetman: ^Asset_Manager, path: cstring) -> (curve: Curve_2
return
}
// Reads a two column comma separated csv file as a curve
get_scene_desc :: proc(
assetman: ^Asset_Manager,
path: name.Name,
force_no_reload := false,
) -> (
scene: Scene_Desc,
reloaded: bool,
) {
res: Asset_Cache_Result
scene, _, res = assetcache_fetch_or_load(&assetman.scenes, name.to_cstring(path))
return scene, (res == .Loaded || res == .Reloaded)
}
get_convex :: proc(assetman: ^Asset_Manager, path: cstring) -> (result: Loaded_Convex) {
bytes, err := physfs.read_entire_file(string(path), context.temp_allocator)
if err != nil {
@ -826,9 +985,10 @@ shutdown :: proc(assetman: ^Asset_Manager) {
assetcache_destroy(&assetman.music)
assetcache_destroy(&assetman.curves_1d)
assetcache_destroy(&assetman.curves_2d)
assetcache_destroy(&assetman.scenes)
for _, loaded_bvh in assetman.bvhs {
destroy_loaded_bvh(loaded_bvh)
for _, &loaded_bvh in assetman.bvhs {
destroy_loaded_bvh(&loaded_bvh)
}
delete(assetman.bvhs)
}

View File

@ -17,12 +17,10 @@ package game
import "assets"
import "common:name"
import "core:fmt"
import "core:hash"
import "core:log"
import "core:math"
import "core:math/linalg"
import "game:physics"
import "game:physics/bvh"
import "game:render"
import rl "libs:raylib"
import "libs:raylib/rlgl"
@ -49,6 +47,65 @@ Debug_Draw_State :: struct {
draw_physics_scene: bool,
}
Scene :: struct {
scene_desc_path: name.Name,
level_geoms: []physics.Level_Geom_Handle,
}
scene_destroy :: proc(world: ^World, scene: ^Scene) {
scene.scene_desc_path = name.NONE
sim_state := physics.get_sim_state(&world.physics_scene)
for handle in scene.level_geoms {
physics.remove_level_geom(sim_state, handle)
}
delete(scene.level_geoms)
scene.level_geoms = nil
}
immediate_scene :: proc(world: ^World, scene: ^Scene, path: cstring) {
path_name := name.from_string(string(path))
desc, reloaded := assets.get_scene_desc(&g_mem.assetman, path_name)
if reloaded || scene.scene_desc_path != path_name {
scene_destroy(world, scene)
scene.scene_desc_path = path_name
sim_state := physics.get_sim_state(&world.physics_scene)
for inst in desc.instances {
physics.add_level_geom(
sim_state,
physics.Level_Geom_Config {
position = inst.pos,
rotation = inst.rot,
source = physics.Level_Geometry_Asset(name.to_string(inst.model)),
},
)
}
}
}
scene_copy :: proc(dst, src: ^Scene) {
dst.scene_desc_path = src.scene_desc_path
if len(dst.level_geoms) != len(src.level_geoms) {
delete(dst.level_geoms)
dst.level_geoms = make([]physics.Level_Geom_Handle, len(src.level_geoms))
}
copy(dst.level_geoms, src.level_geoms)
}
scene_draw :: proc(scene: ^Scene) {
desc, _ := assets.get_scene_desc(&g_mem.assetman, scene.scene_desc_path, true)
for geo in desc.instances {
render.draw_model(
assets.get_model(&g_mem.assetman, name.to_cstring(geo.model)),
{},
auto_cast linalg.matrix4_from_trs(geo.pos, geo.rot, geo.scale),
)
}
}
World :: struct {
player_pos: rl.Vector3,
track: Track,
@ -58,10 +115,15 @@ World :: struct {
car_handle: physics.Body_Handle,
engine_handle: physics.Engine_Handle,
debug_state: Debug_Draw_State,
main_scene: Scene,
}
world_init :: proc(world: ^World) {
physics.scene_init(&world.physics_scene, &g_mem.assetman)
}
copy_world :: proc(dst, src: ^World) {
copy_track(&dst.track, &src.track)
physics.copy_physics_scene(&dst.physics_scene, &src.physics_scene)
scene_copy(&dst.main_scene, &src.main_scene)
dst.player_pos = src.player_pos
dst.pause = src.pause
dst.car_com = src.car_com
@ -71,7 +133,8 @@ copy_world :: proc(dst, src: ^World) {
}
destroy_world :: proc(world: ^World) {
destroy_track(&world.track)
physics.destroy_physics_scene(&world.physics_scene)
scene_destroy(world, &world.main_scene)
physics.scene_destroy(&world.physics_scene)
}
Runtime_World :: struct {
@ -91,6 +154,7 @@ Runtime_World :: struct {
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)
physics.scene_init(&world.physics_scene, &g_mem.assetman)
world.debug_state.show_menu = true
world.debug_state.draw_physics_scene = true
}
@ -125,7 +189,6 @@ Game_Memory :: struct {
name_container: name.Container,
assetman: assets.Asset_Manager,
runtime_world: Runtime_World,
fluid_scene: Fluid_Scene,
es: Editor_State,
ui_context: ui.Context,
default_font: rl.Font,
@ -169,6 +232,8 @@ world_stack_init :: proc(stack: ^World_Stack, num_snapshots: int, allocator := c
assert(num_snapshots > 0)
stack.worlds = make([]World, num_snapshots, allocator)
world_stack_push(stack)
world_init(world_stack_current(stack))
}
world_stack_destroy :: proc(stack: ^World_Stack, allocator := context.allocator) {
@ -695,30 +760,7 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
wheel.turn_angle = TURN_ANGLE * turn_vel_correction * turn_input
}
{
level_model := assets.get_model(&g_mem.assetman, "assets/testblend.glb")
for i in 0 ..< level_model.meshCount {
mesh := &level_model.meshes[i]
if mesh.triangleCount > 0 {
assert(mesh.vertexCount <= i32(max(u16)))
m := level_model.transform
pos := physics.Vec3{m[3][0], m[3][1], m[3][2]}
rotation := linalg.quaternion_from_matrix4_f32(auto_cast level_model.transform)
physics.immediate_level_geom(
&world.physics_scene,
hash.fnv32a(transmute([]byte)(fmt.tprintf("level mesh {}", i))),
{
position = pos,
rotation = rotation,
vertices = (cast([^]rl.Vector3)mesh.vertices)[:mesh.vertexCount],
indices = mesh.indices[:mesh.triangleCount * 3],
},
)
}
}
}
immediate_scene(world, &world.main_scene, "assets/blender/test_level_blend/Scene.scn")
if len(world.track.points) > 1 {
interpolated_points := calculate_spline_interpolated_points(
@ -734,9 +776,11 @@ update_world :: proc(world: ^World, dt: f32, config: World_Update_Config) {
#hash("track", "fnv32a"),
{
rotation = linalg.QUATERNIONF32_IDENTITY,
source = physics.Level_Geometry_Mesh {
vertices = track_verts,
indices = track_inds,
},
},
)
}
@ -973,33 +1017,33 @@ update :: proc() {
dt := rl.GetFrameTime()
// Debug BVH traversal
mesh_bvh := assets.get_bvh(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb")
// mesh_bvh := assets.get_bvh(&g_mem.assetman, "assets/toyota_corolla_ae86_trueno.glb")
if rl.IsKeyDown(.LEFT_SHIFT) {
if g_mem.preview_bvh >= 0 && g_mem.preview_bvh < len(mesh_bvh.bvhs) {
b := mesh_bvh.bvhs[g_mem.preview_bvh]
node := &b.nodes[g_mem.preview_node]
// if rl.IsKeyDown(.LEFT_SHIFT) {
// if g_mem.preview_bvh >= 0 && g_mem.preview_bvh < len(mesh_bvh.bvhs) {
// b := mesh_bvh.bvhs[g_mem.preview_bvh]
// node := &b.nodes[g_mem.preview_node]
if !bvh.is_leaf_node(node^) {
if rl.IsKeyPressed(.LEFT_BRACKET) {
g_mem.preview_node = int(node.child_or_prim_start)
} else if rl.IsKeyPressed(.RIGHT_BRACKET) {
g_mem.preview_node = int(node.child_or_prim_start + 1)
} else if rl.IsKeyPressed(.P) {
g_mem.preview_node = 0
}
}
}
} else {
if rl.IsKeyPressed(.LEFT_BRACKET) {
g_mem.preview_bvh -= 1
g_mem.preview_node = 0
}
if rl.IsKeyPressed(.RIGHT_BRACKET) {
g_mem.preview_bvh += 1
g_mem.preview_node = 0
}
}
// if !bvh.is_leaf_node(node^) {
// if rl.IsKeyPressed(.LEFT_BRACKET) {
// g_mem.preview_node = int(node.child_or_prim_start)
// } else if rl.IsKeyPressed(.RIGHT_BRACKET) {
// g_mem.preview_node = int(node.child_or_prim_start + 1)
// } else if rl.IsKeyPressed(.P) {
// g_mem.preview_node = 0
// }
// }
// }
// } else {
// if rl.IsKeyPressed(.LEFT_BRACKET) {
// g_mem.preview_bvh -= 1
// g_mem.preview_node = 0
// }
// if rl.IsKeyPressed(.RIGHT_BRACKET) {
// g_mem.preview_bvh += 1
// g_mem.preview_node = 0
// }
// }
if rl.IsKeyPressed(.SPACE) {
g_mem.physics_pause = !g_mem.physics_pause
@ -1072,7 +1116,6 @@ draw_world :: proc(world: ^World) {
sim_state := physics.get_sim_state(&world.physics_scene)
level_model := assets.get_model(&g_mem.assetman, "assets/testblend.glb")
car_model := assets.get_model(&g_mem.assetman, "assets/ice_cream_truck.glb")
phys_debug_state: physics.Debug_State
@ -1161,7 +1204,7 @@ draw_world :: proc(world: ^World) {
// .VEC3,
// )
render.draw_model(level_model, {}, 1)
scene_draw(&world.main_scene)
render.draw_model(car_model, {}, car_matrix)
@ -1429,9 +1472,9 @@ game_init :: proc() {
name.init(&g_mem.name_container)
name.setup_global_container(&g_mem.name_container)
init_physifs_raylib_callbacks()
assets.assetman_init(&g_mem.assetman)
fluid_scene_init(&g_mem.fluid_scene)
assets.assetman_init(&g_mem.assetman)
assets.init(&g_mem.assetman)
editor_state_init(&g_mem.es, 100)
runtime_world_init(&g_mem.runtime_world, DEV_BUILD ? 100 : 2)
@ -1454,7 +1497,6 @@ game_shutdown :: proc() {
editor_state_destroy(&g_mem.es)
delete(g_mem.es.point_selection)
runtime_world_destroy(&g_mem.runtime_world)
fluid_scene_destroy(&g_mem.fluid_scene)
free(g_mem)
}

View File

@ -1,5 +1,6 @@
package physics
import "bvh"
import "collision"
import "core:log"
import "core:math"
@ -299,13 +300,14 @@ body_get_convex_shapes_world :: proc(
level_geom_get_convex_shape :: proc(
sim_state: ^Sim_State,
level_geom: Level_Geom_Ptr,
level_geom_handle: Level_Geom_Handle,
tri_idx: int,
allocator := context.temp_allocator,
) -> (
mesh: collision.Convex,
) {
vertices, indices := get_level_geom_data(sim_state, level_geom.geometry)
level_geom := get_level_geom(sim_state, level_geom_handle)
vertices, indices := get_level_geom_data(sim_state, level_geom_handle)
return collision.double_sided_triangle_to_convex(
get_transformed_triangle(vertices, indices, tri_idx, level_geom.x, level_geom.q),
allocator,
@ -396,3 +398,8 @@ rpm_to_angular_velocity :: proc(rpm: f32) -> f32 {
angular_velocity_to_rpm :: proc(w: f32) -> f32 {
return (w / (2.0 * math.PI)) * 60.0
}
// TODO: use one format everywhere...
bvh_aabb_to_phys_aabb :: proc(aabb: bvh.AABB) -> AABB {
return AABB{center = (aabb.max + aabb.min) * 0.5, extent = (aabb.max - aabb.min) * 0.5}
}

View File

@ -4,6 +4,8 @@ import "bvh"
import "collision"
import "common:name"
import lg "core:math/linalg"
import "core:strings"
import "game:assets"
import "game:container/spanpool"
import "libs:tracy"
@ -78,6 +80,7 @@ contact_container_copy :: proc(dst: ^Contact_Container, src: Contact_Container)
}
Sim_State :: struct {
scene: ^Scene,
bodies: #soa[dynamic]Body,
suspension_constraints: #soa[dynamic]Suspension_Constraint,
engines: [dynamic]Engine,
@ -120,6 +123,7 @@ Sim_State :: struct {
}
Scene :: struct {
assetman: ^assets.Asset_Manager,
simulation_states: [2]Sim_State,
simulation_state_index: i32,
solver_state: Solver_State,
@ -130,6 +134,7 @@ copy_sim_state :: proc(dst: ^Sim_State, src: ^Sim_State) {
tracy.Zone()
convex_container_reconcile(&src.convex_container)
dst.scene = src.scene
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
@ -168,12 +173,12 @@ copy_sim_state :: proc(dst: ^Sim_State, src: ^Sim_State) {
copy_physics_scene :: proc(dst, src: ^Scene) {
tracy.Zone()
dst.assetman = src.assetman
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)
}
@ -391,11 +396,24 @@ BLAS_Handle :: struct {
primitives: spanpool.Handle,
}
Level_Geom_Source_Local :: struct {
geometry: Geometry_Handle,
blas: BLAS_Handle,
}
Level_Geom_Source_Asset :: struct {
assetpath: name.Name,
}
Level_Geom_Source :: union #no_nil {
Level_Geom_Source_Local,
Level_Geom_Source_Asset,
}
Level_Geom :: struct {
alive: bool,
aabb: AABB,
geometry: Geometry_Handle,
blas: BLAS_Handle,
source: Level_Geom_Source,
x: Vec3,
q: Quat,
next_plus_one: i32,
@ -584,11 +602,23 @@ Engine_Config :: struct {
axle: Drive_Axle_Config,
}
Level_Geometry_Asset :: distinct string
Level_Geometry_Mesh :: struct {
vertices: []Vec3,
indices: []u16,
}
// Either runtime mesh or asset reference which will be fetched from assetmanager
Level_Geometry_Source :: union #no_nil {
Level_Geometry_Mesh,
Level_Geometry_Asset,
}
Level_Geom_Config :: struct {
position: Vec3,
rotation: Quat,
vertices: []Vec3,
indices: []u16,
source: Level_Geometry_Source,
}
calculate_body_params_from_config :: proc(
@ -919,20 +949,52 @@ get_level_geom :: proc(sim_state: ^Sim_State, handle: Level_Geom_Handle) -> Leve
get_level_geom_data :: proc(
sim_state: ^Sim_State,
handle: Geometry_Handle,
handle: Level_Geom_Handle,
) -> (
vertices: []Vec3,
indices: []u16,
) {
vertices = spanpool.resolve_slice(&sim_state.geometry_vertices_pool, handle.vertices)
indices = spanpool.resolve_slice(&sim_state.geometry_indices_pool, handle.indices)
level_geom := get_level_geom(sim_state, handle)
switch s in level_geom.source {
case Level_Geom_Source_Local:
vertices = spanpool.resolve_slice(&sim_state.geometry_vertices_pool, s.geometry.vertices)
indices = spanpool.resolve_slice(&sim_state.geometry_indices_pool, s.geometry.indices)
case Level_Geom_Source_Asset:
loaded_bvh, reloaded := assets.get_bvh(
sim_state.scene.assetman,
strings.unsafe_string_to_cstring(name.to_string(s.assetpath)),
)
if reloaded {
level_geom.aabb = bvh_aabb_to_phys_aabb(loaded_bvh.aabb)
level_geom.aabb = aabb_transform(level_geom.aabb, level_geom.x, level_geom.q)
}
vertices = loaded_bvh.vertices
indices = loaded_bvh.indices
}
return
}
get_level_geom_blas :: proc(sim_state: ^Sim_State, handle: BLAS_Handle) -> (bvh: bvh.BVH) {
bvh.nodes = spanpool.resolve_slice(&sim_state.blas_nodes_pool, handle.nodes)
bvh.primitives = spanpool.resolve_slice(&sim_state.blas_primitives_pool, handle.primitives)
get_level_geom_blas :: proc(sim_state: ^Sim_State, handle: Level_Geom_Handle) -> (bvh: bvh.BVH) {
level_geom := get_level_geom(sim_state, handle)
switch s in level_geom.source {
case Level_Geom_Source_Local:
bvh.nodes = spanpool.resolve_slice(&sim_state.blas_nodes_pool, s.blas.nodes)
bvh.primitives = spanpool.resolve_slice(&sim_state.blas_primitives_pool, s.blas.primitives)
bvh.nodes_used = i32(len(bvh.nodes))
case Level_Geom_Source_Asset:
loaded_bvh, reloaded := assets.get_bvh(
sim_state.scene.assetman,
strings.unsafe_string_to_cstring(name.to_string(s.assetpath)),
)
if reloaded {
level_geom.aabb = bvh_aabb_to_phys_aabb(loaded_bvh.aabb)
level_geom.aabb = aabb_transform(level_geom.aabb, level_geom.x, level_geom.q)
}
bvh = loaded_bvh.bvh
}
return
}
@ -945,32 +1007,36 @@ update_level_geom_from_config :: proc(
level_geom.q = config.rotation
// TODO: figure out if asset changed and rebuild only then
}
add_level_geom :: proc(sim_state: ^Sim_State, config: Level_Geom_Config) -> Level_Geom_Handle {
assert(len(config.vertices) > 0)
assert(len(config.vertices) <= int(max(u16)))
sim_state.num_level_geoms += 1
level_geom: Level_Geom
level_geom.alive = true
update_level_geom_from_config(sim_state, &level_geom, config)
switch s in config.source {
case Level_Geometry_Mesh:
assert(len(s.vertices) > 0)
assert(len(s.vertices) <= int(max(u16)))
blas :=
bvh.build_bvh_from_mesh(bvh.Mesh{vertices = config.vertices, indices = config.indices}, context.temp_allocator).bvh
bvh.build_bvh_from_mesh(bvh.Mesh{vertices = s.vertices, indices = s.indices}, context.temp_allocator).bvh
level_geom.blas.nodes = spanpool.allocate_elems(
source: Level_Geom_Source_Local
source.blas.nodes = spanpool.allocate_elems(
&sim_state.blas_nodes_pool,
..blas.nodes[:blas.nodes_used],
)
level_geom.blas.primitives = spanpool.allocate_elems(
source.blas.primitives = spanpool.allocate_elems(
&sim_state.blas_primitives_pool,
..blas.primitives,
)
aabb_min, aabb_max: Vec3 = max(f32), min(f32)
for v in config.vertices {
for v in s.vertices {
aabb_min = lg.min(aabb_min, v)
aabb_max = lg.max(aabb_max, v)
}
@ -978,19 +1044,39 @@ add_level_geom :: proc(sim_state: ^Sim_State, config: Level_Geom_Config) -> Leve
level_geom.aabb.center = (aabb_max + aabb_min) * 0.5
level_geom.aabb.extent = (aabb_max - aabb_min) * 0.5
if spanpool.is_handle_valid(&sim_state.geometry_vertices_pool, level_geom.geometry.vertices) {
spanpool.free(&sim_state.geometry_vertices_pool, level_geom.geometry.vertices)
spanpool.free(&sim_state.geometry_indices_pool, level_geom.geometry.indices)
// if spanpool.is_handle_valid(
// &sim_state.geometry_vertices_pool,
// level_geom.geometry.vertices,
// ) {
// spanpool.free(&sim_state.geometry_vertices_pool, level_geom.geometry.vertices)
// spanpool.free(&sim_state.geometry_indices_pool, level_geom.geometry.indices)
// }
source.geometry.vertices = spanpool.allocate_elems(
&sim_state.geometry_vertices_pool,
..s.vertices,
)
source.geometry.indices = spanpool.allocate_elems(
&sim_state.geometry_indices_pool,
..s.indices,
)
level_geom.source = source
case Level_Geometry_Asset:
level_geom.source = Level_Geom_Source_Asset {
assetpath = name.from_string(string(s)),
}
level_geom.geometry.vertices = spanpool.allocate_elems(
&sim_state.geometry_vertices_pool,
..config.vertices,
)
level_geom.geometry.indices = spanpool.allocate_elems(
&sim_state.geometry_indices_pool,
..config.indices,
bvh, _ := assets.get_bvh(
sim_state.scene.assetman,
strings.unsafe_string_to_cstring(string(s)),
)
level_geom.aabb = AABB {
center = (bvh.aabb.max + bvh.aabb.min) * 0.5,
extent = (bvh.aabb.max - bvh.aabb.min) * 0.5,
}
}
level_geom.aabb = aabb_transform(level_geom.aabb, level_geom.x, level_geom.q)
if sim_state.first_free_level_geom_plus_one > 0 {
index := sim_state.first_free_level_geom_plus_one
@ -1009,11 +1095,15 @@ add_level_geom :: proc(sim_state: ^Sim_State, config: Level_Geom_Config) -> Leve
remove_level_geom :: proc(sim_state: ^Sim_State, handle: Level_Geom_Handle) {
level_geom := get_level_geom(sim_state, handle)
spanpool.free(&sim_state.geometry_vertices_pool, level_geom.geometry.vertices)
spanpool.free(&sim_state.geometry_indices_pool, level_geom.geometry.indices)
switch s in level_geom.source {
case Level_Geom_Source_Local:
spanpool.free(&sim_state.geometry_vertices_pool, s.geometry.vertices)
spanpool.free(&sim_state.geometry_indices_pool, s.geometry.indices)
spanpool.free(&sim_state.blas_nodes_pool, level_geom.blas.nodes)
spanpool.free(&sim_state.blas_primitives_pool, level_geom.blas.primitives)
spanpool.free(&sim_state.blas_nodes_pool, s.blas.nodes)
spanpool.free(&sim_state.blas_primitives_pool, s.blas.primitives)
case Level_Geom_Source_Asset:
}
}
_get_first_free_body :: proc(sim_state: ^Sim_State) -> i32 {
@ -1039,7 +1129,14 @@ destry_sim_state :: proc(sim_state: ^Sim_State) {
dynamic_tlas_destroy(&sim_state.dynamic_tlas)
}
destroy_physics_scene :: proc(scene: ^Scene) {
scene_init :: proc(scene: ^Scene, assetman: ^assets.Asset_Manager) {
scene.assetman = assetman
for &sim_state in scene.simulation_states {
sim_state.scene = scene
}
}
scene_destroy :: proc(scene: ^Scene) {
for &sim_state in scene.simulation_states {
destry_sim_state(&sim_state)
}

View File

@ -201,8 +201,8 @@ raycasts_level :: proc(
int(tlas.bvh_tree.primitives[leaf_node.child_or_prim_start + j]),
)
level_geom := get_level_geom(sim_state, level_geom_handle)
blas := get_level_geom_blas(sim_state, level_geom.blas)
vertices, indices := get_level_geom_data(sim_state, level_geom.geometry)
blas := get_level_geom_blas(sim_state, level_geom_handle)
vertices, indices := get_level_geom_data(sim_state, level_geom_handle)
// TODO: transform ray into blas space and back
@ -211,7 +211,13 @@ raycasts_level :: proc(
for k in 0 ..< blas_leaf_node.prim_len {
tri_idx := int(blas.primitives[blas_leaf_node.child_or_prim_start + k])
tri := get_triangle(vertices, indices, tri_idx)
tri := get_transformed_triangle(
vertices,
indices,
tri_idx,
level_geom.x,
level_geom.q,
)
hit_t, tmp_normal, _, ok := collision.intersect_ray_triangle(
{origin, origin + dir},
@ -388,12 +394,18 @@ remove_invalid_contacts :: proc(
if !should_remove {
tri_idx := int(contact.local_tri_idx)
vertices, indices := get_level_geom_data(sim_state, level_geom.geometry)
vertices, indices := get_level_geom_data(sim_state, level_geom_handle)
should_remove |= tri_idx * 3 >= len(indices)
if !should_remove {
aabb_a := dyn_tlas.body_aabbs[body_handle_to_index(contact.a)]
tri := get_triangle(vertices, indices, tri_idx)
tri := get_transformed_triangle(
vertices,
indices,
tri_idx,
level_geom.x,
level_geom.q,
)
aabb_b := get_triangle_aabb(tri)
should_remove |= !bvh.test_aabb_vs_aabb(aabb_a, aabb_b)
@ -547,11 +559,8 @@ find_new_contacts :: proc(
)
level_geom := get_level_geom(sim_state, level_geom_handle)
if level_geom.alive {
blas := get_level_geom_blas(sim_state, level_geom.blas)
vertices, indices := get_level_geom_data(
sim_state,
level_geom.geometry,
)
blas := get_level_geom_blas(sim_state, level_geom_handle)
vertices, indices := get_level_geom_data(sim_state, level_geom_handle)
blas_it := bvh.iterator_intersect_leaf_aabb(&blas, body_aabb)
for blas_leaf_node in bvh.iterator_intersect_leaf_next(&blas_it) {
@ -559,7 +568,13 @@ find_new_contacts :: proc(
tri_idx := int(
blas.primitives[blas_leaf_node.child_or_prim_start + k],
)
tri := get_triangle(vertices, indices, tri_idx)
tri := get_transformed_triangle(
vertices,
indices,
tri_idx,
level_geom.x,
level_geom.q,
)
prim_aabb := get_triangle_aabb(tri)
if bvh.test_aabb_vs_aabb(body_aabb, prim_aabb) {
@ -809,8 +824,14 @@ update_contacts :: proc(sim_state: ^Sim_State, static_tlas: ^Static_TLAS) {
case .Body_vs_Level:
level_geom_handle := Level_Geom_Handle(contact.b)
level_geom := get_level_geom(sim_state, level_geom_handle)
vertices, indices := get_level_geom_data(sim_state, level_geom.geometry)
tri := get_triangle(vertices, indices, int(contact.local_tri_idx))
vertices, indices := get_level_geom_data(sim_state, level_geom_handle)
tri := get_transformed_triangle(
vertices,
indices,
int(contact.local_tri_idx),
level_geom.x,
level_geom.q,
)
m2 = collision.double_sided_triangle_to_convex(tri, context.temp_allocator)
}

View File

@ -0,0 +1,9 @@
# This is an Asset Catalog Definition file for Blender.
#
# Empty lines and lines starting with `#` will be ignored.
# The first non-ignored line should be the version indicator.
# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name"
VERSION 1
65988f84-4b5f-463a-bcae-896eba4975c5:Buildings:Buildings

View File

@ -0,0 +1,9 @@
# This is an Asset Catalog Definition file for Blender.
#
# Empty lines and lines starting with `#` will be ignored.
# The first non-ignored line should be the version indicator.
# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name"
VERSION 1
65988f84-4b5f-463a-bcae-896eba4975c5:Buildings:Buildings

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

Binary file not shown.

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

Binary file not shown.

BIN
src_assets/test_level.blend (Stored with Git LFS) Normal file

Binary file not shown.

BIN
src_assets/test_level.blend1 (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

BIN
src_assets/tools.blend (Stored with Git LFS) Normal file

Binary file not shown.

BIN
src_assets/tools.blend1 (Stored with Git LFS) Normal file

Binary file not shown.