141 lines
6.3 KiB
Python
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()
|