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>
308 lines
10 KiB
Python
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()
|