The typed buffers are slices: words/floats/fixeds/doubles/pointers(n) make
zeroed, bounds-checked []int/[]float/... and the type names mean them. buffer(n)
is a []byte, with text_of, Fs.read_bytes/write_bytes and view(xs, start, n).
bytes(), indexing a raw pointer or bytes, free, resize, Memory.*, raw file calls,
data_of and C externs are refused outside unsafe { } / unsafe function, and a
project's own files may write unsafe only with --unsafe; the runtime and packages
are the platform. A slice passed to an extern goes as its data.
What the change found: Sync's atomics on a slice header, words(n) uninitialised,
input's fixed axes in ints, truetype's fixed outlines as ints, skin matrices
typed int, gl_shader's source table made from raw bytes. render3d gets safe
entry points (safe_api.ludic). Rendering is byte-identical; a frame costs the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
10 KiB
Text
228 lines
10 KiB
Text
# ============================================================================
|
|
# skin.ludic — skeletal skinning for glTF models. A Skin is the file's node
|
|
# hierarchy (rest translation / rotation / scale per node) plus the skin's joint
|
|
# list and inverse bind matrices. The game poses it by giving any node an extra
|
|
# rotation and offset IN THE MODEL'S FRAME (X right, Y up, -Z forward, whatever
|
|
# the bone's own axes happen to be), skin_pose() folds those into the hierarchy
|
|
# and produces the joint matrices, and skin.vert blends four of them per vertex.
|
|
#
|
|
# Posing in the model frame is what makes a procedural gait writable: "swing the
|
|
# thigh forward" is a rotation about the model's X axis, not about whichever axis
|
|
# the exporter gave the thigh bone. Per node the delta D is brought into the
|
|
# parent's rest frame G_p (the parent's global rest rotation): local rotation =
|
|
# (G_p^-1 D G_p) * R_rest.
|
|
# ============================================================================
|
|
|
|
const SKIN_MAX_JOINTS: int = 48
|
|
|
|
property Skin {
|
|
n_nodes: int = 0,
|
|
par: words, # parent node per node, -1 at a root
|
|
walk: words, # the nodes ordered parents-first
|
|
rest_t: floats, # 3 per node
|
|
rest_r: floats, # 4 per node (x, y, z, w)
|
|
rest_s: floats, # 3 per node
|
|
rest_g: floats, # 4 per node: the global rest rotation
|
|
names: []string,
|
|
pose_r: floats, # 4 per node: the pose rotation, model frame
|
|
pose_t: floats, # 3 per node: the pose offset, model frame (metres)
|
|
gmat: floats, # 16 per node: global matrix this pose
|
|
gmat_v: [][]float, # a view of each node's matrix in gmat
|
|
n_joints: int = 0,
|
|
joints: words, # node index per joint
|
|
inv_bind: floats, # 16 per joint
|
|
inv_v: [][]float, # a view of each joint's inverse bind matrix
|
|
bones: floats, # 16 per joint: what the vertex shader skins with
|
|
bones_v: [][]float, # a view of each joint's matrix in bones
|
|
tmp_l: floats,
|
|
tmp_q: floats,
|
|
tmp_a: floats,
|
|
tmp_b: floats,
|
|
tmp_c: floats,
|
|
tmp_v: floats
|
|
}
|
|
|
|
# a 3-vector of a JSON array (float bits), or a default
|
|
function skin_jv3(o: floats, at: int, nd: Val, key: pointer, dx: float, dy: float, dz: float) -> void {
|
|
if value_has(nd, key) == 0 { o[at] = dx; o[at + 1] = dy; o[at + 2] = dz; return }
|
|
let arr = value_get(nd, key)
|
|
for i in 0 .. 3 { o[at + i] = jnum(value_at(arr, i)) }
|
|
}
|
|
|
|
# JOINTS_0 / WEIGHTS_0 onto attributes 5 and 6 of the VAO being built (gltf_prim)
|
|
function skin_attribs(m: Mesh, attrs: Val) -> bool {
|
|
if value_has(attrs, "JOINTS_0") == 0 or value_has(attrs, "WEIGHTS_0") == 0 { return false }
|
|
let jd = gltf_accessor(value_as_int(value_get(attrs, "JOINTS_0")))
|
|
var jsz = 1
|
|
var jtype = GPU_U8
|
|
if gltf_ctype == 5123 { jsz = 2; jtype = GPU_U16 }
|
|
gpu_mesh_vertices(m, jd, gltf_count * gltf_comps * jsz, GPU_STATIC)
|
|
gpu_mesh_attr(m, 5, gltf_comps, jtype, 0, 0, false) # integers, read as floats
|
|
free(jd)
|
|
let wd = gltf_accessor(value_as_int(value_get(attrs, "WEIGHTS_0")))
|
|
var wsz = 4
|
|
var wtype = GPU_F32
|
|
var norm = false
|
|
if gltf_ctype == 5123 { wsz = 2; wtype = GPU_U16; norm = true }
|
|
if gltf_ctype == 5121 { wsz = 1; wtype = GPU_U8; norm = true }
|
|
gpu_mesh_vertices(m, wd, gltf_count * gltf_comps * wsz, GPU_STATIC)
|
|
gpu_mesh_attr(m, 6, gltf_comps, wtype, 0, 0, norm)
|
|
free(wd)
|
|
return true
|
|
}
|
|
|
|
# the skin `idx` of the document being loaded (gltf_load holds gltf_doc / gltf_bin open)
|
|
function skin_load(idx: int) -> Skin {
|
|
let sk = new Skin
|
|
let nodes = value_get(gltf_doc, "nodes")
|
|
let n = value_count(nodes)
|
|
sk.n_nodes = n
|
|
sk.par = words(n); sk.walk = words(n)
|
|
sk.rest_t = floats(n * 3); sk.rest_r = floats(n * 4); sk.rest_s = floats(n * 3); sk.rest_g = floats(n * 4)
|
|
sk.pose_r = floats(n * 4); sk.pose_t = floats(n * 3); sk.gmat = floats(n * 16)
|
|
sk.gmat_v = m4_views(sk.gmat, n)
|
|
sk.names = new []string
|
|
sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = floats(3)
|
|
for i in 0 .. n { sk.par[i] = -1 }
|
|
for i in 0 .. n {
|
|
let nd = value_at(nodes, i)
|
|
var nm: string = ""
|
|
if value_has(nd, "name") != 0 { nm = value_as_str(value_get(nd, "name")) }
|
|
push(sk.names, nm)
|
|
skin_jv3(sk.rest_t, i * 3, nd, "translation", 0.0, 0.0, 0.0)
|
|
skin_jv3(sk.rest_s, i * 3, nd, "scale", 1.0, 1.0, 1.0)
|
|
if value_has(nd, "rotation") != 0 {
|
|
let r = value_get(nd, "rotation")
|
|
for k in 0 .. 4 { sk.rest_r[i * 4 + k] = jnum(value_at(r, k)) }
|
|
} else { sk.rest_r[i * 4] = 0.0; sk.rest_r[i * 4 + 1] = 0.0; sk.rest_r[i * 4 + 2] = 0.0; sk.rest_r[i * 4 + 3] = 1.0 }
|
|
if value_has(nd, "matrix") != 0 { print(`skin: node {nm} uses a matrix transform (unsupported, treated as identity)`) }
|
|
if value_has(nd, "children") != 0 {
|
|
let ch = value_get(nd, "children")
|
|
for k in 0 .. value_count(ch) { sk.par[value_as_int(value_at(ch, k))] = i }
|
|
}
|
|
}
|
|
# parents first: order the nodes by depth
|
|
let depth = words(n)
|
|
for i in 0 .. n {
|
|
var d = 0
|
|
var p = sk.par[i]
|
|
while p >= 0 and d < n { d += 1; p = sk.par[p] }
|
|
depth[i] = d
|
|
}
|
|
var k = 0
|
|
for d in 0 .. n { for i in 0 .. n { if depth[i] == d { sk.walk[k] = i; k += 1 } } }
|
|
free(depth)
|
|
# the global rest rotation of every node
|
|
for w in 0 .. n {
|
|
let i = sk.walk[w]
|
|
let p = sk.par[i]
|
|
q_load(sk.tmp_a, sk.rest_r, i)
|
|
if p >= 0 { q_load(sk.tmp_b, sk.rest_g, p); q_mul(sk.tmp_q, sk.tmp_b, sk.tmp_a); q_store(sk.rest_g, i, sk.tmp_q) }
|
|
else { q_store(sk.rest_g, i, sk.tmp_a) }
|
|
}
|
|
# the skin: joints and inverse bind matrices
|
|
let skv = value_at(value_get(gltf_doc, "skins"), idx)
|
|
let jl = value_get(skv, "joints")
|
|
var nj = value_count(jl)
|
|
if nj > SKIN_MAX_JOINTS { print(`skin: {nj} joints, only the first {SKIN_MAX_JOINTS} are used`); nj = SKIN_MAX_JOINTS }
|
|
sk.n_joints = nj
|
|
sk.joints = words(nj)
|
|
sk.inv_bind = floats(nj * 16)
|
|
sk.inv_v = m4_views(sk.inv_bind, nj)
|
|
sk.bones = floats(nj * 16)
|
|
sk.bones_v = m4_views(sk.bones, nj)
|
|
for j in 0 .. nj { sk.joints[j] = value_as_int(value_at(jl, j)) }
|
|
if value_has(skv, "inverseBindMatrices") != 0 {
|
|
let ib = gltf_accessor(value_as_int(value_get(skv, "inverseBindMatrices")))
|
|
for i in 0 .. nj * 16 { sk.inv_bind[i] = float_from_bits(mem_get_f32_bits(ib, i)) }
|
|
free(ib)
|
|
} else {
|
|
for j in 0 .. nj { m4_identity(sk.inv_v[j]) }
|
|
}
|
|
skin_reset(sk)
|
|
skin_pose(sk)
|
|
print(`skin: {nj} joints over {n} nodes`)
|
|
return sk
|
|
}
|
|
|
|
function skin_find(sk: Skin, name: string) -> int {
|
|
for i in 0 .. sk.n_nodes { if sk.names[i] == name { return i } }
|
|
print(`skin: no node {name}`)
|
|
return -1
|
|
}
|
|
function skin_mat(sk: Skin, node: int) -> floats { return sk.gmat_v[node] }
|
|
|
|
# back to the rest pose
|
|
function skin_reset(sk: Skin) -> void {
|
|
for i in 0 .. sk.n_nodes {
|
|
sk.pose_r[i * 4] = 0.0; sk.pose_r[i * 4 + 1] = 0.0; sk.pose_r[i * 4 + 2] = 0.0; sk.pose_r[i * 4 + 3] = 1.0
|
|
sk.pose_t[i * 3] = 0.0; sk.pose_t[i * 3 + 1] = 0.0; sk.pose_t[i * 3 + 2] = 0.0
|
|
}
|
|
}
|
|
# a node's pose rotation in the model frame: pitch about X, yaw about Y, roll about Z (radians)
|
|
function skin_set_rot(sk: Skin, node: int, pitch: float, yaw: float, roll: float) -> void {
|
|
if node < 0 { return }
|
|
q_euler(sk.tmp_q, pitch, yaw, roll)
|
|
q_store(sk.pose_r, node, sk.tmp_q)
|
|
}
|
|
function skin_set_quat(sk: Skin, node: int, q: floats) -> void { if node >= 0 { q_store(sk.pose_r, node, q) } }
|
|
# a node's pose offset in the model frame (metres)
|
|
function skin_set_offset(sk: Skin, node: int, x: float, y: float, z: float) -> void {
|
|
if node < 0 { return }
|
|
sk.pose_t[node * 3] = x; sk.pose_t[node * 3 + 1] = y; sk.pose_t[node * 3 + 2] = z
|
|
}
|
|
|
|
# fold the pose into the hierarchy: global matrices, then the joint matrices
|
|
function skin_pose(sk: Skin) -> void {
|
|
for w in 0 .. sk.n_nodes {
|
|
let i = sk.walk[w]
|
|
let p = sk.par[i]
|
|
q_load(sk.tmp_a, sk.pose_r, i) # D, model frame
|
|
var tx = sk.rest_t[i * 3]; var ty = sk.rest_t[i * 3 + 1]; var tz = sk.rest_t[i * 3 + 2]
|
|
let ox = sk.pose_t[i * 3]; let oy = sk.pose_t[i * 3 + 1]; let oz = sk.pose_t[i * 3 + 2]
|
|
if p >= 0 {
|
|
q_load(sk.tmp_b, sk.rest_g, p) # G_p
|
|
q_conj(sk.tmp_c, sk.tmp_b) # G_p^-1
|
|
q_mul(sk.tmp_q, sk.tmp_c, sk.tmp_a)
|
|
q_mul(sk.tmp_a, sk.tmp_q, sk.tmp_b) # G_p^-1 D G_p
|
|
if ox != 0.0 or oy != 0.0 or oz != 0.0 {
|
|
v3_set(sk.tmp_v, ox, oy, oz)
|
|
q_rotate(sk.tmp_v, sk.tmp_c, sk.tmp_v) # the offset in the parent's frame
|
|
tx = tx + sk.tmp_v[0]; ty = ty + sk.tmp_v[1]; tz = tz + sk.tmp_v[2]
|
|
}
|
|
} else { tx = tx + ox; ty = ty + oy; tz = tz + oz }
|
|
q_load(sk.tmp_b, sk.rest_r, i)
|
|
q_mul(sk.tmp_q, sk.tmp_a, sk.tmp_b) # local rotation
|
|
m4_trs_q(sk.tmp_l, tx, ty, tz, sk.tmp_q, sk.rest_s[i * 3], sk.rest_s[i * 3 + 1], sk.rest_s[i * 3 + 2])
|
|
if p >= 0 { m4_mul(skin_mat(sk, i), skin_mat(sk, p), sk.tmp_l) }
|
|
else { m4_copy(skin_mat(sk, i), sk.tmp_l) }
|
|
}
|
|
for j in 0 .. sk.n_joints {
|
|
m4_mul(sk.bones_v[j], skin_mat(sk, sk.joints[j]), sk.inv_v[j])
|
|
}
|
|
}
|
|
|
|
# the joint matrices onto a program's u_bones[]
|
|
function skin_bind(sk: Skin, prog: int) -> void {
|
|
var loc = gpu_uniform(prog, "u_bones[0]")
|
|
if loc < 0 { loc = gpu_uniform(prog, "u_bones") }
|
|
u_mat4n(loc, sk.n_joints, sk.bones)
|
|
}
|
|
|
|
# the same skeleton posed on its own: shares the rest data, owns the pose and the matrices
|
|
function skin_clone(src: Skin) -> Skin {
|
|
let sk = new Skin
|
|
sk.n_nodes = src.n_nodes; sk.par = src.par; sk.walk = src.walk
|
|
sk.rest_t = src.rest_t; sk.rest_r = src.rest_r; sk.rest_s = src.rest_s; sk.rest_g = src.rest_g
|
|
sk.names = src.names
|
|
sk.n_joints = src.n_joints; sk.joints = src.joints; sk.inv_bind = src.inv_bind; sk.inv_v = src.inv_v
|
|
let n = src.n_nodes
|
|
sk.pose_r = floats(n * 4); sk.pose_t = floats(n * 3); sk.gmat = floats(n * 16)
|
|
sk.gmat_v = m4_views(sk.gmat, n)
|
|
sk.bones = floats(src.n_joints * 16)
|
|
sk.bones_v = m4_views(sk.bones, src.n_joints)
|
|
sk.tmp_l = m4_new(); sk.tmp_q = q_new(); sk.tmp_a = q_new(); sk.tmp_b = q_new(); sk.tmp_c = q_new(); sk.tmp_v = floats(3)
|
|
skin_reset(sk)
|
|
skin_pose(sk)
|
|
return sk
|
|
}
|