#!/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 [-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("= 4: msgs.append((struct.unpack(" [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()