# ============================================================================ # gltf.ludic — a glTF 2.0 loader for scanned models: one named node's mesh, # each primitive uploaded straight from the .bin (positions, normals, uvs, # indices) with its baseColor / normal / ARM textures. Only the byte ranges the # node needs are read, so a 500 MB scan library costs what one tree costs. # ============================================================================ property Prim { mesh: Mesh, diff: int = 0, nrm: int = 0, arm: int = 0, name: string # the material's name (a part to tint: "hk_jacket") } property Model { prims: []Prim, radius: int = 0, # float bits: max horizontal extent from the origin height: int = 0, # float bits: y extent above ymin ymin: int = 0, tris: int = 0, skin: Skin # the skeleton, for a skinned node (skin.ludic); null for a rigid model } var gltf_dir: string = null var gltf_bin: pointer = null var gltf_doc: Val = null var gltf_count: int = 0 var gltf_ctype: int = 0 var gltf_comps: int = 0 var gltf_tex_paths: []pointer = null var gltf_tex_ids: words = null var gltf_tex_n: int = 0 var gltf_white: int = 0 var gltf_flat: int = 0 # a JSON number as float bits (ints and fixed-point decimals both) function jnum(v: Val) -> int { if v.tag == 2 { return fx_to_f32(v.num) } return f_from_int(v.num) } function jint(v: Val, key: pointer, fallback: int) -> int { if value_has(v, key) == 0 { return fallback } return value_as_int(value_get(v, key)) } # Materials seen so far, by name: a LOD chain exported from the .blend (tools/glgen/ # lod_export.py) carries the material names but no images — the blend links textures # that are not downloaded with it — so its primitives take the textures the LOD0 # download's material of the same name loaded. var gltf_mat_names: []string = null var gltf_mat_diff: words = null var gltf_mat_nrm: words = null var gltf_mat_arm: words = null var gltf_mat_n: int = 0 function gltf_mat_find(name: string) -> int { if gltf_mat_names == null { return -1 } for i in 0 .. gltf_mat_n { if gltf_mat_names[i] == name { return i } } return -1 } function gltf_mat_remember(name: string, diff: int, nrm: int, arm: int) -> void { if gltf_mat_names == null { gltf_mat_names = new []string; gltf_mat_diff = words(256); gltf_mat_nrm = words(256); gltf_mat_arm = words(256) } if gltf_mat_find(name) >= 0 or gltf_mat_n >= 256 { return } push(gltf_mat_names, name); gltf_mat_diff[gltf_mat_n] = diff; gltf_mat_nrm[gltf_mat_n] = nrm; gltf_mat_arm[gltf_mat_n] = arm gltf_mat_n += 1 } var gltf_cutout: bool = false # the material being loaded is alpha-blended (a cut-out atlas) function gltf_texture(uri: pointer, srgb: bool) -> int { if gltf_tex_paths == null { gltf_tex_paths = new []pointer; gltf_tex_ids = words(256) } var png: string = uri let n = len(uri) if n > 4 and uri[n - 4] == '.' and uri[n - 3] == 'j' { png = uri[0 .. n - 4] + ".png" } let path = gltf_dir + "/" + png var i = 0 while i < gltf_tex_n { if gltf_tex_paths[i] == path { return gltf_tex_ids[i] }; i += 1 } var dil = 0 if gltf_cutout { dil = 24 } let id = tex_load_ex(path, srgb, dil) if gltf_tex_n < 256 { push(gltf_tex_paths, path); gltf_tex_ids[gltf_tex_n] = id; gltf_tex_n += 1 } return id } # the image uri behind materials[m]..index, or null function gltf_mat_uri(mat: Val, slot: pointer) -> pointer { var holder = mat if slot == "baseColorTexture" or slot == "metallicRoughnessTexture" { if value_has(mat, "pbrMetallicRoughness") == 0 { return null } holder = value_get(mat, "pbrMetallicRoughness") } if value_has(holder, slot) == 0 { return null } let ti = value_as_int(value_get(value_get(holder, slot), "index")) let tex = value_at(value_get(gltf_doc, "textures"), ti) let src = value_as_int(value_get(tex, "source")) let img = value_at(value_get(gltf_doc, "images"), src) return value_as_str(value_get(img, "uri")) } # read one accessor's bytes from the .bin; sets gltf_count / gltf_ctype / gltf_comps function gltf_accessor(idx: int) -> pointer { let acc = value_at(value_get(gltf_doc, "accessors"), idx) let bv = value_at(value_get(gltf_doc, "bufferViews"), jint(acc, "bufferView", 0)) let off = jint(bv, "byteOffset", 0) + jint(acc, "byteOffset", 0) gltf_count = jint(acc, "count", 0) gltf_ctype = jint(acc, "componentType", 5126) let ty = value_as_str(value_get(acc, "type")) gltf_comps = 1 if ty == "VEC2" { gltf_comps = 2 } if ty == "VEC3" { gltf_comps = 3 } if ty == "VEC4" { gltf_comps = 4 } if ty == "MAT2" { gltf_comps = 4 } if ty == "MAT3" { gltf_comps = 9 } if ty == "MAT4" { gltf_comps = 16 } # a skin's inverse bind matrices var csz = 4 if gltf_ctype == 5123 or gltf_ctype == 5122 { csz = 2 } if gltf_ctype == 5121 or gltf_ctype == 5120 { csz = 1 } let n = gltf_count * gltf_comps * csz let buf = bytes(n + 8) file_seek(gltf_bin, off, 0) file_read(gltf_bin, buf, n) return buf } function gltf_attrib(m: Mesh, attrs: Val, name: pointer, loc: int) -> bool { if value_has(attrs, name) == 0 { return false } let data = gltf_accessor(value_as_int(value_get(attrs, name))) let b = gl_buffer() gl_bind_buffer(GL_ARRAY_BUFFER, b) gl_buffer_data(GL_ARRAY_BUFFER, gltf_count * gltf_comps * 4, data, GL_STATIC_DRAW) gl_enable_vertex_attrib_array(loc) gl_vertex_attrib_pointer(loc, gltf_comps, GL_FLOAT, 0, 0, null) free(data) if loc == 0 { m.vbo = b } return true } function gltf_prim(p: Val) -> Prim { let pr = new Prim let m = new Mesh m.vao = gl_vao() let attrs = value_get(p, "attributes") gltf_attrib(m, attrs, "POSITION", 0) gltf_attrib(m, attrs, "NORMAL", 1) gltf_attrib(m, attrs, "TEXCOORD_0", 2) skin_attribs(m, attrs) # JOINTS_0 / WEIGHTS_0 onto 5 / 6, when the mesh has them let idx = gltf_accessor(value_as_int(value_get(p, "indices"))) var isz = 4 m.itype = GL_UNSIGNED_INT if gltf_ctype == 5123 { isz = 2; m.itype = GL_UNSIGNED_SHORT } m.ebo = gl_buffer() gl_bind_buffer(GL_ELEMENT_ARRAY_BUFFER, m.ebo) gl_buffer_data(GL_ELEMENT_ARRAY_BUFFER, gltf_count * isz, idx, GL_STATIC_DRAW) free(idx) m.count = gltf_count gl_bind_vertex_array(0) pr.mesh = m # material textures if gltf_white == 0 { gltf_white = tex_solid(200, 200, 200, 255); gltf_flat = tex_solid(128, 128, 255, 255) } pr.diff = gltf_white; pr.nrm = gltf_flat; pr.arm = gltf_white if value_has(p, "material") != 0 { let mat = value_at(value_get(gltf_doc, "materials"), value_as_int(value_get(p, "material"))) gltf_cutout = value_has(mat, "alphaMode") != 0 let ud = gltf_mat_uri(mat, "baseColorTexture") let un = gltf_mat_uri(mat, "normalTexture") let ua = gltf_mat_uri(mat, "metallicRoughnessTexture") if ud != null { pr.diff = gltf_texture(ud, true) } if un != null { pr.nrm = gltf_texture(un, false) } if ua != null { pr.arm = gltf_texture(ua, false) } var mname: string = null if value_has(mat, "name") != 0 { mname = value_as_str(value_get(mat, "name")) } pr.name = mname if mname != null { if ud != null { gltf_mat_remember(mname, pr.diff, pr.nrm, pr.arm) } else { let k = gltf_mat_find(mname) if k >= 0 { pr.diff = gltf_mat_diff[k]; pr.nrm = gltf_mat_nrm[k]; pr.arm = gltf_mat_arm[k] } else { print(`gltf: material {mname} has no textures and none were loaded before it`) } } } } return pr } # Load the mesh of the node called `node_name` from dir/file. function gltf_load(dir: string, file: string, node_name: string) -> Model { gltf_dir = dir let text = Fs.read_text(dir + "/" + file) if text == null { print(`gltf: cannot read {dir}/{file}`); return null } gltf_doc = Json.parse(text) let buffers = value_get(gltf_doc, "buffers") let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri")) gltf_bin = file_open(dir + "/" + bin_uri, "rb") if gltf_bin == null { print(`gltf: cannot open {bin_uri}`); return null } let nodes = value_get(gltf_doc, "nodes") var mesh_idx = -1 var skin_idx = -1 for i in 0 .. value_count(nodes) { let nd = value_at(nodes, i) if value_as_str(value_get(nd, "name")) == node_name { mesh_idx = jint(nd, "mesh", -1); skin_idx = jint(nd, "skin", -1) } } if mesh_idx < 0 { print(`gltf: no node {node_name} in {file}`); file_close(gltf_bin); return null } let model = new Model model.prims = new []Prim let mesh = value_at(value_get(gltf_doc, "meshes"), mesh_idx) let prims = value_get(mesh, "primitives") var r2 = F_ZERO; var ymin = fi(1000); var ymax = fi(-1000) for i in 0 .. value_count(prims) { let p = value_at(prims, i) push(model.prims, gltf_prim(p)) model.tris += gltf_count / 3 # bounds from the accessor min/max let acc = value_at(value_get(gltf_doc, "accessors"), value_as_int(value_get(value_get(p, "attributes"), "POSITION"))) let mn = value_get(acc, "min"); let mx = value_get(acc, "max") let x0 = f_abs(jnum(value_at(mn, 0))); let x1 = f_abs(jnum(value_at(mx, 0))) let z0 = f_abs(jnum(value_at(mn, 2))); let z1 = f_abs(jnum(value_at(mx, 2))) let rx = f_max(x0, x1); let rz = f_max(z0, z1) let rr = f_add(f_mul(rx, rx), f_mul(rz, rz)) if f_gt(rr, r2) { r2 = rr } let y0 = jnum(value_at(mn, 1)); let y1 = jnum(value_at(mx, 1)) if f_ls(y0, ymin) { ymin = y0 } if f_gt(y1, ymax) { ymax = y1 } } if skin_idx >= 0 { model.skin = skin_load(skin_idx) } file_close(gltf_bin) model.radius = f_sqrt(r2) model.ymin = ymin model.height = f_sub(ymax, ymin) print(`gltf: {node_name}: {len(model.prims)} prims, {model.tris} tris`) return model }