Files
TeslaRel410/emulator/render_game.py
T
CydandClaude Opus 4.8 99652d894b Phase 3d: game world decodes and renders -- offline and LIVE
The full DPL hierarchy the game uses (vs flyk's flat scene) is now decoded
and rendered:
- stride-aware set_geom_verts (header word 3 = floats/vertex: 3/4/5/8/9;
  mech meshes carry normals + UVs)
- instances are list_add children of DCS nodes; instance flush field 4 ->
  object; object->lod->geogroup->geometry; dcs_link builds the articulation
  tree of 4x4s (payload floats 4..19, row-major, row 3 = translation)
- game world is y-down (DCS matrices carry a reflection); projection flips
  x (Division mirror) and y

render_game.py reconstructs a captured game stream offline: the mission
arena (10km, 246 instances, 330 geometries), the player's Thor at the
camera, six enemy mechs 1.5km north -- game-mech-decoded.png shows one with
real hull/armor/glass materials; game-cockpit-decoded.png the cockpit view.

The live backend (vpxlog.cpp) gained the same traversal and now draws the
game's out-the-window view in real time (game-live-gl.png): sky, arena
floor to the horizon, own gun barrels at frame bottom.

Next: texturing (action-26 texel maps + UVs), lighting from wire normals,
per-frame articulation once the RIO drives the sim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:22:35 -05:00

308 lines
10 KiB
Python

#!/usr/bin/env python3
"""Phase 3d: render a captured *game* VPX FIFO stream (full DPL hierarchy).
Unlike flyk's flat DIVRGB scene (render_capture.py), the game builds the real
DPL graph: instances reference objects and DCS transform nodes; objects hold
LODs holding geogroups holding geometries; dcs_link builds an articulation
tree of 4x4 matrices (mech torso/legs/arms). This reconstructs that graph
from a VPX_FIFODUMP capture and renders the scene each draw_scene commits.
Node types (empirical, Rel4.10 wire): 2=? 3=view 4=instance 5=dcs 6=material
(old) 7=object 8=lod 9=geogroup 10=geometry 11=material 12=texmap 13=? 14=?
Usage: render_game.py <dump> [-o out.png] [--frame N] [--eye x,y,z]
"""
import struct
import sys
from PIL import Image, ImageDraw
def read_messages(path):
msgs = []
with open(path, "rb") as f:
while True:
hdr = f.read(8)
if len(hdr) < 8:
break
if hdr[:4] != b"VPXM":
raise SystemExit("bad magic")
d = f.read(struct.unpack("<I", hdr[4:])[0])
if len(d) >= 4:
msgs.append((struct.unpack("<I", d[:4])[0], d[4:]))
return msgs
def U(b):
return list(struct.unpack(f"<{len(b) // 4}I", b[: len(b) // 4 * 4]))
def F(b):
return list(struct.unpack(f"<{len(b) // 4}f", b[: len(b) // 4 * 4]))
def mat_mul(a, b):
"""4x4 (row-major, row-vector convention: v' = v @ M)"""
return [[sum(a[i][k] * b[k][j] for k in range(4)) for j in range(4)]
for i in range(4)]
def mat_id():
return [[1.0 if i == j else 0.0 for j in range(4)] for i in range(4)]
def xform(v, m):
x, y, z = v
return (x * m[0][0] + y * m[1][0] + z * m[2][0] + m[3][0],
x * m[0][1] + y * m[1][1] + z * m[2][1] + m[3][1],
x * m[0][2] + y * m[1][2] + z * m[2][2] + m[3][2])
class Scene:
def __init__(self):
self.types = {}
self.verts = {}
self.polys = {}
self.material = {}
self.gg_material = {}
self.children = {} # list_add: parent -> [children]
self.inst_object = {} # instance -> object
self.inst_dcs = {} # instance -> dcs
self.dcs_mat = {} # dcs -> 4x4 local matrix
self.dcs_parent = {} # dcs child -> parent (dcs_link)
self.view = None
self.background = (0, 0, 0)
self.frames = []
def parse_dcs_matrix(f):
"""The 132-byte dcs flush payload: [name][type] then fields; the 4x4 is
the 16 floats starting at float index 4 (rows [x,y,z,0], row 3 = T)."""
m = [f[4 + r * 4: 8 + r * 4] for r in range(4)]
# sanity: last column should be ~(0,0,0,1)
if abs(m[3][3] - 1.0) > 0.5 and abs(m[0][3]) < 0.01:
# some builds put w=1 elsewhere; force affine
m[3][3] = 1.0
for r in range(3):
m[r][3] = 0.0
m[3][3] = 1.0
return m
def reconstruct(msgs):
sc = Scene()
camera = None
geom_pend = None
conn_pend = None
for action, d in msgs:
if action == 1:
w = U(d)
if len(w) >= 2:
sc.types[w[1]] = w[0]
elif action == 3:
w = U(d)
if len(w) < 2:
continue
name, t = w[0], w[1]
if t == 11 and len(d) >= 92:
f = F(d)
sc.material[name] = tuple(f[12:15])
elif t == 9 and len(d) >= 80:
sc.gg_material[name] = w[16]
elif t == 3 and len(d) >= 104:
f = F(d)
sc.view = (f[6], f[7], f[8], f[9], f[10],
f[11], f[12], f[13], f[14])
sc.background = tuple(f[15:18])
elif t == 5 and len(d) >= 132:
sc.dcs_mat[name] = parse_dcs_matrix(F(d))
elif t == 4:
# instance: find object/dcs refs by node-type lookup
for val in w[2:]:
if val and val != 0xFFFFFFFF:
vt = sc.types.get(val)
if vt == 7:
sc.inst_object[name] = val
elif vt == 5:
sc.inst_dcs[name] = val
elif action == 7 and len(d) >= 8: # dcs_link parent -> child
w = U(d)
sc.dcs_parent[w[1]] = w[0]
elif action == 11 and len(d) >= 8:
w = U(d)
sc.children.setdefault(w[0], []).append(w[1])
elif action == 23:
if geom_pend is None:
w = U(d)
if len(d) >= 36:
# header: [name][0][n_verts][stride_floats][n_msgs]...
geom_pend = [w[0], w[2], w[3], []]
sc.verts[w[0]] = []
else:
name, n, stride, acc = geom_pend
acc.extend(F(d))
if len(acc) >= n * stride:
sc.verts[name] = [
(acc[i], acc[i + 1], acc[i + 2])
for i in range(0, n * stride, stride)]
geom_pend = None
elif action == 25:
if conn_pend is None:
w = U(d)
if len(d) >= 16:
conn_pend = (w[0], w[1], w[2])
sc.polys[w[0]] = []
else:
name, n_polys, loop = conn_pend
idx = U(d)
pl = sc.polys[name]
if loop >= 2:
for i in range(0, len(idx), loop):
pl.append(idx[i:i + loop - 1])
if len(pl) >= n_polys:
conn_pend = None
elif action == 31:
f = F(d)
camera = ([f[2:5], f[5:8], f[8:11]], f[11:14])
elif action == 9:
sc.frames.append(camera)
return sc
def dcs_world(sc, dcs, cache, depth=0):
if dcs in cache:
return cache[dcs]
m = sc.dcs_mat.get(dcs, mat_id())
p = sc.dcs_parent.get(dcs)
if p is not None and p != dcs and depth < 64:
m = mat_mul(m, dcs_world(sc, p, cache, depth + 1))
cache[dcs] = m
return m
def gather_polys(sc):
"""instance -> object -> lod -> geogroup -> geometry, transformed.
Placement: instances are list_add children of DCS nodes (dcs -> instance);
dcs_link builds the dcs->dcs articulation tree above them."""
inst_parent = {}
for parent, kids in sc.children.items():
if sc.types.get(parent) == 5:
for k in kids:
if sc.types.get(k) == 4:
inst_parent[k] = parent
out = []
cache = {}
for inst, obj in sc.inst_object.items():
world = mat_id()
d = inst_parent.get(inst, sc.inst_dcs.get(inst))
if d is not None:
world = dcs_world(sc, d, cache)
for lod in sc.children.get(obj, []):
# use only the first (highest-detail) LOD child set
ggs = sc.children.get(lod, [])
if not ggs:
continue
for gg in ggs:
rgb = sc.material.get(sc.gg_material.get(gg, -1),
(0.8, 0.2, 0.8))
col = tuple(max(0, min(255, int(c * 255 + .5))) for c in rgb)
for geo in sc.children.get(gg, []):
vl = sc.verts.get(geo)
if not vl:
continue
wv = [xform(v, world) for v in vl]
for poly in sc.polys.get(geo, []):
pts = [wv[i] for i in poly if i < len(wv)]
if len(pts) >= 3:
out.append((pts, col))
break # first LOD only
return out
def render(sc, frame, out, ss=2):
if sc.view:
wl, wb, wr, wt, wd, vw, vh, near, far = sc.view
vw, vh = int(vw), int(vh)
else:
wl, wb, wr, wt, wd = -1, -0.6154, 1, 0.6154, 1.732
vw, vh, near, far = 832, 512, 0.25, 1150
cam = sc.frames[frame]
if cam is None:
raise SystemExit("no camera at that frame")
rot, eye = cam
W, H = vw * ss, vh * ss
bg = tuple(max(0, min(255, int(c * 255 + .5))) for c in sc.background)
img = Image.new("RGB", (W, H), bg)
draw = ImageDraw.Draw(img)
def project(p):
x, y, z = (p[0] - eye[0], p[1] - eye[1], p[2] - eye[2])
ex = rot[0][0] * x + rot[0][1] * y + rot[0][2] * z
ey = rot[1][0] * x + rot[1][1] * y + rot[1][2] * z
ez = rot[2][0] * x + rot[2][1] * y + rot[2][2] * z
return ex, ey, ez
def clip_near(evs, zn):
"""Sutherland-Hodgman clip against eye-space plane z = -zn."""
out = []
n = len(evs)
for i in range(n):
a, b = evs[i], evs[(i + 1) % n]
ain, bin_ = a[2] <= -zn, b[2] <= -zn
if ain:
out.append(a)
if ain != bin_:
t = (-zn - a[2]) / (b[2] - a[2])
out.append((a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]), -zn))
return out
polys = gather_polys(sc)
print(f"gathered {len(polys)} world polys")
items = []
for pts, col in polys:
evs = clip_near([project(p) for p in pts], near)
if len(evs) < 3:
continue
depth = sum(p[2] for p in evs) / len(evs)
scr = []
for ex, ey, ez in evs:
# game world is y-down (DCS matrices carry a y reflection), so
# screen-up = -eye_y; screen-x mirrored as with flyk.
ndc_x = (-ex * wd / -ez - wl) / (wr - wl)
ndc_y = (-ey * wd / -ez - wb) / (wt - wb)
scr.append((ndc_x * W, (1 - ndc_y) * H))
items.append((depth, scr, col))
items.sort(key=lambda it: it[0])
for _, scr, col in items:
draw.polygon(scr, fill=col)
if ss > 1:
img = img.resize((vw, vh), Image.LANCZOS)
img.save(out)
print(f"rendered frame {frame}: {len(items)} polys -> {out} ({vw}x{vh})")
def main():
path = sys.argv[1]
out = "game_frame.png"
frame = -1
if "-o" in sys.argv:
out = sys.argv[sys.argv.index("-o") + 1]
if "--frame" in sys.argv:
frame = int(sys.argv[sys.argv.index("--frame") + 1])
msgs = read_messages(path)
sc = reconstruct(msgs)
print(f"{len(msgs)} msgs: {len(sc.verts)} geoms, {len(sc.material)} mats, "
f"{len(sc.inst_object)} placed instances, {len(sc.dcs_mat)} dcs, "
f"{len(sc.frames)} frames, view={sc.view}")
if "--eye" in sys.argv:
eye = [float(v) for v in sys.argv[sys.argv.index("--eye") + 1].split(",")]
sc.frames = [([[1, 0, 0], [0, 1, 0], [0, 0, 1]], eye)]
frame = 0
render(sc, frame, out)
if __name__ == "__main__":
main()