"""Validate the portable skin/animation asset, independent of Godot's wrapper.""" from pathlib import Path import json,math,sys sys.path.insert(0,str(Path(__file__).resolve().parent)) from verify_phase4_glb import GLB,topology p=Path(__file__).resolve().parents[1];g=GLB(p/'assets/characters/ringborke.glb');d=g.doc checks=[] def check(v,msg): assert v,msg checks.append(msg) check(len(d['skins'])==1,'one portable skin') check(len(d['skins'][0]['joints'])==27,'27 distinct joint bindings') expected={'idle','start','walk','stop','attack_a','attack_b','hit','ability','death'} check({a['name'] for a in d['animations']}==expected,'nine complete clips') weighted=0;total=0;max_error=0 for m in d['meshes']: for q in m['primitives']: attr=q['attributes'] vertices=g.accessor(attr['POSITION']);indices=[x[0] for x in g.accessor(q['indices'])] t=topology(vertices,[indices[i:i+3] for i in range(0,len(indices),3)]) check(t['closed'] and t['signed_volume']>0 and t['degenerate_triangles']==0,'closed outward geometry '+d['materials'][q['material']]['name']) check(all(x in attr for x in ('JOINTS_0','WEIGHTS_0','POSITION','NORMAL','TEXCOORD_0')),'skin, normals and UVs') joints=g.accessor(attr['JOINTS_0']);weights=g.accessor(attr['WEIGHTS_0']);normals=g.accessor(attr['NORMAL']) for js,ws,n in zip(joints,weights,normals): assert all(0<=j<27 for j in js) assert all(math.isfinite(w) and 0<=w<=1 for w in ws) max_error=max(max_error,abs(sum(ws)-1));weighted+=sum(w>.001 for w in ws)>1;total+=1 assert all(math.isfinite(v) for v in n) check(max_error<.0001,'all vertex weights normalized') check(weighted/total>.15,'flexible tissue has blended joints') for a in d['animations']: check(len(a['channels'])>5,a['name']+' animates multiple joints') for samp in a['samplers']: times=[v[0] for v in g.accessor(samp['input'])];assert times==sorted(times) and times[0]>=0 assert all(math.isfinite(x) for row in g.accessor(samp['output']) for x in row) report={'checks':len(checks),'failures':[],'vertices':total,'blended_vertices':weighted,'blended_percent':round(weighted/total*100,2),'max_weight_sum_error':max_error,'clips':sorted(expected),'bones':27} (p/'docs/phase11-skin-audit.json').write_text(json.dumps(report,indent=2)+'\n');print(report)