286 lines
15 KiB
Markdown
286 lines
15 KiB
Markdown
# wgt01 Texture Kit Implementation Plan
|
||
|
||
> **STATUS: EXECUTED & MERGED (2026-07-05).** Historical record. As-shipped
|
||
> deviations are in `../specs/2026-07-05-wgt01-texture-kit-design.md` ("As
|
||
> shipped"); remaining slots in `../specs/2026-07-05-wgt01-texture-kit-gaps.md`.
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Build the 8-slot dark-gothic PBR texture kit for the wgt01 tileset (`wgt01/textures/`), regraded to the concept-art palette, with provenance notes and materials wired into `wgt01.blend`.
|
||
|
||
**Architecture:** Copy the best `tcn01` PBR maps out of `/home/vicky/Blender/PBR_Procampur` into a `wgt01_` namespace, run every diffuse through one shared ImageMagick regrade script, pull CC0 sets from ambientCG only for the plaza (and optionally sidewalk) slots, author the water map, then load everything into `wgt01.blend` as `MAT_wgt01_*` materials via headless Blender.
|
||
|
||
**Tech Stack:** bash + ImageMagick (`magick`), `curl` (ambientCG API), Blender 4.0.2 headless (via `nix develop ../twu01` → `$BLENDER_BIN`), NWN `.mtr` text files.
|
||
|
||
## Global Constraints
|
||
|
||
- All shipped names ≤ 16 chars, lowercase, `wgt01_` prefix (spec: resref limit).
|
||
- Kit lives in `wgt01/textures/`; never modify originals under `/home/vicky/Blender`.
|
||
- Every diffuse goes through `wgt01/regrade.sh` — no hand-edited one-off images.
|
||
- Height/normal maps copy through untouched.
|
||
- Every shipped file gets a `SOURCES.md` line (origin path or CC0 URL + license).
|
||
- TGA only in the kit (convert any DDS); NWN also reads TGA directly.
|
||
- Working branch: `wgt01-tileset-kit`. NixOS: no ad-hoc global installs; use `nix shell nixpkgs#imagemagick -c` / the twu01 dev shell.
|
||
- This is an asset pipeline, not TDD code: each task's "test" is a runnable visual or scripted check, and the plan's self-checks (`assert`-style bash) stand in for unit tests.
|
||
|
||
---
|
||
|
||
### Task 1: Kit scaffold + regrade script
|
||
|
||
**Files:**
|
||
- Create: `wgt01/textures/` (dir), `wgt01/regrade.sh`, `wgt01/textures/SOURCES.md` (header only)
|
||
|
||
**Interfaces:**
|
||
- Produces: `regrade.sh <in> <out.tga>` — darkens ~35%, desaturates, cool-shifts any diffuse. All later tasks call it. Env overrides: `MOD` (modulate triple, default `65,55,100`), `TINT` (default `#232830`), `TINTPCT` (default `12`).
|
||
|
||
- [ ] **Step 1: Write the script**
|
||
|
||
```bash
|
||
mkdir -p wgt01/textures
|
||
cat > wgt01/regrade.sh <<'EOF'
|
||
#!/usr/bin/env bash
|
||
# Uniform Westgate regrade: darken, desaturate, cool tint.
|
||
# Usage: regrade.sh <in> <out.tga> (env: MOD, TINT, TINTPCT to override per-slot)
|
||
set -euo pipefail
|
||
MOD="${MOD:-65,55,100}"; TINT="${TINT:-#232830}"; TINTPCT="${TINTPCT:-12}"
|
||
magick "$1" -modulate "$MOD" -fill "$TINT" -colorize "${TINTPCT}%" -level '0%,95%' "$2"
|
||
EOF
|
||
chmod +x wgt01/regrade.sh
|
||
printf '# wgt01/textures — sources & licenses\n\n| file | origin | license |\n|---|---|---|\n' > wgt01/textures/SOURCES.md
|
||
```
|
||
|
||
- [ ] **Step 2: Self-check — regraded output is darker and less saturated**
|
||
|
||
```bash
|
||
nix shell nixpkgs#imagemagick -c bash -c '
|
||
./wgt01/regrade.sh /home/vicky/Blender/PBR_Procampur/tcn01_cobb02_d.tga /tmp/rg_test.tga
|
||
b_in=$(magick /home/vicky/Blender/PBR_Procampur/tcn01_cobb02_d.tga -colorspace Gray -format "%[fx:mean]" info:)
|
||
b_out=$(magick /tmp/rg_test.tga -colorspace Gray -format "%[fx:mean]" info:)
|
||
awk -v a="$b_in" -v b="$b_out" "BEGIN{ if (b < a*0.75) print \"OK darker: \" a \" -> \" b; else { print \"FAIL\"; exit 1 } }"'
|
||
```
|
||
Expected: `OK darker: <in> -> <out>` with out ≲ 0.75×in.
|
||
|
||
- [ ] **Step 3: Eyeball it** — `magick montage` the original vs regraded cobble side by side, Read the PNG, compare against the concept sheet's wet-cobblestone swatch. Tune `MOD`/`TINT` defaults in the script if it reads too green or too bright; re-run Step 2.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add wgt01/regrade.sh wgt01/textures/SOURCES.md
|
||
git commit -m "wgt01: texture kit scaffold + shared regrade script"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Procampur slot extraction (slots 1,2,4,5,6 + grime decals)
|
||
|
||
**Files:**
|
||
- Create: `wgt01/textures/wgt01_{cobble,worn,sidewlk,wall,bridge}_{d,n,h}.tga`, matching `wgt01_<slot>.mtr`, `wgt01_grime1_d.tga`, `wgt01_wtrline_d.tga`
|
||
- Modify: `wgt01/textures/SOURCES.md`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `wgt01/regrade.sh` (Task 1).
|
||
- Produces: slot basenames `wgt01_cobble`, `wgt01_worn`, `wgt01_sidewlk`, `wgt01_wall`, `wgt01_bridge` (each `_d/_n/_h` + `.mtr`) — Tasks 5–6 reference exactly these.
|
||
|
||
- [ ] **Step 1: Extract, regrade diffuses, copy n/h, emit .mtr**
|
||
|
||
```bash
|
||
cd wgt01 && P=/home/vicky/Blender/PBR_Procampur
|
||
declare -A SRC=( [cobble]=tcn01_cobb02 [worn]=tcn01_cobb05 [sidewlk]=tcn01_stone04 [wall]=tcn01_brick03 [bridge]=tcn01_stone04 )
|
||
nix shell nixpkgs#imagemagick -c bash -c '
|
||
set -euo pipefail; P=/home/vicky/Blender/PBR_Procampur
|
||
for slot in cobble worn sidewlk wall bridge; do
|
||
case $slot in cobble) s=tcn01_cobb02;; worn) s=tcn01_cobb05;; sidewlk) s=tcn01_stone04;; wall) s=tcn01_brick03;; bridge) s=tcn01_stone04;; esac
|
||
./regrade.sh "$P/${s}_d.tga" "textures/wgt01_${slot}_d.tga"
|
||
cp "$P/${s}_n.tga" "textures/wgt01_${slot}_n.tga"
|
||
cp "$P/${s}_h.tga" "textures/wgt01_${slot}_h.tga"
|
||
printf "renderhint NormalAndSpecMapped\r\n\r\n// Textures\r\ntexture0 wgt01_%s_d\r\ntexture1 wgt01_%s_n\r\ntexture2 black\r\ntexture3 null\r\ntexture4 wgt01_%s_h\r\ntexture5 null\r\n" "$slot" "$slot" "$slot" > "textures/wgt01_${slot}.mtr"
|
||
printf "| wgt01_%s_* | PBR_Procampur/%s_* | see PBR_Procampur readme |\n" "$slot" "$s" >> textures/SOURCES.md
|
||
done
|
||
# grime decals: pass through (alpha overlays, already neutral)
|
||
magick "$P/tcn01_splotch2_d.tga" textures/wgt01_grime1_d.tga
|
||
magick "$P/tcn01_watrstn02.tga" textures/wgt01_wtrline_d.tga
|
||
printf "| wgt01_grime1_d | PBR_Procampur/tcn01_splotch2_d | see readme |\n| wgt01_wtrline_d | PBR_Procampur/tcn01_watrstn02 | see readme |\n" >> textures/SOURCES.md'
|
||
```
|
||
|
||
Note: `bridge` starts as a duplicate of `stone04` with a darker override (`MOD=58,50,100 ./regrade.sh …`) so it separates visually from sidewalk; if it still reads identical at Task 6, the internet arch-stone fallback from Task 3's API search replaces it.
|
||
|
||
- [ ] **Step 2: Check — every slot complete, names ≤16**
|
||
|
||
```bash
|
||
cd wgt01/textures && for f in wgt01_{cobble,worn,sidewlk,wall,bridge}{_d.tga,_n.tga,_h.tga,.mtr}; do
|
||
[ -f "$f" ] || { echo "MISSING $f"; exit 1; }
|
||
b="${f%.*}"; [ ${#b} -le 16 ] || { echo "TOO LONG $b"; exit 1; }
|
||
done && echo OK
|
||
```
|
||
Expected: `OK`.
|
||
|
||
- [ ] **Step 3: Contact sheet** — montage all `wgt01_*_d.tga`, Read it, compare with concept-art material swatches. Adjust per-slot `MOD`/`TINT` env overrides and re-run Step 1 for any slot that's off.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add wgt01/textures && git commit -m "wgt01: Procampur-derived slots (cobble/worn/sidewalk/wall/bridge) + grime decals"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: Internet pull — plaza slabs (slot 3)
|
||
|
||
**Files:**
|
||
- Create: `wgt01/textures/wgt01_plaza_{d,n,h}.tga`, `wgt01/textures/wgt01_plaza.mtr`
|
||
- Modify: `wgt01/textures/SOURCES.md`
|
||
|
||
**Interfaces:**
|
||
- Consumes: `regrade.sh`. Produces: basename `wgt01_plaza` for Tasks 5–6.
|
||
|
||
- [ ] **Step 1: Find a CC0 large-slab flagstone on ambientCG**
|
||
|
||
```bash
|
||
curl -s 'https://ambientcg.com/api/v2/full_json?q=paving+stones&type=Material&limit=50' \
|
||
| nix shell nixpkgs#jq -c jq -r '.foundAssets[] | [.assetId, .displayName] | @tsv' | head -30
|
||
```
|
||
Pick a large/irregular slab asset (PavingStones-family; prefer big flat slates over small cobbles — this is the plaza). Download the 1K JPG zip:
|
||
|
||
```bash
|
||
cd /tmp/claude-*/*/scratchpad 2>/dev/null || cd "$SCRATCHPAD"
|
||
curl -Lo plaza.zip 'https://ambientcg.com/get?file=<AssetID>_1K-JPG.zip' && nix shell nixpkgs#unzip -c unzip -o plaza.zip -d plaza/
|
||
ls plaza/ # expect <AssetID>_1K_Color.jpg, _NormalGL.jpg, _Displacement.jpg
|
||
```
|
||
|
||
- [ ] **Step 2: Convert + regrade into the kit**
|
||
|
||
```bash
|
||
cd <repo>/wgt01 && S=<scratchpad>/plaza A=<AssetID>
|
||
nix shell nixpkgs#imagemagick -c bash -c '
|
||
./regrade.sh "'$S'/'$A'_1K_Color.jpg" textures/wgt01_plaza_d.tga
|
||
magick "'$S'/'$A'_1K_NormalGL.jpg" textures/wgt01_plaza_n.tga
|
||
magick "'$S'/'$A'_1K_Displacement.jpg" -colorspace Gray textures/wgt01_plaza_h.tga'
|
||
printf "renderhint NormalAndSpecMapped\r\n\r\n// Textures\r\ntexture0 wgt01_plaza_d\r\ntexture1 wgt01_plaza_n\r\ntexture2 black\r\ntexture3 null\r\ntexture4 wgt01_plaza_h\r\ntexture5 null\r\n" > textures/wgt01_plaza.mtr
|
||
printf "| wgt01_plaza_* | ambientcg.com asset %s (1K JPG) | CC0 |\n" "$A" >> textures/SOURCES.md
|
||
```
|
||
|
||
- [ ] **Step 3: Check** — same completeness/length loop as Task 2 Step 2 for `wgt01_plaza`; montage plaza next to cobble/sidewalk and Read: plaza must read as *larger, flatter slabs* than both, in the same palette.
|
||
|
||
- [ ] **Step 4: Commit** — `git add wgt01/textures && git commit -m "wgt01: plaza slab slot from ambientCG (CC0)"`
|
||
|
||
---
|
||
|
||
### Task 4: Authored slots — canal water (8) + gutter/drain (7)
|
||
|
||
**Files:**
|
||
- Create: `wgt01/textures/wgt01_water_d.tga`, `wgt01_water.mtr`, `wgt01_gutter_{d,n,h}.tga`, `wgt01_gutter.mtr`
|
||
- Modify: `wgt01/textures/SOURCES.md`
|
||
|
||
**Interfaces:**
|
||
- Produces: basenames `wgt01_water`, `wgt01_gutter` for Tasks 5–6.
|
||
|
||
- [ ] **Step 1: Water — near-black, subtle ripple, from procedural noise (no source needed)**
|
||
|
||
```bash
|
||
cd wgt01 && nix shell nixpkgs#imagemagick -c bash -c '
|
||
magick -size 512x512 plasma:gray20-gray35 -blur 0x6 -modulate 40,20,100 \
|
||
-fill "#0a0e14" -colorize 70% -level "0%,60%" textures/wgt01_water_d.tga'
|
||
printf "renderhint NormalAndSpecMapped\r\n\r\n// Textures\r\ntexture0 wgt01_water_d\r\ntexture1 null\r\ntexture2 black\r\ntexture3 null\r\ntexture4 null\r\ntexture5 null\r\n" > textures/wgt01_water.mtr
|
||
printf "| wgt01_water_d | authored (ImageMagick plasma) | ours |\n" >> textures/SOURCES.md
|
||
```
|
||
(EnvMap-driven sheen comes from the `.set` later — out of scope here.)
|
||
|
||
- [ ] **Step 2: Gutter — extra-dark stone channel from the cobble source**
|
||
|
||
```bash
|
||
cd wgt01 && nix shell nixpkgs#imagemagick -c bash -c '
|
||
P=/home/vicky/Blender/PBR_Procampur
|
||
MOD=45,40,100 TINT="#14181f" TINTPCT=20 ./regrade.sh "$P/tcn01_cobb02_d.tga" textures/wgt01_gutter_d.tga
|
||
cp "$P/tcn01_cobb02_n.tga" textures/wgt01_gutter_n.tga
|
||
cp "$P/tcn01_cobb02_h.tga" textures/wgt01_gutter_h.tga'
|
||
printf "renderhint NormalAndSpecMapped\r\n\r\n// Textures\r\ntexture0 wgt01_gutter_d\r\ntexture1 wgt01_gutter_n\r\ntexture2 black\r\ntexture3 null\r\ntexture4 wgt01_gutter_h\r\ntexture5 null\r\n" > textures/wgt01_gutter.mtr
|
||
printf "| wgt01_gutter_* | PBR_Procampur/tcn01_cobb02_* (extra-dark regrade) | see readme |\n" >> textures/SOURCES.md
|
||
```
|
||
Try converting `$P/tcn01_grate01.dds` for an iron grate decal (`magick` failed on its header earlier; try `nix shell nixpkgs#directx-shader-compiler`? No — try `texconv` alt: `nix shell nixpkgs#imagemagick -c magick dds:...` or Gimp headless). If unreadable, skip: the drain grate becomes modeled geometry with `wgt01_gutter`, note it in SOURCES.md — do not block on it.
|
||
|
||
- [ ] **Step 3: Check + commit** — completeness loop for `wgt01_water`, `wgt01_gutter`; montage; `git add wgt01/textures && git commit -m "wgt01: authored water + gutter slots"`.
|
||
|
||
---
|
||
|
||
### Task 5: Wire materials into wgt01.blend
|
||
|
||
**Files:**
|
||
- Create: `wgt01/wire_materials.py`
|
||
- Modify: `wgt01/wgt01.blend` (headless save)
|
||
|
||
**Interfaces:**
|
||
- Consumes: all 8 slot basenames from Tasks 2–4.
|
||
- Produces: Blender materials named `MAT_wgt01_<slot>` (Principled BSDF, image texture node on `_d`, normal-map node on `_n` where present, relative paths `//textures/…`).
|
||
|
||
- [ ] **Step 1: Write `wgt01/wire_materials.py`**
|
||
|
||
```python
|
||
"""Create/refresh MAT_wgt01_* materials in wgt01.blend (run headless)."""
|
||
import bpy, os
|
||
SLOTS = ["cobble", "worn", "plaza", "sidewlk", "wall", "bridge", "gutter", "water"]
|
||
BASE = os.path.dirname(bpy.data.filepath)
|
||
|
||
for slot in SLOTS:
|
||
name = f"MAT_wgt01_{slot}"
|
||
mat = bpy.data.materials.get(name) or bpy.data.materials.new(name)
|
||
mat.use_nodes = True
|
||
nt = mat.node_tree; nt.nodes.clear()
|
||
out = nt.nodes.new("ShaderNodeOutputMaterial")
|
||
bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled")
|
||
nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
|
||
d = os.path.join(BASE, "textures", f"wgt01_{slot}_d.tga")
|
||
img = bpy.data.images.load(d, check_existing=True)
|
||
img.filepath = f"//textures/wgt01_{slot}_d.tga"
|
||
tex = nt.nodes.new("ShaderNodeTexImage"); tex.image = img
|
||
nt.links.new(tex.outputs["Color"], bsdf.inputs["Base Color"])
|
||
n = os.path.join(BASE, "textures", f"wgt01_{slot}_n.tga")
|
||
if os.path.exists(n):
|
||
nimg = bpy.data.images.load(n, check_existing=True)
|
||
nimg.filepath = f"//textures/wgt01_{slot}_n.tga"; nimg.colorspace_settings.name = "Non-Color"
|
||
ntex = nt.nodes.new("ShaderNodeTexImage"); ntex.image = nimg
|
||
nmap = nt.nodes.new("ShaderNodeNormalMap")
|
||
nt.links.new(ntex.outputs["Color"], nmap.inputs["Color"])
|
||
nt.links.new(nmap.outputs["Normal"], bsdf.inputs["Normal"])
|
||
# wet look: low roughness on ground slots
|
||
bsdf.inputs["Roughness"].default_value = 0.35 if slot in ("cobble", "gutter", "water") else 0.6
|
||
|
||
bpy.ops.wm.save_mainfile()
|
||
print("WIRED:", ", ".join(f"MAT_wgt01_{s}" for s in SLOTS))
|
||
```
|
||
|
||
- [ ] **Step 2: Run headless and verify**
|
||
|
||
```bash
|
||
cd wgt01 && nix develop ../twu01 -c "$BLENDER_BIN" --background wgt01.blend --python wire_materials.py 2>&1 | grep WIRED
|
||
```
|
||
(If `$BLENDER_BIN` is unset outside the dev shell, run `nix develop ../twu01` first — it exports it.) Expected: `WIRED: MAT_wgt01_cobble, …` and `wgt01.blend` mtime updated.
|
||
|
||
- [ ] **Step 3: Verify images resolve** — re-open headless: `--python-expr 'import bpy; missing=[i.name for i in bpy.data.images if not i.has_data]; print("MISSING:", missing); assert not missing'`. Expected: `MISSING: []`.
|
||
|
||
- [ ] **Step 4: Commit** — `git add wgt01/wire_materials.py wgt01/wgt01.blend && git commit -m "wgt01: wire MAT_wgt01_* materials into blend"`.
|
||
|
||
---
|
||
|
||
### Task 6: Final verification — palette contact sheet
|
||
|
||
**Files:**
|
||
- Create: `wgt01/textures/contact-sheet.png` (committed as the kit's visual record)
|
||
|
||
- [ ] **Step 1: Build the sheet**
|
||
|
||
```bash
|
||
cd wgt01 && nix shell nixpkgs#imagemagick -c bash -c '
|
||
magick montage -label "%f" -background "#111" -fill "#ccc" textures/wgt01_*_d.tga \
|
||
-tile 4x -geometry 256x256+6+6 textures/contact-sheet.png'
|
||
```
|
||
|
||
- [ ] **Step 2: Read the sheet next to `concept-art-1.png`'s Material Language column.** Acceptance: all 8 slots present, one shared dark/cool palette, plaza ≠ sidewalk ≠ cobble at a glance, bridge ≠ sidewalk, water near-black. Fix any failing slot by adjusting its regrade override and re-running that task's Step 1.
|
||
|
||
- [ ] **Step 3: SOURCES.md audit**
|
||
|
||
```bash
|
||
cd wgt01/textures && for f in wgt01_*; do grep -q "${f%%_[dnhs].tga}" SOURCES.md || grep -q "${f%.mtr}" SOURCES.md || { echo "UNSOURCED $f"; exit 1; }; done && echo OK
|
||
```
|
||
|
||
- [ ] **Step 4: Commit** — `git add wgt01/textures/contact-sheet.png && git commit -m "wgt01: texture kit contact sheet"`.
|