# ============================================================================ # 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(render3d_st: mut Render3dState, 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(render3d_st, value_as_int(value_get(attrs, "JOINTS_0"))) var jsz = 1 var jtype = GPU_U8 if render3d_st.gltf_ctype == 5123 { jsz = 2; jtype = GPU_U16 } gpu_mesh_vertices(render3d_st, m, jd, render3d_st.gltf_count * render3d_st.gltf_comps * jsz, GPU_STATIC) gpu_mesh_attr(render3d_st, m, 5, render3d_st.gltf_comps, jtype, 0, 0, false) # integers, read as floats free(jd) let wd = gltf_accessor(render3d_st, value_as_int(value_get(attrs, "WEIGHTS_0"))) var wsz = 4 var wtype = GPU_F32 var norm = false if render3d_st.gltf_ctype == 5123 { wsz = 2; wtype = GPU_U16; norm = true } if render3d_st.gltf_ctype == 5121 { wsz = 1; wtype = GPU_U8; norm = true } gpu_mesh_vertices(render3d_st, m, wd, render3d_st.gltf_count * render3d_st.gltf_comps * wsz, GPU_STATIC) gpu_mesh_attr(render3d_st, m, 6, render3d_st.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(render3d_st: mut Render3dState, idx: int) -> Skin { let sk = new Skin let nodes = value_get(render3d_st.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 = "" # a copy: the document goes, strings and all, at the next load if value_has(nd, "name") != 0 { let s = value_as_str(value_get(nd, "name")); nm = s[0 .. len(s)] } 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(render3d_st.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(render3d_st, 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(render3d_st: mut Render3dState, sk: Skin, node: int, pitch: float, yaw: float, roll: float) -> void { if node < 0 { return } q_euler(render3d_st, 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(render3d_st: mut Render3dState, sk: Skin, prog: int) -> void { var loc = gpu_uniform(render3d_st, prog, "u_bones[0]") if loc < 0 { loc = gpu_uniform(render3d_st, prog, "u_bones") } u_mat4n(render3d_st, 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 } # A skin loaded with a model, let go with it (model_release): every array it owns, its joints' names # and itself. The views into gmat, inv_bind and bones are slices of those, and freeing a view would # free its owner's data again, so only the lists that hold them go (a few bytes a joint stay). function skin_free(sk: Skin) -> void { if sk == null { return } if sk.names != null { for i in 0 .. len(sk.names) { if sk.names[i] != null and len(sk.names[i]) > 0 { free(sk.names[i]) } } free(sk.names) } free(sk.par); free(sk.walk); free(sk.rest_t); free(sk.rest_r); free(sk.rest_s); free(sk.rest_g) free(sk.pose_r); free(sk.pose_t); free(sk.gmat); free(sk.joints); free(sk.inv_bind); free(sk.bones) free(sk.tmp_l); free(sk.tmp_q); free(sk.tmp_a); free(sk.tmp_b); free(sk.tmp_c); free(sk.tmp_v) if sk.gmat_v != null { free(sk.gmat_v) } if sk.inv_v != null { free(sk.inv_v) } if sk.bones_v != null { free(sk.bones_v) } free(sk) }