Fixed the readout: the effect packets are 3-4 edge TEXTURED quads (0x2c/0x42/ 0x0d edges + z + texz/texu/texv planes -- NO Gouraud colour, confirming the scene is textured). Two bugs in the first pass: (1) 0x2c edges were treated as 'accept both sides', turning every quad into a full-plane fill; (2) only 2-edge packets were caught, dropping ~3/4 of the geometry. render_battle.py applies the render_wide30 polygon-winding rule (inside = all-edges>=0 OR all<0) -> 78 polygons of 141 setups. Result (flat-per-polygon + depth): a geometrically coherent first-person scene -- sky at far depth, a ground plane ramping smoothly from horizon to foreground, a clean horizon line, upright structures. The perspective is consistent; this is real scene geometry, not the muddy u/v wedges of the first attempt. Surfaces still need texels (TXDN/M3) for their skin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
"""Honest battle geometry: 0x2c/0x42/0x0d edges as proper half-planes with
|
|
polygon-level winding (inside = all>=0 OR all<0), real z-buffer. The scene is
|
|
TEXTURED (texz/texu/texv planes, no Gouraud colour), so with no TXDN texels we
|
|
show STRUCTURE: flat distinct colour per polygon + depth + u/v. numpy, offline."""
|
|
import pickle, struct, os
|
|
import numpy as np
|
|
from PIL import Image
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
d = pickle.load(open(os.path.join(HERE, 'battle_prog.pkl'), 'rb'))
|
|
prog = d['prog']
|
|
def f32(w): return struct.unpack('<f', struct.pack('<I', w & 0xffffffff))[0]
|
|
def rd(a): return prog.get(a, 0)
|
|
W, H = 832, 512
|
|
setups = [a for a in sorted(prog) if rd(a) == 0x100]
|
|
EDGE = {0x42, 0x0d, 0x2c}
|
|
|
|
yy, xx = np.mgrid[0:H, 0:W].astype(np.float64)
|
|
zbuf = np.full((H, W), -1e30)
|
|
flat = np.zeros((H, W, 3), np.uint8)
|
|
uv = np.zeros((H, W, 3))
|
|
PAL = [(200,80,80),(80,200,120),(90,140,220),(220,200,90),(200,120,220),
|
|
(120,220,220),(240,150,80),(150,120,230),(90,210,160),(230,110,150)]
|
|
drawn = 0
|
|
for a in setups:
|
|
p = a + 4
|
|
edges = []
|
|
while ((rd(p) >> 8) & 0xff) in EDGE and len(edges) < 6:
|
|
edges.append((f32(rd(p+4)), f32(rd(p+8)), f32(rd(p+12))))
|
|
p += 16
|
|
if len(edges) < 3:
|
|
continue
|
|
vsum_pos = np.ones((H, W), bool)
|
|
vsum_neg = np.ones((H, W), bool)
|
|
for A, B, C in edges:
|
|
v = A * xx + B * yy + C
|
|
vsum_pos &= v >= 0
|
|
vsum_neg &= v < 0
|
|
en = vsum_pos | vsum_neg
|
|
if not en.any():
|
|
continue
|
|
# z = the op-0x21 within the next ~10 words
|
|
zp = None; zq = p
|
|
for _ in range(12):
|
|
if ((rd(zq) >> 8) & 0xff) == 0x21:
|
|
zp = (f32(rd(zq+4)), f32(rd(zq+8)), f32(rd(zq+12))); break
|
|
zq += 4
|
|
if zp is None:
|
|
continue
|
|
z = zp[0] * xx + zp[1] * yy + zp[2]
|
|
en &= z > zbuf
|
|
if not en.any():
|
|
continue
|
|
zbuf[en] = z[en]
|
|
flat[en] = PAL[drawn % len(PAL)]
|
|
# texu/texv planes (addr 58 / 78 via op 0x43) for the uv view
|
|
up = vp = None; q = zq
|
|
for _ in range(50):
|
|
w = rd(q)
|
|
if ((w >> 8) & 0xff) == 0x43 and (w & 0xff) == 58 and up is None:
|
|
up = (f32(rd(q+4)), f32(rd(q+8)), f32(rd(q+12)))
|
|
if ((w >> 8) & 0xff) == 0x43 and (w & 0xff) == 78 and vp is None:
|
|
vp = (f32(rd(q+4)), f32(rd(q+8)), f32(rd(q+12)))
|
|
if up and vp: break
|
|
q += 4
|
|
if up and vp:
|
|
u = up[0]*xx + up[1]*yy + up[2]
|
|
v = vp[0]*xx + vp[1]*yy + vp[2]
|
|
uv[..., 0][en] = u[en]; uv[..., 1][en] = v[en]
|
|
drawn += 1
|
|
|
|
print("drawn %d polygons of %d setups; covered %d px" %
|
|
(drawn, len(setups), int((zbuf > -1e29).sum())))
|
|
Image.fromarray(flat, 'RGB').save(os.path.join(HERE, 'battle_flat.png'))
|
|
zv = zbuf.copy(); m = zv > -1e29
|
|
zi = np.zeros((H, W), np.uint8)
|
|
if m.any():
|
|
zn = (zv - zv[m].min()) / max(1e-9, np.ptp(zv[m]))
|
|
zi[m] = (40 + 215*zn[m]).astype(np.uint8)
|
|
Image.fromarray(zi, 'L').save(os.path.join(HERE, 'battle_depth.png'))
|
|
# uv normalized globally (spatially coherent, unlike per-prim)
|
|
um, vm = uv[..., 0], uv[..., 1]
|
|
rgb = np.zeros((H, W, 3), np.uint8)
|
|
if m.any():
|
|
un = (um - um[m].min())/max(1e-9, np.ptp(um[m]))
|
|
vn = (vm - vm[m].min())/max(1e-9, np.ptp(vm[m]))
|
|
rgb[..., 0][m] = (40 + 200*un[m]).astype(np.uint8)
|
|
rgb[..., 1][m] = (40 + 200*vn[m]).astype(np.uint8)
|
|
rgb[..., 2][m] = 120
|
|
Image.fromarray(rgb, 'RGB').save(os.path.join(HERE, 'battle_uv.png'))
|
|
print("wrote battle_flat.png + battle_depth.png + battle_uv.png")
|