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>
167 lines
5.3 KiB
Python
167 lines
5.3 KiB
Python
"""Build the Westgate_Urban_Foundation_MVP Blender scene (run in Blender 4.0.2).
|
|
|
|
blender-4.0.2 --background --python build.py -- out.blend
|
|
|
|
Realises tiles.py: one Collection per tile (VIS_/WOK_/BLOCK_ sub-objects + edge
|
|
custom props), a TILE_EDGE_METADATA text block, and the two preview assemblies.
|
|
Geometry is intentionally simple (KICKOFF.md: a geometry/grammar prototype).
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import bpy
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import tiles # noqa: E402
|
|
|
|
|
|
def get_mat(name):
|
|
m = bpy.data.materials.get(name)
|
|
if m:
|
|
return m
|
|
m = bpy.data.materials.new(name)
|
|
r, g, b = tiles.COLORS.get(name, (0.5, 0.5, 0.5))
|
|
m.diffuse_color = (r, g, b, 1.0) # viewport (solid) colour
|
|
m.use_nodes = True
|
|
bsdf = m.node_tree.nodes.get("Principled BSDF")
|
|
if bsdf:
|
|
bsdf.inputs["Base Color"].default_value = (r, g, b, 1.0)
|
|
return m
|
|
|
|
|
|
def build_vis(spec):
|
|
"""Subdivided 10x10 plane (100 quads): per-face material via paint(), per-vertex Z."""
|
|
n, step = 10, 1.0
|
|
paint = tiles.make_paint(spec)
|
|
verts, vidx = [], {}
|
|
|
|
def vi(i, j):
|
|
if (i, j) not in vidx:
|
|
x, y = -5 + i * step, -5 + j * step
|
|
vidx[(i, j)] = len(verts)
|
|
verts.append((x, y, tiles.height(spec, x, y)))
|
|
return vidx[(i, j)]
|
|
|
|
faces, face_mat, mats, midx = [], [], [], {}
|
|
for i in range(n):
|
|
for j in range(n):
|
|
faces.append((vi(i, j), vi(i + 1, j), vi(i + 1, j + 1), vi(i, j + 1)))
|
|
cx, cy = -5 + (i + 0.5) * step, -5 + (j + 0.5) * step
|
|
mn = paint(cx, cy)
|
|
if mn not in midx:
|
|
midx[mn] = len(mats)
|
|
mats.append(mn)
|
|
face_mat.append(midx[mn])
|
|
|
|
me = bpy.data.meshes.new("VIS_" + spec["code"])
|
|
me.from_pydata(verts, [], faces)
|
|
for mn in mats:
|
|
me.materials.append(get_mat(mn))
|
|
for poly, mi in zip(me.polygons, face_mat):
|
|
poly.material_index = mi
|
|
me.update()
|
|
ob = bpy.data.objects.new("VIS_" + spec["code"], me)
|
|
n_, e_, s_, w_ = spec["edges"]
|
|
ob["edge_N"], ob["edge_E"], ob["edge_S"], ob["edge_W"] = n_, e_, s_, w_
|
|
return ob
|
|
|
|
|
|
def build_quads(name, quads, matname):
|
|
if not quads:
|
|
return None
|
|
verts, faces = [], []
|
|
for q in quads:
|
|
b = len(verts)
|
|
verts += q
|
|
faces.append((b, b + 1, b + 2, b + 3))
|
|
me = bpy.data.meshes.new(name)
|
|
me.from_pydata(verts, [], faces)
|
|
me.materials.append(get_mat(matname))
|
|
me.update()
|
|
return bpy.data.objects.new(name, me)
|
|
|
|
|
|
def build_tile(spec):
|
|
col = bpy.data.collections.new(f"{spec['code']}_{spec['name']}")
|
|
bpy.context.scene.collection.children.link(col)
|
|
col.objects.link(build_vis(spec))
|
|
quads = tiles.wok_quads(spec)
|
|
wok = build_quads("WOK_" + spec["code"], [q for q, w in quads if w], "MAT_debug_walkmesh")
|
|
blk = build_quads("BLOCK_" + spec["code"], [q for q, w in quads if not w], "MAT_debug_blocker")
|
|
if wok:
|
|
col.objects.link(wok)
|
|
if blk:
|
|
col.objects.link(blk)
|
|
return col
|
|
|
|
|
|
def instance(col, loc, parent, name):
|
|
ob = bpy.data.objects.new(name, None)
|
|
ob.instance_type = "COLLECTION"
|
|
ob.instance_collection = col
|
|
ob.location = loc
|
|
parent.objects.link(ob)
|
|
|
|
|
|
def label(text, loc, parent):
|
|
cu = bpy.data.curves.new(text, "FONT")
|
|
cu.body = text
|
|
cu.size = 1.4
|
|
ob = bpy.data.objects.new("LBL_" + text, cu)
|
|
ob.location = loc
|
|
parent.objects.link(ob)
|
|
|
|
|
|
def main():
|
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
|
out = os.path.abspath(argv[0]) if argv else os.path.abspath("westgate_urban_foundation_mvp.blend")
|
|
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
bpy.context.scene.unit_settings.system = "METRIC"
|
|
|
|
tilecol = {s["code"]: build_tile(s) for s in tiles.TILES}
|
|
|
|
# edge metadata text block (KICKOFF.md)
|
|
txt = bpy.data.texts.new("TILE_EDGE_METADATA")
|
|
for s in tiles.TILES:
|
|
n_, e_, s_, w_ = s["edges"]
|
|
txt.write(f"{s['code']}_{s['name']}\nN: {n_}\nE: {e_}\nS: {s_}\nW: {w_}\n\n")
|
|
|
|
# library grid preview (8 wide, 12 m pitch, labelled)
|
|
grid = bpy.data.collections.new("PREVIEW_tile_library_grid")
|
|
bpy.context.scene.collection.children.link(grid)
|
|
cols = 8
|
|
for i, s in enumerate(tiles.TILES):
|
|
gx, gy = (i % cols) * 12, -(i // cols) * 12
|
|
instance(tilecol[s["code"]], (gx, gy, 0), grid, "GRID_" + s["code"])
|
|
label(s["code"], (gx - 4.5, gy + 5.5, 0), grid)
|
|
|
|
# 12x12 test district (10 m pitch), row south-ward, col east-ward
|
|
dist = bpy.data.collections.new("PREVIEW_12x12_test_district")
|
|
bpy.context.scene.collection.children.link(dist)
|
|
skipped = []
|
|
for r, row in enumerate(tiles.MAP):
|
|
for c, code in enumerate(row):
|
|
tc = tilecol.get(code)
|
|
if tc is None:
|
|
skipped.append([r, c, code])
|
|
continue
|
|
instance(tc, (c * 10, -r * 10, 0), dist, f"D_{r:02d}_{c:02d}_{code}")
|
|
|
|
bpy.ops.wm.save_as_mainfile(filepath=out)
|
|
|
|
report = {
|
|
"blend": out,
|
|
"tile_collections": len(tilecol),
|
|
"district_cells": sum(len(r) for r in tiles.MAP),
|
|
"district_skipped": skipped,
|
|
"materials": len(tiles.COLORS),
|
|
}
|
|
print("<<<WGT_REPORT_BEGIN>>>" + json.dumps(report, indent=2) + "<<<WGT_REPORT_END>>>")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|