package assets import "core:c" import "core:log" import "core:math" import lg "core:math/linalg" import "core:os/os2" import "core:strconv" import "game:debug" import "game:halfedge" import "game:physics/bvh" import "game:physics/collision" import "libs:tracy" import rl "vendor:raylib" import "vendor:raylib/rlgl" Loaded_Texture :: struct { texture: rl.Texture2D, modtime: c.long, } Loaded_Model :: struct { model: rl.Model, modtime: c.long, } Loaded_BVH :: struct { // AABB of all bvhs aabb: bvh.AABB, // BVH for each mesh in a model bvhs: []bvh.BVH, modtime: c.long, } Loaded_Convex :: struct { mesh: collision.Convex, center_of_mass: rl.Vector3, } 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) } Asset_Manager :: struct { textures: map[cstring]Loaded_Texture, models: map[cstring]Loaded_Model, bvhs: map[cstring]Loaded_BVH, } get_texture :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Texture2D { tracy.Zone() modtime := rl.GetFileModTime(path) existing, ok := assetman.textures[path] if ok && existing.modtime == modtime { return existing.texture } if ok { rl.UnloadTexture(existing.texture) delete_key(&assetman.textures, path) log.infof("deleted texture %s. New textures len: %d", path, len(assetman.textures)) } loaded := rl.LoadTexture(path) if rl.IsTextureValid(loaded) { assetman.textures[path] = { texture = loaded, modtime = modtime, } return loaded } else { return rl.Texture2D{} } } get_model_ex :: proc( assetman: ^Asset_Manager, path: cstring, ref_modtime: c.long = 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: c.long, reloaded: bool, ) { tracy.Zone() new_modtime := rl.GetFileModTime(path) existing, ok := assetman.models[path] if ok && existing.modtime == new_modtime { return existing.model, existing.modtime, ref_modtime == 0 ? false : existing.modtime != ref_modtime } if ok { rl.UnloadModel(existing.model) delete_key(&assetman.textures, path) log.infof("deleted model %s. New models len: %d", path, len(assetman.textures)) } loaded := rl.LoadModel(path) if rl.IsModelValid(loaded) { assetman.models[path] = { model = loaded, modtime = new_modtime, } return loaded, new_modtime, true } else { return rl.Model{}, 0, true } } get_model :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Model { model, _, _ := get_model_ex(assetman, path) return model } 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 = math.F32_MAX, max = -math.F32_MAX, } 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_convex :: proc(assetman: ^Asset_Manager, path: cstring) -> (result: Loaded_Convex) { bytes, err := os2.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) center: rl.Vector3 // 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' { 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 at line %d", ctx.line) return } advance(&ctx, nr) or_break vertex[coord_idx] = coord_val coord_idx += 1 } append(&vertices, halfedge.Vertex{pos = vertex, edge = -1}) center += vertex 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' { 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 } 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, _ := &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 /= f32(len(vertices)) center_of_mass: rl.Vector3 mesh := halfedge.Half_Edge_Mesh { vertices = vertices[:], edges = edges[:], faces = faces[:], center = center, } // 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 return {mesh = mesh, center_of_mass = center_of_mass} } 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() for _, texture in assetman.textures { rl.UnloadTexture(texture.texture) } for _, model in assetman.models { rl.UnloadModel(model.model) } for _, loaded_bvh in assetman.bvhs { destroy_loaded_bvh(loaded_bvh) } delete(assetman.textures) delete(assetman.models) delete(assetman.bvhs) }