Files
sow-module-tilesets/docs/superpowers/plans/2026-07-05-wgt01-texture-kit.md
T
2026-07-05 01:46:27 +02:00

14 KiB
Raw Blame History

wgt01 Texture Kit Implementation Plan

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

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
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 itmagick 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

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 56 reference exactly these.

  • Step 1: Extract, regrade diffuses, copy n/h, emit .mtr

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
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

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 56.

  • Step 1: Find a CC0 large-slab flagstone on ambientCG

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:

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
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: Commitgit 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 56.

  • Step 1: Water — near-black, subtle ripple, from procedural noise (no source needed)

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
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 24.

  • 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

"""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
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: Commitgit 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

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

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: Commitgit add wgt01/textures/contact-sheet.png && git commit -m "wgt01: texture kit contact sheet".