wgt01: real .mdl/.wok export via Neverblender

Neverblender is installed + enabled in this machine's Blender 4.0.2 config and
confirmed working headless via the flake. Add generator/export_mdl.py:
assembles each tile as a Neverblender-native model (AuroraBase dummy/tile,
trimesh, single aabb walkmesh) in its own scene and calls scene.nvb_mdlexport
to emit real ASCII <resref>.mdl + <resref>.wok for all 40 tiles.

Walkmesh surfacemats via create_wok_materials() slot order (walkable Stone=4,
Nonwalk=7) -> verified correct per tile (CP1 all 4, CN1 all 7, BR_1/CE_S mixed).

flake.nix: add `export` app (nix run .#export -- <blend> <outdir> [CODE...]).
AGENTS.md: record the Neverblender node/surfacemat facts and the kit's tools;
note the slot-index (not material-name) surfacemat gotcha.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 17:04:41 +02:00
co-authored by Claude Opus 4.8
parent 60af184be5
commit 1284f15595
3 changed files with 148 additions and 8 deletions
+18
View File
@@ -44,6 +44,20 @@
runtimeInputs = [ py ];
text = ''exec python ${gen}/make_wok.py "$@"'';
};
# Real .mdl + .wok export via the Neverblender addon (must be enabled in
# this Blender's user config — confirmed working headless).
mkmdl = pkgs.writeShellApplication {
name = "wgt-export";
runtimeInputs = [ blender ];
text = ''
blend="''${1:?usage: wgt-export <blend> <outdir> [CODE...]}"
outdir="''${2:-mdl}"
shift 2 || true
exec blender-4.0.2 --background "$blend" --python ${gen}/export_mdl.py \
--python-exit-code 1 -- "$outdir" "$@"
'';
};
in
{
packages.${system}.default = build;
@@ -65,6 +79,10 @@
type = "app";
program = "${mkwok}/bin/wgt-wok";
};
export = {
type = "app";
program = "${mkmdl}/bin/wgt-export";
};
};
devShells.${system}.default = pkgs.mkShell {
+103
View File
@@ -0,0 +1,103 @@
"""Export every tile to real NWN ASCII .mdl + .wok via Neverblender (Blender 4.0.2).
blender-4.0.2 --background <built.blend> --python export_mdl.py -- <outdir> [CODE ...]
Requires the Neverblender addon enabled in this Blender (Import-Export:
Neverblender). Confirmed working headless against ~/.config/blender/4.0. For each
tile it assembles a Neverblender-native model in its own scene — an AuroraBase
(Empty, classification TILE, name == resref), a trimesh child (the VIS geometry),
and one aabb walkmesh child (from tiles.wok_quads) — then calls
scene.nvb_mdlexport(export_walkmesh=True) to emit <resref>.mdl and <resref>.wok.
Walkmesh surface materials use Neverblender's name->surfacemat mapping
(nvb_def.wok_materials): walkable faces = wok_Stone (idx 4), blockers = wok_Nonwalk
(idx 7). Refine per tile later (wok_Water/wok_StoneBridge/etc.) if desired.
"""
import os
import sys
import bpy
import addon_utils
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import tiles # noqa: E402
# Neverblender writes each walkmesh face's material_index (slot) verbatim as the
# surfacemat id (nvb_node.py), and create_wok_materials() fills slots 0..22 in
# nvb_def.wok_materials order. So the slot index IS the surfacemat index:
WALK_SURF = 4 # wok_Stone
BLOCK_SURF = 7 # wok_Nonwalk
def build_wok(spec, name):
from neverblender import nvb_utils
quads = tiles.wok_quads(spec)
verts, faces, surf = [], [], []
for quad, walk in quads:
b = len(verts)
verts += quad
faces.append((b, b + 1, b + 2, b + 3))
surf.append(WALK_SURF if walk else BLOCK_SURF)
me = bpy.data.meshes.new(name)
me.from_pydata(verts, [], faces)
nvb_utils.create_wok_materials(me) # slots 0..22 == wok_materials order
me.update()
for poly, s in zip(me.polygons, surf):
poly.material_index = s # slot == surfacemat id on export
ob = bpy.data.objects.new(name, me)
ob.nvb.meshtype = "aabb"
return ob
def export_tile(spec, outdir):
ref = tiles.resref(spec)
vis = bpy.data.objects.get("VIS_" + spec["code"])
if vis is None:
return {"code": spec["code"], "ok": False, "error": "missing VIS object"}
scn = bpy.data.scenes.new("exp_" + spec["code"])
bpy.context.window.scene = scn
root = bpy.data.objects.new(ref, None) # AuroraBase; name == filename
root.nvb.emptytype = "dummy"
root.nvb.classification = "tile"
scn.collection.objects.link(root)
v = vis.copy()
v.data = vis.data.copy()
v.name = ref + "_01"
v.nvb.meshtype = "trimesh"
scn.collection.objects.link(v)
v.parent = root
w = build_wok(spec, ref + "_wg") # single aabb walkmesh (rule: 1 per tile)
scn.collection.objects.link(w)
w.parent = root
mdl = os.path.join(outdir, ref + ".mdl")
bpy.ops.scene.nvb_mdlexport(filepath=mdl, export_walkmesh=True,
check_existing=False, batch_mode="OFF")
wok = os.path.join(outdir, ref + ".wok")
return {"code": spec["code"], "ref": ref, "ok": os.path.isfile(mdl),
"mdl": os.path.isfile(mdl), "wok": os.path.isfile(wok)}
def main():
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
outdir = os.path.abspath(argv[0]) if argv else os.path.abspath("mdl")
only = set(argv[1:])
os.makedirs(outdir, exist_ok=True)
addon_utils.enable("neverblender", default_set=False)
results = [export_tile(s, outdir) for s in tiles.TILES if not only or s["code"] in only]
ok = sum(1 for r in results if r["ok"])
bad = [r for r in results if not r["ok"]]
print(f"EXPORTED {ok}/{len(results)} tiles -> {outdir}")
if bad:
print("FAILED:", bad)
if __name__ == "__main__":
main()