Forge — PBR Materials & glTF Shading
The lookdev layer. A correctly authored PBR material graph is the prerequisite for every Forge
render-check and for clean glTF export. The discipline: glTF metallic-roughness is the
interchange truth — every material authored in Blender's Principled BSDF must survive the
Blender→glTF export mapping without loss of intent.
Project memory: if FORGE.md exists at the project root, read it first — it carries
the PBR workflow (metallic-roughness vs spec-gloss), ORM channel-packing policy, texel density
budget, target engine, and verified output paths. When ATELIER.md also exists, extract the
aesthetic direction and primary OKLCH hue before choosing material presets.
Suite map — where this skill fits:
Upstream: forge-uv (UV layout required before any map can be applied) · forge-texture
(baked normal/AO/curvature/displacement maps arrive here as inputs)
Downstream: forge-render (consumes the material-complete .blend for headless Cycles renders)
· forge-export (Blender→glTF pipeline; material node topology must match the export
whitelist) · forge-validate (material schema QA: gltf-validator, colorspace checks,
anisotropy tangent verification)
Sibling: forge-light (HDRI/IBL wiring needed to evaluate material quality) · forge-data
(BM25 lookup for roughness presets, IOR values, F0 reference tables)
Atelier seam: atelier-webgl (Three.js MeshStandardMaterial / MeshPhysicalMaterial
implement the same metal/rough model; GLB produced here is consumed there via GLTFLoader)
· atelier-direction (art-direction sets metallic vs dielectric, roughness register, coat
presence — read it before choosing presets)
Run = call the Skill tool with the exact name. Narrating "now run forge-render" runs nothing.
Decide first: tool + availability
Before writing any material code, verify Blender is present:
# PowerShell — check Blender availability (do NOT run headlessly if missing)
$blender = Get-Command blender -ErrorAction SilentlyContinue
if (-not $blender) {
Write-Error "Blender not found in PATH. Install via: winget install BlenderFoundation.Blender"
exit 1
}
blender --version
If Blender is absent, stop and report. Do not attempt a material workflow without it.
Engine gate: Headless rendering of materials uses Cycles (CPU fallback). EEVEE Next is
unsupported headless on Windows. EEVEE-based material checks run only inside the Blender GUI.
All scripts in this skill target Cycles; the EEVEE surface_render_method API is noted for
reference only.
Workflow gate: Always use metallic-roughness (glTF core). Avoid spec-gloss for new work
(KHR_materials_pbrSpecularGlossiness was archived 2021; deprecated in Three.js r147).
The flow
Read FORGE.md → confirm PBR workflow, ORM policy, target engine, output paths.
Decide material complexity (choose one):
- Solid / constant — no textures, scalar BSDF inputs only → skip to step 4.
- Textured / ORM — albedo + ORM (AO/rough/metal packed) + normal → steps 3–5.
- Advanced — clearcoat, transmission, volume, sheen, anisotropy, iridescence → step 6.
Verify / prepare maps (textured path):
- All maps must be authored before material wiring. Check they exist on disk.
- Confirm color-space assignment: albedo/emission → sRGB; ORM/normal/height → Non-Color.
- Verify ORM channel layout (R=AO, G=Roughness, B=Metallic — glTF convention).
- Full color-space table and DX→GL normal flip:
references/pbr-theory.md §§3–4.
Build the Principled BSDF node graph via Python (bpy):
- Use socket names, never numeric indices (names changed in Blender 4.0 OpenPBR rewrite).
- Set
bsdf.distribution = 'MULTI_GGX' (energy-conserving; prevents dark rough metals).
- Wire ORM via
ShaderNodeSeparateColor (4.2+) G→Roughness, B→Metallic.
- Normal map requires:
Image Texture → ShaderNodeNormalMap (space='TANGENT') → BSDF.Normal.
- Wire AO to
glTF Material Output.Occlusion node group (not to BSDF directly).
- Full commented build script:
references/bpy-material-scripts.md §1.
Verify glTF export compatibility (a round-trip QA check, not the production export —
forge-export owns the egress; see step 8b):
- Only Principled BSDF node topology is exported; all other BSDF nodes are silently dropped.
- Procedural nodes (Noise, Voronoi, ColorRamp chains) are dropped — bake to image first.
- UV mapping: use
ShaderNodeMapping with vector_type = 'POINT' for KHR_texture_transform.
- The inline
export_scene.gltf here exists only to confirm the material survives the export
whitelist — keep it scoped to QA; the canonical production GLB is produced by forge-export.
- Full export support/drop table:
references/gltf-export-mapping.md §§1–2.
Wire advanced extensions (when FORGE.md or brief specifies):
- Clearcoat / car paint →
Coat Weight + Coat Roughness + Coat Normal input sockets.
- Transmission (thin glass) →
Transmission Weight (>=0) → KHR_materials_transmission.
- Volume (thick glass, amber) → combine with
KHR_materials_volume; mesh must be manifold.
- Sheen (cloth, velvet) →
Sheen Weight + Sheen Roughness + Sheen Tint sockets.
- Anisotropy (brushed metal) →
Anisotropic + Anisotropic Rotation; requires TANGENT attribute.
- Iridescence (soap bubble, oil slick) →
KHR_materials_iridescence JSON params.
- Emissive HDR bloom →
Emission Strength > 1.0 → KHR_materials_emissive_strength.
- Extension JSON schemas and Three.js mappings:
references/gltf-extensions.md.
Headless Cycles render for visual QA:
- Invoke
forge-render for the full headless render loop.
- For a quick material-only check (sphere preview): use the script in
references/bpy-material-scripts.md §2 (pbr_material_setup.py pattern).
- Invocation pattern:
blender --background --python script.py -- <args> (the -- is mandatory).
- After render:
Read the PNG. For the numeric gate, don't hand-compute a mean — run the
battle-tested Pillow checks check_luminance_range + check_no_clipping from
Skill("forge-light") references/color-management-qa.md §2 on the preview PNG:
mean <0.02 = failed material/lighting, >0.98 = overexposed, near-zero variance = uniform
(material not active). The local verify() in references/bpy-material-scripts.md §2 is a
standalone fallback when forge-light is not loaded.
- Run = call
Skill("forge-render") for full turntable + contact-sheet QA.
Validate the round-trip GLB (material-survival check, not the shipping artifact):
- Run
gltf-validator (Khronos CLI) on the QA .glb to confirm the material round-trips.
- Run
npx gltf-transform inspect for quick material/extension audit.
- For anisotropy: verify TANGENT attribute present (
gltf-validator flags missing tangents).
- For volume: verify mesh is manifold (open-mesh + volume = undefined behavior in engines).
- Run = call
Skill("forge-validate") for the full gate.
8b. Hand the production GLB to forge-export (the egress boundary):
- forge-material verifies only that the material survives the glTF export whitelist; it does
not own the shipping export. For the canonical GLB,
Skill("forge-export") — it owns the
format matrix, axis/unit handling, Draco/Meshopt flags, and per-engine import recipes
(Unreal/Unity/Godot/Three.js). Pass it the material-complete .blend and the verified node
topology; forge-export emits the egress artifact (and on a web handoff, the
public/forge/<slug>-hero.glb consumed by atelier-webgl).
- Run = call
Skill("forge-export").
- Fix → re-render loop until visual check passes:
- Wrong color space → fix
img.colorspace_settings.name; re-render.
- Bumps inverted (DX normal on GL engine) → flip G channel (script in
references/pbr-theory.md §3.9).
- Anisotropy noise → normalize only RG channels as 2D vector (B = strength, not direction).
- Metallic gray values (not 0/1) → threshold map (script in
references/pbr-theory.md §5).
- Full gotcha→fix table:
references/pbr-theory.md §5, references/gltf-export-mapping.md §3.
Quick-reference: Principled BSDF 4.x socket names
Socket names changed in Blender 4.0 (OpenPBR rewrite). Use these names; never numeric indices.
| Old (3.x) name |
New (4.x) name |
Range / Default |
Specular |
Specular IOR Level |
0–1, default 0.5 |
Clearcoat |
Coat Weight |
0–1 |
Clearcoat Roughness |
Coat Roughness |
0–1 |
Transmission |
Transmission Weight |
0–1 |
Subsurface |
Subsurface Weight |
0–1 |
Emission |
Emission Color |
RGBA |
Full socket index table (0–27): references/bpy-material-scripts.md §3.
Key material rules (memorize these)
- Base color — never bake lighting in. Albedo should be flat, shadow-free. Clamp non-metals
to sRGB [30, 240]; metal albedo encodes the specular tint (F0 color), not diffuse.
- Metallic — binary 0 or 1 only. Intermediate values (0.1–0.9) are physically implausible;
allow at most a 2–3 px transition zone at metal/non-metal boundaries.
- Roughness ranges. Polished metal: 0.05–0.2; brushed metal: 0.3–0.5; plastic: 0.3–0.5;
concrete/rubber: 0.85–0.95. Never author pure 0.0 or 1.0 for real materials.
- ORM must be PNG, not JPEG. JPEG chroma subsampling corrupts the metallic and roughness
channels. Use lossless PNG for ORM; JPEG only for albedo when filesize is critical.
- Transmission + alphaMode. Glass:
alphaMode = OPAQUE + transmission > 0. Do NOT use
BLEND + transmission — undefined behavior in most engines.
IOR and F0 reference tables (metals + dielectrics): references/pbr-theory.md §2.
Operating principles
- glTF is the interchange truth. Design every Principled BSDF graph to survive the export
mapping. Bake anything the glTF exporter cannot understand (procedurals, displacement, SSS)
before claiming the material is done.
- Color space is load-bearing. Set it explicitly on every image node immediately after load;
never rely on Blender's auto-detection. Wrong color space on a roughness map silently corrupts
every render and every export.
- MULTI_GGX always. Set
bsdf.distribution = 'MULTI_GGX' in every scripted material.
Plain GGX loses energy at high roughness, making rough metals artificially dark — a common,
subtle, and hard-to-diagnose error.
- Verify by rendering. No material is done until a Cycles render has been read back as a PNG
and visually inspected. The minimum gate is the reusable Pillow checks (
check_luminance_range
/ check_no_clipping) in Skill("forge-light") references/color-management-qa.md §2 — run them
rather than eyeballing or hand-computing a mean; forge-render turntable is the full gate.
- forge-export owns the egress. forge-material's own
export_scene.gltf is a round-trip QA
check — it proves the material survives the glTF whitelist, nothing more. The shipping GLB
(format matrix, axis/unit handling, Draco/Meshopt, per-engine recipes) is produced by
Skill("forge-export"). Never treat this skill's QA export as the canonical egress artifact.
- Run = call the Skill tool. Invoking
forge-render, forge-validate, or forge-export
means calling Skill("forge-render") / Skill("forge-validate") / Skill("forge-export")
with the Skill tool. Writing the name in prose runs nothing — this is the exact failure mode
documented in the Forge suite.
1---2name: forge-material3description: Forge suite — PBR shading, glTF material model, and Blender Principled BSDF authoring (the lookdev layer). Produces verified Blender materials and glTF-export-ready (round-trip-checked) node graphs with correct metal/rough maps, ORM channel packing, normal maps, and glTF KHR extension wiring — the shipping GLB itself is emitted by forge-export — then renders to PNG for visual QA via Cycles headless. Use whenever authoring or debugging PBR materials, setting metallic/roughness/IOR/clearcoat/transmission/sheen/anisotropy, building Principled BSDF node graphs in Python (bpy), packing ORM textures, exporting glTF/GLB with correct material settings, wiring KHR_materials_clearcoat / KHR_materials_transmission / KHR_materials_volume / KHR_materials_emissive_strength / KHR_materials_anisotropy / KHR_materials_iridescence, converting DirectX to OpenGL normal maps, diagnosing sRGB/non-color space errors, validating glTF material output with gltf-validator, or understanding the Blender→glTF Principled BSDF mapping table4---56# Forge — PBR Materials & glTF Shading78The lookdev layer. A correctly authored PBR material graph is the prerequisite for every Forge9render-check and for clean glTF export. The discipline: **glTF metallic-roughness is the10interchange truth — every material authored in Blender's Principled BSDF must survive the11Blender→glTF export mapping without loss of intent.**1213> **Project memory:** if **`FORGE.md`** exists at the project root, read it first — it carries14> the PBR workflow (metallic-roughness vs spec-gloss), ORM channel-packing policy, texel density15> budget, target engine, and verified output paths. When **`ATELIER.md`** also exists, extract the16> aesthetic direction and primary OKLCH hue before choosing material presets.1718---1920> **Suite map — where this skill fits:**21>22> Upstream: **forge-uv** (UV layout required before any map can be applied) · **forge-texture**23> (baked normal/AO/curvature/displacement maps arrive here as inputs)24>25> Downstream: **forge-render** (consumes the material-complete .blend for headless Cycles renders)26> · **forge-export** (Blender→glTF pipeline; material node topology must match the export27> whitelist) · **forge-validate** (material schema QA: gltf-validator, colorspace checks,28> anisotropy tangent verification)29>30> Sibling: **forge-light** (HDRI/IBL wiring needed to evaluate material quality) · **forge-data**31> (BM25 lookup for roughness presets, IOR values, F0 reference tables)32>33> Atelier seam: **atelier-webgl** (Three.js `MeshStandardMaterial` / `MeshPhysicalMaterial`34> implement the same metal/rough model; GLB produced here is consumed there via `GLTFLoader`)35> · **atelier-direction** (art-direction sets metallic vs dielectric, roughness register, coat36> presence — read it before choosing presets)37>38> **Run = call the Skill tool with the exact name. Narrating "now run forge-render" runs nothing.**3940---4142## Decide first: tool + availability4344Before writing any material code, verify Blender is present:4546```powershell47# PowerShell — check Blender availability (do NOT run headlessly if missing)48$blender = Get-Command blender -ErrorAction SilentlyContinue49if (-not $blender) {50 Write-Error "Blender not found in PATH. Install via: winget install BlenderFoundation.Blender"51 exit 152}53blender --version54```5556If Blender is absent, stop and report. Do not attempt a material workflow without it.5758**Engine gate:** Headless rendering of materials uses **Cycles** (CPU fallback). EEVEE Next is59unsupported headless on Windows. EEVEE-based material checks run only inside the Blender GUI.60All scripts in this skill target Cycles; the EEVEE `surface_render_method` API is noted for61reference only.6263**Workflow gate:** Always use **metallic-roughness** (glTF core). Avoid spec-gloss for new work64(KHR_materials_pbrSpecularGlossiness was archived 2021; deprecated in Three.js r147).6566---6768## The flow69701. **Read FORGE.md** → confirm PBR workflow, ORM policy, target engine, output paths.71722. **Decide material complexity** (choose one):73 - **Solid / constant** — no textures, scalar BSDF inputs only → skip to step 4.74 - **Textured / ORM** — albedo + ORM (AO/rough/metal packed) + normal → steps 3–5.75 - **Advanced** — clearcoat, transmission, volume, sheen, anisotropy, iridescence → step 6.76773. **Verify / prepare maps** (textured path):78 - All maps must be authored before material wiring. Check they exist on disk.79 - Confirm color-space assignment: albedo/emission → sRGB; ORM/normal/height → Non-Color.80 - Verify ORM channel layout (R=AO, G=Roughness, B=Metallic — glTF convention).81 - Full color-space table and DX→GL normal flip: **`references/pbr-theory.md`** §§3–4.82834. **Build the Principled BSDF node graph via Python (bpy)**:84 - Use socket names, never numeric indices (names changed in Blender 4.0 OpenPBR rewrite).85 - Set `bsdf.distribution = 'MULTI_GGX'` (energy-conserving; prevents dark rough metals).86 - Wire ORM via `ShaderNodeSeparateColor` (4.2+) G→Roughness, B→Metallic.87 - Normal map requires: `Image Texture → ShaderNodeNormalMap (space='TANGENT') → BSDF.Normal`.88 - Wire AO to `glTF Material Output.Occlusion` node group (not to BSDF directly).89 - Full commented build script: **`references/bpy-material-scripts.md`** §1.90915. **Verify glTF export compatibility** (a *round-trip QA check*, not the production export —92 **forge-export owns the egress**; see step 8b):93 - Only Principled BSDF node topology is exported; all other BSDF nodes are silently dropped.94 - Procedural nodes (Noise, Voronoi, ColorRamp chains) are dropped — bake to image first.95 - UV mapping: use `ShaderNodeMapping` with `vector_type = 'POINT'` for `KHR_texture_transform`.96 - The inline `export_scene.gltf` here exists only to confirm the material survives the export97 whitelist — keep it scoped to QA; the canonical production GLB is produced by `forge-export`.98 - Full export support/drop table: **`references/gltf-export-mapping.md`** §§1–2.991006. **Wire advanced extensions** (when FORGE.md or brief specifies):101 - Clearcoat / car paint → `Coat Weight` + `Coat Roughness` + `Coat Normal` input sockets.102 - Transmission (thin glass) → `Transmission Weight` (>=0) → `KHR_materials_transmission`.103 - Volume (thick glass, amber) → combine with `KHR_materials_volume`; mesh must be manifold.104 - Sheen (cloth, velvet) → `Sheen Weight` + `Sheen Roughness` + `Sheen Tint` sockets.105 - Anisotropy (brushed metal) → `Anisotropic` + `Anisotropic Rotation`; requires TANGENT attribute.106 - Iridescence (soap bubble, oil slick) → `KHR_materials_iridescence` JSON params.107 - Emissive HDR bloom → `Emission Strength > 1.0` → `KHR_materials_emissive_strength`.108 - Extension JSON schemas and Three.js mappings: **`references/gltf-extensions.md`**.1091107. **Headless Cycles render for visual QA**:111 - Invoke `forge-render` for the full headless render loop.112 - For a quick material-only check (sphere preview): use the script in113 **`references/bpy-material-scripts.md`** §2 (pbr_material_setup.py pattern).114 - Invocation pattern: `blender --background --python script.py -- <args>` (the `--` is mandatory).115 - After render: `Read` the PNG. For the numeric gate, don't hand-compute a mean — run the116 battle-tested Pillow checks `check_luminance_range` + `check_no_clipping` from117 **`Skill("forge-light")` references/color-management-qa.md §2** on the preview PNG:118 mean <0.02 = failed material/lighting, >0.98 = overexposed, near-zero variance = uniform119 (material not active). The local `verify()` in `references/bpy-material-scripts.md` §2 is a120 standalone fallback when forge-light is not loaded.121 - Run = call `Skill("forge-render")` for full turntable + contact-sheet QA.1221238. **Validate the round-trip GLB** (material-survival check, not the shipping artifact):124 - Run `gltf-validator` (Khronos CLI) on the QA .glb to confirm the material round-trips.125 - Run `npx gltf-transform inspect` for quick material/extension audit.126 - For anisotropy: verify TANGENT attribute present (`gltf-validator` flags missing tangents).127 - For volume: verify mesh is manifold (open-mesh + volume = undefined behavior in engines).128 - Run = call `Skill("forge-validate")` for the full gate.1291308b. **Hand the production GLB to forge-export** (the egress boundary):131 - forge-material verifies only that the material *survives* the glTF export whitelist; it does132 not own the shipping export. For the canonical GLB, `Skill("forge-export")` — it owns the133 format matrix, axis/unit handling, Draco/Meshopt flags, and per-engine import recipes134 (Unreal/Unity/Godot/Three.js). Pass it the material-complete .blend and the verified node135 topology; forge-export emits the egress artifact (and on a web handoff, the136 `public/forge/<slug>-hero.glb` consumed by atelier-webgl).137 - Run = call `Skill("forge-export")`.1381399. **Fix → re-render loop** until visual check passes:140 - Wrong color space → fix `img.colorspace_settings.name`; re-render.141 - Bumps inverted (DX normal on GL engine) → flip G channel (script in `references/pbr-theory.md` §3.9).142 - Anisotropy noise → normalize only RG channels as 2D vector (B = strength, not direction).143 - Metallic gray values (not 0/1) → threshold map (script in `references/pbr-theory.md` §5).144 - Full gotcha→fix table: **`references/pbr-theory.md`** §5, **`references/gltf-export-mapping.md`** §3.145146---147148## Quick-reference: Principled BSDF 4.x socket names149150Socket names changed in Blender 4.0 (OpenPBR rewrite). Use these names; never numeric indices.151152| Old (3.x) name | New (4.x) name | Range / Default |153|-------------------|-------------------------|-----------------|154| `Specular` | `Specular IOR Level` | 0–1, default 0.5 |155| `Clearcoat` | `Coat Weight` | 0–1 |156| `Clearcoat Roughness` | `Coat Roughness` | 0–1 |157| `Transmission` | `Transmission Weight` | 0–1 |158| `Subsurface` | `Subsurface Weight` | 0–1 |159| `Emission` | `Emission Color` | RGBA |160161Full socket index table (0–27): **`references/bpy-material-scripts.md`** §3.162163---164165## Key material rules (memorize these)166167- **Base color — never bake lighting in.** Albedo should be flat, shadow-free. Clamp non-metals168 to sRGB [30, 240]; metal albedo encodes the specular tint (F0 color), not diffuse.169- **Metallic — binary 0 or 1 only.** Intermediate values (0.1–0.9) are physically implausible;170 allow at most a 2–3 px transition zone at metal/non-metal boundaries.171- **Roughness ranges.** Polished metal: 0.05–0.2; brushed metal: 0.3–0.5; plastic: 0.3–0.5;172 concrete/rubber: 0.85–0.95. Never author pure 0.0 or 1.0 for real materials.173- **ORM must be PNG, not JPEG.** JPEG chroma subsampling corrupts the metallic and roughness174 channels. Use lossless PNG for ORM; JPEG only for albedo when filesize is critical.175- **Transmission + alphaMode.** Glass: `alphaMode = OPAQUE` + `transmission > 0`. Do NOT use176 `BLEND` + transmission — undefined behavior in most engines.177178IOR and F0 reference tables (metals + dielectrics): **`references/pbr-theory.md`** §2.179180---181182## Operating principles183184- **glTF is the interchange truth.** Design every Principled BSDF graph to survive the export185 mapping. Bake anything the glTF exporter cannot understand (procedurals, displacement, SSS)186 before claiming the material is done.187- **Color space is load-bearing.** Set it explicitly on every image node immediately after load;188 never rely on Blender's auto-detection. Wrong color space on a roughness map silently corrupts189 every render and every export.190- **MULTI_GGX always.** Set `bsdf.distribution = 'MULTI_GGX'` in every scripted material.191 Plain GGX loses energy at high roughness, making rough metals artificially dark — a common,192 subtle, and hard-to-diagnose error.193- **Verify by rendering.** No material is done until a Cycles render has been read back as a PNG194 and visually inspected. The minimum gate is the reusable Pillow checks (`check_luminance_range`195 / `check_no_clipping`) in `Skill("forge-light")` references/color-management-qa.md §2 — run them196 rather than eyeballing or hand-computing a mean; `forge-render` turntable is the full gate.197- **forge-export owns the egress.** forge-material's own `export_scene.gltf` is a *round-trip QA198 check* — it proves the material survives the glTF whitelist, nothing more. The shipping GLB199 (format matrix, axis/unit handling, Draco/Meshopt, per-engine recipes) is produced by200 `Skill("forge-export")`. Never treat this skill's QA export as the canonical egress artifact.201- **Run = call the Skill tool.** Invoking `forge-render`, `forge-validate`, or `forge-export`202 means calling `Skill("forge-render")` / `Skill("forge-validate")` / `Skill("forge-export")`203 with the Skill tool. Writing the name in prose runs nothing — this is the exact failure mode204 documented in the Forge suite.