KAZIK/tools/generators/pack_bowling.py

141 lines
6.3 KiB
Python

"""Offline bowling art, furniture and collision data. Runtime is native MASM."""
from pathlib import Path
import json
import struct
import cv2
import numpy as np
from PIL import Image
ROOT = Path(__file__).resolve().parents[2]
ART = ROOT/'assets/sprites/bowling'
OUT = ROOT/'assets/animations/bowling'
def pack(name, frames):
w,h = frames[0].size
data = b''.join(f.tobytes('raw','BGRA') for f in frames)
(OUT/name).write_bytes(struct.pack('<4s4I',b'ANM1',w,h,len(frames),len(data))+data)
def cells(name, columns, rows, seated=False):
source = Image.open(ART/name).convert('RGBA')
frames = []
for y in range(rows):
for x in range(columns):
cell = source.crop((round(x*source.width/columns),round(y*source.height/rows),
round((x+1)*source.width/columns),round((y+1)*source.height/rows)))
a = np.asarray(cell).copy()
a[:,:,3] = np.where(a[:,:,3] >= 128,255,0)
# Keep the actor, not detached released balls or neighbour-cell fragments.
if not seated:
count,labels,stats,_ = cv2.connectedComponentsWithStats((a[:,:,3]>0).astype(np.uint8))
keep = 1+int(np.argmax(stats[1:,cv2.CC_STAT_AREA]))
a[:,:,3] = np.where(labels == keep,255,0)
cell = Image.fromarray(a)
frames.append(cell)
scale = min(240/max(f.getbbox()[3]-f.getbbox()[1] for f in frames),
248/max(f.width for f in frames))
result = []
for cell in frames:
box = cell.getbbox()
resized = cell.resize((round(cell.width*scale),round(cell.height*scale)),Image.Resampling.LANCZOS)
frame = Image.new('RGBA',(256,256))
frame.paste(resized,((256-resized.width)//2,250-round(box[3]*scale)))
result.append(frame)
return result
def tween(a,b,t):
a,b = np.asarray(a),np.asarray(b)
ga,gb = cv2.cvtColor(a,cv2.COLOR_RGBA2GRAY),cv2.cvtColor(b,cv2.COLOR_RGBA2GRAY)
fa = cv2.calcOpticalFlowFarneback(ga,gb,None,.5,4,25,4,5,1.2,0)
fb = cv2.calcOpticalFlowFarneback(gb,ga,None,.5,4,25,4,5,1.2,0)
xy = np.mgrid[:256,:256][::-1].transpose(1,2,0).astype(np.float32)
wa = cv2.remap(a,xy-fa*t,None,cv2.INTER_LINEAR,borderMode=cv2.BORDER_CONSTANT)
wb = cv2.remap(b,xy-fb*(1-t),None,cv2.INTER_LINEAR,borderMode=cv2.BORDER_CONSTANT)
return Image.fromarray(np.clip(wa*(1-t)+wb*t,0,255).astype(np.uint8))
def main():
OUT.mkdir(parents=True,exist_ok=True)
source = Image.open(ART/'materials-v1.png').convert('RGBA')
materials = [source.crop((round(x*source.width/4),round(y*source.height/3),
round((x+1)*source.width/4),round((y+1)*source.height/3)))
.resize((256,256),Image.Resampling.LANCZOS) for y in range(3) for x in range(4)]
materials += [Image.new('RGBA',(256,256),(40,210,208,255)),
Image.new('RGBA',(256,256),(229,79,65,255))]
pack('materials-v1.bin',materials)
man,woman = cells('bowler-man-v1.png',4,4),cells('bowler-woman-v1.png',4,2)
actors = []
for poses in (man,woman):
motion = []
for a,b in zip(poses[:7],poses[1:8]):
motion += [a,tween(a,b,1/3),tween(a,b,2/3)]
motion += [poses[7]]*3
extras = poses[8:16] if len(poses) == 16 else [poses[0]]*4+[poses[7]]*4
actors += motion+extras
pack('bowlers-v1.bin',actors)
pack('seated-v1.bin',cells('seated-v1.png',4,2,True))
preview = Image.new('RGBA',(8*256,2*256))
for i,f in enumerate(actors[:24:3]+actors[32:56:3]):
preview.paste(f,((i%8)*256,(i//8)*256))
preview.save(ART/'bowling-preview.png')
props,collisions,seats = [],[],[]
def box(a,b,c,d,e,f,m,solid=False):
props.append((a,b,c,d,e,f,m))
if solid:
collisions.append((a-.17,b+.17,c-.17,d+.17))
def sofa(x,y,w=2.8):
box(x-w/2,x+w/2,y-.45,y+.45,.08,.3,236,True)
box(x-w/2+.1,x+w/2-.1,y-.43,y+.36,.3,.4,235)
box(x-w/2,x+w/2,y+.3,y+.52,.3,.85,235)
for dx in (-w/2,w/2-.16):
box(x+dx,x+dx+.16,y-.45,y+.45,.3,.56,240)
seats.append((x,y-.1,384))
for pair in range(4):
x = 3.95+3.8*pair
sofa(x,28.1)
box(x-.6,x+.6,26.7,27.25,.38,.46,237,True)
box(x-.12,x+.12,26.85,27.05,0,.4,238)
box(x-.38,x+.38,24.8,26,.05,.62,236,True)
box(x-.33,x+.33,24.85,25.8,.62,.72,238)
for x in (22,27):
for y in (12,18):
box(x-1,x+1,y-.75,y+.75,.48,.55,237,True)
box(x-.1,x+.1,y-.1,y+.1,0,.49,238)
sofa(x,y+1.55,2.2)
box(24,30,4.5,5.5,0,1.05,236,True)
box(23.9,30.1,4.4,5.6,1.05,1.16,237)
box(24,30,1.15,1.45,0,3.1,236,True)
for z in (.9,1.7,2.5):
box(24,30,1.15,1.8,z,z+.08,238)
for x in (25,27,29):
box(x-.08,x+.08,6.7,6.86,0,.8,238,True)
box(x-.3,x+.3,6.5,7.1,.8,.9,240)
seats.append((x,6.8,384))
box(19.1,19.3,1,24.8,0,.9,236,True)
box(19.05,19.35,1,24.8,.9,1,238)
# Floor-standing speakers and the pinsetter housings have real side/back faces.
for x in (1.45,18.5):
box(x-.3,x+.3,2,2.7,0,2.8,233)
data = ['; Generated by pack_bowling.py.','BOWL_PROP_COUNT equ '+str(len(props)),'.data',
'bwProps3D LABEL DWORD']
for p in props:
data.append(' dd '+','.join(str(round(v*65536)) for v in p[:6])+','+str(p[6]))
data += ['BOWL_COLLISION_COUNT equ '+str(len(collisions)),'bwCollision3D LABEL DWORD']
data += [' dd '+','.join(str(round(v*65536)) for v in p) for p in collisions]
data += ['BOWL_SEAT_COUNT equ '+str(len(seats)),'bwSeats3D LABEL DWORD']
data += [' dd '+','.join([str(round(p[0]*65536)),str(round(p[1]*65536)),str(p[2])]) for p in seats]
room = [[232 if x in (0,31) or y in (0,31) else 0 for x in range(32)] for y in range(32)]
data += ['bwMap3D LABEL BYTE']
data += [' db '+','.join(map(str,row)) for row in room]
data += ['.code']
(ROOT/'src_graphics/bowling_layout.inc').write_text('\n'.join(data)+'\n',encoding='ascii')
(ART/'packed-v1.json').write_text(json.dumps({'materials':[256,256,14],
'bowlers':[256,256,64],'seated':[256,256,8],'lanes':8,'active_lanes':[0,3,6],
'room':[30,30,6],'props':len(props),'collisions':collisions,'seats':seats},indent=2)+'\n')
print('Packed bowling:',len(props),'solid prop parts, 64 whole-body motion frames, 8 seated poses')
if __name__ == '__main__':
main()