gutter_runner/game/assets/assets.odin
2025-07-27 15:33:00 +04:00

806 lines
18 KiB
Odin

package assets
import "common:encoding/sexp"
import "common:name"
import "core:log"
import "core:math"
import lg "core:math/linalg"
import "game:physics/bvh"
import "game:physics/collision"
import "libs:physfs"
import rl "libs:raylib"
import "libs:tracy"
_ :: math
DEV_BUILD :: #config(DEV, false)
Loaded_BVH :: struct {
// AABB of all bvhs
aabb: bvh.AABB,
// BVH for each mesh in a model
bvh: bvh.BVH,
vertices: []rl.Vector3,
indices: []u16,
}
Loaded_Convex :: struct {
mesh: collision.Convex,
center_of_mass: rl.Vector3,
inertia_tensor: lg.Matrix3f32,
total_volume: f32,
}
destroy_loaded_bvh :: proc(loaded_bvh: ^Loaded_BVH) {
tracy.Zone()
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_Type :: enum {
Texture,
Model,
Shader,
Sound,
Music,
Curve_1D,
Curve_2D,
Scene,
BVH,
Convex,
}
Curve_1D :: []f32
Asset :: union {
rl.Texture2D,
rl.Model,
Loaded_Shader,
rl.Sound,
rl.Music,
Curve_1D,
Curve_2D,
Scene_Desc,
Loaded_BVH,
Loaded_Convex,
}
Asset_Key :: struct {
path: name.Name,
type: Asset_Type,
}
Asset_Entry :: struct {
asset: Asset,
modtime: physfs.sint64,
// Current modtime on file system, used to detect when asset changes
fs_modtime: physfs.sint64,
}
Asset_Manager :: struct {
assets: map[Asset_Key]Asset_Entry,
watcher: Asset_Modtime_Watcher,
}
Asset_Cache_Entry :: struct($E: typeid) {
value: E,
modtime: i64,
fs_modtime: i64,
}
Asset_Cache_Loader_Payload :: union {
cstring,
}
Asset_Cache_Loader :: struct {
load: proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
bool,
),
unload: proc(value: Asset),
should_reload: proc(assetman: ^Asset_Manager, key: Asset_Key, entry: Asset_Entry) -> bool,
}
Asset_Cache :: struct($E: typeid) {
cache: map[cstring]Asset_Cache_Entry(E),
loader: Asset_Cache_Loader(E),
}
Shader_Location :: enum {
Ambient,
LightDir,
LightColor,
Heightmap,
}
Shader_Location_Set :: bit_set[Shader_Location]
Shader_Location_Array :: [Shader_Location]i32
SHADER_LOCATION_NAMES := [Shader_Location]cstring {
.Ambient = "ambient",
.LightDir = "lightDir",
.LightColor = "lightColor",
.Heightmap = "heightmap",
}
Loaded_Shader :: struct {
shader: rl.Shader,
location_set: Shader_Location_Set,
locations: Shader_Location_Array,
}
SHADER_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
bool,
) {
shader := rl.LoadShader(path, payload.(cstring))
return Loaded_Shader{shader = shader}, rl.IsShaderValid(shader)
},
unload = proc(asset: Asset) {
rl.UnloadShader(asset.(Loaded_Shader).shader)
},
}
SOUND_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
bool,
) {
sound := rl.LoadSound(path)
return sound, rl.IsSoundValid(sound)
},
unload = proc(asset: Asset) {
rl.UnloadSound(asset.(rl.Sound))
},
}
MUSIC_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
bool,
) {
music := rl.LoadMusicStream(path)
return music, rl.IsMusicValid(music)
},
unload = proc(asset: Asset) {
rl.UnloadMusicStream(asset.(rl.Music))
},
}
MODEL_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
bool,
) {
model := rl.LoadModel(path)
return model, rl.IsModelValid(model)
},
unload = proc(asset: Asset) {
rl.UnloadModel(asset.(rl.Model))
},
}
TEXTURE_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
bool,
) {
texture := rl.LoadTexture(path)
return texture, rl.IsTextureValid(texture)
},
unload = proc(asset: Asset) {
rl.UnloadTexture(asset.(rl.Texture2D))
},
}
CURVE_1D_CSV_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
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 nil, false
}
values, err2 := parse_csv_1d(data)
if err2 != nil {
log.errorf("Failed to parse curve: %s, %v", path, err2)
}
return values, true
},
unload = proc(asset: Asset) {
delete(asset.(Curve_1D))
},
}
CURVE_2D_CSV_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
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 nil, false
}
curve, err2 := parse_csv_2d(data)
if err2 != nil {
log.errorf("Failed to parse curve: %s, %v", path, err2)
}
return curve, true
},
unload = proc(asset: Asset) {
delete(asset.(Curve_2D))
},
}
SCENE_DESC_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
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(asset: Asset) {
delete(asset.(Scene_Desc).instances)
},
}
BVH_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
bool,
) {
model_asset, _, model_res := assetman_fetch_or_load(
assetman,
Asset_Key{path = name.from_cstring(path), type = .Model},
)
if model_res == .Error {
return nil, false
}
model := model_asset.(rl.Model)
vert_count := 0
indices_count := 0
for i in 0 ..< model.meshCount {
mesh := model.meshes[i]
vert_count += int(mesh.vertexCount)
indices_count += int(mesh.triangleCount * 3)
}
assert(vert_count < int(max(u16)))
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])
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},
context.allocator,
)
root_aabb := mesh_bvh.bvh.nodes[0].aabb
return Loaded_BVH {
aabb = root_aabb,
bvh = mesh_bvh.bvh,
vertices = vertices,
indices = indices,
},
true
},
unload = proc(asset: Asset) {
loaded_bvh := asset.(Loaded_BVH)
bvh.destroy_bvh(&loaded_bvh.bvh)
delete(loaded_bvh.vertices)
delete(loaded_bvh.indices)
},
should_reload = proc(assetman: ^Asset_Manager, key: Asset_Key, entry: Asset_Entry) -> bool {
_, modtime, _ := assetman_fetch_or_load(
assetman,
Asset_Key{path = key.path, type = .Model},
)
return entry.modtime != modtime
},
}
CONVEX_LOADER :: Asset_Cache_Loader {
load = proc(
assetman: ^Asset_Manager,
path: cstring,
payload: Asset_Cache_Loader_Payload,
) -> (
Asset,
bool,
) {
tracy.Zone()
bytes, err := physfs.read_entire_file(string(path), context.temp_allocator)
if err != nil {
log.errorf("error reading file %v %s", err)
return {}, false
}
return parse_convex(bytes)
},
unload = proc(asset: Asset) {
convex := asset.(Loaded_Convex)
delete(convex.mesh.vertices)
delete(convex.mesh.edges)
delete(convex.mesh.faces)
},
}
ASSET_LOADERS := [Asset_Type]Asset_Cache_Loader {
.Texture = TEXTURE_LOADER,
.Model = MODEL_LOADER,
.Shader = SHADER_LOADER,
.Sound = SOUND_LOADER,
.Music = MUSIC_LOADER,
.Curve_1D = CURVE_1D_CSV_LOADER,
.Curve_2D = CURVE_2D_CSV_LOADER,
.Scene = SCENE_DESC_LOADER,
.BVH = BVH_LOADER,
.Convex = CONVEX_LOADER,
}
assetman_init :: proc(assetman: ^Asset_Manager) {
modtime_watcher_init(&assetman.watcher)
}
assetman_tick :: proc(assetman: ^Asset_Manager) {
tracy.Zone()
for asset in modtime_watcher_next(&assetman.watcher) {
key := Asset_Key {
path = asset.path,
type = asset.type,
}
entry, ok := &assetman.assets[key]
if ok {
log.debugf(
"asset changed {} {} {} {}",
name.to_string(asset.path),
asset,
entry.modtime,
asset.modtime,
)
entry.fs_modtime = asset.modtime
}
}
}
Asset_Cache_Result :: enum {
Cached,
Loaded,
Reloaded,
Error,
}
assetman_fetch_or_load_internal :: proc(
assetman: ^Asset_Manager,
key: Asset_Key,
payload: Asset_Cache_Loader_Payload = nil,
force_no_reload := false,
) -> (
value: Asset,
modtime: physfs.sint64,
result: Asset_Cache_Result,
) {
existing, has_existing := assetman.assets[key]
if has_existing {
wants_reload :=
ASSET_LOADERS[key.type].should_reload(assetman, key, existing) if ASSET_LOADERS[key.type].should_reload != nil else false
if force_no_reload || (existing.modtime == existing.fs_modtime && !wants_reload) {
result = .Cached
return existing.asset, existing.modtime, result
} else {
path := name.to_cstring(key.path)
new_modtime := physfs.getLastModTime(path)
// Try to load the new version
new_value, ok := ASSET_LOADERS[key.type].load(assetman, path, payload)
if ok {
result = .Reloaded
ASSET_LOADERS[key.type].unload(existing.asset)
assetman.assets[key] = {
asset = new_value,
modtime = new_modtime,
fs_modtime = new_modtime,
}
log.debugf("reloaded asset: %s", path)
return new_value, new_modtime, result
} else {
log.warnf("failed to reload asset after modification %s", path)
result = .Cached
return existing.asset, existing.modtime, result
}
}
} else {
path := name.to_cstring(key.path)
modtime = physfs.getLastModTime(path)
ok: bool
value, ok = ASSET_LOADERS[key.type].load(assetman, path, payload)
if ok {
assetman.assets[key] = {
asset = value,
modtime = modtime,
fs_modtime = modtime,
}
result = .Loaded
log.debugf("loaded asset: %s", path)
return value, modtime, result
} else {
log.errorf("failed to load asset %s", path)
result = .Error
return {}, 0, .Error
}
}
}
assetman_fetch_or_load :: proc(
assetman: ^Asset_Manager,
key: Asset_Key,
payload: Asset_Cache_Loader_Payload = nil,
force_no_reload := false,
) -> (
value: Asset,
modtime: physfs.sint64,
result: Asset_Cache_Result,
) {
value, modtime, result = assetman_fetch_or_load_internal(
assetman,
key,
payload,
force_no_reload,
)
if result != .Cached {
log.debugf("asset {}: [{}] {}", key.type, name.to_string(key.path), result)
}
if result == .Loaded {
modtime_watcher_add_asset(&assetman.watcher, key.type, key.path, modtime)
}
return
}
assetcache_destroy :: proc(ac: ^$T/Asset_Cache($E)) {
for _, v in ac.cache {
ac.loader.unload(v.value)
}
delete(ac.cache)
}
get_texture :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Texture2D {
tracy.Zone()
texture, _, _ := assetman_fetch_or_load(
assetman,
Asset_Key{path = name.from_cstring(path), type = .Texture},
)
return texture.(rl.Texture2D)
}
get_model_ex :: proc(
assetman: ^Asset_Manager,
path: cstring,
ref_modtime: physfs.sint64 = 0, // will check reload status using reference load time. When 0 reloaded will be true only if this call triggered reload
) -> (
model: rl.Model,
modtime: physfs.sint64,
reloaded: bool,
) {
tracy.Zone()
asset: Asset
result: Asset_Cache_Result
asset, modtime, result = assetman_fetch_or_load(
assetman,
Asset_Key{path = name.from_cstring(path), type = .Model},
)
model = asset.(rl.Model)
reloaded = result == .Reloaded || ref_modtime != modtime
return
}
get_model :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Model {
model, _, _ := get_model_ex(assetman, path)
return model
}
get_shader :: proc(
assetman: ^Asset_Manager,
vs_path: cstring,
ps_path: cstring,
location_set: Shader_Location_Set,
) -> Loaded_Shader {
asset, _, result := assetman_fetch_or_load(
assetman,
Asset_Key{path = name.from_cstring(vs_path), type = .Shader},
ps_path,
)
loaded_shader := asset.(Loaded_Shader)
if location_set > loaded_shader.location_set || result == .Loaded || result == .Reloaded {
loaded_shader.location_set = location_set
loaded_shader.locations = {}
for location in location_set {
loaded_shader.locations[location] = rl.GetShaderLocation(
loaded_shader.shader,
SHADER_LOCATION_NAMES[location],
)
}
}
return loaded_shader
}
get_sound :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Sound {
sound, _, _ := assetman_fetch_or_load(
assetman,
Asset_Key{path = name.from_cstring(path), type = .Sound},
)
return sound.(rl.Sound)
}
get_music :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Music {
music, _, _ := assetman_fetch_or_load(
assetman,
Asset_Key{path = name.from_cstring(path), type = .Music},
)
return music.(rl.Music)
}
get_bvh :: proc(assetman: ^Asset_Manager, path: name.Name) -> (Loaded_BVH, bool) {
tracy.Zone()
asset, _, res := assetman_fetch_or_load(assetman, Asset_Key{path = path, type = .BVH})
return asset.(Loaded_BVH), res != Asset_Cache_Result.Error
}
get_curve_1d :: proc(assetman: ^Asset_Manager, path: cstring) -> (curve: Curve_1D) {
asset, _, _ := assetman_fetch_or_load(
assetman,
Asset_Key{path = name.from_cstring(path), type = .Curve_1D},
)
return asset.(Curve_1D)
}
// Reads a two column comma separated csv file as a curve
get_curve_2d :: proc(assetman: ^Asset_Manager, path: cstring) -> (curve: Curve_2D) {
asset, _, _ := assetman_fetch_or_load(
assetman,
Asset_Key{path = name.from_cstring(path), type = .Curve_2D},
)
return asset.(Curve_2D)
}
// 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,
modtime: i64,
) {
asset, mtime, _ := assetman_fetch_or_load(
assetman,
Asset_Key{path = path, type = .Scene},
nil,
force_no_reload,
)
return asset.(Scene_Desc), mtime
}
get_convex :: proc(assetman: ^Asset_Manager, path: cstring) -> (result: Loaded_Convex) {
asset, _, _ := assetman_fetch_or_load(
assetman,
Asset_Key{path = name.from_cstring(path), type = .Convex},
)
return asset.(Loaded_Convex)
}
shutdown :: proc(assetman: ^Asset_Manager) {
tracy.Zone()
modtime_watcher_deinit(&assetman.watcher)
for k, v in assetman.assets {
ASSET_LOADERS[k.type].unload(v.asset)
}
delete_map(assetman.assets)
}