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>
150 lines
6.1 KiB
Python
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)
|