tools
This commit is contained in:
@@ -96,6 +96,58 @@ Keep the terrain/crosser vocabulary **small and deliberate** per tileset.
|
|||||||
Visual water/void may dip (e.g. canal at Z = −0.3) **only under a non-walkable
|
Visual water/void may dip (e.g. canal at Z = −0.3) **only under a non-walkable
|
||||||
walkmesh** — never a playable under/over space (matches `KICKOFF.md`).
|
walkmesh** — never a playable under/over space (matches `KICKOFF.md`).
|
||||||
|
|
||||||
|
### 2.3 How a builder places tiles — TWO systems (this defines "paintable")
|
||||||
|
|
||||||
|
The toolset has two **independent** placement systems. Confusing them is why a
|
||||||
|
palette can list tile names that refuse to place (the wgt01 2026-07-04 symptom).
|
||||||
|
|
||||||
|
**System 1 — Paint Terrain / Paint Features (this lays the ground).** The builder
|
||||||
|
picks a **terrain** (or a **crosser**) and brushes it across the grid. The toolset
|
||||||
|
then **auto-selects which tile model to drop** by matching the four **corner
|
||||||
|
terrains + heights** (and edge **crossers**) against the neighbours — the builder
|
||||||
|
never picks a specific model. This is how streets/plazas/canals actually get laid.
|
||||||
|
It needs, in the `.set`:
|
||||||
|
- `[TERRAIN TYPES]` / `[CROSSER TYPES]` — the paintable vocabulary;
|
||||||
|
- enough `[TILE#]` entries that **every corner combination the brush can produce**
|
||||||
|
is covered by at least one tile (else the brush has nothing to place);
|
||||||
|
- `[GENERAL]`: `Default` (terrain that fills a new area), `Border` (area edge),
|
||||||
|
`Floor` (the terrain the **eraser** paints), `HasHeightTransition=1` (turns on
|
||||||
|
Raise/Lower).
|
||||||
|
`[PRIMARY RULES]` only *refine* auto-picking (cascading corner transitions) and are
|
||||||
|
**optional** — stock custom sets ship `Count=0` (verified: `wut01.set`). **Eraser,
|
||||||
|
Raise/Lower, and fill are built-in toolbar actions driven by those `[GENERAL]`
|
||||||
|
fields — they are not palette entries.**
|
||||||
|
|
||||||
|
**System 2 — the palette (`.itp`): Features & Groups only.** The right-side
|
||||||
|
"Standard" tree click-places **prefabs**: a *feature* (1×1) or a *group* (N×M). A
|
||||||
|
palette entry is placeable **only if it is backed by a `[GROUP#]` in the `.set`** —
|
||||||
|
the entry's `RESREF` is that group's **first-tile** model name and its `NAME`
|
||||||
|
matches the group's `Name`; the toolset locates the first tile in `[GROUPS]` and
|
||||||
|
pulls the rest from there. A palette entry pointing at a **raw tile with no
|
||||||
|
`[GROUP#]`** shows its name but **cannot be placed**.
|
||||||
|
|
||||||
|
**Verified palette shape** (`wut01palstd.itp`, GFF `ITP `): `MAIN` = a few
|
||||||
|
**category** structs, each `{ID byte, STRREF (dialog.tlk folder label), LIST}`;
|
||||||
|
leaves are `{NAME cexostring, RESREF}`. `RESREF` means different things per folder:
|
||||||
|
|
||||||
|
| Folder (its `STRREF`) | Leaf `RESREF` is… | Holds |
|
||||||
|
|---|---|---|
|
||||||
|
| **Terrain** (8282) | the **terrain/crosser _name_** (`Sand`, `Bridge`, …), **plus a magic `eraser` leaf** (STRREF 63291, no `NAME`) | the paintable terrains + crossers + eraser — i.e. the "Terrain" section a builder expects |
|
||||||
|
| **Features** (63261) | the feature's **first-tile resref** | 1×1 prefabs; `NAME` == its `[GROUP#]` `Name` |
|
||||||
|
| **Groups** (63262) | the group's **first-tile resref** | multi-tile prefabs (3×3, 4×1, 3×5…) |
|
||||||
|
|
||||||
|
So the "Terrain" section (paintable terrains + eraser) **is category-2 of the
|
||||||
|
`.itp`** — terrains listed by *name* with an `eraser` entry — not a list of models.
|
||||||
|
The three folder `STRREF`s above are stock dialog.tlk labels; reuse them.
|
||||||
|
|
||||||
|
**wgt01's bug (root cause):** its `.itp` was **one flat folder of 40 raw tile
|
||||||
|
resrefs** — no Terrain folder, no `eraser`, and the `.set` had `[GROUPS] Count=0`.
|
||||||
|
So none of the 40 entries is a feature/group → names render but nothing places; and
|
||||||
|
with no Terrain folder there's no paint/eraser UI at all. **Fix:** emit the
|
||||||
|
3-folder palette (Terrain+eraser / Features / Groups) and add `[GROUP#]` entries
|
||||||
|
for anything click-placeable, then lay ground via **Paint Terrain**, not by
|
||||||
|
clicking tiles. See `wgt01/STATUS.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. The tile model (`.mdl`)
|
## 3. The tile model (`.mdl`)
|
||||||
@@ -247,7 +299,7 @@ and `Tile0..` indices **ordered bottom-left → right → up → top-right**. A
|
|||||||
| File | Purpose | Notes |
|
| File | Purpose | Notes |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `<prefix>.set` | tileset definition | CRLF, references tiles by index |
|
| `<prefix>.set` | tileset definition | CRLF, references tiles by index |
|
||||||
| `<prefix>palstd.itp` | toolset palette tree | **GFF binary**; references tiles **by name** — must match `.set` |
|
| `<prefix>palstd.itp` | toolset palette tree | **GFF binary**; 3 folders — Terrain+eraser / Features / Groups (§2.3). Feature/Group leaves must be backed by a `[GROUP#]`; Terrain leaves are terrain/crosser **names**. |
|
||||||
| `*.mdl` | tile models (ASCII) | one per tile/variant |
|
| `*.mdl` | tile models (ASCII) | one per tile/variant |
|
||||||
| `*.wok` | tile walkmeshes | baked from the AABB node |
|
| `*.wok` | tile walkmeshes | baked from the AABB node |
|
||||||
| `*.tga` / `*.dds` | minimap icons (`ImageMap2D`) | ≥16×16 px, name ≤16 chars |
|
| `*.tga` / `*.dds` | minimap icons (`ImageMap2D`) | ≥16×16 px, name ≤16 chars |
|
||||||
@@ -291,6 +343,15 @@ tracks Blender 3.x–4.x, **not** nixpkgs' Blender 5.x. Do not "upgrade" it.
|
|||||||
- **blender-mcp-nwn** (see `/nix/nixos/blender-mcp-nwn`) — MCP server exposing
|
- **blender-mcp-nwn** (see `/nix/nixos/blender-mcp-nwn`) — MCP server exposing
|
||||||
`run_blender_script`, `read_blend_metadata`, `export_for_nwn` so Claude can
|
`run_blender_script`, `read_blend_metadata`, `export_for_nwn` so Claude can
|
||||||
drive Blender 4.0.2 headless against these `.blend` sources.
|
drive Blender 4.0.2 headless against these `.blend` sources.
|
||||||
|
- **Community authoring/inspection tools** (not in-repo; handy for reverse-checking
|
||||||
|
our generated files against stock sets):
|
||||||
|
[SetEditor 0.85 (Jlen)](https://neverwintervault.org/project/nwn1/other/tool/set-editor-beta-085)
|
||||||
|
— GUI `.set` editor; [BioWare GFF Editor](https://neverwintervault.org/project/nwn1/other/tool/gffeditor)
|
||||||
|
— edit `.itp`/GFF; [NWN Explorer Reborn](https://neverwintervault.org/project/nwn1/other/tool/nwn-explorer-reborn)
|
||||||
|
— browse/extract stock BIFs (pull a reference `.set`/`.itp`);
|
||||||
|
[Basic Interior Tileset Template (Symphony)](https://neverwintervault.org/project/nwn1/hakpak/tileset/basic-interior-tileset-template)
|
||||||
|
— minimal working set to diff against. We can inspect any of these headlessly via
|
||||||
|
`nwn_gff` / `nwn_erf` instead of the GUIs.
|
||||||
- **wgt01 generator kit** (`wgt01/generator/`, driven by `wgt01/flake.nix` apps):
|
- **wgt01 generator kit** (`wgt01/generator/`, driven by `wgt01/flake.nix` apps):
|
||||||
`build.py` (`nix run .#build`) builds the `.blend` tile library from the shared
|
`build.py` (`nix run .#build`) builds the `.blend` tile library from the shared
|
||||||
`tiles.py` table; `export_mdl.py` (`nix run .#export -- <blend> <outdir>`)
|
`tiles.py` table; `export_mdl.py` (`nix run .#export -- <blend> <outdir>`)
|
||||||
@@ -383,6 +444,32 @@ area with the tileset and walk-test. Much faster than pack→reload; pack the
|
|||||||
- **NeverBlender not installed** (other machines) → `export_for_nwn` gives you
|
- **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
|
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.
|
ASCII `.wok` fallback, or install the addon; say so rather than claiming MDL.
|
||||||
|
- **NeverBlender flattens multi-material tiles.** A mesh with N materials exports
|
||||||
|
as **one** `trimesh` with a single arbitrary `materialname` — the per-face
|
||||||
|
grammar is lost. NWN wants one `trimesh` node **per texture**. Either split the
|
||||||
|
mesh by material into separate objects before export, or emit the `.mdl`
|
||||||
|
directly (wgt01's `make_mdl.py` does the latter, in pure Python).
|
||||||
|
- **A tile `.mdl` needs `tverts` + `bitmap`.** No UV map → NeverBlender emits no
|
||||||
|
`tverts`; a node-based material with no image → `materialname` (a `.mtr`
|
||||||
|
lookup) instead of a loadable `bitmap`. A trimesh with faces but no tverts/
|
||||||
|
bitmap won't load → **blank tileset name + "could not create area"** in the New
|
||||||
|
Area wizard. Give each material a `bitmap <resref≤16>` and ship the `.tga`.
|
||||||
|
- **Toolset needs `<set>palstd.itp`** to register a tileset. Every stock/custom
|
||||||
|
set ships one (GFF `ITP `: `MAIN` list of category structs, each `ID` byte +
|
||||||
|
`LIST` of tile entries). Missing → set won't register.
|
||||||
|
- **`.itp` entries need a local `NAME` cexostring, not just a `STRREF`.** Sets
|
||||||
|
without a tileset TLK give each leaf `{NAME (cexostring), RESREF}` (verified
|
||||||
|
vs `wut01palstd.itp`) — the `NAME` is the palette label. An entry with only
|
||||||
|
`STRREF 0xFFFFFFFF` (or no name field) renders **blank**.
|
||||||
|
- **A named `.itp` leaf still won't place unless it's a real terrain or a
|
||||||
|
`[GROUP#]`-backed prefab (§2.3).** The palette only *paints terrain* or
|
||||||
|
*click-places features/groups*; it does not place raw tile models. A flat
|
||||||
|
palette of tile resrefs (wgt01's first cut) shows names but nothing is
|
||||||
|
selectable/paintable — you need a **Terrain** folder (terrains-by-name +
|
||||||
|
`eraser`) and `[GROUPS]` for any click-placeable tile. Ground is laid by **Paint
|
||||||
|
Terrain** (corner-matching auto-picks the model), not by picking tiles.
|
||||||
|
- **`WalkMesh=` in the `.set` is a label, not a file** — 95k stock tiles use
|
||||||
|
`msb01`, others use author codes; per-tile walkmesh is matched by model name.
|
||||||
- **Don't upgrade Blender past 4.x** here — breaks NeverBlender/cleanmodels.
|
- **Don't upgrade Blender past 4.x** here — breaks NeverBlender/cleanmodels.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -396,6 +483,10 @@ area with the tileset and walk-test. Much faster than pack→reload; pack the
|
|||||||
- [Walkmesh — NWNWiki](https://nwn.fandom.com/wiki/Walkmesh)
|
- [Walkmesh — NWNWiki](https://nwn.fandom.com/wiki/Walkmesh)
|
||||||
- [Tile — NWNWiki](https://nwn.fandom.com/wiki/Tile)
|
- [Tile — NWNWiki](https://nwn.fandom.com/wiki/Tile)
|
||||||
- [Common Errors and Their Causes — nwn.wiki](https://nwn.wiki/display/NWN1/Common%20Errors%20and%20Their%20Causes)
|
- [Common Errors and Their Causes — nwn.wiki](https://nwn.wiki/display/NWN1/Common%20Errors%20and%20Their%20Causes)
|
||||||
|
- [SET (file format) — nwn.wiki](https://nwn.wiki/display/NWN1/SET) — terrain/crosser/rules/groups fields (§2.3, §4)
|
||||||
|
- [Tile Path Nodes — nwn.wiki](https://nwn.wiki/spaces/NWN1/pages/139689996/Tile+Path+Nodes) — pathnode types A–Z/a–z, exit-to-region, alignment
|
||||||
|
- [Tutorial: Creating Custom Tiles (Mr X) — Neverwinter Vault](https://neverwintervault.org/article/tutorial/tutorial-creating-custom-tiles) — comprehensive walkthrough (Cloudflare-gated to WebFetch; open in a browser)
|
||||||
|
- [Resources for custom NWN tileset creation — NWN Workshop](https://www.nwnworkshop.org/forums/topic/resources-for-custom-nwn-tileset-creation/) — tool/tutorial index (SetEditor, GFF Editor, NWN Explorer, templates)
|
||||||
|
|
||||||
_Verify specifics against the wiki before relying on them; correct this file when
|
_Verify specifics against the wiki before relying on them; correct this file when
|
||||||
you find drift._
|
you find drift._
|
||||||
|
|||||||
+81
-27
@@ -15,10 +15,12 @@ Blender, so they're directly testable.
|
|||||||
| File | Role | Needs Blender? |
|
| File | Role | Needs Blender? |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `tiles.py` | **Single source of truth**: 40-tile table, 12 materials, the 12×12 test map, and all pure derivations (paint / height / walkmesh quads / `.set` corner+crosser mapping). `PREFIX=twu01`. | no |
|
| `tiles.py` | **Single source of truth**: 40-tile table, 12 materials, the 12×12 test map, and all pure derivations (paint / height / walkmesh quads / `.set` corner+crosser mapping). `PREFIX=twu01`. | no |
|
||||||
| `build.py` | Builds `westgate_urban_foundation_mvp.blend` — 40 tile collections (`VIS_`/`WOK_`/`BLOCK_` + edge custom props), `TILE_EDGE_METADATA` text block, `PREVIEW_tile_library_grid`, `PREVIEW_12x12_test_district`. | yes (4.0.2) |
|
| `build.py` | Builds `westgate_urban_foundation_mvp.blend` — 40 tile collections (`VIS_`/`WOK_`/`BLOCK_` + edge custom props), `TILE_EDGE_METADATA` text block, `PREVIEW_tile_library_grid`, `PREVIEW_12x12_test_district`. Editable preview only (quads OK — the exporters triangulate). | yes (4.0.2) |
|
||||||
| `export_mdl.py` | Real NWN export via **Neverblender** → 40 `.mdl` + 40 `.wok` (AuroraBase `tile`/`dummy`, `trimesh`, one `aabb` walkmesh; correct surfacemats). | yes + addon |
|
| `make_mdl.py` | **Shipping model path.** Pure-Python `.mdl` + `.wok` + solid-colour `.tga`: faces triangulated, one `trimesh` node **per texture** (each with `bitmap`+`tverts`), embedded `aabb` walkmesh. | no |
|
||||||
| `make_wok.py` | Pure-Python ASCII `.wok` emitter (verts+faces+surfacemat+AABB tree). Fallback when Neverblender absent — format **validated identical** to Neverblender's output. | no |
|
| `make_itp.py` | `twu01palstd.itp` (GFF `ITP ` via `nwn_gff`) — the palette the toolset needs to register the set. | no (needs `nwn_gff`) |
|
||||||
| `make_set.py` | First-pass `twu01.set` (INI, **CRLF**) from the tile grammar. | no |
|
| `make_wok.py` | Pure-Python ASCII `.wok` emitter (verts+faces+surfacemat+AABB tree); shared `aabb_node()` reused by `make_mdl`. | no |
|
||||||
|
| `make_set.py` | `twu01.set` (INI, **CRLF**) from the tile grammar; field order/values mirror stock. | no |
|
||||||
|
| `export_mdl.py` | _Alt path, not shipped._ Neverblender export — **flattens** multi-material tiles to one unloadable material, so superseded by `make_mdl.py`. | yes + addon |
|
||||||
|
|
||||||
`flake.nix` exposes these as apps + a dev shell (pinned **Blender 4.0.2**,
|
`flake.nix` exposes these as apps + a dev shell (pinned **Blender 4.0.2**,
|
||||||
`neverwinter-nim`, `cleanmodels`).
|
`neverwinter-nim`, `cleanmodels`).
|
||||||
@@ -27,26 +29,41 @@ Blender, so they're directly testable.
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd wgt01
|
cd wgt01
|
||||||
nix run .#build -- out.blend # 1. Blender tile library
|
nix run .#mdl -- mdl/ # .mdl + .wok + .tga (shipping models)
|
||||||
nix run .#export -- out.blend mdl/ # 2. real .mdl + .wok (Neverblender)
|
nix run .#set -- twu01.set # twu01.set (CRLF)
|
||||||
nix run .#set -- twu01.set # 3. first-pass .set (CRLF)
|
nix run .#itp -- twu01palstd.itp # palette (needs nwn_gff)
|
||||||
# nix run .#wok -- wok/ # ASCII .wok fallback only (export_mdl preferred)
|
nix run .#build -- out.blend # optional: editable .blend preview
|
||||||
|
# nix run .#export -- out.blend mdl/ # alt Neverblender path (flattens materials)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Verified (2026-07-04)
|
## Verified (2026-07-04)
|
||||||
|
|
||||||
- **build**: 40 collections, 144/144 district cells placed, 0 skipped, 12 materials.
|
- **models (`make_mdl`)**: 40 `.mdl` + 40 `.wok` + 12 `.tga`. Each `.mdl` has
|
||||||
- **export**: 40 `.mdl` + 40 `.wok`. Surfacemats correct — CP1 all `wok_Stone`(4),
|
`newmodel`, `classification TILE`, AuroraBase name == resref, one `trimesh`
|
||||||
CN1 all `wok_Nonwalk`(7), BR_1/CE_S walkable+blocker mixed. `.mdl` has
|
**per texture** with `bitmap`+`tverts` (tris), and the embedded `aabb`
|
||||||
`newmodel twu01_cp1`, `classification TILE`, AuroraBase name == resref.
|
walkmesh. E.g. `st_s` → 3 trimesh nodes (swlk/curb/wcob); `cp1` → 1 with
|
||||||
- **Neverblender**: enabled + working **headless** via the flake's
|
`bitmap twu01_wcob`. Walkmesh surfacemats: 4 (`wok_Stone`, walkable) / 7
|
||||||
`blender-4.0.2 --background`, reading `~/.config/blender/4.0` (same config as
|
(`wok_Nonwalk`). Matches the stock `trs02` tile structure field-for-field.
|
||||||
the interactive session). Surfacemat = walkmesh face **material slot index**
|
- **`.itp`**: `twu01palstd.itp` round-trips through `nwn_gff` — `ITP ` type,
|
||||||
after `nvb_utils.create_wok_materials(mesh)` — not the material name.
|
40 named entries. Names render, but the **structure is wrong** (flat folder of
|
||||||
- **`.set`**: CRLF (1224/1224 lines), 40 tiles, 3 terrains (`cobble`/`raised`/
|
raw tile resrefs → unplaceable, no Terrain/eraser folder). See Diagnosis below;
|
||||||
`canal`), 2 crossers (`ramp`/`canal_edge`). Corners/heights derive from each
|
needs a rebuild to the 3-folder Terrain/Features/Groups shape.
|
||||||
tile's N/E/S/W edge grammar; ramp N-corners → height 1, canal edges →
|
- **`.set`**: CRLF, 40 tiles, 3 terrains (`cobble`/`raised`/`canal`), 2 crossers
|
||||||
`canal_edge` crosser, non-walkable tiles → `PathNode=N`.
|
(`ramp`/`canal_edge`). `Version=V1.0`, stock field order (`WalkMesh=msb01`,
|
||||||
|
lights=1, `VisibilityNode=A`, `ImageMap2D` last). Corners/heights from each
|
||||||
|
tile's N/E/S/W edge grammar; non-walkable tiles → `PathNode=N`.
|
||||||
|
- **install path**: `~/Documents/Neverwinter Nights/development` is a symlink to
|
||||||
|
`~/.local/share/Neverwinter Nights/development` (the toolset's real dir); one
|
||||||
|
`nix run .#dev-install` stages all 94 loose files there.
|
||||||
|
|
||||||
|
### Why the models were rebuilt (2026-07-04, pass 2)
|
||||||
|
The Neverblender export produced tiles the toolset couldn't load — **no `tverts`,
|
||||||
|
no `bitmap`, a dangling `materialname` (>16 chars, no `.mtr`)** — and it
|
||||||
|
**flattened** each multi-material tile to a single arbitrary material. NWN needs
|
||||||
|
UV'd, bitmapped, one-texture-per-node trimeshes. Fixed by generating the `.mdl`
|
||||||
|
in pure Python (`make_mdl.py`) with full control, plus the required
|
||||||
|
`twu01palstd.itp`. Symptom that surfaced it: blank tileset name + "could not
|
||||||
|
create area" in the New Area wizard.
|
||||||
|
|
||||||
## Testing in the toolset — no `.hak` needed
|
## Testing in the toolset — no `.hak` needed
|
||||||
|
|
||||||
@@ -68,9 +85,9 @@ validated.)
|
|||||||
|
|
||||||
- **PathNode** is a placeholder (`A` walkable / `N` non-walkable) — verify creature
|
- **PathNode** is a placeholder (`A` walkable / `N` non-walkable) — verify creature
|
||||||
routing per tile shape in the toolset; real pathnode letters matter.
|
routing per tile shape in the toolset; real pathnode letters matter.
|
||||||
- **No `.itp` palette** yet — tiles may not appear in the toolset palette tree
|
- **Textures are flat solid-colour `.tga`s** (one per material, KICKOFF "geometry
|
||||||
until one is authored (GFF; `neverwinter-nim`'s `nwn_gff`). The `.set` still
|
not art"). They double as an in-toolset grammar read-out; swap in real art
|
||||||
loads for area creation.
|
later without touching geometry.
|
||||||
- **No minimaps** — `ImageMap2D` refs exist in the `.set` but the `.tga`s don't
|
- **No minimaps** — `ImageMap2D` refs exist in the `.set` but the `.tga`s don't
|
||||||
(skipped by decision; Westgate doesn't use them).
|
(skipped by decision; Westgate doesn't use them).
|
||||||
- **Substitutions**: `RA_C` = straight-ramp geometry; `STA` = smooth ramp WOK
|
- **Substitutions**: `RA_C` = straight-ramp geometry; `STA` = smooth ramp WOK
|
||||||
@@ -85,9 +102,46 @@ wgt01-tileset-kit` to promote). Commits:
|
|||||||
- `60af184` — build guide + 40-tile generator kit
|
- `60af184` — build guide + 40-tile generator kit
|
||||||
- `1284f15` — real `.mdl`/`.wok` export via Neverblender
|
- `1284f15` — real `.mdl`/`.wok` export via Neverblender
|
||||||
|
|
||||||
|
## Diagnosis 2026-07-04 — palette names show but nothing paints
|
||||||
|
|
||||||
|
Tiles now carry names, but they're **not selectable/paintable**, and the standard
|
||||||
|
**Terrain** section (paint terrains + eraser, Raise/Lower) is missing. Root cause,
|
||||||
|
confirmed against the working `wut01palstd.itp` + `wut01.set` (see ../AGENTS.md
|
||||||
|
§2.3 for the full model):
|
||||||
|
|
||||||
|
- The toolset has **two placement systems** and our `.itp` uses neither correctly.
|
||||||
|
Ground is laid by **Paint Terrain** (the toolset corner-matches and auto-picks
|
||||||
|
the model); the palette only **click-places `[GROUP#]`-backed prefabs**. It never
|
||||||
|
places raw tile models.
|
||||||
|
- Our `.itp` is **one flat folder of 40 raw tile resrefs**, no Terrain folder, no
|
||||||
|
`eraser`, and the `.set` has **`[GROUPS] Count=0`**. So: none of the 40 entries is
|
||||||
|
a feature/group → unplaceable; and with no Terrain folder there's no paint/eraser
|
||||||
|
UI. `wut01` (123 tiles) lists only its **21 groups** in the palette and paints the
|
||||||
|
rest as terrain; its `PRIMARY/SECONDARY RULES` are `Count=0`, so rules aren't the
|
||||||
|
blocker.
|
||||||
|
|
||||||
|
**The real palette (verified):** `MAIN` = 3 category structs `{ID, STRREF folder
|
||||||
|
label, LIST}`; leaves `{NAME, RESREF}`. Folder 8282 = **Terrain** (leaf RESREF =
|
||||||
|
terrain/crosser *name*, plus a magic `eraser` leaf STRREF 63291); 63261 =
|
||||||
|
**Features** (1×1, RESREF = first-tile resref); 63262 = **Groups** (N×M).
|
||||||
|
|
||||||
## Next passes
|
## Next passes
|
||||||
|
|
||||||
1. Author `twu01palstd.itp` palette so tiles show in the toolset palette tree.
|
1. **Rebuild `make_itp.py` + `make_set.py` to the real structure** (../AGENTS.md
|
||||||
2. Walk-test in the toolset (`nix run .#dev-install`) → tune PathNodes, crossers,
|
§2.3):
|
||||||
corner terrains.
|
- `.itp`: a **Terrain** folder (each `tiles.TERRAINS`/`CROSSERS` by name + an
|
||||||
3. Pack `.hak` (`nwn_erf`) once validated.
|
`eraser` leaf) so terrain-painting + eraser exist; a **Features** folder with a
|
||||||
|
1×1 entry per tile we want click-placeable.
|
||||||
|
- `.set`: add `[GROUP#]` (1×1 feature) entries backing those Features leaves
|
||||||
|
(`Name` == palette NAME, `Tile0` == that tile's index).
|
||||||
|
- Then in the toolset lay ground via **Paint Terrain** (cobble/raised/canal), not
|
||||||
|
by clicking tiles.
|
||||||
|
2. Walk-test → tune PathNodes, crossers, corner terrains; add a tileset TLK if
|
||||||
|
we later want localized palette names (local `NAME` strings suffice for now).
|
||||||
|
3. **Textures (art pass, separate note).** Source assets live in
|
||||||
|
`/home/vicky/Blender/` (e.g. `textures/`, `PBR_Procampur/`, `td_floor/`,
|
||||||
|
`new_floors/`, `pd textures main lib/`). Map each `MAT_*` in `tiles.TEXTURES`
|
||||||
|
to a chosen texture, replacing the flat solid-colour `.tga`s. **Downscale to
|
||||||
|
≤2k** — NWN handles 4k textures poorly (perf/artifacts). Geometry is
|
||||||
|
unaffected; only the `.tga`/material path changes.
|
||||||
|
4. Pack `.hak` (`nwn_erf`) once validated.
|
||||||
|
|||||||
+34
-10
@@ -45,6 +45,21 @@
|
|||||||
text = ''exec python ${gen}/make_wok.py "$@"'';
|
text = ''exec python ${gen}/make_wok.py "$@"'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Pure-Python .mdl + .wok + solid-colour .tga (the shipping model path;
|
||||||
|
# no Blender, and unlike Neverblender it keeps per-texture trimesh nodes).
|
||||||
|
mkmdlpy = pkgs.writeShellApplication {
|
||||||
|
name = "wgt-mdl";
|
||||||
|
runtimeInputs = [ py ];
|
||||||
|
text = ''exec python ${gen}/make_mdl.py "$@"'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Tileset palette (GFF ITP) so the toolset registers the set.
|
||||||
|
mkitp = pkgs.writeShellApplication {
|
||||||
|
name = "wgt-itp";
|
||||||
|
runtimeInputs = [ py pkgs.neverwinter-nim ];
|
||||||
|
text = ''exec python ${gen}/make_itp.py "$@"'';
|
||||||
|
};
|
||||||
|
|
||||||
# Real .mdl + .wok export via the Neverblender addon (must be enabled in
|
# Real .mdl + .wok export via the Neverblender addon (must be enabled in
|
||||||
# this Blender's user config — confirmed working headless).
|
# this Blender's user config — confirmed working headless).
|
||||||
mkmdl = pkgs.writeShellApplication {
|
mkmdl = pkgs.writeShellApplication {
|
||||||
@@ -59,24 +74,25 @@
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# One shot: build -> export -> .set, then stage the loose files into the
|
# One shot: models (.mdl/.wok/.tga) + .set + .itp, staged into the toolset's
|
||||||
# toolset's development/ folder (no .hak). Work dir defaults to ./build.
|
# development/ folder (no .hak, no Blender). Work dir defaults to ./build.
|
||||||
mkdev = pkgs.writeShellApplication {
|
mkdev = pkgs.writeShellApplication {
|
||||||
name = "wgt-dev-install";
|
name = "wgt-dev-install";
|
||||||
runtimeInputs = [ blender py ];
|
runtimeInputs = [ py pkgs.neverwinter-nim ];
|
||||||
text = ''
|
text = ''
|
||||||
work="''${1:-$PWD/build}"
|
work="''${1:-$PWD/build}"
|
||||||
dev="$HOME/Documents/Neverwinter Nights/development"
|
dev="$HOME/Documents/Neverwinter Nights/development"
|
||||||
mkdir -p "$work/mdl" "$dev"
|
mkdir -p "$work/mdl" "$dev"
|
||||||
blend="$work/westgate_urban_foundation_mvp.blend"
|
python ${gen}/make_mdl.py "$work/mdl" # .mdl + .wok + .tga
|
||||||
blender-4.0.2 --background --python ${gen}/build.py --python-exit-code 1 -- "$blend"
|
|
||||||
blender-4.0.2 --background "$blend" --python ${gen}/export_mdl.py --python-exit-code 1 -- "$work/mdl"
|
|
||||||
python ${gen}/make_set.py "$work/twu01.set"
|
python ${gen}/make_set.py "$work/twu01.set"
|
||||||
# replace only our own prefix in development/, leave everything else alone
|
python ${gen}/make_itp.py "$work/twu01palstd.itp"
|
||||||
rm -f "$dev"/twu01_*.mdl "$dev"/twu01_*.wok "$dev"/twu01.set
|
# replace only our own files in development/, leave everything else alone
|
||||||
cp "$work"/mdl/twu01_*.mdl "$work"/mdl/twu01_*.wok "$work"/twu01.set "$dev"/
|
rm -f "$dev"/twu01_*.mdl "$dev"/twu01_*.wok "$dev"/twu01_*.tga \
|
||||||
|
"$dev"/twu01.set "$dev"/twu01palstd.itp
|
||||||
|
cp "$work"/mdl/twu01_*.mdl "$work"/mdl/twu01_*.wok "$work"/mdl/twu01_*.tga \
|
||||||
|
"$work"/twu01.set "$work"/twu01palstd.itp "$dev"/
|
||||||
n=$(find "$work/mdl" -name 'twu01_*.mdl' | wc -l)
|
n=$(find "$work/mdl" -name 'twu01_*.mdl' | wc -l)
|
||||||
echo "installed $n tiles + twu01.set -> $dev"
|
echo "installed $n tiles + twu01.set + twu01palstd.itp -> $dev"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
@@ -100,6 +116,14 @@
|
|||||||
type = "app";
|
type = "app";
|
||||||
program = "${mkwok}/bin/wgt-wok";
|
program = "${mkwok}/bin/wgt-wok";
|
||||||
};
|
};
|
||||||
|
mdl = {
|
||||||
|
type = "app";
|
||||||
|
program = "${mkmdlpy}/bin/wgt-mdl";
|
||||||
|
};
|
||||||
|
itp = {
|
||||||
|
type = "app";
|
||||||
|
program = "${mkitp}/bin/wgt-itp";
|
||||||
|
};
|
||||||
export = {
|
export = {
|
||||||
type = "app";
|
type = "app";
|
||||||
program = "${mkmdl}/bin/wgt-export";
|
program = "${mkmdl}/bin/wgt-export";
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Generate the tileset palette `twu01palstd.itp` from tiles.py.
|
||||||
|
|
||||||
|
python make_itp.py [out.itp] # default: twu01palstd.itp
|
||||||
|
|
||||||
|
The toolset loads `<set>palstd.itp` when it registers a tileset; without it the
|
||||||
|
set fails to register (blank name in the New Area wizard, "could not create
|
||||||
|
area"). Every stock/custom set on disk ships one. It's a GFF `ITP ` file: a MAIN
|
||||||
|
list of palette categories, each a struct with an ID byte and a LIST of
|
||||||
|
{RESREF, STRREF} tile entries.
|
||||||
|
|
||||||
|
We emit one flat category listing all 40 tile model resrefs. Each entry carries
|
||||||
|
a local NAME cexostring (NOT a STRREF) — this is what stock/custom sets without a
|
||||||
|
tileset TLK do (verified against wut01palstd.itp). Entries with neither NAME nor
|
||||||
|
a valid STRREF render blank AND non-selectable in the toolset palette. Built as
|
||||||
|
JSON and converted to GFF with neverwinter.nim's nwn_gff (must be on PATH).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import tiles
|
||||||
|
|
||||||
|
NO_STRREF = 0xFFFFFFFF # category folder: no dialog.tlk name (cosmetic, blank folder)
|
||||||
|
|
||||||
|
|
||||||
|
def tile_label(s):
|
||||||
|
"""Readable palette name, e.g. '[flat] cobble_plain'."""
|
||||||
|
return f"[{s['kind']}] {s['name']}"
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
entries = [{"__struct_id": 0,
|
||||||
|
"NAME": {"type": "cexostring", "value": tile_label(s)},
|
||||||
|
"RESREF": {"type": "resref", "value": tiles.resref(s)}}
|
||||||
|
for s in tiles.TILES]
|
||||||
|
return {
|
||||||
|
"__data_type": "ITP ",
|
||||||
|
"MAIN": {"type": "list", "value": [
|
||||||
|
{"__struct_id": 0,
|
||||||
|
"ID": {"type": "byte", "value": 0},
|
||||||
|
"STRREF": {"type": "dword", "value": NO_STRREF},
|
||||||
|
"LIST": {"type": "list", "value": entries}},
|
||||||
|
]},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
out = sys.argv[1] if len(sys.argv) > 1 else f"{tiles.PREFIX}palstd.itp"
|
||||||
|
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as tf:
|
||||||
|
json.dump(build(), tf)
|
||||||
|
js = tf.name
|
||||||
|
try:
|
||||||
|
subprocess.run(["nwn_gff", "-i", js, "-l", "json", "-k", "gff", "-o", out],
|
||||||
|
check=True)
|
||||||
|
finally:
|
||||||
|
os.unlink(js)
|
||||||
|
print(f"wrote {out} ({len(tiles.TILES)} tile palette entries)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"""Generate loadable NWN tile models (`twu01_<code>.mdl` + `.wok` + textures).
|
||||||
|
|
||||||
|
python make_mdl.py [outdir] # default: ./mdl
|
||||||
|
|
||||||
|
Pure Python, no Blender. Emits Aurora ASCII models directly from tiles.py so the
|
||||||
|
result is deterministic and testable without a headless Blender/Neverblender run.
|
||||||
|
(Neverblender is kept as an alternate path in export_mdl.py, but it flattens our
|
||||||
|
multi-material tiles to one unloadable material, so this is the shipping path.)
|
||||||
|
|
||||||
|
Each tile .mdl contains:
|
||||||
|
* an AuroraBase dummy node named == resref (classification TILE),
|
||||||
|
* one trimesh node PER texture used on the tile (NWN allows one bitmap/node) —
|
||||||
|
faces triangulated, real tverts, and a `bitmap <resref>` the engine can load,
|
||||||
|
* the aabb walkmesh node (shared with make_wok, embedded per NWN tile rules).
|
||||||
|
The standalone `.wok` (same walkmesh) is written alongside.
|
||||||
|
|
||||||
|
Textures: a solid-colour <=16-char .tga per tiles.TEXTURES (KICKOFF.md: geometry
|
||||||
|
not art; flat colours double as an in-toolset grammar read-out). Real art can
|
||||||
|
replace these files later without touching geometry.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import tiles
|
||||||
|
import make_wok
|
||||||
|
|
||||||
|
VIS_MATID = 65536 # constant last-field on visible faces (matches stock tiles)
|
||||||
|
|
||||||
|
|
||||||
|
def write_tga(path, rgb):
|
||||||
|
"""Minimal 2x2 uncompressed 24-bit TGA in solid `rgb` (0..1 floats)."""
|
||||||
|
r, g, b = (max(0, min(255, round(c * 255))) for c in rgb)
|
||||||
|
w = h = 2
|
||||||
|
header = bytes([0, 0, 2, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
w & 0xFF, w >> 8, h & 0xFF, h >> 8, 24, 0])
|
||||||
|
body = bytes([b, g, r]) * (w * h) # TGA stores BGR
|
||||||
|
with open(path, "wb") as fh:
|
||||||
|
fh.write(header + body)
|
||||||
|
|
||||||
|
|
||||||
|
def _trimesh(ref, idx, tex, verts, quads):
|
||||||
|
"""One trimesh node: local verts + planar tverts + triangulated faces."""
|
||||||
|
local, lmap, tvs = [], {}, []
|
||||||
|
|
||||||
|
def li(gi):
|
||||||
|
if gi not in lmap:
|
||||||
|
x, y, z = verts[gi]
|
||||||
|
lmap[gi] = len(local)
|
||||||
|
local.append((x, y, z))
|
||||||
|
tvs.append(((x + tiles.HALF) / (2 * tiles.HALF),
|
||||||
|
(y + tiles.HALF) / (2 * tiles.HALF)))
|
||||||
|
return lmap[gi]
|
||||||
|
|
||||||
|
tris = []
|
||||||
|
for a, b, c, d in quads:
|
||||||
|
la, lb, lc, ld = li(a), li(b), li(c), li(d)
|
||||||
|
tris.append((la, lb, lc))
|
||||||
|
tris.append((la, lc, ld))
|
||||||
|
|
||||||
|
L = [f"node trimesh {ref}_{idx:02d}",
|
||||||
|
f" parent {ref}",
|
||||||
|
" position 0.00000 0.00000 0.00000",
|
||||||
|
" orientation 0.00000 0.00000 0.00000 0.00000",
|
||||||
|
" wirecolor 1.0 1.0 1.0",
|
||||||
|
" ambient 1.0 1.0 1.0",
|
||||||
|
" diffuse 1.0 1.0 1.0",
|
||||||
|
" specular 0.0 0.0 0.0",
|
||||||
|
" shininess 1",
|
||||||
|
f" bitmap {tex}",
|
||||||
|
" render 1",
|
||||||
|
" shadow 0",
|
||||||
|
" tilefade 0",
|
||||||
|
" rotatetexture 0",
|
||||||
|
f" verts {len(local)}"]
|
||||||
|
L += [f" {v[0]:.5f} {v[1]:.5f} {v[2]:.5f}" for v in local]
|
||||||
|
L.append(f" tverts {len(tvs)}")
|
||||||
|
L += [f" {u:.6f} {v:.6f} 0" for u, v in tvs]
|
||||||
|
L.append(f" faces {len(tris)}")
|
||||||
|
# face: v1 v2 v3 smoothgroup tv1 tv2 tv3 matid (tvert idx == vert idx here)
|
||||||
|
L += [f" {a} {b} {c} 1 {a} {b} {c} {VIS_MATID}" for a, b, c in tris]
|
||||||
|
L.append("endnode")
|
||||||
|
return L
|
||||||
|
|
||||||
|
|
||||||
|
def emit_mdl(spec):
|
||||||
|
ref = tiles.resref(spec)
|
||||||
|
verts, faces = tiles.vis_geometry(spec)
|
||||||
|
|
||||||
|
# group quads by texture, preserving first-seen order for stable node numbers
|
||||||
|
groups, order = {}, []
|
||||||
|
for a, b, c, d, mat in faces:
|
||||||
|
tex = tiles.texname(mat)
|
||||||
|
if tex not in groups:
|
||||||
|
groups[tex] = []
|
||||||
|
order.append(tex)
|
||||||
|
groups[tex].append((a, b, c, d))
|
||||||
|
|
||||||
|
L = [f"# {ref} - Westgate Urban Foundation MVP tile (ASCII, generated)",
|
||||||
|
f"newmodel {ref}",
|
||||||
|
f"setsupermodel {ref} null",
|
||||||
|
"classification TILE",
|
||||||
|
"setanimationscale 1.0",
|
||||||
|
f"beginmodelgeom {ref}",
|
||||||
|
f"node dummy {ref}",
|
||||||
|
" parent null",
|
||||||
|
"endnode"]
|
||||||
|
for idx, tex in enumerate(order, start=1):
|
||||||
|
L += _trimesh(ref, idx, tex, verts, groups[tex])
|
||||||
|
_, aabb = make_wok.aabb_node(spec)
|
||||||
|
L += aabb
|
||||||
|
L += [f"endmodelgeom {ref}", f"donemodel {ref}"]
|
||||||
|
return ref, "\n".join(L) + "\n", set(order)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
outdir = sys.argv[1] if len(sys.argv) > 1 else "mdl"
|
||||||
|
os.makedirs(outdir, exist_ok=True)
|
||||||
|
|
||||||
|
used = set()
|
||||||
|
for spec in tiles.TILES:
|
||||||
|
ref, mdl, texs = emit_mdl(spec)
|
||||||
|
used |= texs
|
||||||
|
with open(os.path.join(outdir, ref + ".mdl"), "w") as fh:
|
||||||
|
fh.write(mdl)
|
||||||
|
_, wok = make_wok.emit(spec)
|
||||||
|
with open(os.path.join(outdir, ref + ".wok"), "w") as fh:
|
||||||
|
fh.write(wok)
|
||||||
|
|
||||||
|
# write every texture (not just `used`) so nothing dangles if a tile changes
|
||||||
|
for mat, tex in tiles.TEXTURES.items():
|
||||||
|
write_tga(os.path.join(outdir, tex + ".tga"), tiles.COLORS[mat])
|
||||||
|
|
||||||
|
print(f"wrote {len(tiles.TILES)} .mdl + .wok and "
|
||||||
|
f"{len(tiles.TEXTURES)} .tga to {outdir}/")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -27,11 +27,12 @@ def tile_block(i, spec):
|
|||||||
tl, tr, bl, br = cor["TopLeft"], cor["TopRight"], cor["BottomLeft"], cor["BottomRight"]
|
tl, tr, bl, br = cor["TopLeft"], cor["TopRight"], cor["BottomLeft"], cor["BottomRight"]
|
||||||
ref = tiles.resref(spec)
|
ref = tiles.resref(spec)
|
||||||
walkable = spec["kind"] != "canal"
|
walkable = spec["kind"] != "canal"
|
||||||
|
# Field order/values mirror the stock BioWare sets (the toolset's .set reader
|
||||||
|
# is picky); WalkMesh=msb01 is the universal legacy label, not a file.
|
||||||
L = [f"[TILE{i}]"]
|
L = [f"[TILE{i}]"]
|
||||||
L += [
|
L += [
|
||||||
f"Model={ref}",
|
f"Model={ref}",
|
||||||
"WalkMesh=", # legacy field; per-model .wok is matched by name
|
"WalkMesh=msb01",
|
||||||
f"ImageMap2D={ref}",
|
|
||||||
f"TopLeft={tl[0]}", f"TopLeftHeight={tl[1]}",
|
f"TopLeft={tl[0]}", f"TopLeftHeight={tl[1]}",
|
||||||
f"TopRight={tr[0]}", f"TopRightHeight={tr[1]}",
|
f"TopRight={tr[0]}", f"TopRightHeight={tr[1]}",
|
||||||
f"BottomLeft={bl[0]}", f"BottomLeftHeight={bl[1]}",
|
f"BottomLeft={bl[0]}", f"BottomLeftHeight={bl[1]}",
|
||||||
@@ -40,13 +41,15 @@ def tile_block(i, spec):
|
|||||||
f"Right={tiles.nwn_crosser(e)}",
|
f"Right={tiles.nwn_crosser(e)}",
|
||||||
f"Bottom={tiles.nwn_crosser(s)}",
|
f"Bottom={tiles.nwn_crosser(s)}",
|
||||||
f"Left={tiles.nwn_crosser(w)}",
|
f"Left={tiles.nwn_crosser(w)}",
|
||||||
"MainLight1=0", "MainLight2=0", "SourceLight1=0", "SourceLight2=0",
|
"MainLight1=1", "MainLight2=1", "SourceLight1=1", "SourceLight2=1",
|
||||||
"AnimLoop1=0", "AnimLoop2=0", "AnimLoop3=0",
|
"AnimLoop1=1", "AnimLoop2=1", "AnimLoop3=1",
|
||||||
"Orientation=0",
|
|
||||||
f"PathNode={'A' if walkable else 'N'}", # placeholder — verify in toolset
|
|
||||||
"VisibilityNode=",
|
|
||||||
"Doors=0",
|
"Doors=0",
|
||||||
"Sounds=0",
|
"Sounds=0",
|
||||||
|
f"PathNode={'A' if walkable else 'N'}", # placeholder — verify in toolset
|
||||||
|
"Orientation=0",
|
||||||
|
"VisibilityNode=A",
|
||||||
|
"VisibilityOrientation=0",
|
||||||
|
f"ImageMap2D={ref}", # minimap resref (no .tga yet; blank in toolset)
|
||||||
]
|
]
|
||||||
return L
|
return L
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ def build():
|
|||||||
"Floor=cobble",
|
"Floor=cobble",
|
||||||
"DisplayName=-1",
|
"DisplayName=-1",
|
||||||
"UnlocalizedName=Westgate Urban Foundation",
|
"UnlocalizedName=Westgate Urban Foundation",
|
||||||
"Version=1",
|
"Version=V1.0",
|
||||||
"EnvMap=",
|
"EnvMap=",
|
||||||
"SelectorHeight=3",
|
"SelectorHeight=3",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import sys
|
|||||||
|
|
||||||
import tiles
|
import tiles
|
||||||
|
|
||||||
WALK = 1 # a walkable stone-ish row in surfacemat.2da (verify)
|
WALK = 4 # surfacemat.2da row 4 = Stone, walkable (Neverblender wok_Stone)
|
||||||
BLOCK = 7 # a non-walkable row (verify)
|
BLOCK = 7 # surfacemat.2da row 7 = Nonwalk, non-walkable (wok_Nonwalk)
|
||||||
|
|
||||||
|
|
||||||
def geometry(spec):
|
def geometry(spec):
|
||||||
@@ -71,15 +71,21 @@ def aabb_tree(verts, faces):
|
|||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
def emit(spec):
|
def aabb_node(spec):
|
||||||
|
"""The `node aabb <ref>_wok ... endnode` block (reused by make_mdl embed)."""
|
||||||
verts, faces = geometry(spec)
|
verts, faces = geometry(spec)
|
||||||
nodes = aabb_tree(verts, faces)
|
nodes = aabb_tree(verts, faces)
|
||||||
name = tiles.resref(spec)
|
name = tiles.resref(spec)
|
||||||
L = [f"# {name} walkmesh (ASCII, first-pass) - compile via nwnmdlcomp/cleanmodels",
|
L = [f"node aabb {name}_wg",
|
||||||
f"node aabb {name}_wok",
|
|
||||||
f" parent {name}",
|
f" parent {name}",
|
||||||
" position 0.0 0.0 0.0",
|
" position 0.0 0.0 0.0",
|
||||||
" orientation 0.0 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)}"]
|
f" verts {len(verts)}"]
|
||||||
L += [f" {v[0]:.5f} {v[1]:.5f} {v[2]:.5f}" for v in verts]
|
L += [f" {v[0]:.5f} {v[1]:.5f} {v[2]:.5f}" for v in verts]
|
||||||
L.append(f" faces {len(faces)}")
|
L.append(f" faces {len(faces)}")
|
||||||
@@ -89,7 +95,13 @@ def emit(spec):
|
|||||||
L += [f" {mn[0]:.5f} {mn[1]:.5f} {mn[2]:.5f} {mx[0]:.5f} {mx[1]:.5f} {mx[2]:.5f} {fi}"
|
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]
|
for mn, mx, fi in nodes]
|
||||||
L.append("endnode")
|
L.append("endnode")
|
||||||
return name, "\n".join(L) + "\n"
|
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():
|
def main():
|
||||||
|
|||||||
@@ -32,6 +32,28 @@ COLORS = {
|
|||||||
}
|
}
|
||||||
CURB = "MAT_curb_dark_stone"
|
CURB = "MAT_curb_dark_stone"
|
||||||
|
|
||||||
|
# NWN needs a real texture (bitmap) per trimesh; material name -> texture resref
|
||||||
|
# (<=16 chars, prefix eats 6). make_mdl.py writes a solid-colour .tga per entry.
|
||||||
|
TEXTURES = {
|
||||||
|
"MAT_wet_cobble_dark": "twu01_wcob",
|
||||||
|
"MAT_cobble_patch": "twu01_cpat",
|
||||||
|
"MAT_plaza_slab": "twu01_plaz",
|
||||||
|
"MAT_sidewalk_stone": "twu01_swlk",
|
||||||
|
"MAT_curb_dark_stone": "twu01_curb",
|
||||||
|
"MAT_canal_black_water": "twu01_watr",
|
||||||
|
"MAT_void_black": "twu01_void",
|
||||||
|
"MAT_ramp_stone": "twu01_ramp",
|
||||||
|
"MAT_drain_iron": "twu01_iron",
|
||||||
|
"MAT_foundation_pad": "twu01_fnd",
|
||||||
|
"MAT_debug_walkmesh": "twu01_dwlk",
|
||||||
|
"MAT_debug_blocker": "twu01_dblk",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def texname(mat):
|
||||||
|
return TEXTURES.get(mat, "twu01_wcob")
|
||||||
|
|
||||||
|
|
||||||
F, R, C, RP, MX = "FLAT", "RAISED", "CANAL", "RAMP", "MIXED"
|
F, R, C, RP, MX = "FLAT", "RAISED", "CANAL", "RAMP", "MIXED"
|
||||||
|
|
||||||
|
|
||||||
@@ -281,6 +303,32 @@ def wok_quads(spec):
|
|||||||
return [(_rect(-5, 5, -5, 5, 0.0), True)]
|
return [(_rect(-5, 5, -5, 5, 0.0), True)]
|
||||||
|
|
||||||
|
|
||||||
|
# --- visible mesh: subdivided 10x10 plane, faces tagged by material ----------
|
||||||
|
# Same source as build.py's build_vis so the Blender preview and the exported
|
||||||
|
# .mdl never drift. Faces are quads here; make_mdl.py triangulates on emit.
|
||||||
|
|
||||||
|
def vis_geometry(spec):
|
||||||
|
"""Return (verts, faces): 100-quad plane. faces = (a, b, c, d, matname)."""
|
||||||
|
n, step = 10, 1.0
|
||||||
|
paint = make_paint(spec)
|
||||||
|
verts, vidx = [], {}
|
||||||
|
|
||||||
|
def vi(i, j):
|
||||||
|
if (i, j) not in vidx:
|
||||||
|
x, y = -HALF + i * step, -HALF + j * step
|
||||||
|
vidx[(i, j)] = len(verts)
|
||||||
|
verts.append((x, y, height(spec, x, y)))
|
||||||
|
return vidx[(i, j)]
|
||||||
|
|
||||||
|
faces = []
|
||||||
|
for i in range(n):
|
||||||
|
for j in range(n):
|
||||||
|
cx, cy = -HALF + (i + 0.5) * step, -HALF + (j + 0.5) * step
|
||||||
|
faces.append((vi(i, j), vi(i + 1, j), vi(i + 1, j + 1), vi(i, j + 1),
|
||||||
|
paint(cx, cy)))
|
||||||
|
return verts, faces
|
||||||
|
|
||||||
|
|
||||||
# --- NWN .set mapping (AGENTS.md §4, §7) -------------------------------------
|
# --- NWN .set mapping (AGENTS.md §4, §7) -------------------------------------
|
||||||
# Minimal, deliberate vocabulary (AGENTS.md §2.1: every terrain pairing multiplies
|
# Minimal, deliberate vocabulary (AGENTS.md §2.1: every terrain pairing multiplies
|
||||||
# tile count). Ground tiles all share 'cobble' so they interconnect freely
|
# tile count). Ground tiles all share 'cobble' so they interconnect freely
|
||||||
|
|||||||
Reference in New Issue
Block a user