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:
@@ -273,17 +273,31 @@ tracks Blender 3.x–4.x, **not** nixpkgs' Blender 5.x. Do not "upgrade" it.
|
||||
|
||||
### 6.1 Tools (all on `PATH` in the dev shell)
|
||||
- **NeverBlender** — Blender addon that exports NWN `.mdl` + `.wok` and defines
|
||||
node types (trimesh, **AABB walkmesh**, dummy, light). Install a NeverBlender
|
||||
release compatible with **Blender 4.0.x** into this Blender
|
||||
(`Edit ▸ Preferences ▸ Add-ons ▸ Install`). It is **not** bundled in the stock
|
||||
binary — an agent cannot assume it is present; check and tell the user if a
|
||||
real MDL export is requested.
|
||||
node types (trimesh, **AABB walkmesh**, dummy, light). It is **not** bundled in
|
||||
the stock binary, but is **installed and enabled in this machine's Blender 4.0.2
|
||||
user config** (`~/.config/blender/4.0`) and works headless — verified via the
|
||||
flake's `blender-4.0.2 --background`. `wgt01/generator/export_mdl.py` drives it
|
||||
(see below). If working on another machine, don't assume it's present; check
|
||||
`addon_utils.enable("neverblender")` and tell the user.
|
||||
Key facts: root = Empty `nvb.emptytype='dummy'` + `nvb.classification='tile'`
|
||||
(name == resref); geometry meshes `nvb.meshtype='trimesh'`; one walkmesh mesh
|
||||
`nvb.meshtype='aabb'`. Walkmesh face surfacemat = the mesh's material *slot*
|
||||
index after `nvb_utils.create_wok_materials(mesh)` fills slots 0..22 in
|
||||
`nvb_def.wok_materials` order (walkable Stone=4, Nonwalk=7). Export:
|
||||
`scene.nvb_mdlexport(filepath=..., export_walkmesh=True)` on the active scene.
|
||||
- **cleanmodels** — cleans/optimizes ASCII MDL, fixes common walkmesh problems.
|
||||
- **neverwinter-nim** — CLI (`nwn_erf`, `nwn_gff`, …): pack the `.hak`/ERF, and
|
||||
read/write GFF (the `.itp` is GFF).
|
||||
- **blender-mcp-nwn** (see `/nix/nixos/blender-mcp-nwn`) — MCP server exposing
|
||||
`run_blender_script`, `read_blend_metadata`, `export_for_nwn` so Claude can
|
||||
drive Blender 4.0.2 headless against these `.blend` sources.
|
||||
- **wgt01 generator kit** (`wgt01/generator/`, driven by `wgt01/flake.nix` apps):
|
||||
`build.py` (`nix run .#build`) builds the `.blend` tile library from the shared
|
||||
`tiles.py` table; `export_mdl.py` (`nix run .#export -- <blend> <outdir>`)
|
||||
exports real `.mdl`+`.wok` per tile via NeverBlender; `make_wok.py`
|
||||
(`nix run .#wok`) and `make_set.py` (`nix run .#set`) are pure-Python (no
|
||||
Blender) emitters — an ASCII `.wok` fallback (format validated against
|
||||
NeverBlender's own output) and a first-pass `.set`.
|
||||
|
||||
### 6.2 Build → export → pack
|
||||
1. **Generate `.blend` source** — `nix run .#build -- out.blend` (or drive via the
|
||||
@@ -353,9 +367,14 @@ tracks Blender 3.x–4.x, **not** nixpkgs' Blender 5.x. Do not "upgrade" it.
|
||||
- **Custom doors not in `doortypes.2da`** (and not merged/shipped) → blank doors.
|
||||
- **`Transition=0`** disables adjacent placement of groups/features.
|
||||
- **Group first tile `-1` or non-unique filename** → crash / no autopick.
|
||||
- **NeverBlender not installed** in this Blender → `export_for_nwn` gives you
|
||||
OBJ/FBX + an `mdl.json` config, **not** a real `.mdl`. Install the addon for
|
||||
true MDL/WOK export; say so rather than claiming MDL output.
|
||||
- **NeverBlender walkmesh surfacemat** comes from the face's material *slot*
|
||||
index, **not** the material name — you must call
|
||||
`nvb_utils.create_wok_materials(mesh)` first (fills slots 0..22) then set each
|
||||
face's `material_index` to the surfacemat id. Naming a lone material `wok_Stone`
|
||||
is silently ignored (falls back to slot order). `export_mdl.py` does this right.
|
||||
- **NeverBlender not installed** (other machines) → `export_for_nwn` gives you
|
||||
OBJ/FBX + an `mdl.json` config, **not** a real `.mdl`. Use `make_wok.py` for an
|
||||
ASCII `.wok` fallback, or install the addon; say so rather than claiming MDL.
|
||||
- **Don't upgrade Blender past 4.x** here — breaks NeverBlender/cleanmodels.
|
||||
|
||||
---
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user