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

120 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""
spl.py -- reader/evaluator for DPL3 ".SPL" camera-path splines.
Faithful port of DPL3/EXAMPLES/SPLINE.C. A .SPL is plain text:
N number of control points
x y z ax ay az x N -- position + euler angles (degrees)
The path is a CLOSED LOOP of cubic-Hermite segments with Catmull-Rom tangents
(vel = (next - prev) / 2). Position and the three euler angles are each splined.
We sample the loop densely and bake, per sample, a camera basis (eye / center /
up) so downstream code just needs lookAt(). Camera looks down local -Z (proved by
CAMERA.SPL starting at ay=180 and facing into the +Z scene).
"""
import math
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import scn # reuse the DPL row-vector rotation matrices
def _solve_cubic(v0, v1, d0, d1):
# v = a t^3 + b t^2 + c t + d ; endpoints v0,v1 and tangents d0,d1
a = d1 + d0 - 2.0 * v1 + 2.0 * v0
b = v1 - v0 - d0 - a
return (a, b, d0, v0) # (c0=a, c1=b, c2=c, c3=d) matching eval order
def _solve_rot_cubic(v0, v1, d0, d1):
if d0 > 360.0:
d0 -= 360.0
if d1 > 360.0:
d1 -= 360.0
a = d1 + d0 - 2.0 * v1 + 2.0 * v0
b = v1 - v0 - d0 - a
return (a, b, d0, v0)
def _eval(c, t):
return c[0]*t*t*t + c[1]*t*t + c[2]*t + c[3]
def load_points(path):
with open(path, "r", encoding="latin-1") as fp:
toks = fp.read().split()
n = int(toks[0])
vals = [float(x) for x in toks[1:]]
pts = []
for i in range(n):
base = i * 6
if base + 6 > len(vals):
break
pts.append({"pos": vals[base:base+3], "ang": vals[base+3:base+6]})
return pts
def build_segments(pts):
"""Compute per-knot tangents (closed loop) and per-segment cubic coeffs."""
n = len(pts)
for i in range(n):
p, c, nx = pts[(i-1) % n], pts[i], pts[(i+1) % n]
c["vel"] = [(nx["pos"][k] - p["pos"][k]) / 2.0 for k in range(3)]
rot = []
for k in range(3):
delta = nx["ang"][k] - c["ang"][k]
while delta > 180:
delta -= 180
while delta < -180:
delta += 180
rot.append(delta / 2.0)
while c["ang"][k] > 360.0:
c["ang"][k] -= 360.0
c["rot"] = rot
segs = []
for i in range(n):
a, b = pts[i], pts[(i+1) % n]
seg = {"pos": [], "rot": []}
for k in range(3):
seg["pos"].append(_solve_cubic(a["pos"][k], b["pos"][k], a["vel"][k], b["vel"][k]))
seg["rot"].append(_solve_rot_cubic(a["ang"][k], b["ang"][k], a["rot"][k], b["rot"][k]))
segs.append(seg)
return segs
def _basis(pos, ang):
"""eye/center/up from position + euler (ax,ay,az) via DPL rotation order."""
R = scn.matmul(scn.matmul(scn.m_rotZ(ang[2]), scn.m_rotX(ang[0])), scn.m_rotY(ang[1]))
fwd = scn.xform_dir(R, 0.0, 0.0, -1.0) # camera looks down local -Z
up = scn.xform_dir(R, 0.0, 1.0, 0.0)
return {"eye": list(pos),
"center": [pos[i] + fwd[i] for i in range(3)],
"up": list(up)}
def sample_flythrough(path, per_seg=12):
"""Return a list of camera frames {eye,center,up} around the closed loop."""
pts = load_points(path)
segs = build_segments(pts)
frames = []
for seg in segs:
for s in range(per_seg):
t = s / float(per_seg)
pos = [_eval(seg["pos"][k], t) for k in range(3)]
ang = [_eval(seg["rot"][k], t) for k in range(3)]
frames.append(_basis(pos, ang))
return frames
if __name__ == "__main__":
pts = load_points(sys.argv[1])
frames = sample_flythrough(sys.argv[1])
print("%d control points -> %d camera frames" % (len(pts), len(frames)))
xs = [f["eye"][0] for f in frames]; ys = [f["eye"][1] for f in frames]; zs = [f["eye"][2] for f in frames]
print("eye path X[%.0f,%.0f] Y[%.0f,%.0f] Z[%.0f,%.0f]" %
(min(xs), max(xs), min(ys), max(ys), min(zs), max(zs)))
print("frame0:", {k: [round(x, 1) for x in v] for k, v in frames[0].items()})