saumwacht-godot/tools/verify_ringborke.py
Martin Stickelbroeck c2b447610c Baseline: Saumwacht 0.15.0 (Phase 20A) vor dem Umbau zu Champions of Luneraria
Unveränderter Ausgangsstand inklusive Masterprompt und Konzeptbildern
unter assets/. .godot/ und docs/screenshots/ bleiben laut .gitignore
unversioniert; letztere sind unter ../backups gesichert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:00:36 +02:00

37 lines
2.2 KiB
Python

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