mesh files use game space (right-handed, y up): obj/gltf import and export no longer mirror models, per-mesh handedness keeps meshes of old projects as they were, python tools follow (--export-space for the old files)

This commit is contained in:
MineTec committed 2026-11-07 12:48:43 +01:00
1 parent c4fa500198
commit 4a58910fba
26 files changed
+525 -96

No files matched your search

+18 -11
View File
@@ -11,7 +11,9 @@ Writes to outdir:
objects.obj (--objects) static STPC object models side by side
preview_top.png, preview_persp.png textured software renderings for checking
Coordinates: export space = MAP floats (y up, right-handed, z < 0). Game space = (x, y, -z).
Coordinates: OBJ file space = game space (x, y, z_game): right-handed, y up, z > 0 in the grid, as
Blender shows the level the same way round as the game (geomlib.to_file). --export-space writes
the raw export space (MAP floats, z < 0; mirrored in other tools), the convention before 2026-11-07.
Format: notes/geometry.md
"""
import argparse
@@ -61,7 +63,7 @@ def flat_color(t):
return (t['u0'] / 255, t['u1'] / 255, t['v0'] / 255)
def write_obj(path, tris, texs, mtlname):
def write_obj(path, tris, texs, mtlname, raw=False):
with open(path, 'w') as fo:
fo.write('# Ein Koenigreich fuer ein Lama - world geometry (geom_export.py)\n')
fo.write(f'mtllib {mtlname}\n')
@@ -77,10 +79,11 @@ def write_obj(path, tris, texs, mtlname):
if mat != cur_mat:
fo.write(f'usemtl {mat}\n')
cur_mat = mat
for p, c in zip(P, C):
fo.write('v %.5f %.5f %.5f %.3f %.3f %.3f\n' % (*p, *c))
for u, v in uv:
fo.write('vt %.6f %.6f\n' % (u, 1 - v))
order = [0, 1, 2] if raw else G.file_order(range(3))
for k in order:
fo.write('v %.5f %.5f %.5f %.3f %.3f %.3f\n' % (*(P[k] if raw else G.to_file(P[k])), *C[k]))
for k in order:
fo.write('vt %.6f %.6f\n' % (uv[k][0], 1 - uv[k][1]))
fo.write(f'f {vi}/{vi} {vi + 1}/{vi + 1} {vi + 2}/{vi + 2}\n')
vi += 3
@@ -168,12 +171,16 @@ def main():
ap.add_argument('--objects', action='store_true')
ap.add_argument('--no-png', action='store_true', help='no page PNGs/previews')
ap.add_argument('--size', default='1200x900')
ap.add_argument('--export-space', action='store_true',
help='raw export space (z < 0, mirrored in Blender) instead of game space')
a = ap.parse_args()
out = (lambda p: p) if a.export_space else G.to_file
order = (lambda s: list(s)) if a.export_space else G.file_order
os.makedirs(a.outdir, exist_ok=True)
ch, wf, trak, mp = G.load_level(a.wad)
pages, texs = G.parse_text(G.wad_get(ch, 'TEXT'))
tris = world_tris(trak, mp, texs)
write_obj(os.path.join(a.outdir, 'level.obj'), tris, texs, 'level.mtl')
write_obj(os.path.join(a.outdir, 'level.obj'), tris, texs, 'level.mtl', a.export_space)
write_mtl(os.path.join(a.outdir, 'level.mtl'), tris, texs, len(pages))
print(f'{a.wad}: WFPC={wf:#x} TRAK models={len(trak)} tiles={mp["N"]} grid={mp["W"]}x{mp["H"]} '
f'triangles={len(tris)} textures={len(texs)} pages={len(pages)}')
@@ -194,8 +201,8 @@ def main():
if nrm @ np.array(c['plane'][:3]) < 0:
pts = pts[::-1]
fo.write(f'g mat{c["mat"]:02d}_grp{g}\n')
for p in pts:
fo.write('v %.5f %.5f %.5f\n' % f(*p))
for p in order(pts):
fo.write('v %.5f %.5f %.5f\n' % out(f(*p)))
fo.write('f ' + ' '.join(str(vi + k) for k in range(len(pts))) + '\n')
vi += len(pts)
@@ -211,7 +218,7 @@ def main():
dx = ox - min(xs)
fo.write(f'o stpc_model_{k:03d}\n')
for v in m.verts:
fo.write('v %.5f %.5f %.5f\n' % (v[0] + dx, v[1], v[2]))
fo.write('v %.5f %.5f %.5f\n' % out((v[0] + dx, v[1], v[2])))
for fc in m.faces:
t = texs[fc['tex']] if fc['tex'] < len(texs) else None
if t and not (t['flags'] & 1) and t['page'] >= 0:
@@ -220,7 +227,7 @@ def main():
uv = [(0, 0)] * 3; fo.write(f'usemtl flat_{fc["tex"]}\n')
for u, v in uv:
fo.write('vt %.6f %.6f\n' % (u, 1 - v))
fo.write('f ' + ' '.join(f'{vi + fc["v"][j]}/{vt + j}' for j in range(3)) + '\n')
fo.write('f ' + ' '.join(f'{vi + fc["v"][j]}/{vt + j}' for j in order(range(3))) + '\n')
vt += 3
vi += len(m.verts)
ox += max(xs) - min(xs) + 0.5