Help us improve
Share bugs, ideas, or general feedback.
Guides Adobe Substance 3D Painter workflows for PBR material creation, web-optimized texture export to Three.js, Babylon.js, Unity, Unreal, and Python API batch automation.
npx claudepluginhub freshtechbro/claudedesignskills --plugin substance-3d-texturingHow this skill is triggered — by the user, by Claude, or both
Slash command
/substance-3d-texturing:substance-3d-texturingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Master PBR (Physically Based Rendering) texture creation and export workflows for web and real-time engines. This skill covers Substance 3D Painter workflows from material creation through web-optimized texture export, with Python automation for batch processing and integration with WebGL/WebGPU engines.
assets/README.mdassets/export_templates/babylonjs_pbr.jsonassets/export_templates/gltf_standard.jsonassets/export_templates/mobile_webgl.jsonassets/export_templates/threejs_optimized.jsonassets/export_templates/web_orm_packed.jsonreferences/export_presets.mdreferences/pbr_channel_guide.mdreferences/python_api_reference.mdscripts/batch_export.pyscripts/generate_export_preset.pyscripts/web_optimizer.pyGuides Adobe Substance 3D Painter workflows for PBR material creation, web-optimized texture export to Three.js, Babylon.js, Unity, Unreal, and Python API batch automation.
Exports Blender 3D models and animations to web-optimized glTF via Python bpy scripts. Handles batch processing, asset optimization, texture baking, model compression for Three.js and Babylon.js.
Loads, configures, and optimizes textures in Three.js. Covers UV mapping, environment maps, texture settings like color space, wrapping, filtering, and repeat.
Share bugs, ideas, or general feedback.
Master PBR (Physically Based Rendering) texture creation and export workflows for web and real-time engines. This skill covers Substance 3D Painter workflows from material creation through web-optimized texture export, with Python automation for batch processing and integration with WebGL/WebGPU engines.
Key capabilities:
Substance 3D Painter uses the metallic/roughness PBR workflow with these core channels:
Base Texture Maps:
baseColor (Albedo) - RGB diffuse color, no lighting informationnormal - RGB normal map (tangent space)metallic - Grayscale metalness (0 = dielectric, 1 = metal)roughness - Grayscale surface roughness (0 = smooth/glossy, 1 = rough/matte)Additional Maps:
ambientOcclusion (AO) - Grayscale cavity/occlusionheight - Grayscale displacement/heightemissive - RGB self-illuminationopacity - Grayscale transparencySubstance 3D Painter includes built-in export presets for common engines:
For web engines, PBR Metallic Roughness is the universal standard.
Common resolutions for web (powers of 2):
Web optimization rule: Start at 1024×1024, scale up only when texture detail is visible.
Manual export workflow for single texture set:
Steps:
Result files:
MyAsset_baseColor.png
MyAsset_normal.png
MyAsset_metallicRoughness.png // Packed: R=nothing, G=roughness, B=metallic
MyAsset_emissive.png // Optional
Three.js usage:
import * as THREE from 'three';
const textureLoader = new THREE.TextureLoader();
const material = new THREE.MeshStandardMaterial({
map: textureLoader.load('MyAsset_baseColor.png'),
normalMap: textureLoader.load('MyAsset_normal.png'),
metalnessMap: textureLoader.load('MyAsset_metallicRoughness.png'),
roughnessMap: textureLoader.load('MyAsset_metallicRoughness.png'),
aoMap: textureLoader.load('MyAsset_ambientOcclusion.png'),
});
Automate export for multiple texture sets:
import substance_painter.export
import substance_painter.resource
import substance_painter.textureset
# Define export preset
export_preset = substance_painter.resource.ResourceID(
context="starter_assets",
name="PBR Metallic Roughness"
)
# Configure export for all texture sets
config = {
"exportShaderParams": False,
"exportPath": "C:/export/web_textures",
"defaultExportPreset": export_preset.url(),
"exportList": [],
"exportParameters": [{
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"dithering": True,
"paddingAlgorithm": "infinite",
"sizeLog2": 10 // 1024×1024
}
}]
}
# Add all texture sets to export list
for texture_set in substance_painter.textureset.all_texture_sets():
config["exportList"].append({
"rootPath": texture_set.name()
})
# Execute export
result = substance_painter.export.export_project_textures(config)
if result.status == substance_painter.export.ExportStatus.Success:
for stack, files in result.textures.items():
print(f"Exported {stack}: {len(files)} textures")
else:
print(f"Export failed: {result.message}")
Export different resolutions for different assets (e.g., hero vs. background):
config = {
"exportPath": "C:/export",
"defaultExportPreset": export_preset.url(),
"exportList": [
{"rootPath": "HeroCharacter"}, # Will use 2048 (override below)
{"rootPath": "BackgroundProp"} # Will use 512 (override below)
],
"exportParameters": [
{
"filter": {"dataPaths": ["HeroCharacter"]},
"parameters": {"sizeLog2": 11} # 2048×2048
},
{
"filter": {"dataPaths": ["BackgroundProp"]},
"parameters": {"sizeLog2": 9} # 512×512
}
]
}
Create custom preset to export metallic and roughness as separate files:
custom_preset = {
"exportPresets": [{
"name": "WebGL_Separated",
"maps": [
{
"fileName": "$textureSet_baseColor",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "baseColor"},
{"destChannel": "G", "srcChannel": "G", "srcMapType": "documentMap", "srcMapName": "baseColor"},
{"destChannel": "B", "srcChannel": "B", "srcMapType": "documentMap", "srcMapName": "baseColor"}
]
},
{
"fileName": "$textureSet_normal",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "normal"},
{"destChannel": "G", "srcChannel": "G", "srcMapType": "documentMap", "srcMapName": "normal"},
{"destChannel": "B", "srcChannel": "B", "srcMapType": "documentMap", "srcMapName": "normal"}
]
},
{
"fileName": "$textureSet_metallic",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "metallic"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8"}
},
{
"fileName": "$textureSet_roughness",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "roughness"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8"}
}
]
}]
}
config = {
"exportPath": "C:/export",
"exportPresets": custom_preset["exportPresets"],
"exportList": [{"rootPath": "MyAsset", "exportPreset": "WebGL_Separated"}]
}
Aggressive compression for mobile WebGL:
mobile_config = {
"exportPath": "C:/export/mobile",
"defaultExportPreset": export_preset.url(),
"exportList": [{"rootPath": texture_set.name()}],
"exportParameters": [{
"parameters": {
"fileFormat": "jpeg", # JPEG for baseColor (lossy but smaller)
"bitDepth": "8",
"sizeLog2": 9, # 512×512 maximum
"paddingAlgorithm": "infinite"
}
}, {
"filter": {"outputMaps": ["$textureSet_normal", "$textureSet_metallicRoughness"]},
"parameters": {
"fileFormat": "png" # PNG for data maps (need lossless)
}
}]
}
Post-export: Use tools like pngquant or tinypng for further compression.
Export textures for glTF 2.0 format:
gltf_config = {
"exportPath": "C:/export/gltf",
"defaultExportPreset": substance_painter.resource.ResourceID(
context="starter_assets",
name="PBR Metallic Roughness"
).url(),
"exportList": [{"rootPath": texture_set.name()}],
"exportParameters": [{
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10, # 1024×1024
"paddingAlgorithm": "infinite"
}
}]
}
# After export, reference in glTF:
# {
# "materials": [{
# "name": "Material",
# "pbrMetallicRoughness": {
# "baseColorTexture": {"index": 0},
# "metallicRoughnessTexture": {"index": 1}
# },
# "normalTexture": {"index": 2}
# }]
# }
Auto-export on save using Python plugin:
import substance_painter.event
import substance_painter.export
import substance_painter.project
def auto_export(e):
if not substance_painter.project.is_open():
return
config = {
"exportPath": substance_painter.project.file_path().replace('.spp', '_textures'),
"defaultExportPreset": substance_painter.resource.ResourceID(
context="starter_assets", name="PBR Metallic Roughness"
).url(),
"exportList": [{"rootPath": ts.name()} for ts in substance_painter.textureset.all_texture_sets()],
"exportParameters": [{
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}]
}
substance_painter.export.export_project_textures(config)
print("Auto-export completed")
# Register event
substance_painter.event.DISPATCHER.connect(
substance_painter.event.ProjectSaved,
auto_export
)
Use exported textures in R3F:
import { useTexture } from '@react-three/drei';
function TexturedMesh() {
const [baseColor, normal, metallicRoughness, ao] = useTexture([
'/textures/Asset_baseColor.png',
'/textures/Asset_normal.png',
'/textures/Asset_metallicRoughness.png',
'/textures/Asset_ambientOcclusion.png',
]);
return (
<mesh>
<boxGeometry />
<meshStandardMaterial
map={baseColor}
normalMap={normal}
metalnessMap={metallicRoughness}
roughnessMap={metallicRoughness}
aoMap={ao}
/>
</mesh>
);
}
See react-three-fiber skill for advanced R3F material workflows.
import { PBRMaterial, Texture } from '@babylonjs/core';
const pbr = new PBRMaterial("pbr", scene);
pbr.albedoTexture = new Texture("/textures/Asset_baseColor.png", scene);
pbr.bumpTexture = new Texture("/textures/Asset_normal.png", scene);
pbr.metallicTexture = new Texture("/textures/Asset_metallicRoughness.png", scene);
pbr.useRoughnessFromMetallicTextureAlpha = false;
pbr.useRoughnessFromMetallicTextureGreen = true;
pbr.useMetallnessFromMetallicTextureBlue = true;
See babylonjs-engine skill for advanced PBR workflows.
.gltf JSONgltf-pipeline for Draco compression:gltf-pipeline -i model.gltf -o model.glb -d
See blender-web-pipeline skill for complete 3D asset pipeline.
Desktop WebGL: ~100-150MB total texture memory Mobile WebGL: ~30-50MB total texture memory
Budget per asset:
.basis) - GPU texture compression (90% smaller)Pack grayscale maps into RGB channels to reduce texture count:
Packed ORM (Occlusion-Roughness-Metallic):
Export in Substance:
orm_map = {
"fileName": "$textureSet_ORM",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "ambientOcclusion"},
{"destChannel": "G", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "roughness"},
{"destChannel": "B", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "metallic"}
]
}
Always enable mipmaps in engine for textures viewed at distance:
// Three.js (automatic)
texture.generateMipmaps = true;
// Babylon.js
texture.updateSamplingMode(Texture.TRILINEAR_SAMPLINGMODE);
Problem: BaseColor exported in linear space looks washed out.
Solution: Substance exports baseColor in sRGB by default (correct). Ensure engine uses sRGB:
// Three.js
baseColorTexture.colorSpace = THREE.SRGBColorSpace;
// Babylon.js (automatic for albedoTexture)
Problem: Normal maps show inverted or incorrect shading.
Solution:
Problem: Metallic/roughness texture has swapped channels.
Solution: Default Substance export:
Problem: Black or colored lines appear at UV seams.
Solution: Set padding algorithm to "infinite" in export settings:
"paddingAlgorithm": "infinite"
Problem: 4K textures cause long load times and memory issues on web.
Solution:
Problem: AO map exported but not visible in engine.
Solution:
geometry.attributes.uv2)material.useAmbientOcclusionFromMetallicTextureRed = trueSee bundled resources for complete workflows: