98 lines
3.8 KiB
Python
98 lines
3.8 KiB
Python
|
|
"""Pure-python linear-blend skinning check: skin a GLB's verts with its current node TRS
|
||
|
|
(what Godot would render) and report max displacement of finger-weighted verts vs the
|
||
|
|
original file. usage: skin_check.py posed.glb original.glb"""
|
||
|
|
import json, struct, sys, math
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
FING = ("thumb", "index", "middle", "ring", "pinky")
|
||
|
|
|
||
|
|
def read_glb(path):
|
||
|
|
d = Path(path).read_bytes()
|
||
|
|
length = struct.unpack_from("<I", d, 8)[0]
|
||
|
|
off = 12; g = None; b = None
|
||
|
|
while off < length:
|
||
|
|
clen, ct = struct.unpack_from("<II", d, off); off += 8
|
||
|
|
if ct == 0x4E4F534A: g = json.loads(d[off:off+clen])
|
||
|
|
else: b = d[off:off+clen]
|
||
|
|
off += clen
|
||
|
|
return g, b
|
||
|
|
|
||
|
|
def acc(g, b, i):
|
||
|
|
a = g["accessors"][i]; bv = g["bufferViews"][a["bufferView"]]
|
||
|
|
nc = {"SCALAR":1,"VEC2":2,"VEC3":3,"VEC4":4,"MAT4":16}[a["type"]]
|
||
|
|
fmt = {5121:"B",5123:"H",5125:"I",5126:"f"}[a["componentType"]]
|
||
|
|
size = struct.calcsize(fmt)*nc; stride = bv.get("byteStride") or size
|
||
|
|
off = bv.get("byteOffset",0)+a.get("byteOffset",0)
|
||
|
|
return [struct.unpack_from("<%d%s"%(nc,fmt), b, off+i*stride) for i in range(a["count"])], a["componentType"]
|
||
|
|
|
||
|
|
def quat_mat(q):
|
||
|
|
x,y,z,w = q
|
||
|
|
return [[1-2*(y*y+z*z),2*(x*y-z*w),2*(x*z+y*w)],
|
||
|
|
[2*(x*y+z*w),1-2*(x*x+z*z),2*(y*z-x*w)],
|
||
|
|
[2*(x*z-y*w),2*(y*z+x*w),1-2*(x*x+y*y)]]
|
||
|
|
|
||
|
|
def node_local(nd):
|
||
|
|
t = nd.get("translation",[0,0,0]); r = nd.get("rotation",[0,0,0,1]); s = nd.get("scale",[1,1,1])
|
||
|
|
R = quat_mat(r)
|
||
|
|
M = [[R[i][j]*s[j] for j in range(3)]+[t[i]] for i in range(3)]
|
||
|
|
return M+[[0,0,0,1]]
|
||
|
|
|
||
|
|
def matmul(A,B):
|
||
|
|
return [[sum(A[i][k]*B[k][j] for k in range(4)) for j in range(4)] for i in range(4)]
|
||
|
|
|
||
|
|
def globals_(g):
|
||
|
|
loc = [node_local(nd) for nd in g["nodes"]]
|
||
|
|
parent = {}
|
||
|
|
for i,nd in enumerate(g["nodes"]):
|
||
|
|
for c in nd.get("children",[]): parent[c] = i
|
||
|
|
memo = {}
|
||
|
|
def gm(i):
|
||
|
|
if i in memo: return memo[i]
|
||
|
|
m = loc[i] if i not in parent else matmul(gm(parent[i]), loc[i])
|
||
|
|
memo[i] = m; return m
|
||
|
|
return [gm(i) for i in range(len(g["nodes"]))]
|
||
|
|
|
||
|
|
def skinned_positions(g, b, only_finger=True):
|
||
|
|
names = [nd.get("name","") for nd in g["nodes"]]
|
||
|
|
G = globals_(g)
|
||
|
|
skin = g["skins"][0]
|
||
|
|
joints = skin["joints"]
|
||
|
|
ibm, _ = acc(g, b, skin["inverseBindMatrices"])
|
||
|
|
# glTF matrices are column-major
|
||
|
|
def m16(row):
|
||
|
|
return [[row[c*4+r] for c in range(4)] for r in range(4)]
|
||
|
|
JM = [matmul(G[joints[j]], m16(ibm[j])) for j in range(len(joints))]
|
||
|
|
prim = g["meshes"][0]["primitives"][0]
|
||
|
|
P,_ = acc(g,b,prim["attributes"]["POSITION"])
|
||
|
|
J,_ = acc(g,b,prim["attributes"]["JOINTS_0"])
|
||
|
|
W,wt = acc(g,b,prim["attributes"]["WEIGHTS_0"])
|
||
|
|
wsc = 1.0 if wt==5126 else (1/255 if wt==5121 else 1/65535)
|
||
|
|
out = {}
|
||
|
|
for vi,(p,jr,wr) in enumerate(zip(P,J,W)):
|
||
|
|
if only_finger and not any(any(t in names[joints[j]].lower() for t in FING)
|
||
|
|
for j,w in zip(jr,wr) if w>0):
|
||
|
|
continue
|
||
|
|
x=y=z=0.0
|
||
|
|
for j,w in zip(jr,wr):
|
||
|
|
w*=wsc
|
||
|
|
if w<=0: continue
|
||
|
|
M=JM[j]
|
||
|
|
x+=w*(M[0][0]*p[0]+M[0][1]*p[1]+M[0][2]*p[2]+M[0][3])
|
||
|
|
y+=w*(M[1][0]*p[0]+M[1][1]*p[1]+M[1][2]*p[2]+M[1][3])
|
||
|
|
z+=w*(M[2][0]*p[0]+M[2][1]*p[1]+M[2][2]*p[2]+M[2][3])
|
||
|
|
out[vi]=(x,y,z)
|
||
|
|
return out, names, joints
|
||
|
|
|
||
|
|
posed_g, posed_b = read_glb(sys.argv[1])
|
||
|
|
orig_g, orig_b = read_glb(sys.argv[2])
|
||
|
|
a, names, joints = skinned_positions(posed_g, posed_b)
|
||
|
|
c, _, _ = skinned_positions(orig_g, orig_b)
|
||
|
|
dmax = 0; worst = None
|
||
|
|
for vi in a:
|
||
|
|
d = math.dist(a[vi], c[vi])
|
||
|
|
if d > dmax: dmax, worst = d, vi
|
||
|
|
print(f"finger-weighted verts: {len(a)}; max displacement posed-vs-original: {dmax*100:.1f} cm (vert {worst})")
|
||
|
|
import statistics
|
||
|
|
ds = sorted(math.dist(a[vi], c[vi]) for vi in a)
|
||
|
|
print(f"median: {ds[len(ds)//2]*100:.2f} cm, p95: {ds[int(len(ds)*0.95)]*100:.2f} cm")
|