package resource import "core:encoding/json" import "core:log" import "core:mem" import glsl "core:math/linalg/glsl" import "core:os" import "core:strings" import bk "../backend" // --- glTF Parsed Data (intermediate, freed after GPU upload) --- Gltf_Mesh_Data :: struct { vertex_data: []byte, vertex_count: i32, layout: bk.Vertex_Layout, indices: []u32, material_idx: int, transform: glsl.mat4x4, } Gltf_Material_Data :: struct { texture_path: string, // resolved absolute path (allocated) normal_map_path: string, // resolved absolute path (allocated) color: [4]f32, double_sided: bool, metallic_factor: f32, roughness_factor: f32, emissive_factor: [3]f32, } Gltf_Model :: struct { meshes: []Gltf_Mesh_Data, materials: []Gltf_Material_Data, root_transform: glsl.mat4x4, // Scene root node transform (not baked into vertices) } // glTF internal parsing types @(private) Gltf_Buffer_View :: struct { byte_offset: int, byte_length: int, byte_stride: int, } @(private) Gltf_Accessor :: struct { buffer_view: int, byte_offset: int, component_type: int, count: int, } @(private) Gltf_Node :: struct { children: [dynamic]int, xform: glsl.mat4x4, has_xform: bool, mesh_idx: int, has_mesh: bool, } // --- Public resource API --- load_model_from_gltf :: proc( state: ^Resource_State, b: ^bk.Backend, path: string, ) -> (id: u32, mesh_count: int, ok: bool) { gltf, gltf_ok := parse_gltf(path) if !gltf_ok { return 0, 0, false } defer free_gltf_model(&gltf) slot, slot_ok := allocate_model_slot(state) if !slot_ok { return 0, 0, false } mc := len(gltf.meshes) model := &state.models[slot] model.mesh_ids = make([]u32, mc) model.texture_ids = make([]u32, mc) model.normal_map_ids = make([]u32, mc) model.material_colors = make([][4]f32, mc) model.material_double_sided = make([]bool, mc) model.material_metallic = make([]f32, mc) model.material_roughness = make([]f32, mc) model.material_emissive = make([][4]f32, mc) model.mesh_count = mc model.transform = gltf.root_transform // Deduplicate textures by path Loaded_Tex :: struct { path: string, id: u32, } loaded: [dynamic]Loaded_Tex defer delete(loaded) for i in 0..= 0 && mat_idx < len(gltf.materials) { mat := &gltf.materials[mat_idx] model.material_colors[i] = mat.color model.material_double_sided[i] = mat.double_sided model.material_metallic[i] = mat.metallic_factor model.material_roughness[i] = mat.roughness_factor model.material_emissive[i] = {mat.emissive_factor.x, mat.emissive_factor.y, mat.emissive_factor.z, 0} if len(mat.texture_path) > 0 { // Check if already loaded found := false for < in loaded { if lt.path == mat.texture_path { model.texture_ids[i] = lt.id found = true break } } if !found { tid, _, _, tex_ok := load_texture_from_file(state, b, mat.texture_path) if tex_ok { model.texture_ids[i] = tid append(&loaded, Loaded_Tex{path = mat.texture_path, id = tid}) } else { log.errorf("gpu/gltf: failed to load texture: %s", mat.texture_path) } } } if len(mat.normal_map_path) > 0 { found := false for < in loaded { if lt.path == mat.normal_map_path { model.normal_map_ids[i] = lt.id found = true break } } if !found { nid, _, _, nok := load_texture_from_file(state, b, mat.normal_map_path) if nok { model.normal_map_ids[i] = nid append(&loaded, Loaded_Tex{path = mat.normal_map_path, id = nid}) } else { log.errorf("gpu/gltf: failed to load normal map: %s", mat.normal_map_path) } } } } else { model.material_colors[i] = {1, 1, 1, 1} } } model.active = true return slot, mc, true } unload_model_resource :: proc(state: ^Resource_State, b: ^bk.Backend, id: u32) { if id >= MAX_MODELS || !state.models[id].active { return } model := &state.models[id] for i in 0.. (model: Gltf_Model, ok: bool) { // Read glTF JSON gltf_data, read_err := os.read_entire_file(path, context.allocator) if read_err != nil { log.errorf("gpu/gltf: failed to read file: %s", path) return {}, false } defer delete(gltf_data, context.allocator) parsed, json_err := json.parse(gltf_data) if json_err != nil { log.errorf("gpu/gltf: JSON parse error in %s: %v", path, json_err) return {}, false } defer json.destroy_value(parsed) root, root_ok := parsed.(json.Object) if !root_ok { log.error("gpu/gltf: root is not an object") return {}, false } // Resolve base directory base_dir := gltf_dir_from_path(path) // Read binary buffer buffers_arr := json_array(root, "buffers") or_return buf0, buf0_ok := buffers_arr[0].(json.Object) if !buf0_ok { return {}, false } bin_uri := json_string(buf0, "uri") or_return bin_path := strings.concatenate({base_dir, bin_uri}, context.temp_allocator) bin_data, bin_err := os.read_entire_file(bin_path, context.allocator) if bin_err != nil { log.errorf("gpu/gltf: failed to read binary buffer: %s", bin_path) return {}, false } defer delete(bin_data, context.allocator) // Parse buffer views views_arr := json_array(root, "bufferViews") or_return views := make([]Gltf_Buffer_View, len(views_arr)) defer delete(views) for v, i in views_arr { vo, vo_ok := v.(json.Object) if !vo_ok { return {}, false } views[i].byte_length = json_int_val(vo, "byteLength") views[i].byte_offset = json_int_val(vo, "byteOffset") views[i].byte_stride = json_int_val(vo, "byteStride") } // Parse accessors acc_arr := json_array(root, "accessors") or_return accessors := make([]Gltf_Accessor, len(acc_arr)) defer delete(accessors) for a, i in acc_arr { ao, ao_ok := a.(json.Object) if !ao_ok { return {}, false } accessors[i].buffer_view = json_int_val(ao, "bufferView") accessors[i].byte_offset = json_int_val(ao, "byteOffset") accessors[i].component_type = json_int_val(ao, "componentType") accessors[i].count = json_int_val(ao, "count") } // Parse images -> resolved paths images_arr, _ := json_array(root, "images") image_uris := make([]string, len(images_arr)) defer { // Don't delete strings that were moved into materials // free_gltf_model handles those } for img, i in images_arr { io, io_ok := img.(json.Object) if !io_ok { continue } uri, uri_ok := json_string(io, "uri") if !uri_ok { continue } image_uris[i] = strings.concatenate({base_dir, uri}) } defer { // Free image URIs not transferred to materials for &u in image_uris { // Only free if not empty and not transferred // Actually, materials copy the pointer, so we should NOT free here // free_gltf_model will handle it } delete(image_uris) } // Parse textures (texture index -> image index) textures_arr, _ := json_array(root, "textures") tex_to_image := make([]int, len(textures_arr)) defer delete(tex_to_image) for t, i in textures_arr { to, to_ok := t.(json.Object) if !to_ok { continue } tex_to_image[i] = json_int_val(to, "source") } // Parse materials materials_arr, _ := json_array(root, "materials") model.materials = make([]Gltf_Material_Data, len(materials_arr)) for m, i in materials_arr { mo, mo_ok := m.(json.Object) if !mo_ok { continue } model.materials[i].color = {1, 1, 1, 1} model.materials[i].roughness_factor = 0.5 // sensible default if ds, ds_ok := mo["doubleSided"].(json.Boolean); ds_ok { model.materials[i].double_sided = bool(ds) } pbr, has_pbr := mo["pbrMetallicRoughness"].(json.Object) if has_pbr { bct, has_bct := pbr["baseColorTexture"].(json.Object) if has_bct { tex_idx := json_int_val(bct, "index") if tex_idx >= 0 && tex_idx < len(tex_to_image) { img_idx := tex_to_image[tex_idx] if img_idx >= 0 && img_idx < len(image_uris) { // Transfer ownership of the string model.materials[i].texture_path = image_uris[img_idx] image_uris[img_idx] = "" } } } // Parse baseColorFactor if bcf, bcf_ok := pbr["baseColorFactor"].(json.Array); bcf_ok && len(bcf) >= 4 { model.materials[i].color = { f32(bcf[0].(json.Float) or_else 1), f32(bcf[1].(json.Float) or_else 1), f32(bcf[2].(json.Float) or_else 1), f32(bcf[3].(json.Float) or_else 1), } } // Parse metallicFactor (glTF default: 1.0) if mf, mf_ok := pbr["metallicFactor"].(json.Float); mf_ok { model.materials[i].metallic_factor = f32(mf) } // Parse roughnessFactor (glTF default: 1.0, we default to 0.5 for better visuals) if rf, rf_ok := pbr["roughnessFactor"].(json.Float); rf_ok { model.materials[i].roughness_factor = f32(rf) } } // Parse emissiveFactor if ef, ef_ok := mo["emissiveFactor"].(json.Array); ef_ok && len(ef) >= 3 { model.materials[i].emissive_factor = { f32(ef[0].(json.Float) or_else 0), f32(ef[1].(json.Float) or_else 0), f32(ef[2].(json.Float) or_else 0), } } // Parse normalTexture if nt, nt_ok := mo["normalTexture"].(json.Object); nt_ok { tex_idx := json_int_val(nt, "index") if tex_idx >= 0 && tex_idx < len(tex_to_image) { img_idx := tex_to_image[tex_idx] if img_idx >= 0 && img_idx < len(image_uris) && len(image_uris[img_idx]) > 0 { model.materials[i].normal_map_path = strings.clone(image_uris[img_idx]) } } } } // Free any image URIs not transferred to materials for &u in image_uris { if len(u) > 0 { delete(u) u = "" } } // Parse meshes meshes_arr := json_array(root, "meshes") or_return // Parse nodes nodes_arr, _ := json_array(root, "nodes") nodes := make([]Gltf_Node, len(nodes_arr)) defer { for &n in nodes { delete(n.children) } delete(nodes) } for n, i in nodes_arr { no, no_ok := n.(json.Object) if !no_ok { continue } if ch, ch_ok := no["children"].(json.Array); ch_ok { for c in ch { append(&nodes[i].children, int(c.(json.Float) or_else 0)) } } if mat, mat_ok := no["matrix"].(json.Array); mat_ok && len(mat) == 16 { nodes[i].has_xform = true // glTF column-major -> Odin m[row, col] for col in 0..<4 { for row in 0..<4 { nodes[i].xform[row, col] = f32(mat[col * 4 + row].(json.Float) or_else 0) } } } else { // Parse TRS (translation / rotation / scale) t := glsl.vec3{0, 0, 0} r := glsl.vec4{0, 0, 0, 1} // quaternion (x, y, z, w) s := glsl.vec3{1, 1, 1} has_trs := false if ta, ta_ok := no["translation"].(json.Array); ta_ok && len(ta) >= 3 { t = {f32(ta[0].(json.Float) or_else 0), f32(ta[1].(json.Float) or_else 0), f32(ta[2].(json.Float) or_else 0)} has_trs = true } if ra, ra_ok := no["rotation"].(json.Array); ra_ok && len(ra) >= 4 { r = {f32(ra[0].(json.Float) or_else 0), f32(ra[1].(json.Float) or_else 0), f32(ra[2].(json.Float) or_else 0), f32(ra[3].(json.Float) or_else 0)} has_trs = true } if sa, sa_ok := no["scale"].(json.Array); sa_ok && len(sa) >= 3 { s = {f32(sa[0].(json.Float) or_else 0), f32(sa[1].(json.Float) or_else 0), f32(sa[2].(json.Float) or_else 0)} has_trs = true } if has_trs { nodes[i].has_xform = true nodes[i].xform = gltf_compose_trs(t, r, s) } } if m_val, m_ok := no["mesh"]; m_ok { nodes[i].mesh_idx = int(m_val.(json.Float) or_else 0) nodes[i].has_mesh = true } } // Compute per-mesh transforms by walking node tree mesh_transforms := make([]glsl.mat4x4, len(meshes_arr)) defer delete(mesh_transforms) for i in 0.. 0 { scene0, s0_ok := scenes_arr[0].(json.Object) if s0_ok { if scene_nodes, sn_ok := scene0["nodes"].(json.Array); sn_ok { // If exactly one root node, extract its transform as the model transform if len(scene_nodes) == 1 { ri := int(scene_nodes[0].(json.Float) or_else 0) if ri >= 0 && ri < len(nodes) && nodes[ri].has_xform { root_xform = nodes[ri].xform } } for sn in scene_nodes { gltf_walk_nodes(nodes[:], int(sn.(json.Float) or_else 0), IDENTITY_MAT4, mesh_transforms) } } } } // Factor out the root transform from per-mesh transforms so vertices // are stored in model space (pre-root-transform). if root_xform != IDENTITY_MAT4 { root_inv := glsl.inverse(root_xform) for i in 0..= 0 { normals = gltf_read_vec3(bin_data, views[:], &accessors[norm_idx]) } uvs: []glsl.vec2 defer delete(uvs) if uv_idx >= 0 { uvs = gltf_read_vec2(bin_data, views[:], &accessors[uv_idx]) } colors: []glsl.vec4 defer delete(colors) if color_idx >= 0 { colors = gltf_read_vec4(bin_data, views[:], &accessors[color_idx]) } tangents: []glsl.vec4 defer delete(tangents) if tangent_idx >= 0 { tangents = gltf_read_vec4(bin_data, views[:], &accessors[tangent_idx]) } indices := gltf_read_indices(bin_data, views[:], &accessors[indices_idx]) // Build vertex layout from present attributes. layout_attribs: [bk.MAX_VERTEX_ATTRIBS]bk.Vertex_Attrib layout_count := 0 layout_attribs[layout_count] = .Position; layout_count += 1 if normals != nil { layout_attribs[layout_count] = .Normal; layout_count += 1 } if uvs != nil { layout_attribs[layout_count] = .Tex_Coord; layout_count += 1 } if colors != nil { layout_attribs[layout_count] = .Color; layout_count += 1 } if tangents != nil { layout_attribs[layout_count] = .Tangent; layout_count += 1 } layout := bk.vertex_layout_build(..layout_attribs[:layout_count]) // Assemble interleaved vertex data vert_count := accessors[pos_idx].count vertex_data := make([]byte, vert_count * int(layout.stride)) // Bake node transform into vertex data xform := mesh_transforms[mi] is_identity := xform == IDENTITY_MAT4 for v in 0.. 0 { delete(mat.texture_path) } if len(mat.normal_map_path) > 0 { delete(mat.normal_map_path) } } delete(model.materials) } // --- Node tree traversal --- @(private) gltf_walk_nodes :: proc( nodes: []Gltf_Node, idx: int, parent_transform: glsl.mat4x4, mesh_transforms: []glsl.mat4x4, ) { if idx < 0 || idx >= len(nodes) { return } node := &nodes[idx] local := parent_transform if node.has_xform { local = parent_transform * node.xform } if node.has_mesh && node.mesh_idx >= 0 && node.mesh_idx < len(mesh_transforms) { mesh_transforms[node.mesh_idx] = local } for child in node.children { gltf_walk_nodes(nodes, child, local, mesh_transforms) } } // --- Binary buffer readers --- @(private) gltf_read_vec3 :: proc(bin: []byte, views: []Gltf_Buffer_View, acc: ^Gltf_Accessor) -> []glsl.vec3 { view := views[acc.buffer_view] base := view.byte_offset + acc.byte_offset stride := view.byte_stride if view.byte_stride > 0 else 12 result := make([]glsl.vec3, acc.count) for i in 0.. len(bin) { break } ptr := cast(^[3]f32)&bin[offset] result[i] = {ptr[0], ptr[1], ptr[2]} } return result } @(private) gltf_read_vec2 :: proc(bin: []byte, views: []Gltf_Buffer_View, acc: ^Gltf_Accessor) -> []glsl.vec2 { view := views[acc.buffer_view] base := view.byte_offset + acc.byte_offset stride := view.byte_stride if view.byte_stride > 0 else 8 result := make([]glsl.vec2, acc.count) for i in 0.. len(bin) { break } ptr := cast(^[2]f32)&bin[offset] result[i] = {ptr[0], ptr[1]} } return result } @(private) gltf_read_vec4 :: proc(bin: []byte, views: []Gltf_Buffer_View, acc: ^Gltf_Accessor) -> []glsl.vec4 { view := views[acc.buffer_view] base := view.byte_offset + acc.byte_offset stride := view.byte_stride if view.byte_stride > 0 else 16 result := make([]glsl.vec4, acc.count) for i in 0.. len(bin) { break } ptr := cast(^[4]f32)&bin[offset] result[i] = {ptr[0], ptr[1], ptr[2], ptr[3]} } return result } @(private) gltf_read_indices :: proc(bin: []byte, views: []Gltf_Buffer_View, acc: ^Gltf_Accessor) -> []u32 { view := views[acc.buffer_view] base := view.byte_offset + acc.byte_offset result := make([]u32, acc.count) switch acc.component_type { case 5125: // UNSIGNED_INT stride := view.byte_stride if view.byte_stride > 0 else 4 for i in 0.. len(bin) { break } result[i] = (cast(^u32)&bin[offset])^ } case 5123: // UNSIGNED_SHORT stride := view.byte_stride if view.byte_stride > 0 else 2 for i in 0.. len(bin) { break } result[i] = u32((cast(^u16)&bin[offset])^) } case 5121: // UNSIGNED_BYTE for i in 0..= len(bin) { break } result[i] = u32(bin[offset]) } } return result } // Compose a TRS (translation, rotation, scale) into a 4x4 matrix. // Quaternion is (x, y, z, w) as in glTF spec. @(private) gltf_compose_trs :: proc(t: glsl.vec3, q: glsl.vec4, s: glsl.vec3) -> glsl.mat4x4 { // Rotation matrix from quaternion (x, y, z, w) x, y, z, w := q.x, q.y, q.z, q.w x2, y2, z2 := x + x, y + y, z + z xx := x * x2; xy := x * y2; xz := x * z2 yy := y * y2; yz := y * z2; zz := z * z2 wx := w * x2; wy := w * y2; wz := w * z2 m: glsl.mat4x4 m[0, 0] = (1 - (yy + zz)) * s.x m[0, 1] = (xy - wz) * s.y m[0, 2] = (xz + wy) * s.z m[0, 3] = t.x m[1, 0] = (xy + wz) * s.x m[1, 1] = (1 - (xx + zz)) * s.y m[1, 2] = (yz - wx) * s.z m[1, 3] = t.y m[2, 0] = (xz - wy) * s.x m[2, 1] = (yz + wx) * s.y m[2, 2] = (1 - (xx + yy)) * s.z m[2, 3] = t.z m[3, 0] = 0; m[3, 1] = 0; m[3, 2] = 0; m[3, 3] = 1 return m } // --- JSON helpers --- @(private) gltf_dir_from_path :: proc(path: string) -> string { for i := len(path) - 1; i >= 0; i -= 1 { if path[i] == '/' || path[i] == '\\' { return path[:i + 1] } } return "" } @(private) json_array :: proc(obj: json.Object, key: string) -> (json.Array, bool) { val, has := obj[key] if !has { return nil, false } arr, arr_ok := val.(json.Array) return arr, arr_ok } @(private) json_string :: proc(obj: json.Object, key: string) -> (string, bool) { val, has := obj[key] if !has { return "", false } s, s_ok := val.(json.String) return s, s_ok } @(private) json_int_val :: proc(obj: json.Object, key: string) -> int { val, has := obj[key] if !has { return 0 } return int(val.(json.Float) or_else 0) } @(private) json_int_or :: proc(obj: json.Object, key: string, default_val: int) -> int { val, has := obj[key] if !has { return default_val } f, f_ok := val.(json.Float) if !f_ok { return default_val } return int(f) } // --- Vertex data writers (interleaved byte buffer) --- @(private) write_vec2 :: proc(buf: []byte, offset: int, v: glsl.vec2) { data := [2]f32{v.x, v.y} mem.copy(&buf[offset], &data, 8) } @(private) write_vec3 :: proc(buf: []byte, offset: int, v: glsl.vec3) { data := [3]f32{v.x, v.y, v.z} mem.copy(&buf[offset], &data, 12) } @(private) write_vec4 :: proc(buf: []byte, offset: int, v: glsl.vec4) { data := [4]f32{v.x, v.y, v.z, v.w} mem.copy(&buf[offset], &data, 16) }