ludic/packages/ludic.render3d/gltf.ludic
Orkuncakilkaya 6289564382 render3d: R3D_VKMEM=<frame> says where the GPU memory is, by owner, and the renderer's big CPU arrays
Every allocation carries the owner its maker was wrapped in (models, terrain, impostor atlases,
scatter, grass, shadow maps, frame targets, sky, water, game textures, made in a frame), and the
report prints each owner's images and buffers, the 25 largest images and the scatter layers',
streams' and terrain's CPU copies. Nothing drawn changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:58:59 +03:00

241 lines
12 KiB
Text

# ============================================================================
# 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 {
@owns(Mesh) mesh: Mesh,
diff: int = 0,
nrm: int = 0,
arm: int = 0,
verts: int = 0, # vertices in the mesh (each attribute buffer holds this many)
name: string # the material's name (a part to tint: "hk_jacket")
}
property Model {
prims: []Prim,
radius: float = 0.0, # float bits: max horizontal extent from the origin
height: float = 0.0, # float bits: y extent above ymin
ymin: float = 0.0,
tris: int = 0,
@owns(Skin) skin: Skin # the skeleton, for a skinned node (skin.ludic); null for a rigid model
}
# a JSON number as float bits (ints and fixed-point decimals both)
function jnum(v: Val) -> float {
if v.tag == 2 { return float(v.num) / 65536.0 }
return float(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.
function gltf_mat_find(render3d_st: Render3dState, name: string) -> int {
if render3d_st.gltf_mat_names == null { return -1 }
for i in 0 .. render3d_st.gltf_mat_n { if render3d_st.gltf_mat_names[i] == name { return i } }
return -1
}
function gltf_mat_remember(render3d_st: mut Render3dState, name: string, diff: int, nrm: int, arm: int) -> void {
if render3d_st.gltf_mat_names == null { render3d_st.gltf_mat_names = new []string; render3d_st.gltf_mat_diff = words(256); render3d_st.gltf_mat_nrm = words(256); render3d_st.gltf_mat_arm = words(256) }
if gltf_mat_find(render3d_st, name) >= 0 or render3d_st.gltf_mat_n >= 256 { return }
# its own copy of the name: a primitive frees its own with it (model_release)
push(render3d_st.gltf_mat_names, name[0 .. len(name)])
render3d_st.gltf_mat_diff[render3d_st.gltf_mat_n] = diff; render3d_st.gltf_mat_nrm[render3d_st.gltf_mat_n] = nrm; render3d_st.gltf_mat_arm[render3d_st.gltf_mat_n] = arm
render3d_st.gltf_mat_n += 1
}
@alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)")
function gltf_texture(render3d_st: mut Render3dState, uri: string, srgb: bool) -> int {
if render3d_st.gltf_tex_paths == null { render3d_st.gltf_tex_paths = new []pointer; render3d_st.gltf_tex_ids = words(256); render3d_st.gltf_tex_refs = words(256) }
var png: string = uri
let n = len(uri)
var made = false
if n > 4 and uri[n - 4] == '.' and uri[n - 3] == 'j' { png = uri[0 .. n - 4] + ".png"; made = true }
let path = render3d_st.gltf_dir + "/" + png
if made { free(png) }
var i = 0
while i < render3d_st.gltf_tex_n {
if render3d_st.gltf_tex_paths[i] == path {
free(path) # the cache holds its own
render3d_st.gltf_tex_refs[i] += 1
return render3d_st.gltf_tex_ids[i]
}
i += 1
}
var dil = 0
if render3d_st.gltf_cutout { dil = 24 }
let id = tex_load_ex(render3d_st, path, srgb, dil)
if render3d_st.gltf_tex_n < 256 { push(render3d_st.gltf_tex_paths, path); render3d_st.gltf_tex_ids[render3d_st.gltf_tex_n] = id; render3d_st.gltf_tex_refs[render3d_st.gltf_tex_n] = 1; render3d_st.gltf_tex_n += 1 } else { free(path) }
return id
}
# the image uri behind materials[m].<slot>.index, or null
function gltf_mat_uri(render3d_st: Render3dState, mat: Val, slot: pointer) -> string {
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(render3d_st.gltf_doc, "textures"), ti)
let src = value_as_int(value_get(tex, "source"))
let img = value_at(value_get(render3d_st.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(render3d_st: mut Render3dState, idx: int) -> pointer {
let acc = value_at(value_get(render3d_st.gltf_doc, "accessors"), idx)
let bv = value_at(value_get(render3d_st.gltf_doc, "bufferViews"), jint(acc, "bufferView", 0))
let off = jint(bv, "byteOffset", 0) + jint(acc, "byteOffset", 0)
render3d_st.gltf_count = jint(acc, "count", 0)
render3d_st.gltf_ctype = jint(acc, "componentType", 5126)
let ty = value_as_str(value_get(acc, "type"))
render3d_st.gltf_comps = 1
if ty == "VEC2" { render3d_st.gltf_comps = 2 }
if ty == "VEC3" { render3d_st.gltf_comps = 3 }
if ty == "VEC4" { render3d_st.gltf_comps = 4 }
if ty == "MAT2" { render3d_st.gltf_comps = 4 }
if ty == "MAT3" { render3d_st.gltf_comps = 9 }
if ty == "MAT4" { render3d_st.gltf_comps = 16 } # a skin's inverse bind matrices
var csz = 4
if render3d_st.gltf_ctype == 5123 or render3d_st.gltf_ctype == 5122 { csz = 2 }
if render3d_st.gltf_ctype == 5121 or render3d_st.gltf_ctype == 5120 { csz = 1 }
let n = render3d_st.gltf_count * render3d_st.gltf_comps * csz
let buf = bytes(n + 8)
file_seek(render3d_st.gltf_bin, off, 0)
file_read(render3d_st.gltf_bin, buf, n)
return buf
}
function gltf_attrib(render3d_st: mut Render3dState, m: Mesh, attrs: Val, name: pointer, loc: int) -> bool {
if value_has(attrs, name) == 0 { return false }
let data = gltf_accessor(render3d_st, value_as_int(value_get(attrs, name)))
gpu_mesh_vertices(render3d_st, m, data, render3d_st.gltf_count * render3d_st.gltf_comps * 4, GPU_STATIC)
gpu_mesh_attr(render3d_st, m, loc, render3d_st.gltf_comps, GPU_F32, 0, 0, false)
free(data)
return true
}
function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim {
let pr = new Prim
let m = gpu_mesh_new(render3d_st)
let attrs = value_get(p, "attributes")
gltf_attrib(render3d_st, m, attrs, "POSITION", 0)
let nverts = render3d_st.gltf_count
gltf_attrib(render3d_st, m, attrs, "NORMAL", 1)
gltf_attrib(render3d_st, m, attrs, "TEXCOORD_0", 2)
skin_attribs(render3d_st, m, attrs) # JOINTS_0 / WEIGHTS_0 onto 5 / 6, when the mesh has them
let idx = gltf_accessor(render3d_st, value_as_int(value_get(p, "indices")))
var isz = 4
if render3d_st.gltf_ctype == 5123 { isz = 2 }
gpu_mesh_indices(render3d_st, m, idx, render3d_st.gltf_count * isz, isz)
free(idx)
m.count = render3d_st.gltf_count
gpu_mesh_done(render3d_st, m)
pr.mesh = m
pr.verts = nverts
# material textures
if render3d_st.gltf_white == 0 { render3d_st.gltf_white = tex_solid(render3d_st, 200, 200, 200, 255); render3d_st.gltf_flat = tex_solid(render3d_st, 128, 128, 255, 255) }
pr.diff = render3d_st.gltf_white; pr.nrm = render3d_st.gltf_flat; pr.arm = render3d_st.gltf_white
if value_has(p, "material") != 0 {
let mat = value_at(value_get(render3d_st.gltf_doc, "materials"), value_as_int(value_get(p, "material")))
render3d_st.gltf_cutout = value_has(mat, "alphaMode") != 0
let ud = gltf_mat_uri(render3d_st, mat, "baseColorTexture")
let un = gltf_mat_uri(render3d_st, mat, "normalTexture")
let ua = gltf_mat_uri(render3d_st, mat, "metallicRoughnessTexture")
if ud != null { pr.diff = gltf_texture(render3d_st, ud, true) }
if un != null { pr.nrm = gltf_texture(render3d_st, un, false) }
if ua != null { pr.arm = gltf_texture(render3d_st, ua, false) }
var mname: string = null
# a copy: the document goes, strings and all, at the next load
if value_has(mat, "name") != 0 { let s = value_as_str(value_get(mat, "name")); mname = s[0 .. len(s)] }
pr.name = mname
if mname != null {
if ud != null { gltf_mat_remember(render3d_st, mname, pr.diff, pr.nrm, pr.arm) }
else {
let k = gltf_mat_find(render3d_st, mname)
if k >= 0 {
pr.diff = render3d_st.gltf_mat_diff[k]; pr.nrm = render3d_st.gltf_mat_nrm[k]; pr.arm = render3d_st.gltf_mat_arm[k]
gltf_tex_ref(render3d_st, pr.diff); gltf_tex_ref(render3d_st, pr.nrm); gltf_tex_ref(render3d_st, pr.arm)
}
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.
# the GPU memory it makes is counted as VKM_MODEL (R3D_VKMEM)
function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model {
let was = render3d_st.gvk_tag
render3d_st.gvk_tag = VKM_MODEL
let r = gltf_load__t(render3d_st, dir, file, node_name)
render3d_st.gvk_tag = was
return r
}
@alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)")
@creates(Model)
function gltf_load__t(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model {
render3d_st.gltf_dir = dir
let at = dir + "/" + file
let text = Fs.read_text(at)
free(at)
if text == null { print(`gltf: cannot read {dir}/{file}`); return null }
# the last file's tree goes first (it is read only until the next load: ludic.anim's read.ludic),
# and the text once parsed - nothing gives memory back on its own (plan 23 of maroon-lake)
if render3d_st.gltf_doc != null { Json.free_all(render3d_st.gltf_doc) }
render3d_st.gltf_doc = Json.parse(text)
free(text)
let buffers = value_get(render3d_st.gltf_doc, "buffers")
let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri"))
let bin_at = dir + "/" + bin_uri
render3d_st.gltf_bin = file_open(bin_at, "rb")
free(bin_at)
if render3d_st.gltf_bin == null { print(`gltf: cannot open {bin_uri}`); return null }
let nodes = value_get(render3d_st.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(render3d_st.gltf_bin); return null }
let model = new Model
model.prims = new []Prim
let mesh = value_at(value_get(render3d_st.gltf_doc, "meshes"), mesh_idx)
let prims = value_get(mesh, "primitives")
var r2 = 0.0; var ymin = 1000.0; var ymax = -1000.0
for i in 0 .. value_count(prims) {
let p = value_at(prims, i)
push(model.prims, gltf_prim(render3d_st, p))
model.tris += render3d_st.gltf_count / 3
# bounds from the accessor min/max
let acc = value_at(value_get(render3d_st.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 = Math.abs(jnum(value_at(mn, 0))); let x1 = Math.abs(jnum(value_at(mx, 0)))
let z0 = Math.abs(jnum(value_at(mn, 2))); let z1 = Math.abs(jnum(value_at(mx, 2)))
let rx = Math.max(x0, x1); let rz = Math.max(z0, z1)
let rr = rx * rx + rz * rz
if rr > r2 { r2 = rr }
let y0 = jnum(value_at(mn, 1)); let y1 = jnum(value_at(mx, 1))
if y0 < ymin { ymin = y0 }
if y1 > ymax { ymax = y1 }
}
if skin_idx >= 0 { model.skin = skin_load(render3d_st, skin_idx) }
file_close(render3d_st.gltf_bin)
model.radius = Math.sqrt(r2)
model.ymin = ymin
model.height = ymax - ymin
print(`gltf: {node_name}: {len(model.prims)} prims, {model.tris} tris`)
return model
}