package assets import "core:log" import "core:math" import lg "core:math/linalg" import "core:strconv" import "game:debug" import "game:halfedge" import "game:physics/bvh" import "game:physics/collision" import "libs:physfs" import rl "libs:raylib" import "libs:raylib/rlgl" import "libs:tracy" _ :: math Loaded_BVH :: struct { // AABB of all bvhs aabb: bvh.AABB, // BVH for each mesh in a model bvhs: []bvh.BVH, modtime: physfs.sint64, } Loaded_Convex :: struct { mesh: collision.Convex, center_of_mass: rl.Vector3, inertia_tensor: lg.Matrix3f32, } Loaded_Curve_2D :: struct { points: [][2]f32, } 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) } Curve_2D :: [][2]f32 Asset_Manager :: struct { textures: Asset_Cache(rl.Texture2D), models: Asset_Cache(rl.Model), shaders: Asset_Cache(Loaded_Shader), curves_1d: Asset_Cache([]f32), curves_2d: Asset_Cache(Curve_2D), bvhs: map[cstring]Loaded_BVH, curves: map[cstring]Loaded_Curve_2D, } Asset_Cache_Entry :: struct($E: typeid) { value: E, modtime: i64, } Asset_Cache_Loader_Payload :: union { cstring, } Asset_Cache_Loader :: struct($E: typeid) { load: proc(path: cstring, payload: Asset_Cache_Loader_Payload) -> (E, bool), unload: proc(value: E), } Asset_Cache :: struct($E: typeid) { cache: map[cstring]Asset_Cache_Entry(E), loader: Asset_Cache_Loader(E), } Shader_Location :: enum { Ambient, LightDir, LightColor, } 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", } Loaded_Shader :: struct { shader: rl.Shader, location_set: Shader_Location_Set, locations: Shader_Location_Array, } SHADER_LOADER :: Asset_Cache_Loader(Loaded_Shader) { load = proc(path: cstring, payload: Asset_Cache_Loader_Payload) -> (Loaded_Shader, bool) { shader := rl.LoadShader(path, payload.(cstring)) return Loaded_Shader{shader = shader}, rl.IsShaderValid(shader) }, unload = proc(shader: Loaded_Shader) { rl.UnloadShader(shader.shader) }, } MODEL_LOADER :: Asset_Cache_Loader(rl.Model) { load = proc(path: cstring, payload: Asset_Cache_Loader_Payload) -> (rl.Model, bool) { model := rl.LoadModel(path) return model, rl.IsModelValid(model) }, unload = proc(model: rl.Model) { rl.UnloadModel(model) }, } TEXTURE_LOADER :: Asset_Cache_Loader(rl.Texture2D) { load = proc(path: cstring, payload: Asset_Cache_Loader_Payload) -> (rl.Texture2D, bool) { texture := rl.LoadTexture(path) return texture, rl.IsTextureValid(texture) }, unload = proc(texture: rl.Texture2D) { rl.UnloadTexture(texture) }, } CURVE_1D_CSV_LOADER :: Asset_Cache_Loader([]f32) { load = proc(path: cstring, payload: Asset_Cache_Loader_Payload) -> ([]f32, 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(values: []f32) { delete(values) }, } CURVE_2D_CSV_LOADER :: Asset_Cache_Loader(Curve_2D) { load = proc(path: cstring, payload: Asset_Cache_Loader_Payload) -> (Curve_2D, 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.points, true }, unload = proc(curve: Curve_2D) { delete(curve) }, } assetman_init :: proc(assetman: ^Asset_Manager) { assetman.models = { loader = MODEL_LOADER, } assetman.shaders = { loader = SHADER_LOADER, } assetman.textures = { loader = TEXTURE_LOADER, } assetman.curves_1d = { loader = CURVE_1D_CSV_LOADER, } assetman.curves_2d = { loader = CURVE_2D_CSV_LOADER, } } Asset_Cache_Result :: enum { Cached, Loaded, Reloaded, Error, } assetcache_fetch_or_load :: proc( ac: ^$T/Asset_Cache($E), path: cstring, payload: Asset_Cache_Loader_Payload = nil, ) -> ( value: E, modtime: physfs.sint64, result: Asset_Cache_Result, ) { tracy.Zone() existing, has_existing := ac.cache[path] if has_existing { new_modtime := physfs.getLastModTime(path) if existing.modtime == new_modtime { result = .Cached return existing.value, new_modtime, result } else { // Try to load the new version new_value, ok := ac.loader.load(path, payload) if ok { result = .Reloaded ac.loader.unload(existing.value) ac.cache[path] = { value = new_value, 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.value, existing.modtime, result } } } else { modtime = physfs.getLastModTime(path) ok: bool value, ok = ac.loader.load(path, payload) if ok { ac.cache[path] = { value = value, 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 } } } 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, _, _ := assetcache_fetch_or_load(&assetman.textures, path) return texture } 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() result: Asset_Cache_Result model, modtime, result = assetcache_fetch_or_load(&assetman.models, path) 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 { loaded_shader, _, result := assetcache_fetch_or_load(&assetman.shaders, vs_path, ps_path) 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 } null_bvhs: []bvh.BVH get_bvh :: proc(assetman: ^Asset_Manager, path: cstring) -> Loaded_BVH { tracy.Zone() loaded_bvh, ok := assetman.bvhs[path] model, modtime, reloaded := get_model_ex(assetman, path, loaded_bvh.modtime) should_recreate := reloaded || !ok if ok && should_recreate { 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), } for i in 0 ..< model.meshCount { mesh := model.meshes[i] vertices := (cast([^]rl.Vector3)mesh.vertices)[:mesh.vertexCount] indices := mesh.indices[:mesh.triangleCount * 3] mesh_bvh := bvh.build_bvh_from_mesh( {vertices = vertices, indices = indices}, context.allocator, ) 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, modtime = modtime, } } return assetman.bvhs[path] } get_curve_1d :: proc(assetman: ^Asset_Manager, path: cstring) -> (curve: []f32) { curve, _, _ = assetcache_fetch_or_load(&assetman.curves_1d, path) return } // Reads a two column comma separated csv file as a curve get_curve_2d :: proc(assetman: ^Asset_Manager, path: cstring) -> (curve: Curve_2D) { curve, _, _ = assetcache_fetch_or_load(&assetman.curves_2d, path) return } 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 { log.errorf("error reading file %v %s", err) return } Parse_Ctx :: struct { bytes: []byte, it: int, line: int, } advance :: proc(ctx: ^Parse_Ctx, by: int = 1) -> bool { ctx.it = min(ctx.it + by, len(ctx.bytes) + 1) return ctx.it < len(ctx.bytes) } is_whitespace :: proc(b: byte) -> bool { return b == ' ' || b == '\t' || b == '\r' || b == '\n' } skip_line :: proc(ctx: ^Parse_Ctx) { for ctx.it < len(ctx.bytes) && ctx.bytes[ctx.it] != '\n' { advance(ctx) or_break } advance(ctx) ctx.line += 1 } skip_whitespase :: proc(ctx: ^Parse_Ctx) { switch ctx.bytes[ctx.it] { case ' ', '\t', '\r', '\n': if ctx.bytes[ctx.it] == '\n' { ctx.line += 1 } advance(ctx) or_break case '#': skip_line(ctx) } } Edge :: [2]u16 edges_map := make_map(map[Edge]halfedge.Edge_Index, context.temp_allocator) edges := make_dynamic_array([dynamic]halfedge.Half_Edge, context.temp_allocator) vertices := make_dynamic_array([dynamic]halfedge.Vertex, context.temp_allocator) faces := make_dynamic_array([dynamic]halfedge.Face, context.temp_allocator) min_pos, max_pos: rl.Vector3 = max(f32), min(f32) // Parse obj file directly into halfedge data structure { ctx := Parse_Ctx { bytes = bytes, line = 1, } for ctx.it < len(ctx.bytes) { skip_whitespase(&ctx) switch ctx.bytes[ctx.it] { case 'v': // vertex advance(&ctx) or_break vertex: rl.Vector3 coord_idx := 0 for ctx.bytes[ctx.it] != '\n' && ctx.bytes[ctx.it] != '\r' { skip_whitespase(&ctx) s := string(ctx.bytes[ctx.it:]) coord_val, nr, ok := strconv.parse_f32_prefix(s) if !ok { log.errorf( "failed to parse float %v %s at line %d", coord_idx, ctx.bytes[ctx.it:][:12], ctx.line, ) return } advance(&ctx, nr) or_break vertex[coord_idx] = coord_val coord_idx += 1 } append(&vertices, halfedge.Vertex{pos = vertex, edge = -1}) min_pos = lg.min(vertex, min_pos) max_pos = lg.max(vertex, max_pos) if ctx.bytes[ctx.it] == '\r' { advance(&ctx) } advance(&ctx) ctx.line += 1 case 'f': advance(&ctx) or_break MAX_FACE_VERTS :: 10 indices_buf: [MAX_FACE_VERTS]u16 index_count := 0 for ctx.bytes[ctx.it] != '\n' && ctx.bytes[ctx.it] != '\r' { skip_whitespase(&ctx) index_f, nr, ok := strconv.parse_f32_prefix(string(ctx.bytes[ctx.it:])) if !ok { log.errorf("failed to parse index at line %d", ctx.line) return } advance(&ctx, nr) or_break index := u16(index_f) - 1 indices_buf[index_count] = u16(index) index_count += 1 } if ctx.bytes[ctx.it] == '\r' { advance(&ctx) } advance(&ctx) ctx.line += 1 assert(index_count >= 3) indices := indices_buf[:index_count] append(&faces, halfedge.Face{}) face_idx := len(faces) - 1 face := &faces[face_idx] first_edge_idx := len(edges) face.edge = halfedge.Edge_Index(first_edge_idx) plane: collision.Plane { i1, i2, i3 := indices[0], indices[1], indices[2] v1, v2, v3 := vertices[i1].pos, vertices[i2].pos, vertices[i3].pos plane = collision.plane_from_point_normal( v1, lg.normalize0(lg.cross(v2 - v1, v3 - v1)), ) } face.normal = plane.normal for index in indices[3:] { assert( abs(collision.signed_distance_plane(vertices[index].pos, plane)) < 0.01, "mesh has non planar faces", ) } first_vert_pos := vertices[indices[0]].pos for i in 0 ..< len(indices) { edge_idx := halfedge.Edge_Index(first_edge_idx + i) prev_edge_relative := i == 0 ? len(indices) - 1 : i - 1 next_edge_relative := (i + 1) % len(indices) i1, i2 := indices[i], indices[next_edge_relative] v1, v2 := &vertices[i1], &vertices[i2] assert( lg.dot( lg.cross(v1.pos - first_vert_pos, v2.pos - first_vert_pos), plane.normal, ) >= 0, "non convex face or non ccw winding", ) if v1.edge == -1 { v1.edge = edge_idx } edge := halfedge.Half_Edge { origin = halfedge.Vertex_Index(i1), face = halfedge.Face_Index(face_idx), twin = -1, next = halfedge.Edge_Index(first_edge_idx + next_edge_relative), prev = halfedge.Edge_Index(first_edge_idx + prev_edge_relative), } stable_index := [2]u16{min(i1, i2), max(i1, i2)} if stable_index in edges_map { edge.twin = edges_map[stable_index] twin_edge := &edges[edge.twin] assert(twin_edge.twin == -1, "edge has more than two faces attached") twin_edge.twin = edge_idx } else { edges_map[stable_index] = edge_idx } append(&edges, edge) } case: skip_line(&ctx) } } } center := (max_pos + min_pos) * 0.5 extent := (max_pos - min_pos) * 0.5 center_of_mass: rl.Vector3 mesh := halfedge.Half_Edge_Mesh { vertices = vertices[:], edges = edges[:], faces = faces[:], center = center, extent = extent, } // Center of mass calculation total_volume := f32(0.0) { rlgl.Begin(rlgl.TRIANGLES) rlgl.End() rlgl.EnableWireMode() defer rlgl.DisableWireMode() tri_idx := 0 for face_idx in 0 ..< len(faces) { face := faces[face_idx] // for all triangles it := halfedge.iterator_face_edges(mesh, halfedge.Face_Index(face_idx)) i := 0 tri: [3]rl.Vector3 for edge in halfedge.iterate_next_edge(&it) { switch i { case 0 ..< 3: tri[i] = mesh.vertices[edge.origin].pos case: tri[1] = tri[2] tri[2] = mesh.vertices[edge.origin].pos } if i >= 2 { plane := collision.plane_from_point_normal(tri[0], -face.normal) h := max(0, collision.signed_distance_plane(center, plane)) tri_area := lg.dot(lg.cross(tri[1] - tri[0], tri[2] - tri[0]), face.normal) * 0.5 tetra_volume := 1.0 / 3.0 * tri_area * h total_volume += tetra_volume tetra_centroid := (tri[0] + tri[1] + tri[2] + center) * 0.25 center_of_mass += tetra_volume * tetra_centroid tri_idx += 1 } i += 1 } } } assert(total_volume > 0, "degenerate convex hull") center_of_mass /= total_volume inertia_tensor: lg.Matrix3f32 // Find inertia tensor { tri_idx := 0 for face_idx in 0 ..< len(faces) { // for all triangles it := halfedge.iterator_face_edges(mesh, halfedge.Face_Index(face_idx)) i := 0 tri: [3]rl.Vector3 for edge in halfedge.iterate_next_edge(&it) { switch i { case 0 ..< 3: tri[i] = mesh.vertices[edge.origin].pos case: tri[1] = tri[2] tri[2] = mesh.vertices[edge.origin].pos } if i >= 2 { tet := Tetrahedron { p = {tri[0], tri[1], tri[2], center_of_mass}, } inertia_tensor += tetrahedron_inertia_tensor(tet, center_of_mass) tri_idx += 1 } i += 1 } } } inertia_tensor = inertia_tensor * lg.Matrix3f32(1.0 / total_volume) return {mesh = mesh, center_of_mass = center_of_mass, inertia_tensor = inertia_tensor} } // TODO: move convex stuff out of assets.odin Tetrahedron :: struct { p: [4]rl.Vector3, } tetrahedron_volume :: #force_inline proc(tet: Tetrahedron) -> f32 { return( 1.0 / 6.0 * abs(lg.dot(lg.cross(tet.p[1] - tet.p[0], tet.p[2] - tet.p[0]), tet.p[3] - tet.p[0])) \ ) } square :: #force_inline proc(val: f32) -> f32 { return val * val } tetrahedron_inertia_tensor :: proc(tet: Tetrahedron, o: rl.Vector3) -> lg.Matrix3f32 { p1, p2, p3, p4 := tet.p[0] - o, tet.p[1] - o, tet.p[2] - o, tet.p[3] - o // Jacobian determinant is 6*Volume det_j := abs(6.0 * tetrahedron_volume(tet)) moment_of_inertia_term :: proc(p1, p2, p3, p4: rl.Vector3, axis: int) -> f32 { return( square(p1[axis]) + p1[axis] * p2[axis] + square(p2[axis]) + p1[axis] * p3[axis] + p2[axis] * p3[axis] + square(p3[axis]) + p1[axis] * p4[axis] + p2[axis] * p4[axis] + p3[axis] * p4[axis] + square(p4[axis]) \ ) } product_of_inertia_term :: proc(p1, p2, p3, p4: rl.Vector3, axis1, axis2: int) -> f32 { return( 2.0 * p1[axis1] * p1[axis2] + p2[axis1] * p1[axis2] + p3[axis1] * p1[axis2] + p4[axis1] * p1[axis2] + p1[axis1] * p2[axis2] + 2.0 * p2[axis1] * p2[axis2] + p3[axis1] * p2[axis2] + p4[axis1] * p2[axis2] + p1[axis1] * p3[axis2] + p2[axis1] * p3[axis2] + 2.0 * p3[axis1] * p3[axis2] + p4[axis1] * p3[axis2] + p1[axis1] * p4[axis2] + p2[axis1] * p4[axis2] + p3[axis1] * p4[axis2] + 2.0 * p4[axis1] * p4[axis2] \ ) } MOMENT_OF_INERTIA_DENOM :: 1.0 / 60.0 PRODUCT_OF_INERTIA_DENOM :: 1.0 / 120.0 x_term := moment_of_inertia_term(p1, p2, p3, p4, 0) y_term := moment_of_inertia_term(p1, p2, p3, p4, 1) z_term := moment_of_inertia_term(p1, p2, p3, p4, 2) // Moments of intertia with respect to XYZ // Integral(y^2 + z^2) a := det_j * (y_term + z_term) * MOMENT_OF_INERTIA_DENOM // Integral(x^2 + z^2) b := det_j * (x_term + z_term) * MOMENT_OF_INERTIA_DENOM // Integral(x^2 + y^2) c := det_j * (x_term + y_term) * MOMENT_OF_INERTIA_DENOM // Products of inertia a_ := product_of_inertia_term(p1, p2, p3, p4, axis1 = 1, axis2 = 2) * PRODUCT_OF_INERTIA_DENOM b_ := product_of_inertia_term(p1, p2, p3, p4, axis1 = 0, axis2 = 2) * PRODUCT_OF_INERTIA_DENOM c_ := product_of_inertia_term(p1, p2, p3, p4, axis1 = 0, axis2 = 1) * PRODUCT_OF_INERTIA_DENOM return {a, -b_, -c_, -b_, b, -a_, -c_, -a_, c} } debug_draw_tetrahedron_wires :: proc(tri: [3]rl.Vector3, p: rl.Vector3, color: rl.Color) { rlgl.Begin(rlgl.LINES) defer rlgl.End() debug.rlgl_color(color) debug.rlgl_vertex3v2(tri[0], tri[1]) debug.rlgl_vertex3v2(tri[1], tri[2]) debug.rlgl_vertex3v2(tri[2], tri[0]) debug.rlgl_vertex3v2(tri[0], p) debug.rlgl_vertex3v2(tri[1], p) debug.rlgl_vertex3v2(tri[2], p) } shutdown :: proc(assetman: ^Asset_Manager) { tracy.Zone() assetcache_destroy(&assetman.textures) assetcache_destroy(&assetman.models) assetcache_destroy(&assetman.shaders) assetcache_destroy(&assetman.curves_1d) assetcache_destroy(&assetman.curves_2d) for _, loaded_bvh in assetman.bvhs { destroy_loaded_bvh(loaded_bvh) } delete(assetman.bvhs) }