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>
120 lines
4.0 KiB
Python
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()})
|