# ============================================================================ # 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: words, # 3 per node rest_r: words, # 4 per node (x, y, z, w) rest_s: words, # 3 per node rest_g: words, # 4 per node: the global rest rotation names: []string, pose_r: words, # 4 per node: the pose rotation, model frame pose_t: words, # 3 per node: the pose offset, model frame (metres) gmat: words, # 16 per node: global matrix this pose n_joints: int = 0, joints: words, # node index per joint inv_bind: words, # 16 per joint bones: words, # 16 per joint: what the vertex shader skins with tmp_l: words, tmp_q: words, tmp_a: words, tmp_b: words, tmp_c: words, tmp_v: words } # a 3-vector of a JSON array (float bits), or a default function skin_jv3(o: words, at: int, nd: Val, key: pointer, dx: int, dy: int, dz: int) -> 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 = GL_UNSIGNED_BYTE if gltf_ctype == 5123 { jsz = 2; jtype = GL_UNSIGNED_SHORT } let jb = gl_buffer() gl_bind_buffer(GL_ARRAY_BUFFER, jb) gl_buffer_data(GL_ARRAY_BUFFER, gltf_count * gltf_comps * jsz, jd, GL_STATIC_DRAW) gl_enable_vertex_attrib_array(5) gl_vertex_attrib_pointer(5, gltf_comps, jtype, 0, 0, null) # integers, read as floats free(jd) let wd = gltf_accessor(value_as_int(value_get(attrs, "WEIGHTS_0"))) var wsz = 4 var wtype = GL_FLOAT var norm = 0 if gltf_ctype == 5123 { wsz = 2; wtype = GL_UNSIGNED_SHORT; norm = 1 } if gltf_ctype == 5121 { wsz = 1; wtype = GL_UNSIGNED_BYTE; norm = 1 } let wb = gl_buffer() gl_bind_buffer(GL_ARRAY_BUFFER, wb) gl_buffer_data(GL_ARRAY_BUFFER, gltf_count * gltf_comps * wsz, wd, GL_STATIC_DRAW) gl_enable_vertex_attrib_array(6) gl_vertex_attrib_pointer(6, gltf_comps, wtype, norm, 0, null) 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 = words(n * 3); sk.rest_r = words(n * 4); sk.rest_s = words(n * 3); sk.rest_g = words(n * 4) sk.pose_r = words(n * 4); sk.pose_t = words(n * 3); sk.gmat = words(n * 16) 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 = words(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", F_ZERO, F_ZERO, F_ZERO) skin_jv3(sk.rest_s, i * 3, nd, "scale", F_ONE, F_ONE, F_ONE) 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] = F_ZERO; sk.rest_r[i * 4 + 1] = F_ZERO; sk.rest_r[i * 4 + 2] = F_ZERO; sk.rest_r[i * 4 + 3] = F_ONE } 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 = words(nj * 16) sk.bones = words(nj * 16) 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] = mem_get_f32_bits(ib, i) } free(ib) } else { for j in 0 .. nj { m4_identity(mem_off(sk.inv_bind, j * 64)) } } 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) -> words { return mem_off(sk.gmat, node * 64) } # back to the rest pose function skin_reset(sk: Skin) -> void { for i in 0 .. sk.n_nodes { sk.pose_r[i * 4] = F_ZERO; sk.pose_r[i * 4 + 1] = F_ZERO; sk.pose_r[i * 4 + 2] = F_ZERO; sk.pose_r[i * 4 + 3] = F_ONE sk.pose_t[i * 3] = F_ZERO; sk.pose_t[i * 3 + 1] = F_ZERO; sk.pose_t[i * 3 + 2] = F_ZERO } } # 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: int, yaw: int, roll: int) -> 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: words) -> 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: int, y: int, z: int) -> 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 or oy != 0 or oz != 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 = f_add(tx, sk.tmp_v[0]); ty = f_add(ty, sk.tmp_v[1]); tz = f_add(tz, sk.tmp_v[2]) } } else { tx = f_add(tx, ox); ty = f_add(ty, oy); tz = f_add(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(mem_off(sk.bones, j * 64), skin_mat(sk, sk.joints[j]), mem_off(sk.inv_bind, j * 64)) } } # the joint matrices onto a program's u_bones[] function skin_bind(sk: Skin, prog: int) -> void { var loc = gl_uniform(prog, "u_bones[0]") if loc < 0 { loc = gl_uniform(prog, "u_bones") } gl_uniform_matrix4fv(loc, sk.n_joints, 0, 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 let n = src.n_nodes sk.pose_r = words(n * 4); sk.pose_t = words(n * 3); sk.gmat = words(n * 16) sk.bones = words(src.n_joints * 16) 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 = words(3) skin_reset(sk) skin_pose(sk) return sk }