wgt01: tileset build guide + 40-tile generator kit
AGENTS.md: NWN:EE tileset construction reference (constraints, .mdl/.wok walkmesh rules, .set format, Blender->NWN pipeline, prototype->NWN mapping). wgt01/: Westgate Urban Foundation MVP kit. - generator/tiles.py: single source of truth (40-tile table, materials, 12x12 test map, pure paint/height/walkmesh/.set derivations). - generator/build.py: Blender 4.0.2 scene generator (VIS_/WOK_/BLOCK_ per tile, edge metadata, library grid + 12x12 district previews). - generator/make_wok.py: ASCII walkmesh emitter (verts+faces+surfacemat+AABB tree) from the same geometry. - generator/make_set.py: first-pass twu01.set (CRLF) from the tile grammar. - flake.nix: build/wok/set apps + dev shell (pinned Blender 4.0.2, NWN tools). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""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 = 1 # a walkable stone-ish row in surfacemat.2da (verify)
|
||||
BLOCK = 7 # a non-walkable row (verify)
|
||||
|
||||
|
||||
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 emit(spec):
|
||||
verts, faces = geometry(spec)
|
||||
nodes = aabb_tree(verts, faces)
|
||||
name = tiles.resref(spec)
|
||||
L = [f"# {name} walkmesh (ASCII, first-pass) - compile via nwnmdlcomp/cleanmodels",
|
||||
f"node aabb {name}_wok",
|
||||
f" parent {name}",
|
||||
" position 0.0 0.0 0.0",
|
||||
" orientation 0.0 0.0 0.0 0.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, "\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()
|
||||
Reference in New Issue
Block a user