Files
llamamod/tools/geom_export.py
T

245 lines
11 KiB
Python

#!/usr/bin/env python3
"""Level-WAD -> OBJ (+MTL, Texturseiten-PNGs, Vorschaubilder).
geom_export.py level.wad outdir [--collision] [--objects] [--no-png] [--size 1200x900]
Erzeugt in outdir:
level.obj / level.mtl Weltgeometrie (TRAK-Kacheln, platziert nach MAP), mit UVs und
Vertexfarben ("v x y z r g b"), ein OBJ-Objekt pro Kachel
page_NN.png Texturseiten (256x256, aus TEXT)
collision.obj (--collision) Kollisionspolygone, Gruppen nach Material
objects.obj (--objects) statische STPC-Objektmodelle nebeneinander
preview_top.png, preview_persp.png texturierte Software-Renderings zur Kontrolle
Koordinaten: Exportraum = MAP-Floats (y oben, rechtshaendig, z < 0). Spielraum = (x, y, -z).
Format: notes/geometry.md
"""
import argparse
import math
import os
import sys
import numpy as np
from PIL import Image
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import geomlib as G # noqa: E402
def world_tris(trak, mp, texs, use_alt=False):
"""Liefert Liste von Dreiecken: (P[3x3], UV[3x2], C[3x3] 0..1, page, texidx, tileidx)."""
out = []
for i in range(mp['N']):
x, y, z, rot, mi, fl = mp['A'][i]
m = trak[mi]
f, fn = G.tile_xform(x, y, z, rot)
W = [f(*v[:3]) for v in m.verts]
cols = mp['cols'][i][0] if mp['cols'][i] else None
for fc in m.faces:
t = texs[fc['tex']] if fc['tex'] < len(texs) else None
P = [W[k] for k in fc['v']]
if cols is not None and not (fc['flags'] & 0x4000):
C = []
for k in fc['v']:
c = cols[k]
C.append((min(255, (c & 255) * 2) / 255, min(255, (c >> 8 & 255) * 2) / 255,
min(255, (c >> 16 & 255) * 2) / 255))
else:
C = [(1, 1, 1)] * 3
if t is None or (t['flags'] & 1) or t['page'] < 0:
uv = [(0, 0)] * 3
page = -1
else:
uv = G.face_uvs(fc['flags'], G.tex_uv(t))
page = t['page']
out.append((P, uv, C, page, fc['tex'], i))
return out
def flat_color(t):
# Flache Farbe (Texturflag Bit0): u0,u1,v0 = R,G,B (Renderer FUN_00556510)
return (t['u0'] / 255, t['u1'] / 255, t['v0'] / 255)
def write_obj(path, tris, texs, mtlname):
with open(path, 'w') as fo:
fo.write('# Ein Koenigreich fuer ein Lama - Weltgeometrie (geom_export.py)\n')
fo.write(f'mtllib {mtlname}\n')
vi = 1
cur_tile = None
cur_mat = None
for P, uv, C, page, tex, tile in tris:
if tile != cur_tile:
fo.write(f'o tile_{tile:04d}\n')
cur_tile = tile
cur_mat = None
mat = f'page_{page:02d}' if page >= 0 else f'flat_{tex}'
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))
fo.write(f'f {vi}/{vi} {vi + 1}/{vi + 1} {vi + 2}/{vi + 2}\n')
vi += 3
def write_mtl(path, tris, texs, npages):
used = sorted(set((page, tex) for _, _, _, page, tex, _ in tris), key=lambda a: (a[0], a[1]))
with open(path, 'w') as fo:
for p in range(npages):
fo.write(f'newmtl page_{p:02d}\nKd 1 1 1\nmap_Kd page_{p:02d}.png\nmap_d page_{p:02d}.png\n\n')
for page, tex in used:
if page < 0:
c = flat_color(texs[tex]) if tex < len(texs) else (1, 0, 1)
fo.write(f'newmtl flat_{tex}\nKd %.3f %.3f %.3f\n\n' % c)
# ------------------------------------------------------------ Software-Renderer
def render(tris, pages, cam, size, path, texs):
Wd, Hd = size
eye, target, fov = cam
eye = np.array(eye, float); target = np.array(target, float)
fwd = target - eye; fwd /= np.linalg.norm(fwd)
right = np.cross(fwd, [0, 1, 0]); right /= np.linalg.norm(right)
up = np.cross(right, fwd)
focal = (Wd / 2) / math.tan(math.radians(fov) / 2)
img = np.zeros((Hd, Wd, 3), np.float32); img[:] = (0.12, 0.16, 0.24)
zb = np.full((Hd, Wd), np.inf, np.float32)
pg = [p.astype(np.float32) / 255 for p in pages]
L = np.array([0.35, 0.85, 0.4]); L /= np.linalg.norm(L)
for P, uv, C, page, tex, tile in tris:
P = np.array(P) - eye
cz = P @ fwd
if (cz < 0.3).any():
continue
sx = Wd / 2 + focal * (P @ right) / cz
sy = Hd / 2 - focal * (P @ up) / cz
x0, x1 = int(max(0, math.floor(sx.min()))), int(min(Wd - 1, math.ceil(sx.max())))
y0, y1 = int(max(0, math.floor(sy.min()))), int(min(Hd - 1, math.ceil(sy.max())))
if x0 > x1 or y0 > y1:
continue
den = (sy[1] - sy[2]) * (sx[0] - sx[2]) + (sx[2] - sx[1]) * (sy[0] - sy[2])
if abs(den) < 1e-9:
continue
xs, ys = np.meshgrid(np.arange(x0, x1 + 1) + 0.5, np.arange(y0, y1 + 1) + 0.5)
w0 = ((sy[1] - sy[2]) * (xs - sx[2]) + (sx[2] - sx[1]) * (ys - sy[2])) / den
w1 = ((sy[2] - sy[0]) * (xs - sx[2]) + (sx[0] - sx[2]) * (ys - sy[2])) / den
w2 = 1 - w0 - w1
inside = (w0 >= 0) & (w1 >= 0) & (w2 >= 0)
if not inside.any():
continue
iz = w0 / cz[0] + w1 / cz[1] + w2 / cz[2]
depth = 1 / np.where(inside, iz, 1e-9)
sub = zb[y0:y1 + 1, x0:x1 + 1]
# Farbe
nrm = np.cross(np.array(P[1]) - P[0], np.array(P[2]) - P[0])
nl = np.linalg.norm(nrm)
shade = 0.55 + 0.45 * abs(nrm @ L) / nl if nl > 0 else 1
Cc = np.array(C)
if page >= 0:
UV = np.array(uv)
pu = (w0 / cz[0] * UV[0, 0] + w1 / cz[1] * UV[1, 0] + w2 / cz[2] * UV[2, 0]) * depth
pv = (w0 / cz[0] * UV[0, 1] + w1 / cz[1] * UV[1, 1] + w2 / cz[2] * UV[2, 1]) * depth
tp = pg[page]
tx = np.clip((pu * tp.shape[1]).astype(int), 0, tp.shape[1] - 1)
ty = np.clip((pv * tp.shape[0]).astype(int), 0, tp.shape[0] - 1)
texel = tp[ty, tx]
alpha = texel[..., 3] > 0.5
col = texel[..., :3]
else:
t = texs[tex] if tex < len(texs) else None
col = np.broadcast_to(np.array(flat_color(t) if t else (1, 0, 1), np.float32), xs.shape + (3,))
alpha = np.ones(xs.shape, bool)
vc = (w0[..., None] * Cc[0] + w1[..., None] * Cc[1] + w2[..., None] * Cc[2])
col = col * vc * shade
mask = inside & alpha & (depth < sub)
sub[mask] = depth[mask]
img[y0:y1 + 1, x0:x1 + 1][mask] = col[mask]
Image.fromarray((np.clip(img, 0, 1) * 255).astype(np.uint8)).save(path)
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('wad'); ap.add_argument('outdir')
ap.add_argument('--collision', action='store_true')
ap.add_argument('--objects', action='store_true')
ap.add_argument('--no-png', action='store_true', help='keine Seiten-PNGs/Vorschau')
ap.add_argument('--size', default='1200x900')
a = ap.parse_args()
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_mtl(os.path.join(a.outdir, 'level.mtl'), tris, texs, len(pages))
print(f'{a.wad}: WFPC={wf:#x} TRAK-Modelle={len(trak)} Kacheln={mp["N"]} Raster={mp["W"]}x{mp["H"]} '
f'Dreiecke={len(tris)} Texturen={len(texs)} Seiten={len(pages)}')
if a.collision:
with open(os.path.join(a.outdir, 'collision.obj'), 'w') as fo:
vi = 1
for i in range(mp['N']):
x, y, z, rot, mi, fl = mp['A'][i]
f, _ = G.tile_xform(x, y, z, rot)
for g, grp in enumerate(trak[mi].coll):
for r in grp:
c = G.coll_decode(r)
pts = G.coll_polygon(c)
if len(pts) < 3:
continue
nrm = np.cross(np.subtract(pts[1], pts[0]), np.subtract(pts[2], pts[0]))
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))
fo.write('f ' + ' '.join(str(vi + k) for k in range(len(pts))) + '\n')
vi += len(pts)
if a.objects:
models, _ = G.parse_stpc_models(G.wad_get(ch, 'STPC'))
with open(os.path.join(a.outdir, 'objects.obj'), 'w') as fo:
fo.write('mtllib level.mtl\n')
vi = 1; vt = 1; ox = 0.0
for k, m in enumerate(models):
if not m.verts:
continue
xs = [v[0] for v in m.verts]
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]))
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:
uv = G.face_uvs(fc['flags'], G.tex_uv(t)); fo.write(f'usemtl page_{t["page"]:02d}\n')
else:
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')
vt += 3
vi += len(m.verts)
ox += max(xs) - min(xs) + 0.5
if not a.no_png:
imgs = [G.page_rgba(p) for p in pages]
for k, im in enumerate(imgs):
Image.fromarray(im).save(os.path.join(a.outdir, f'page_{k:02d}.png'))
size = tuple(int(s) for s in a.size.split('x'))
P = np.array([p for t in tris for p in t[0]])
lo, hi = P.min(0), P.max(0); c = (lo + hi) / 2
ext = max(hi[0] - lo[0], hi[2] - lo[2])
# Draufsicht (fast senkrecht, Norden = -z oben)
render(tris, imgs, ((c[0], hi[1] + ext * 1.2, c[2] + 0.01), (c[0], lo[1], c[2]), 50), size,
os.path.join(a.outdir, 'preview_top.png'), texs)
render(tris, imgs, ((c[0] + ext * 0.35, hi[1] + ext * 0.45, hi[2] + ext * 0.45), tuple(c), 60), size,
os.path.join(a.outdir, 'preview_persp.png'), texs)
if __name__ == '__main__':
main()