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>
63 lines
2.7 KiB
Python
63 lines
2.7 KiB
Python
"""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)
|