ludic/packages/ludic.render3d/gltf.ludic
Orkuncakilkaya f79390838a render3d: a glTF material's factors are drawn - base colour, roughness, metallic and emission
gltf_factors reads pbrMetallicRoughness.baseColorFactor, metallicFactor, roughnessFactor and the
emissiveFactor into the primitive (pr.fac), and prim_factors hands them to every program that draws its
textures (actors, the scatter's meshes and levels, the impostor bake) as 1 - factor, so a program never
given them - or a material that gives none (pr.fac null) - multiplies by 1 and draws as before. An
untextured material with a colour (or a metal/roughness) samples a pure white for it, so the factor is
exactly what it draws: horse_cornea is its own colour, not the 200 grey every untextured part had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:52:42 +03:00

282 lines
14 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")
fac: floats # the material's factors, or null (all 1, no emission): base r g b a, roughness, metallic, emissive r g b
}
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) }
gltf_factors(render3d_st, pr, mat, ud != null, ua != null)
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
}
# The material's factors (glTF's pbrMetallicRoughness and emissiveFactor), which the shaders multiply in;
# a material that gives none keeps pr.fac null and draws as it always has. An untextured material with
# a colour, or a metal/roughness, samples white for it, so the factor is exactly what it draws.
@alloc_ok("a model being loaded: a primitive's factors, once")
function gltf_factors(render3d_st: mut Render3dState, pr: Prim, mat: Val, has_diff: bool, has_arm: bool) -> void {
let pbr = value_get(mat, "pbrMetallicRoughness")
let has_pbr = value_has(mat, "pbrMetallicRoughness") != 0
let has_base = has_pbr and value_has(pbr, "baseColorFactor") != 0
let has_m = has_pbr and value_has(pbr, "metallicFactor") != 0
let has_r = has_pbr and value_has(pbr, "roughnessFactor") != 0
let has_e = value_has(mat, "emissiveFactor") != 0
if not has_base and not has_m and not has_r and not has_e { return }
let f = floats(9)
for k in 0 .. 6 { f[k] = 1.0 }
for k in 6 .. 9 { f[k] = 0.0 }
if has_base { let b = value_get(pbr, "baseColorFactor"); for k in 0 .. min(value_count(b), 4) { f[k] = jnum(value_at(b, k)) } }
if has_r { f[4] = jnum(value_get(pbr, "roughnessFactor")) }
if has_m { f[5] = jnum(value_get(pbr, "metallicFactor")) }
if has_e { let e = value_get(mat, "emissiveFactor"); for k in 0 .. min(value_count(e), 3) { f[6 + k] = jnum(value_at(e, k)) } }
pr.fac = f
if render3d_st.gltf_pure == 0 { render3d_st.gltf_pure = tex_solid(render3d_st, 255, 255, 255, 255) }
if has_base and not has_diff { pr.diff = render3d_st.gltf_pure }
if (has_m or has_r) and not has_arm { pr.arm = render3d_st.gltf_pure }
}
# a primitive's factors handed to program p (as 1 - factor, so a program never given them reads 1)
function prim_factors(render3d_st: mut Render3dState, p: int, pr: Prim) -> void {
let f = pr.fac
if f == null {
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_fac_base_inv"), 0.0, 0.0, 0.0, 0.0)
u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_fac_mr_inv"), 0.0, 0.0)
u_f3(render3d_st, gpu_uniform(render3d_st, p, "u_fac_emis"), 0.0, 0.0, 0.0)
return
}
u_f4(render3d_st, gpu_uniform(render3d_st, p, "u_fac_base_inv"), 1.0 - f[0], 1.0 - f[1], 1.0 - f[2], 1.0 - f[3])
u_f2(render3d_st, gpu_uniform(render3d_st, p, "u_fac_mr_inv"), 1.0 - f[4], 1.0 - f[5])
u_f3(render3d_st, gpu_uniform(render3d_st, p, "u_fac_emis"), f[6], f[7], f[8])
}
# 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
}