"""Validate the portable Funkenlanze skin/animation asset, independent of Godot.""" 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/funkenlanze.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']) == 26, '26 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)]) name = d['materials'][q['material']]['name'] check(t['closed'] and t['signed_volume'] > 0 and t['degenerate_triangles'] == 0, 'closed outward geometry ' + 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 < 26 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') # Mineral plates stay single-weighted; continuous limb/torso cores carry the blend zones. check(weighted / total > .08, 'joint zones have blended vertices') 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': 26, } (p / 'docs/phase18-skin-audit.json').write_text(json.dumps(report, indent=2) + '\n') print(report)