This commit is contained in:
2026-07-04 18:03:35 +02:00
parent 605d0e73b4
commit e0fdf2ff16
8 changed files with 488 additions and 52 deletions
+140
View File
@@ -0,0 +1,140 @@
"""Generate loadable NWN tile models (`twu01_<code>.mdl` + `.wok` + textures).
python make_mdl.py [outdir] # default: ./mdl
Pure Python, no Blender. Emits Aurora ASCII models directly from tiles.py so the
result is deterministic and testable without a headless Blender/Neverblender run.
(Neverblender is kept as an alternate path in export_mdl.py, but it flattens our
multi-material tiles to one unloadable material, so this is the shipping path.)
Each tile .mdl contains:
* an AuroraBase dummy node named == resref (classification TILE),
* one trimesh node PER texture used on the tile (NWN allows one bitmap/node) —
faces triangulated, real tverts, and a `bitmap <resref>` the engine can load,
* the aabb walkmesh node (shared with make_wok, embedded per NWN tile rules).
The standalone `.wok` (same walkmesh) is written alongside.
Textures: a solid-colour <=16-char .tga per tiles.TEXTURES (KICKOFF.md: geometry
not art; flat colours double as an in-toolset grammar read-out). Real art can
replace these files later without touching geometry.
"""
import os
import sys
import tiles
import make_wok
VIS_MATID = 65536 # constant last-field on visible faces (matches stock tiles)
def write_tga(path, rgb):
"""Minimal 2x2 uncompressed 24-bit TGA in solid `rgb` (0..1 floats)."""
r, g, b = (max(0, min(255, round(c * 255))) for c in rgb)
w = h = 2
header = bytes([0, 0, 2, 0, 0, 0, 0, 0,
0, 0, 0, 0,
w & 0xFF, w >> 8, h & 0xFF, h >> 8, 24, 0])
body = bytes([b, g, r]) * (w * h) # TGA stores BGR
with open(path, "wb") as fh:
fh.write(header + body)
def _trimesh(ref, idx, tex, verts, quads):
"""One trimesh node: local verts + planar tverts + triangulated faces."""
local, lmap, tvs = [], {}, []
def li(gi):
if gi not in lmap:
x, y, z = verts[gi]
lmap[gi] = len(local)
local.append((x, y, z))
tvs.append(((x + tiles.HALF) / (2 * tiles.HALF),
(y + tiles.HALF) / (2 * tiles.HALF)))
return lmap[gi]
tris = []
for a, b, c, d in quads:
la, lb, lc, ld = li(a), li(b), li(c), li(d)
tris.append((la, lb, lc))
tris.append((la, lc, ld))
L = [f"node trimesh {ref}_{idx:02d}",
f" parent {ref}",
" position 0.00000 0.00000 0.00000",
" orientation 0.00000 0.00000 0.00000 0.00000",
" wirecolor 1.0 1.0 1.0",
" ambient 1.0 1.0 1.0",
" diffuse 1.0 1.0 1.0",
" specular 0.0 0.0 0.0",
" shininess 1",
f" bitmap {tex}",
" render 1",
" shadow 0",
" tilefade 0",
" rotatetexture 0",
f" verts {len(local)}"]
L += [f" {v[0]:.5f} {v[1]:.5f} {v[2]:.5f}" for v in local]
L.append(f" tverts {len(tvs)}")
L += [f" {u:.6f} {v:.6f} 0" for u, v in tvs]
L.append(f" faces {len(tris)}")
# face: v1 v2 v3 smoothgroup tv1 tv2 tv3 matid (tvert idx == vert idx here)
L += [f" {a} {b} {c} 1 {a} {b} {c} {VIS_MATID}" for a, b, c in tris]
L.append("endnode")
return L
def emit_mdl(spec):
ref = tiles.resref(spec)
verts, faces = tiles.vis_geometry(spec)
# group quads by texture, preserving first-seen order for stable node numbers
groups, order = {}, []
for a, b, c, d, mat in faces:
tex = tiles.texname(mat)
if tex not in groups:
groups[tex] = []
order.append(tex)
groups[tex].append((a, b, c, d))
L = [f"# {ref} - Westgate Urban Foundation MVP tile (ASCII, generated)",
f"newmodel {ref}",
f"setsupermodel {ref} null",
"classification TILE",
"setanimationscale 1.0",
f"beginmodelgeom {ref}",
f"node dummy {ref}",
" parent null",
"endnode"]
for idx, tex in enumerate(order, start=1):
L += _trimesh(ref, idx, tex, verts, groups[tex])
_, aabb = make_wok.aabb_node(spec)
L += aabb
L += [f"endmodelgeom {ref}", f"donemodel {ref}"]
return ref, "\n".join(L) + "\n", set(order)
def main():
outdir = sys.argv[1] if len(sys.argv) > 1 else "mdl"
os.makedirs(outdir, exist_ok=True)
used = set()
for spec in tiles.TILES:
ref, mdl, texs = emit_mdl(spec)
used |= texs
with open(os.path.join(outdir, ref + ".mdl"), "w") as fh:
fh.write(mdl)
_, wok = make_wok.emit(spec)
with open(os.path.join(outdir, ref + ".wok"), "w") as fh:
fh.write(wok)
# write every texture (not just `used`) so nothing dangles if a tile changes
for mat, tex in tiles.TEXTURES.items():
write_tga(os.path.join(outdir, tex + ".tga"), tiles.COLORS[mat])
print(f"wrote {len(tiles.TILES)} .mdl + .wok and "
f"{len(tiles.TEXTURES)} .tga to {outdir}/")
if __name__ == "__main__":
main()