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:
1 parent
c4fa500198
commit
4a58910fba
26 files changed
+525
-96
No files matched your search
+3
-3
@@ -33,7 +33,7 @@ class ObjW:
|
||||
self.f.append((tex, mat, [base + k for k in range(4)]))
|
||||
|
||||
def box(self, x0, z0, x1, z1, y0, y1, top, side):
|
||||
# coordinates: x0<x1, z0>z1 (negative z = further into the grid)
|
||||
# export space: x0<x1, z0>z1 (negative z = further into the grid); save() writes game space
|
||||
self.quad([(x0, y1, z0), (x1, y1, z0), (x1, y1, z1), (x0, y1, z1)], top)
|
||||
for (ax, az), (bx, bz) in [((x0, z0), (x0, z1)), ((x0, z1), (x1, z1)), ((x1, z1), (x1, z0)), ((x1, z0), (x0, z0))]:
|
||||
# wall from a to b, outside = clockwise
|
||||
@@ -45,12 +45,12 @@ class ObjW:
|
||||
def save(self, path):
|
||||
with open(path, 'w') as fo:
|
||||
for p in self.v:
|
||||
fo.write('v %.4f %.4f %.4f 1 1 1\n' % p)
|
||||
fo.write('v %.4f %.4f %.4f 1 1 1\n' % G.to_file(p))
|
||||
for u, v in self.vt:
|
||||
fo.write('vt %.6f %.6f\n' % (u, 1 - v))
|
||||
for tex, mat, ids in self.f:
|
||||
fo.write(f'g demo_mat{mat:02d}\nusemtl tex_{tex}\n')
|
||||
fo.write('f ' + ' '.join(f'{i + 1}/{i + 1}' for i in ids) + '\n')
|
||||
fo.write('f ' + ' '.join(f'{i + 1}/{i + 1}' for i in G.file_order(ids)) + '\n')
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
+18
-11
@@ -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
|
||||
|
||||
+16
-7
@@ -4,8 +4,9 @@
|
||||
geom_import.py base.wad mesh.obj new.wad [--default-tex N] [--no-coll] [--mat N]
|
||||
[--keep-tiles] [--band 6]
|
||||
|
||||
Input OBJ in the export space of geom_export.py (y up, 1 unit = 1 tile, z < 0 lies in the
|
||||
grid; grid cell (cx,cz) = (floor(x), floor(-z)), must be within 0..W-1 / 0..H-1).
|
||||
Input OBJ in game space as geom_export.py writes it (right-handed, y up, 1 unit = 1 tile, z > 0
|
||||
lies in the grid; grid cell (cx,cz) = (floor(x), floor(z)), must be within 0..W-1 / 0..H-1), i.e.
|
||||
as Blender shows it. --export-space reads the old convention (raw export space, z < 0).
|
||||
|
||||
Textures: the engine has NO free UVs. Each triangle shows one half of a texture
|
||||
rectangle (entry of the TEXT table); the corners are chosen via face flags.
|
||||
@@ -43,7 +44,8 @@ import geomlib as G # noqa: E402
|
||||
PATTERNS = [0x800, 0x810, 0x820, 0x830, 0x000, 0x020]
|
||||
|
||||
|
||||
def read_obj(path):
|
||||
def read_obj(path, export_space=False):
|
||||
"""Faces and points in export space (converted from file space unless export_space)."""
|
||||
V, C, VT = [], [], []
|
||||
faces = [] # (idx list, vt list or None, material, group)
|
||||
mat, grp = None, ''
|
||||
@@ -52,7 +54,8 @@ def read_obj(path):
|
||||
if not p:
|
||||
continue
|
||||
if p[0] == 'v':
|
||||
V.append(tuple(float(x) for x in p[1:4]))
|
||||
v = tuple(float(x) for x in p[1:4])
|
||||
V.append(v if export_space else G.to_file(v))
|
||||
C.append(tuple(float(x) for x in p[4:7]) if len(p) >= 7 else None)
|
||||
elif p[0] == 'vt':
|
||||
VT.append((float(p[1]), float(p[2])))
|
||||
@@ -67,7 +70,11 @@ def read_obj(path):
|
||||
a = int(s[0]); vi.append(a - 1 if a > 0 else len(V) + a)
|
||||
if len(s) > 1 and s[1]:
|
||||
b = int(s[1]); ti.append(b - 1 if b > 0 else len(VT) + b)
|
||||
faces.append((vi, ti if len(ti) == len(vi) else None, mat, grp))
|
||||
ti = ti if len(ti) == len(vi) else None
|
||||
if not export_space:
|
||||
vi = G.file_order(vi)
|
||||
ti = ti and G.file_order(ti)
|
||||
faces.append((vi, ti, mat, grp))
|
||||
return V, C, VT, faces
|
||||
|
||||
|
||||
@@ -116,12 +123,14 @@ def main():
|
||||
ap.add_argument('--zones', choices=['flat', 'keep'], default='flat',
|
||||
help='flat: disable zone visibility (PVS), draw everything; keep: original')
|
||||
ap.add_argument('--band', type=float, default=6.0, help='height band per tile (units)')
|
||||
ap.add_argument('--export-space', action='store_true',
|
||||
help='OBJ in raw export space (z < 0; files of geom_export --export-space / before 2026-11-07)')
|
||||
a = ap.parse_args()
|
||||
|
||||
ch, wf, trak, mp = G.load_level(a.base)
|
||||
pages, texs = G.parse_text(G.wad_get(ch, 'TEXT'))
|
||||
W, H = mp['W'], mp['H']
|
||||
V, C, VT, faces = read_obj(a.obj)
|
||||
V, C, VT, faces = read_obj(a.obj, a.export_space)
|
||||
deftex = a.default_tex
|
||||
if deftex is None:
|
||||
deftex = next(i for i, t in enumerate(texs) if not (t['flags'] & 1) and t['page'] >= 0)
|
||||
@@ -160,7 +169,7 @@ def main():
|
||||
tris.append((P, Cc, tex, fl))
|
||||
|
||||
if a.coll_obj:
|
||||
cV, _, _, cfaces = read_obj(a.coll_obj)
|
||||
cV, _, _, cfaces = read_obj(a.coll_obj, a.export_space)
|
||||
polys = []
|
||||
for vi, ti, mat, grp in cfaces:
|
||||
m = re.search(r'mat(\d+)', grp or '')
|
||||
|
||||
@@ -13,6 +13,24 @@ FUN_0042ac50 MAP, FUN_00406d30 texture table, FUN_00556510 tile renderer).
|
||||
import math
|
||||
import struct
|
||||
|
||||
# ---------------------------------------------------------------- OBJ file space
|
||||
# OBJ files (geom_export/geom_import/geom_demo, the editor's Import mesh and OBJ/glTF export) are
|
||||
# right-handed, y up, in game space: file (x, y, z) = export (x, y, -z). The game shows export space
|
||||
# left-handed, so this is how Blender shows the level the same way round as the game. Converting
|
||||
# mirrors, so the corner order reverses (first corner kept: export (0,1,2) <-> file (0,2,1)).
|
||||
|
||||
|
||||
def to_file(p):
|
||||
"""Export-space point -> OBJ file point."""
|
||||
return (p[0], p[1], -p[2])
|
||||
|
||||
|
||||
def file_order(seq):
|
||||
"""Corner order export <-> file (an involution)."""
|
||||
seq = list(seq)
|
||||
return seq[:1] + seq[:0:-1]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- WAD
|
||||
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ for ln in open(os.path.join(objdir, 'level.obj')):
|
||||
if ln.startswith('v '):
|
||||
p = ln.split()
|
||||
x, y, z = float(p[1]), float(p[2]), float(p[3])
|
||||
V.append((x, z, y)) # game (x, y, -z) -> Blender (x, -game_z, y) = (x, z_obj, y_obj)
|
||||
V.append((x, -z, y)) # OBJ = game space (right-handed, y up) -> Blender z up
|
||||
C.append((float(p[4]), float(p[5]), float(p[6])) if len(p) >= 7 else (1, 1, 1))
|
||||
elif ln.startswith('vt '):
|
||||
p = ln.split(); VT.append((float(p[1]), float(p[2])))
|
||||
@@ -81,7 +81,7 @@ for ln in open(os.path.join(objdir, 'level.obj')):
|
||||
q = t.split('/')
|
||||
vi.append(int(q[0]) - 1)
|
||||
ti.append(int(q[1]) - 1 if len(q) > 1 and q[1] else -1)
|
||||
faces.append((vi[::-1], ti[::-1], mat)) # mirroring -> reverse winding
|
||||
faces.append((vi, ti, mat))
|
||||
|
||||
matnames = sorted(set(f[2] for f in faces))
|
||||
mesh = bpy.data.meshes.new('level')
|
||||
|
||||
Reference in new issue
Block a user