packages feed

moonlight-planar-1.1.0.0: docs/knight-rig/package.py

"""Encode existing native renders and verify the exported glTF payload."""
from __future__ import annotations

from itertools import chain
from pathlib import Path
import json
import math
import struct
import sys
from PIL import Image


def animation(folder: Path,destination: Path) -> dict:
    frames=tuple(map(lambda p:Image.open(p).convert('RGB'),sorted(folder.glob('frame-*.png'))))
    frames[0].save(destination,save_all=True,append_images=frames[1:],duration=83,loop=0,quality=90,method=6)
    return {'file':destination.name,'frames':len(frames),'width':frames[0].width,'height':frames[0].height,'fps':12}


def glb_report(path: Path) -> dict:
    payload=path.read_bytes()
    n,kind=struct.unpack_from('<II',payload,12)
    document=json.loads(payload[20:20+n])
    binary=payload[28+n:]
    component={5126:('f',4),5123:('H',2),5121:('B',1),5125:('I',4)}
    arity={'SCALAR':1,'VEC2':2,'VEC3':3,'VEC4':4,'MAT4':16}
    def accessor(index: int) -> tuple[tuple[float|int,...],...]:
        a=document['accessors'][index]; view=document['bufferViews'][a['bufferView']]
        fmt,size=component[a['componentType']]; count=arity[a['type']]
        offset=view.get('byteOffset',0)+a.get('byteOffset',0)
        stride=view.get('byteStride',size*count)
        return tuple(map(lambda i:struct.unpack_from('<'+fmt*count,binary,offset+stride*i),range(a['count'])))
    weights=tuple(chain.from_iterable(accessor(p['attributes']['WEIGHTS_0']) for mesh in document['meshes'] for p in mesh['primitives']))
    clips=tuple({'name':a['name'],'duration_seconds':max(accessor(s['input'])[-1][0] for s in a['samplers']),
                 'channels':len(a['channels'])} for a in document.get('animations',[]))
    report={'meshes':len(document['meshes']),'skins':len(document.get('skins',[])),
            'joints':tuple(document['nodes'][i]['name'] for i in document['skins'][0]['joints']),
            'animations':clips,'max_weight_sum_error':max(abs(sum(w)-1) for w in weights),
            'all_weights_finite':all(map(math.isfinite,chain.from_iterable(weights)))}
    return {**report,'passed':report['skins']==1 and report['meshes']==18 and len(clips)==2
            and report['max_weight_sum_error']<1e-6 and report['all_weights_finite']}


def contact_sheet(root: Path) -> None:
    rows=tuple(tuple(Image.open(root/name/f'frame-{i:04d}.png').convert('RGB').resize((260,312)) for i in indices)
               for name,indices in (('idle',(1,19,37,55)),('walk',(1,9,17,25))))
    canvas=Image.new('RGB',(1040,624),'#060e19')
    tuple(map(lambda row:tuple(map(lambda cell:canvas.paste(cell[1],(cell[0]*260,row[0]*312)),enumerate(row[1]))),enumerate(rows)))
    canvas.save(root/'motion-contact-sheet.jpg',quality=92)


def main(root: Path) -> None:
    report=glb_report(root/'knight-rig.glb')
    (root/'export-checks.json').write_text(json.dumps(report,indent=2)+'\n')
    if not report['passed']:
        raise SystemExit('Exported skeleton/clip/weight checks failed')
    previews=tuple(animation(root/clip,root/f'knight-{clip}.webp') for clip in ('idle','walk'))
    contact_sheet(root)
    (root/'previews.json').write_text(json.dumps(previews,indent=2)+'\n')
    print(json.dumps({'export':report,'previews':previews},indent=2))


if __name__=='__main__':
    main(Path(sys.argv[1]))