Skip to content

TextureForge

Spec-driven texture studio for the game's painterly UI. Small JSON recipes render deterministic brush-stroke plates, torn parchment panels, frames, dividers, bars and slots, exported as production PNGs plus ready FSlateBrush metadata for the WidgetForge pipeline.

Lives in Tools/TextureForge/. Operational detail is co-located with the tool — this doc is the concept map; Tools/TextureForge/CLAUDE.md (agent workflow, hard rules) and Tools/TextureForge/README.md (studio workflow, recipe anatomy) are the sources of truth for day-to-day use.

The one split that matters: recipes vs comps

Recipes (recipes/*.tex.json) Comps (comps/*.comp.json)
Purpose Define ONE texture Assemble a preview mockup (tooltip, panel, menu)
Output Shippable PNG + brush metadata via npm run export Screenshot only
Ships to UE Yes Never

Comps exist so a texture set is judged in context on #0a0a0a before anything touches UE. They can reference fixtures (fixtures/*.fixtures.json — sample runtime data) for populated previews, but the export path must never read fixtures — the determinism gate enforces this. If a comp looks right, the production path is still: adjust the recipesnpm run export → WidgetForge spec. This is the easiest thing to get wrong when new to the tool.

Recipes

{
  "name": "T_UI_ButtonPlate_M",
  "archetype": "button-plate",
  "preset": "parchment-plate",
  "size": { "w": 300, "h": 52 },
  "seed": 1,
  "slicing": { "mode": "fixed" },
  "params": { "bleed": 0.2 },
  "export": { "scales": [1, 2] }
}
  • Archetypes (registry: core/render.js ARCHETYPES): button-plate, tab-plate, pattern-band, row-highlight, divider, gradient, section-header, panel, slot-well, ornament, emblem, corner-bracket (+ probe, a diagnostic). Icon recipes (T_UI_Icon_*) are generated from the icon manifest (icons/, compiled by export/icons.mjs) — tune the manifest or the icon gallery, never hand-edit a generated recipe.
  • Param merge order (effectiveParams, core/render.js): archetype defaults ⊕ preset ⊕ recipe.params, last wins. Params are overrides on the merged base — the studio shows overridden values in gold.
  • Slicing: fixed (baked plate at final size, S/M/L variants — brush character everywhere) or nine / three-h / three-v with margins (logical px). Sliced recipes confine organic tear detail to corner zones so Slate Box stretching can't smear it. Never confuse the two kinds.
  • Colors: $token from Tools/WidgetForge/tokens.json (single palette source of truth) or #hex — but never hardcode a hex that exists as a token.
  • Overlays: any recipe stacks "overlays": [...] over its base render, applied in array order. Types: pattern (tiled ornament with wear), seal (procedural ghost stamp), scratches, wash, image (external PNG from overlays/ — the hand-paint escape hatch). Shared props on every layer: color, alpha, blend (normal/multiply/screen), tint, clip (paint only where the base has alpha), seed offset.

Comp node language

Defined and documented in studio/comp-render.js (header comment is the node reference; the compose page has a live reference drawer). It deliberately mirrors WidgetForge spec names — VerticalBox, HorizontalBox, Overlay, TextBlock, Image, Border, Spacer, Canvas — with slots flattened onto children, plus a forge-specific Texture node: sliced recipes 9-slice-stretch, fixed plates re-render at the requested logical size (exactly what UE will do). TextBlock supports inline colored spans. See Comp_TalentTooltip for a full example.

Invocations

From Tools/TextureForge/:

Command Does
npm run dev Studio at http://localhost:4173 (texture editor; /studio/compose.html?comp=<Name> for comps; /studio/icons.html for the icon gallery)
npm run export All recipes → exports/: PNGs @1x/@2x, manifest.json (FSlateBrush blocks), spec-snippets.json, import_forge_textures.py
node export/export.mjs <name> Export one recipe
node export/comp-shot.mjs [comp...] Comp screenshots → exports/comps/
node export/ingest.mjs <png> <name> Ingest a black-on-white icon stamp → overlays/icons/ (authoring spec: overlays/icons/README.md)
npm run test:determinism Goldens gate (--comps adds fixtures-isolation + comp-shot goldens; --icons scopes to generated icons)
npm run push [name...] Import into a running editor via the WidgetForge push watcher

Textures land under /Game/TextureLibrary/UI/Forge/. Brush-block ImageSize is logical size — @2x textures draw crisp at half pixel res.

The determinism gate

Same recipe + same seed = same pixels, forever — no Math.random() in the render path, seeded RNG only. npm run test:determinism compares renders against committed goldens; any new or changed recipe must pass it before landing. Icon tuning through the gallery's Save rebaselines its own goldens (--icons --update), so it never needs a manual rebaseline; everything else that intentionally changes pixels does.