Uncategorised
qu_prm_loader
reads a Re-Volt (RVGL) .PRM 3D mesh file, built entirely on the existing packed-struct binary readers (`read_structs`) and typed scalar reads (`read_int16`) -- no new engine code.
catalog/qu_prm_loader.qu
Format (from the game's own loader, read locally, never committed -- third-party asset, kept out of this repo): a 4-byte header of two int16 counts, then that many fixed-size records back to back.
header poly_count:i16, vert_count:i16 (order below) polygon type:i16, tpage:i16, -- 60 bytes vi0..vi3:i16 (4 vertex indices), c0..c3:u32 (4 packed ARGB colors), u0,v0 .. u3,v3:f32 (4 UV pairs) vertex px,py,pz:f32, nx,ny,nz:f32 -- 24 bytes
The header's two counts are written in DIFFERENT orders across game builds (poly-then-vert, or vert-then-poly) -- the loader has no version field to tell them apart, so it tries both readings against the file's actual remaining length and keeps whichever one accounts for every byte. A wrong guess parses garbage records rather than failing loudly, which is why this check exists at all.
Deliberately NOT done here, because each would be a rendering decision rather than a loading one: triangulating the quad polys (bit 0 of `type`) into two triangles, sorting polygons into draw buckets by their type flags (double-sided / env-mapped / semi-transparent), applying any texture-page remap, or flipping the Y axis / triangle winding to match a particular viewer's convention. This hands back the file's own numbers, faithfully, as two Tables -- one row per polygon, one row per vertex -- and stops there.
How it works
A tiny synthesized mesh -- one quad polygon over 4 vertices -- written
by hand at the byte level so this demo needs no external asset. Values chosen to be recognisable after the round trip: a unit square in the XY plane, facing +Z, vertex 2's color carries a distinct alpha byte.
`tmp_file()` rather than a plain name in the working directory -- Qu has no file-delete builtin, so a name like `prm_loader_demo.prm.tmp` here would have been left behind after every run, in whatever directory the demo happened to be invoked from. The OS temp directory is where a throwaway scratch file belongs; the real output of this demo is the printed mesh, not the file.
tmp_path = tmp_file()
w = fopen(tmp_path, "wb")
write_int16(w, 1) # poly_count
write_int16(w, 4) # vert_count
# the one polygon: type=1 (quad bit set), tpage=2, corners 0..3
write_int16(w, 1)
write_int16(w, 2)
write_int16(w, 0)
write_int16(w, 1)
write_int16(w, 2)
write_int16(w, 3)
write_uint32(w, 0xFFFF0000)
write_uint32(w, 0xFF00FF00)
write_uint32(w, 0x800000FF) # alpha 0x80 -- the one this demo checks
write_uint32(w, 0xFFFFFFFF)
write_float(w, 0.0); write_float(w, 0.0)
write_float(w, 1.0); write_float(w, 0.0)
write_float(w, 1.0); write_float(w, 1.0)
write_float(w, 0.0); write_float(w, 1.0)
# the four vertices, unit square, normal pointing +Z
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 0.0)
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 1.0)
write_float(w, 1.0); write_float(w, 0.0); write_float(w, 0.0)
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 1.0)
write_float(w, 1.0); write_float(w, 1.0); write_float(w, 0.0)
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 1.0)
write_float(w, 0.0); write_float(w, 1.0); write_float(w, 0.0)
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 1.0)
close(w)
mesh = load_prm(tmp_path)
print("polys={mesh.poly_count}, verts={mesh.vert_count}")
print("poly 0: type={mesh.polys.type[0]}, tpage={mesh.polys.tpage[0]}, corners=({mesh.polys.vi0[0]},{mesh.polys.vi1[0]},{mesh.polys.vi2[0]},{mesh.polys.vi3[0]})")
print("vertex 2: ({mesh.verts.px[2]}, {mesh.verts.py[2]}, {mesh.verts.pz[2]})")
Results
polys=1, verts=4 poly 0: type=1, tpage=2, corners=(0,1,2,3) vertex 2: (1, 1, 0)
The whole program
Runnable as it stands: qu run catalog/qu_prm_loader.qu
# qu_prm_loader.qu -- reads a Re-Volt (RVGL) .PRM 3D mesh file, built
# entirely on the existing packed-struct binary readers (`read_structs`)
# and typed scalar reads (`read_int16`) -- no new engine code.
#
# Format (from the game's own loader, read locally, never committed --
# third-party asset, kept out of this repo): a 4-byte header of two int16
# counts, then that many fixed-size records back to back.
#
# header poly_count:i16, vert_count:i16 (order below)
# polygon type:i16, tpage:i16, -- 60 bytes
# vi0..vi3:i16 (4 vertex indices),
# c0..c3:u32 (4 packed ARGB colors),
# u0,v0 .. u3,v3:f32 (4 UV pairs)
# vertex px,py,pz:f32, nx,ny,nz:f32 -- 24 bytes
#
# The header's two counts are written in DIFFERENT orders across game
# builds (poly-then-vert, or vert-then-poly) -- the loader has no version
# field to tell them apart, so it tries both readings against the file's
# actual remaining length and keeps whichever one accounts for every
# byte. A wrong guess parses garbage records rather than failing loudly,
# which is why this check exists at all.
#
# Deliberately NOT done here, because each would be a rendering decision
# rather than a loading one: triangulating the quad polys (bit 0 of
# `type`) into two triangles, sorting polygons into draw buckets by their
# type flags (double-sided / env-mapped / semi-transparent), applying any
# texture-page remap, or flipping the Y axis / triangle winding to match
# a particular viewer's convention. This hands back the file's own
# numbers, faithfully, as two Tables -- one row per polygon, one row per
# vertex -- and stops there.
function load_prm(path)
f = fopen(path, "rb")
a = read_int16(f)
b = read_int16(f)
body = file_size(path) - 4
if a * 60 + b * 24 == body then
poly_count = a
vert_count = b
elseif b * 60 + a * 24 == body then
poly_count = b
vert_count = a
else
poly_count = a
vert_count = b
end if
poly_fields = (("type", "i16"), ("tpage", "i16"),
("vi0", "i16"), ("vi1", "i16"), ("vi2", "i16"), ("vi3", "i16"),
("c0", "u32"), ("c1", "u32"), ("c2", "u32"), ("c3", "u32"),
("u0", "f32"), ("v0", "f32"), ("u1", "f32"), ("v1", "f32"),
("u2", "f32"), ("v2", "f32"), ("u3", "f32"), ("v3", "f32"))
polys = read_structs(f, poly_fields, poly_count)
vert_fields = (("px", "f32"), ("py", "f32"), ("pz", "f32"),
("nx", "f32"), ("ny", "f32"), ("nz", "f32"))
verts = read_structs(f, vert_fields, vert_count)
close(f)
return {poly_count = poly_count, vert_count = vert_count,
polys = polys, verts = verts}
end function
backend auto
#%% A tiny synthesized mesh -- one quad polygon over 4 vertices -- written
# by hand at the byte level so this demo needs no external asset. Values
# chosen to be recognisable after the round trip: a unit square in the
# XY plane, facing +Z, vertex 2's color carries a distinct alpha byte.
#
# `tmp_file()` rather than a plain name in the working directory -- Qu has
# no file-delete builtin, so a name like `prm_loader_demo.prm.tmp` here
# would have been left behind after every run, in whatever directory the
# demo happened to be invoked from. The OS temp directory is where a
# throwaway scratch file belongs; the real output of this demo is the
# printed mesh, not the file.
tmp_path = tmp_file()
w = fopen(tmp_path, "wb")
write_int16(w, 1) # poly_count
write_int16(w, 4) # vert_count
# the one polygon: type=1 (quad bit set), tpage=2, corners 0..3
write_int16(w, 1)
write_int16(w, 2)
write_int16(w, 0)
write_int16(w, 1)
write_int16(w, 2)
write_int16(w, 3)
write_uint32(w, 0xFFFF0000)
write_uint32(w, 0xFF00FF00)
write_uint32(w, 0x800000FF) # alpha 0x80 -- the one this demo checks
write_uint32(w, 0xFFFFFFFF)
write_float(w, 0.0); write_float(w, 0.0)
write_float(w, 1.0); write_float(w, 0.0)
write_float(w, 1.0); write_float(w, 1.0)
write_float(w, 0.0); write_float(w, 1.0)
# the four vertices, unit square, normal pointing +Z
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 0.0)
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 1.0)
write_float(w, 1.0); write_float(w, 0.0); write_float(w, 0.0)
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 1.0)
write_float(w, 1.0); write_float(w, 1.0); write_float(w, 0.0)
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 1.0)
write_float(w, 0.0); write_float(w, 1.0); write_float(w, 0.0)
write_float(w, 0.0); write_float(w, 0.0); write_float(w, 1.0)
close(w)
mesh = load_prm(tmp_path)
print("polys={mesh.poly_count}, verts={mesh.vert_count}")
print("poly 0: type={mesh.polys.type[0]}, tpage={mesh.polys.tpage[0]}, corners=({mesh.polys.vi0[0]},{mesh.polys.vi1[0]},{mesh.polys.vi2[0]},{mesh.polys.vi3[0]})")
print("vertex 2: ({mesh.verts.px[2]}, {mesh.verts.py[2]}, {mesh.verts.pz[2]})")