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, inertia_tensor: lg.Matrix3f32, } 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, 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 /= 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 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 } } } log.infof("inertia tensor: %v", inertia_tensor) inertia_tensor = inertia_tensor * lg.Matrix3f32(1.0 / total_volume) return {mesh = mesh, center_of_mass = center_of_mass, inertia_tensor = inertia_tensor} } 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() 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) }