Files
sow-module-tilesets/wgt01/generator/make_wok.py
T
2026-07-04 18:03:35 +02:00

119 lines
4.2 KiB
Python

"""Generate per-tile ASCII walkmeshes (`twu01_<code>.wok`) from tiles.py.
python make_wok.py [outdir] # default: ./wok
Pure Python, no Blender. Emits the Aurora ASCII walkmesh node (verts + faces with
per-face surface material + an AABB bounding-volume tree). This is the usable
intermediate the toolset/cleanmodels/nwnmdlcomp compile to a binary `.wok`
(AGENTS.md §6); NeverBlender would otherwise produce this. Geometry comes from
tiles.wok_quads so it matches the Blender WOK_/BLOCK_ proxies exactly.
Surface material ids (surfacemat.2da, AGENTS.md §3.3): walkable faces use WALK,
non-walkable proxy faces use BLOCK. Verify these two ids against your
surfacemat.2da before shipping.
"""
import os
import sys
import tiles
WALK = 4 # surfacemat.2da row 4 = Stone, walkable (Neverblender wok_Stone)
BLOCK = 7 # surfacemat.2da row 7 = Nonwalk, non-walkable (wok_Nonwalk)
def geometry(spec):
"""Return (verts, faces): dedup verts, faces = (a, b, c, surfacemat)."""
verts, vmap, faces = [], {}, []
def vid(v):
key = tuple(round(c, 4) for c in v)
if key not in vmap:
vmap[key] = len(verts)
verts.append(key)
return vmap[key]
for quad, walkable in tiles.wok_quads(spec):
s = WALK if walkable else BLOCK
a, b, c, d = (vid(p) for p in quad)
faces.append((a, b, c, s)) # triangulate quad -> 2 tris
faces.append((a, c, d, s))
return verts, faces
def _box(verts, face):
pts = [verts[face[0]], verts[face[1]], verts[face[2]]]
return ([min(p[i] for p in pts) for i in range(3)],
[max(p[i] for p in pts) for i in range(3)])
def aabb_tree(verts, faces):
"""Binary BVH, pre-order (node, left subtree, right subtree). Leaf carries its
face index; internal carries -1. Reader reconstructs by the same recursion."""
nodes = []
def rec(items): # items = [(face_index, (min, max)), ...]
mn = [min(b[0][i] for _, b in items) for i in range(3)]
mx = [max(b[1][i] for _, b in items) for i in range(3)]
if len(items) == 1:
nodes.append((mn, mx, items[0][0]))
return
ax = max(range(3), key=lambda i: mx[i] - mn[i]) # split longest axis
items.sort(key=lambda ib: ib[1][0][ax] + ib[1][1][ax]) # by box centre
mid = len(items) // 2
nodes.append((mn, mx, -1)) # internal placeholder
rec(items[:mid])
rec(items[mid:])
rec([(i, _box(verts, f)) for i, f in enumerate(faces)])
assert sum(1 for _, _, fi in nodes if fi >= 0) == len(faces) # every face is a leaf
assert len(nodes) == 2 * len(faces) - 1 # full binary tree
return nodes
def aabb_node(spec):
"""The `node aabb <ref>_wok ... endnode` block (reused by make_mdl embed)."""
verts, faces = geometry(spec)
nodes = aabb_tree(verts, faces)
name = tiles.resref(spec)
L = [f"node aabb {name}_wg",
f" parent {name}",
" position 0.0 0.0 0.0",
" orientation 0.0 0.0 0.0 0.0",
" bitmap NULL",
" ambient 0.0 0.0 0.0",
" diffuse 0.0 0.0 0.0",
" specular 0.0 0.0 0.0",
" render 0",
" shadow 0",
f" verts {len(verts)}"]
L += [f" {v[0]:.5f} {v[1]:.5f} {v[2]:.5f}" for v in verts]
L.append(f" faces {len(faces)}")
# face line: v1 v2 v3 smoothgroup tv1 tv2 tv3 surfacemat
L += [f" {a} {b} {c} 1 0 0 0 {s}" for a, b, c, s in faces]
L.append(" aabb")
L += [f" {mn[0]:.5f} {mn[1]:.5f} {mn[2]:.5f} {mx[0]:.5f} {mx[1]:.5f} {mx[2]:.5f} {fi}"
for mn, mx, fi in nodes]
L.append("endnode")
return name, L
def emit(spec):
name, L = aabb_node(spec)
head = f"# {name} walkmesh (ASCII) - compile via nwnmdlcomp/cleanmodels"
return name, head + "\n" + "\n".join(L) + "\n"
def main():
outdir = sys.argv[1] if len(sys.argv) > 1 else "wok"
os.makedirs(outdir, exist_ok=True)
for spec in tiles.TILES:
name, text = emit(spec)
with open(os.path.join(outdir, name + ".wok"), "w") as fh:
fh.write(text)
print(f"wrote {len(tiles.TILES)} .wok files to {outdir}/")
if __name__ == "__main__":
main()