Files
animation/tools/skin_displacement_check.py
T

98 lines
3.8 KiB
Python
Raw Normal View History

"""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")