#!/usr/bin/env python3 """Offline cockpit-shell renderer (warp-style iteration harness). Loads a *_COP.BGF via bgf_parse and rasterizes it from a cockpit eye with a z-buffer over a placeholder world (sky+ground), so we can iterate the frame/ window reconstruction against the cabinet reference WITHOUT a full game build. Hypothesis under test: the PUNCH patch is the windshield -> DROP it (transparent); keep the non-punch frame patches (dark 'softer'-ramp gray). Env knobs (iterate the camera): EX EY EZ EZOFF TZ FOV ; KEEP_PUNCH=1 keeps it. """ import sys, os, math import numpy as np from PIL import Image from bgf_parse import parse W, H = 480, 360 def envf(k,d): v=os.getenv(k); return float(v) if v not in (None,"") else d def render(path, out): P = parse(path) tris=[] for pt in P: V=pt["verts"] for (a,b,c) in pt["faces"]: if max(a,b,c)>=len(V): continue tris.append((np.array(V[a][:3]),np.array(V[b][:3]),np.array(V[c][:3]),pt["punch"])) allv=np.array([v[:3] for pt in P for v in pt["verts"]]) lo,hi=allv.min(0),allv.max(0); cen=(lo+hi)/2 drop_punch = os.getenv("KEEP_PUNCH") is None eye=np.array([envf("EX",0.0), envf("EY",float(cen[1])), envf("EZ",float(hi[2])+envf("EZOFF",0.4))]) target=np.array([eye[0], eye[1], envf("TZ",float(lo[2])-1.0)]) up=np.array([0.0,1.0,0.0]) f=target-eye; f/=np.linalg.norm(f)+1e-9 r=np.cross(f,up); r/=np.linalg.norm(r)+1e-9 u=np.cross(r,f) fov=math.radians(envf("FOV",75.0)); tanf=math.tan(fov/2); aspect=W/H img=np.zeros((H,W,3),np.float32) for y in range(H): t=y/H img[y,:]= (np.array([0.55,0.52,0.68])+(0.30*(0.5-t))*np.array([0.7,0.7,0.6])) if t<0.5 \ else (np.array([0.72,0.68,0.78])-(0.15*(t-0.5))*np.array([0.5,0.5,0.4])) zb=np.full((H,W),1e9,np.float32) frame=np.array([0.28,0.24,0.34]) def proj(p): d=p-eye; cx=np.dot(d,r); cy=np.dot(d,u); cz=np.dot(d,f) if cz<=0.01: return None return np.array([(cx/(cz*tanf*aspect))*0.5*W+0.5*W, -(cy/(cz*tanf))*0.5*H+0.5*H, cz]) drawn=0 for (p0,p1,p2,punch) in tris: if drop_punch and punch: continue s=[proj(p0),proj(p1),proj(p2)] if any(x is None for x in s): continue s0,s1,s2=s col=frame*(1.0-0.05*min(1.0,(s0[2]+s1[2]+s2[2])/12.0)) raster(img,zb,s0,s1,s2,col); drawn+=1 Image.fromarray((np.clip(img,0,1)*255).astype(np.uint8)).save(out) print(f"{os.path.basename(path)} -> {out} eye={eye.round(2)} tgt={target.round(2)} " f"fov={math.degrees(fov):.0f} drop_punch={drop_punch} drawn={drawn}/{len(tris)}") def raster(img,zb,s0,s1,s2,col): xs=[s0[0],s1[0],s2[0]]; ys=[s0[1],s1[1],s2[1]] x0=max(0,int(min(xs))); x1=min(W-1,int(max(xs))); y0=max(0,int(min(ys))); y1=min(H-1,int(max(ys))) if x11 else "content/VIDEO/GEO/MAX_COP.BGF" out=sys.argv[2] if len(sys.argv)>2 else "scratchpad/cop_offline.png" render(path,out)