initial commit: llamamod 0.2.0 – reverse-engineered level formats (wad, map, trak, text, stpc), python tools and notes, java mod manager (iso install, mod pipeline with exe patching, save editor, wine/windows launcher) with mods (test world, debug wireframe, mirror, randomizer, fog, cheats, intro skip, practice, free camera, boot to level, level projects) and the 3d level editor (geometry, objects, cross-world import, environment, textures, play-test, manual)
This commit is contained in:
commit
b36a366240
826 files changed
+105304
No files matched your search
@@ -0,0 +1,244 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user