Files
CydandClaude Fable 5 afc3fd839e Vendor dpl3-revive: the Division/DPL3 renderer, now ours
Bring the graphics-dev collaborator's dpl3-revive into the repo as first-class
project code (they've handed it off; it's ours now). This is the proven
Division renderer that our in-process rt_draw has been trying to be.

What's here:
- parser/  B2Z/V2Z/SVT/SCN/SPL/BGF/BMF/BSL decoders (pure Python).
- spec/    reverse-engineered format + the definitive VelociRender wire
           protocol (from the original DIVISION source, matches our live
           VPX node/action tables exactly).
- source-ref/  read-only copies of the original DIVISION C (BIZREAD.C,
           DPLTYPES.H, DPL.H) that define the formats.
- patha/   the "virtual VelociRender board": vrboard.py (24-action protocol
           server), vrview.py (numpy software rasterizer, the reference),
           vrview_gl.py (moderngl GPU backend, 832x512@60Hz), plus the
           run/replay/regress tooling and evidence renders. Drives FLYK/BLADE/
           Star Trek demos AND our btl4opt/rpl4opt game binaries.
- viewer/  WebGL archive generators (.py); prebuilt HTML/data regeneratable.
- samples/ small test models/textures.
- bt*.raw.bin  real BTL4OPT arena wire captures (kept for offline renderer
           testing/regression against OUR game).

.gitignore keeps the multi-hundred-MB demo capture dumps + debug logs +
regeneratable viewer bundles out of history (they stay on disk).

Phase 0 of the integration is validated: their board decodes our bt8 capture
with zero errors (3748 nodes, 507 instances, 4 mechs) and renders our arena
(terrain/dome/sky, correct Division DAC gamma). Plan + status in memory;
integration continues in emulator/RENDERER-COLLAB.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 22:06:25 -05:00

150 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""
render_preview.py -- CPU cross-check of the viewer pipeline.
Consumes exactly what the WebGL viewer consumes (bundle.build_model output:
merged positions/normals/uvs/indices + decoded SVT texture) and software-rasterizes
one model to a PNG. If this looks right, the WebGL viewer -- fed identical data --
renders the same. Verification, not the product.
python viewer/render_preview.py 0 out.png # model index (default 0)
"""
import base64
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(os.path.dirname(HERE), "parser"))
import bundle
import svt
W, H = 640, 480
def mv(m, v):
return [sum(m[r][c] * v[c] for c in range(4)) for r in range(4)]
def matmul(a, b):
return [[sum(a[r][k] * b[k][c] for k in range(4)) for c in range(4)] for r in range(4)]
def look_at(eye, ctr, up):
def sub(a, b): return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]
def crs(a, b): return [a[1]*b[2]-a[2]*b[1], a[2]*b[0]-a[0]*b[2], a[0]*b[1]-a[1]*b[0]]
def nrm(a):
l = math.hypot(*a) or 1.0; return [a[0]/l, a[1]/l, a[2]/l]
def dot(a, b): return a[0]*b[0]+a[1]*b[1]+a[2]*b[2]
z = nrm(sub(eye, ctr)); x = nrm(crs(up, z)); y = crs(z, x)
return [[x[0], x[1], x[2], -dot(x, eye)],
[y[0], y[1], y[2], -dot(y, eye)],
[z[0], z[1], z[2], -dot(z, eye)],
[0, 0, 0, 1]]
def persp(fovy, asp, n, f):
t = 1/math.tan(fovy/2)
return [[t/asp, 0, 0, 0], [0, t, 0, 0],
[0, 0, (f+n)/(n-f), 2*f*n/(n-f)], [0, 0, -1, 0]]
def render(md, outpath, cam=None):
# decode textures to (w,h,rgba bytes)
texs = []
for t in md["textures"]:
texs.append((t["w"], t["h"], base64.b64decode(t["rgba"])))
b = md["bounds"]
ctr = [(b["min"][i]+b["max"][i])/2 for i in range(3)]
rad = math.hypot(*(b["max"][i]-b["min"][i] for i in range(3)))/2 or 1.0
up = [0, 0, 1] if md["up"] == "z" else [0, 1, 0]
az, el, dist = 0.9, 0.55, rad*2.6
ce = math.cos(el)
if md["up"] == "z":
eye = [ctr[0]+dist*ce*math.sin(az), ctr[1]+dist*ce*math.cos(az), ctr[2]+dist*math.sin(el)]
else:
eye = [ctr[0]+dist*ce*math.sin(az), ctr[1]+dist*math.sin(el), ctr[2]+dist*ce*math.cos(az)]
if cam is not None:
eye, ctr, up = cam["eye"], cam["center"], cam["up"]
P = matmul(persp(1.0, W/H, 50, rad*5+2000), look_at(eye, ctr, up))
else:
P = matmul(persp(1.0, W/H, dist*0.02, dist*10+rad*4), look_at(eye, ctr, up))
L = [0.4, 0.7, 0.6]; ll = math.hypot(*L); L = [c/ll for c in L]
img = bytearray(b"\x1a\x14\x10" * (W*H)) # dark ground
zbuf = [1e30]*(W*H)
for g in md["groups"]:
pos, nrm, uv, idx = g["positions"], g["normals"], g["uvs"], g["indices"]
col = g["color"]; ti = g["texture"]
tex = texs[ti] if ti >= 0 else None
# project all verts
sx = [0.0]*(len(pos)//3); sy = list(sx); sz = list(sx); sw = list(sx)
for i in range(len(pos)//3):
c = mv(P, [pos[i*3], pos[i*3+1], pos[i*3+2], 1.0])
w = c[3] if abs(c[3]) > 1e-9 else 1e-9
sw[i] = w
sx[i] = (c[0]/w*0.5+0.5)*W
sy[i] = (1-(c[1]/w*0.5+0.5))*H
sz[i] = c[2]/w
for t in range(0, len(idx), 3):
a, bb, cc = idx[t], idx[t+1], idx[t+2]
if sw[a] <= 0 or sw[bb] <= 0 or sw[cc] <= 0:
continue
x0, y0, x1, y1, x2, y2 = sx[a], sy[a], sx[bb], sy[bb], sx[cc], sy[cc]
area = (x1-x0)*(y2-y0)-(x2-x0)*(y1-y0)
if abs(area) < 1e-9:
continue
minx = max(0, int(min(x0, x1, x2))); maxx = min(W-1, int(max(x0, x1, x2))+1)
miny = max(0, int(min(y0, y1, y2))); maxy = min(H-1, int(max(y0, y1, y2))+1)
iw = [1.0/sw[a], 1.0/sw[bb], 1.0/sw[cc]]
# world normals for lighting
nl = []
for vi in (a, bb, cc):
nx, ny, nz = nrm[vi*3], nrm[vi*3+1], nrm[vi*3+2]
nl.append(abs(nx*L[0]+ny*L[1]+nz*L[2]))
for py in range(miny, maxy+1):
for px in range(minx, maxx+1):
w0 = ((x1-px)*(y2-py)-(x2-px)*(y1-py))/area
w1 = ((x2-px)*(y0-py)-(x0-px)*(y2-py))/area
w2 = 1-w0-w1
if w0 < 0 or w1 < 0 or w2 < 0:
continue
z = w0*sz[a]+w1*sz[bb]+w2*sz[cc]
o = py*W+px
if z >= zbuf[o]:
continue
zbuf[o] = z
lit = 0.35+0.65*(w0*nl[0]+w1*nl[1]+w2*nl[2])
if tex:
# perspective-correct uv
pw = w0*iw[0]+w1*iw[1]+w2*iw[2]
u = (w0*uv[a*2]*iw[0]+w1*uv[bb*2]*iw[1]+w2*uv[cc*2]*iw[2])/pw
v = (w0*uv[a*2+1]*iw[0]+w1*uv[bb*2+1]*iw[1]+w2*uv[cc*2+1]*iw[2])/pw
tw, th, td = tex
tx = int((u % 1.0)*tw) % tw; tyv = int((v % 1.0)*th) % th
tp = (tyv*tw+tx)*4
r, gg, bl = td[tp], td[tp+1], td[tp+2]
r *= col[0]; gg *= col[1]; bl *= col[2]
else:
r, gg, bl = col[0]*255, col[1]*255, col[2]*255
# Division DAC gamma 1.7 (out = in^(1/1.25))
img[o*3] = min(255, int(((r*lit/255.0)**(1/1.25))*255))
img[o*3+1] = min(255, int(((gg*lit/255.0)**(1/1.25))*255))
img[o*3+2] = min(255, int(((bl*lit/255.0)**(1/1.25))*255))
# write PNG (reuse svt's encoder, but it expects RGBA)
rgba = bytearray(W*H*4)
for i in range(W*H):
rgba[i*4:i*4+3] = img[i*3:i*3+3]; rgba[i*4+3] = 255
svt.write_png(outpath, W, H, bytes(rgba))
print("wrote %s (%dx%d, model=%r, %d tris)" % (outpath, W, H, md["name"], md["tris"]))
if __name__ == "__main__":
i = int(sys.argv[1]) if len(sys.argv) > 1 else 0
out = sys.argv[2] if len(sys.argv) > 2 else os.path.join(HERE, "preview_%d.png" % i)
render(bundle.build_model(bundle.SHOWCASE[i]), out)