#!/usr/bin/env python3 """Render every mech's *_COP canopy from its AUTHORED eye position (SKL [jointeye] translation, same torso-local frame as the _cop mesh), single-sided with per-face INWARD winding -- the port's exact treatment -- with faces coloured by patch + punch flag so we can see WHICH geometry blocks each mech's view. frame colours: punch patches = warm (red/orange/yellow), non-punch = cool (blue/teal) world (holes) = lavender background Env: FWD=+z|-z (look direction, default -z), CULL=0 disables single-siding, DROP_PUNCH=1 drops punch patches entirely. """ import re, os, math, sys import numpy as np from PIL import Image, ImageDraw import importlib.util spec = importlib.util.spec_from_file_location("bgf_parse", os.path.join(os.path.dirname(__file__), "bgf_parse.py")) bgf = importlib.util.module_from_spec(spec); spec.loader.exec_module(bgf) ROOT = r"C:\git\bt411" MECHS = { # model: (outside skl prefix, X-variant cop prefix) "madcat": ("MAD", "MAX"), "bhk1": ("BLH", "BLX"), "thor": ("THR", "THX"), "owens": ("OWN", "OWX"), "sunder": ("SND", "SNX"), "loki": ("LOK", "LOX"), "vulture": ("VUL", "VUX"), "avatar": ("AVA", "AVX"), } W, H = 300, 225 PUNCH_COLS = [(200,60,50),(230,140,40),(230,210,60)] # warm = punch FRAME_COLS = [(70,90,200),(60,170,180),(120,80,200)] # cool = non-punch def jointeye(skl_path): t = open(skl_path, encoding="latin1").read() m = re.search(r"\[jointeye\]", t) if not m: return None blk = t[m.start():m.start()+400] g = lambda k: float((re.search(rf"{k}=([-\d.e]+)", blk) or [None,"0"])[1]) return (g("tranx"), g("trany"), g("tranz")) def render(model, out_path, fwd_sign=-1.0, cull=True, drop_punch=False): pfx, xpfx = MECHS[model] eye = jointeye(os.path.join(ROOT, "content", "VIDEO", pfx + ".SKL")) P = bgf.parse(os.path.join(ROOT, "content", "VIDEO", "GEO", xpfx + "_COP.BGF")) allv = np.array([v[:3] for pt in P for v in pt["verts"]]) cen = allv.mean(0) e = np.array(eye, float) f = np.array([0.0, 0.0, fwd_sign]); u = np.array([0.0, 1.0, 0.0]) r = np.cross(u, f); r /= np.linalg.norm(r); u2 = np.cross(f, r) tanf = math.tan(math.radians(75)/2); aspect = W/H img = np.zeros((H, W, 3), np.uint8) for y in range(H): # lavender world bg t = y/H img[y,:] = (150,148,205) if t < 0.5 else (185,178,200) zb = np.full((H, W), 1e9) npi = nfi = 0 for pi, pt in enumerate(P): V = pt["verts"] if pt["punch"]: col = PUNCH_COLS[npi % 3]; npi += 1 if drop_punch: continue else: col = FRAME_COLS[nfi % 3]; nfi += 1 for (a, b, c) in pt["faces"]: if max(a, b, c) >= len(V): continue tri = np.array([V[a][:3], V[b][:3], V[c][:3]]) n = np.cross(tri[1]-tri[0], tri[2]-tri[0]); nl = np.linalg.norm(n) if nl < 1e-9: continue n /= nl # per-face INWARD orientation (the loader's rule): normal toward mesh centroid fc = tri.mean(0) inward = n if np.dot(n, cen - fc) >= 0 else -n if cull and np.dot(inward, e - fc) <= 0: # single-sided: cull faces facing away from the eye continue s = []; ok = True for p in tri: d = p - e; cx = np.dot(d, r); cy = np.dot(d, u2); cz = np.dot(d, f) if cz <= 0.02: ok = False; break s.append((cx/(cz*tanf*aspect)*0.5*W + 0.5*W, -cy/(cz*tanf)*0.5*H + 0.5*H, cz)) if not ok: continue shade = 0.55 + 0.45*abs(np.dot(inward, f)) cc = tuple(min(255, int(ch*shade)) for ch in col) raster(img, zb, s, cc) im = Image.new("RGB", (W, H+22), (18,18,22)); im.paste(Image.fromarray(img), (0,22)) d = ImageDraw.Draw(im) d.text((4,5), f"{model} eye=({eye[0]:+.2f},{eye[1]:.2f},{eye[2]:+.2f}) fwd={'-Z' if fwd_sign<0 else '+Z'}", fill=(235,235,240)) im.save(out_path) return eye def raster(img, zb, s, col): (ax,ay,az),(bx,by,bz),(cx,cy,cz) = s x0=max(0,int(min(ax,bx,cx))); x1=min(W-1,int(max(ax,bx,cx))) y0=max(0,int(min(ay,by,cy))); y1=min(H-1,int(max(ay,by,cy))) den=(by-cy)*(ax-cx)+(cx-bx)*(ay-cy) if abs(den)<1e-6 or x1 1 else "eyeview" tiles = [] for m in MECHS: p = os.path.join(ROOT, "scratchpad", f"ev_{m}.png") render(m, p, fwd, cull, dropp) tiles.append(np.array(Image.open(p))) g = np.full((tiles[0].shape[0], 4, 3), 255, np.uint8) rows = [] for i in range(0, len(tiles), 4): row = [] for t in tiles[i:i+4]: row.append(t); row.append(g) rows.append(np.concatenate(row[:-1], axis=1)) wmax = max(r.shape[1] for r in rows) rows = [np.pad(r, ((0,0),(0,wmax-r.shape[1]),(0,0)), constant_values=18) for r in rows] vg = np.full((4, wmax, 3), 255, np.uint8) out = [] for r in rows: out.append(r); out.append(vg) outp = os.path.join(ROOT, "scratchpad", f"{tag}.png") Image.fromarray(np.concatenate(out[:-1], axis=0)).save(outp) print("saved", outp)