
Substance 3d Texturing
- 1.4k installs
- 629 repo stars
- Updated November 20, 2025
- freshtechbro/claudedesignskills
substance-3d-texturing provides documented workflows for Comprehensive skill for Adobe Substance 3D Painter texturing and material creation workflow. Use this skill when creating PBR materials, exporting textures for
About
The substance-3d-texturing skill comprehensive skill for Adobe Substance 3D Painter texturing and material creation workflow Use this skill when creating PBR materials exporting textures for web game engines optimizing 3D assets for real-time rendering or automating texture workflows Triggers on tasks involving Substance 3D Painter PBR texturing material creation texture export for Three js Babylon js Unity Unreal glTF optimization or Python API automation Creates optimized textures for threejs-webgl react-three Substance 3D Texturing Overview 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 PBR material authoring metallic roughness workflow Web-optimized texture export glTF Three js Babylon js Python API automation for batch export Texture compression and optimization for real-time rendering Core Concepts PBR Workflow Substance 3D Painter uses the metallic roughness PBR workflow with these core channels Base Texture.
- PBR material authoring (metallic/roughness workflow)
- Web-optimized texture export (glTF, Three.js, Babylon.js)
- Python API automation for batch export
- Texture compression and optimization for real-time rendering
- `baseColor` (Albedo) - RGB diffuse color, no lighting information
Substance 3d Texturing by the numbers
- 1,384 all-time installs (skills.sh)
- +98 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #169 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
substance-3d-texturing capabilities & compatibility
- Capabilities
- pbr material authoring (metallic/roughness workf · web optimized texture export (gltf, three.js, ba · python api automation for batch export · texture compression and optimization for real ti · `basecolor` (albedo) rgb diffuse color, no lig
- Use cases
- documentation
What substance-3d-texturing says it does
# Substance 3D Texturing ## Overview 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.
npx skills add https://github.com/freshtechbro/claudedesignskills --skill substance-3d-texturingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 629 |
| Security audit | 3 / 3 scanners passed |
| Last updated | November 20, 2025 |
| Repository | freshtechbro/claudedesignskills ↗ |
How do I use substance-3d-texturing for the task described in its SKILL.md triggers?
Comprehensive skill for Adobe Substance 3D Painter texturing and material creation workflow. Use this skill when creating PBR materials, exporting textures for web/game engines, optimizing 3D assets.
Who is it for?
Teams invoking substance-3d-texturing when the user request matches documented triggers and prerequisites.
Skip if: Skip when cached docs are missing, the request is a negative trigger, or another sibling skill owns the workflow.
When should I use this skill?
Comprehensive skill for Adobe Substance 3D Painter texturing and material creation workflow. Use this skill when creating PBR materials, exporting textures for web/game engines, optimizing 3D assets for real-time renderi
What you get
Step-by-step guidance grounded in substance-3d-texturing documentation and reference files.
- PBR texture map files
- Substance exportPresets JSON for BabylonJS_PBR
By the numbers
- Documents BabylonJS_PBR exportPreset with 3 baseColor channel mappings (R, G, B)
Files
Substance 3D Texturing
Overview
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:
- PBR material authoring (metallic/roughness workflow)
- Web-optimized texture export (glTF, Three.js, Babylon.js)
- Python API automation for batch export
- Texture compression and optimization for real-time rendering
Core Concepts
PBR Workflow
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 transparency
Export Presets
Substance 3D Painter includes built-in export presets for common engines:
- PBR Metallic Roughness - Standard glTF/WebGL format
- Unity HDRP/URP - Unity pipelines
- Unreal Engine - UE4/UE5 format
- Arnold (AiStandard) - Renderer-specific
For web engines, PBR Metallic Roughness is the universal standard.
Texture Resolution
Common resolutions for web (powers of 2):
- 512×512 - Low detail props, mobile
- 1024×1024 - Standard props, characters
- 2048×2048 - Hero assets, close-ups
- 4096×4096 - Showcase quality (use sparingly)
Web optimization rule: Start at 1024×1024, scale up only when texture detail is visible.
Common Patterns
1. Basic Web Export (Three.js/Babylon.js)
Manual export workflow for single texture set:
Steps: 1. File → Export Textures 2. Select preset: "PBR Metallic Roughness" 3. Configure export:
- Output directory: Choose target folder
- File format: PNG (8-bit) for web
- Padding: "Infinite" (prevents seams)
- Resolution: 1024×1024 (adjust per asset)
4. Export
Result files:
MyAsset_baseColor.png
MyAsset_normal.png
MyAsset_metallicRoughness.png // Packed: R=nothing, G=roughness, B=metallic
MyAsset_emissive.png // OptionalThree.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'),
});2. Batch Export with Python API
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}")3. Resolution Override per Asset
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
}
]
}4. Custom Export Preset (Separate Channels)
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"}]
}5. Mobile-Optimized Export
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.
6. glTF/GLB Integration
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}
# }]
# }7. Event-Driven Export Plugin
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
)Integration Patterns
Three.js + React Three Fiber
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.
Babylon.js PBR Materials
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 Export Pipeline
1. Export textures from Substance (as above) 2. Export model from Blender with glTF exporter 3. Reference Substance textures in .gltf JSON 4. Use gltf-pipeline for Draco compression:
gltf-pipeline -i model.gltf -o model.glb -dSee blender-web-pipeline skill for complete 3D asset pipeline.
Performance Optimization
Texture Size Budget
Desktop WebGL: ~100-150MB total texture memory Mobile WebGL: ~30-50MB total texture memory
Budget per asset:
- Background/props: 512×512 (1MB per texture × 4 maps = 4MB)
- Standard assets: 1024×1024 (4MB per texture × 4 maps = 16MB)
- Hero assets: 2048×2048 (16MB per texture × 4 maps = 64MB)
Compression Strategies
1. JPEG for baseColor (70-80% quality) - 10× smaller than PNG 2. PNG-8 for data maps (normal, metallic, roughness) - lossless required 3. Basis Universal (.basis) - GPU texture compression (90% smaller) 4. Texture atlasing - Combine multiple assets into single texture
Channel Packing
Pack grayscale maps into RGB channels to reduce texture count:
Packed ORM (Occlusion-Roughness-Metallic):
- Red: Ambient Occlusion
- Green: Roughness
- Blue: 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"}
]
}Mipmaps
Always enable mipmaps in engine for textures viewed at distance:
// Three.js (automatic)
texture.generateMipmaps = true;
// Babylon.js
texture.updateSamplingMode(Texture.TRILINEAR_SAMPLINGMODE);Common Pitfalls
1. Wrong Color Space for BaseColor
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)2. Normal Map Baking Issues
Problem: Normal maps show inverted or incorrect shading.
Solution:
- Verify tangent space normal format (DirectX vs. OpenGL Y-flip)
- Substance uses OpenGL (Y+), same as glTF standard
- If using DirectX engine, flip green channel in export
3. Metallic/Roughness Channel Order
Problem: Metallic/roughness texture has swapped channels.
Solution: Default Substance export:
- Blue channel = Metallic
- Green channel = Roughness
- Matches glTF 2.0 specification
4. Padding Artifacts at UV Seams
Problem: Black or colored lines appear at UV seams.
Solution: Set padding algorithm to "infinite" in export settings:
"paddingAlgorithm": "infinite"5. Oversized Textures for Web
Problem: 4K textures cause long load times and memory issues on web.
Solution:
- Default to 1024×1024 for web
- Use 2048×2048 only for hero assets viewed close-up
- Implement LOD system with multiple resolution sets
6. Missing AO Map in Engine
Problem: AO map exported but not visible in engine.
Solution:
- Three.js: Requires second UV channel (
geometry.attributes.uv2) - Babylon.js: Set
material.useAmbientOcclusionFromMetallicTextureRed = true - Alternative: Bake AO into baseColor in Substance
Resources
See bundled resources for complete workflows:
- references/python_api_reference.md - Complete Substance Painter Python API
- references/export_presets.md - Built-in and custom export preset catalog
- references/pbr_channel_guide.md - Deep dive into PBR texture channels
- scripts/batch_export.py - Batch export all texture sets
- scripts/web_optimizer.py - Post-process textures for web (resize, compress)
- scripts/generate_export_preset.py - Create custom export preset JSON
- assets/export_templates/ - Pre-configured export presets for Three.js, Babylon.js, Unity
Related Skills
- blender-web-pipeline - Complete 3D model → texture → web pipeline
- threejs-webgl - Loading and using PBR textures in Three.js
- react-three-fiber - R3F material workflows with Substance textures
- babylonjs-engine - Babylon.js PBR material system integration
{
"exportPresets": [
{
"name": "BabylonJS_PBR",
"maps": [
{
"fileName": "$textureSet_albedo",
"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"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"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"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"fileName": "$textureSet_metallicRoughness",
"channels": [
{
"destChannel": "G",
"srcChannel": "R",
"srcMapType": "documentMap",
"srcMapName": "roughness"
},
{
"destChannel": "B",
"srcChannel": "R",
"srcMapType": "documentMap",
"srcMapName": "metallic"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
}
]
}
]
}{
"exportPresets": [
{
"name": "glTF_Standard",
"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"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"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"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"fileName": "$textureSet_metallicRoughness",
"channels": [
{
"destChannel": "G",
"srcChannel": "R",
"srcMapType": "documentMap",
"srcMapName": "roughness"
},
{
"destChannel": "B",
"srcChannel": "R",
"srcMapType": "documentMap",
"srcMapName": "metallic"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
}
]
}
]
}{
"exportPresets": [
{
"name": "Mobile_WebGL",
"maps": [
{
"fileName": "$textureSet_color",
"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"
}
],
"parameters": {
"fileFormat": "jpeg",
"bitDepth": "8",
"sizeLog2": 9
}
},
{
"fileName": "$textureSet_normal",
"channels": [
{
"destChannel": "R",
"srcChannel": "R",
"srcMapType": "documentMap",
"srcMapName": "normal"
},
{
"destChannel": "G",
"srcChannel": "G",
"srcMapType": "documentMap",
"srcMapName": "normal"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 9
}
},
{
"fileName": "$textureSet_packed",
"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"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 9
}
}
]
}
]
}{
"exportPresets": [
{
"name": "ThreeJS_Standard",
"maps": [
{
"fileName": "$textureSet_color",
"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"
}
],
"parameters": {
"fileFormat": "jpeg",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"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"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"fileName": "$textureSet_ao",
"channels": [
{
"destChannel": "R",
"srcChannel": "R",
"srcMapType": "documentMap",
"srcMapName": "ambientOcclusion"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"fileName": "$textureSet_metalness",
"channels": [
{
"destChannel": "R",
"srcChannel": "R",
"srcMapType": "documentMap",
"srcMapName": "metallic"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"fileName": "$textureSet_roughness",
"channels": [
{
"destChannel": "R",
"srcChannel": "R",
"srcMapType": "documentMap",
"srcMapName": "roughness"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
}
]
}
]
}{
"exportPresets": [
{
"name": "Web_ORM",
"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"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"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"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"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"
}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
}
]
}
]
}Substance 3D Texturing Assets
Pre-configured export preset templates for various web engines and platforms.
Directory Structure
assets/
└── export_templates/
├── gltf_standard.json # glTF 2.0 compliant preset
├── threejs_optimized.json # Three.js with separate channels
├── babylonjs_pbr.json # Babylon.js PBR format
├── web_orm_packed.json # ORM channel packing
└── mobile_webgl.json # Mobile-optimized presetUsage
Method 1: Copy Preset Configuration
Copy the JSON content from any template file and use directly in your Python script:
import json
import substance_painter.export
# Load preset from file
with open('assets/export_templates/gltf_standard.json', 'r') as f:
custom_preset = json.load(f)
config = {
"exportPath": "C:/export",
"exportPresets": custom_preset["exportPresets"],
"exportList": [{"rootPath": "MyAsset", "exportPreset": "glTF_Standard"}],
"exportParameters": [{
"parameters": {"paddingAlgorithm": "infinite"}
}]
}
result = substance_painter.export.export_project_textures(config)Method 2: Use Preset Generator Script
Generate presets programmatically:
# Generate glTF preset
.claude/skills/substance-3d-texturing/scripts/generate_export_preset.py --preset gltf --output my_preset.json
# Interactive mode
.claude/skills/substance-3d-texturing/scripts/generate_export_preset.pyPreset Descriptions
gltf_standard.json
Format: glTF 2.0 compliant Resolution: 1024×1024 Outputs:
baseColor.png- RGB base color (sRGB)normal.png- RGB normal map (OpenGL format)metallicRoughness.png- Packed: G=Roughness, B=Metallic
Best for:
- glTF/GLB export
- Generic WebGL applications
- Three.js, Babylon.js (standard workflow)
---
threejs_optimized.json
Format: Three.js MeshStandardMaterial Resolution: 1024×1024 Outputs:
color.jpg- JPEG base color (smaller file size)normal.png- RGB normal mapao.png- Ambient occlusionmetalness.png- Metallic channelroughness.png- Roughness channel
Best for:
- Three.js projects requiring separate channels
- Maximum flexibility (individual map control)
- When file size is less critical than editability
---
babylonjs_pbr.json
Format: Babylon.js PBRMaterial Resolution: 1024×1024 Outputs:
albedo.png- Base colornormal.png- Normal mapmetallicRoughness.png- Packed: G=Roughness, B=Metallic
Best for:
- Babylon.js PBR workflows
- Similar to glTF but with Babylon naming conventions
---
web_orm_packed.json
Format: ORM (Occlusion-Roughness-Metallic) Packed Resolution: 1024×1024 Outputs:
baseColor.png- RGB base colornormal.png- RGB normal mapORM.png- Packed: R=AO, G=Roughness, B=Metallic
Best for:
- Maximum efficiency (3 textures instead of 6)
- Production web applications
- When texture count matters more than editability
Texture memory savings: ~50% reduction
---
mobile_webgl.json
Format: Mobile-optimized Resolution: 512×512 Outputs:
color.jpg- JPEG base color (aggressive compression)normal.png- RG normal map (reconstruct Z in shader)packed.png- R=AO, G=Roughness, B=Metallic
Best for:
- Mobile web applications
- Low-end devices
- When load time and memory are critical
File size: ~3MB total vs. ~16MB standard
Customization
All presets can be customized by modifying the JSON files:
Change Resolution
Modify the sizeLog2 parameter:
9= 512×51210= 1024×1024 (default)11= 2048×204812= 4096×4096
{
"parameters": {
"sizeLog2": 11 // Change to 2048×2048
}
}Change File Format
Modify the fileFormat parameter:
"png"- Lossless (recommended for data maps)"jpeg"- Lossy compression (good for baseColor)"tga"- Targa format"exr"- HDR format (32-bit)
{
"parameters": {
"fileFormat": "jpeg" // Change to JPEG
}
}Add Custom Channels
Add new map entries to pack additional channels:
{
"fileName": "$textureSet_emissive",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "emissive"},
{"destChannel": "G", "srcChannel": "G", "srcMapType": "documentMap", "srcMapName": "emissive"},
{"destChannel": "B", "srcChannel": "B", "srcMapType": "documentMap", "srcMapName": "emissive"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}Integration Examples
Three.js with glTF Preset
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load('model.glb', (gltf) => {
// Textures automatically loaded from glTF
scene.add(gltf.scene);
});Three.js with Separate Textures
import * as THREE from 'three';
const textureLoader = new THREE.TextureLoader();
const material = new THREE.MeshStandardMaterial({
map: textureLoader.load('Asset_color.jpg'),
normalMap: textureLoader.load('Asset_normal.png'),
aoMap: textureLoader.load('Asset_ao.png'),
metalnessMap: textureLoader.load('Asset_metalness.png'),
roughnessMap: textureLoader.load('Asset_roughness.png'),
});
// Set color space for baseColor
material.map.colorSpace = THREE.SRGBColorSpace;Babylon.js with PBR Preset
import { PBRMaterial, Texture } from '@babylonjs/core';
const pbr = new PBRMaterial("pbr", scene);
pbr.albedoTexture = new Texture("Asset_albedo.png", scene);
pbr.bumpTexture = new Texture("Asset_normal.png", scene);
pbr.metallicTexture = new Texture("Asset_metallicRoughness.png", scene);
// Configure channel reading
pbr.useRoughnessFromMetallicTextureGreen = true;
pbr.useMetallnessFromMetallicTextureBlue = true;ORM Packed Workflow
// Three.js with ORM texture
const ormTexture = textureLoader.load('Asset_ORM.png');
material.aoMap = ormTexture; // Reads from Red channel
material.roughnessMap = ormTexture; // Reads from Green channel
material.metalnessMap = ormTexture; // Reads from Blue channelTroubleshooting
Preset not found in Substance:
- Presets are for custom export via Python API only
- Not visible in Substance UI preset dropdown
- Use via Python scripts or plugins
Wrong channel order:
- Verify glTF spec: G=Roughness, B=Metallic
- Unreal uses different packing: R=AO, G=Roughness, B=Metallic
- Check engine documentation for channel expectations
Textures too large:
- Reduce
sizeLog2(e.g., 10 → 9 for 512px) - Use mobile preset for aggressive optimization
- Convert baseColor to JPEG post-export
Normal map inverted:
- Substance uses OpenGL format (Y+)
- If using DirectX engine, flip green channel
- Or use engine setting to flip normal Y
Additional Resources
- Substance Python API: https://helpx.adobe.com/substance-3d-painter-python/api/
- glTF 2.0 Spec: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html
- Three.js Materials: https://threejs.org/docs/#api/en/materials/MeshStandardMaterial
- Babylon.js PBR: https://doc.babylonjs.com/features/featuresDeepDive/materials/using/introToPBR
License
These preset templates are provided as examples and can be freely modified for your projects.
Substance 3D Painter Export Presets
Complete catalog of built-in and custom export presets for various game engines and web platforms.
Table of Contents
Built-in Presets
Substance 3D Painter ships with presets for major engines and renderers.
PBR Metallic Roughness
Context: starter_assets Name: PBR Metallic Roughness Use for: glTF, Three.js, Babylon.js, generic PBR engines
Output textures:
$textureSet_baseColor.png- Base color (RGB)$textureSet_normal.png- Normal map (RGB, OpenGL format)$textureSet_metallicRoughness.png- Packed texture:- Red: Unused
- Green: Roughness
- Blue: Metallic
$textureSet_emissive.png- Emissive (RGB) if present
Usage:
preset = substance_painter.resource.ResourceID(
context="starter_assets",
name="PBR Metallic Roughness"
)Best for:
- Web applications (Three.js, Babylon.js)
- glTF/GLB export
- Generic PBR workflows
---
Unity HDRP (Lit)
Context: starter_assets Name: Unity HDRP (Lit) Use for: Unity High Definition Render Pipeline
Output textures:
$textureSet_BaseMap.png- Base color + alpha$textureSet_Normal.png- Normal map (DirectX format)$textureSet_MaskMap.png- Packed texture:- Red: Metallic
- Green: Ambient Occlusion
- Blue: Detail Mask
- Alpha: Smoothness (1 - roughness)
Differences from standard PBR:
- DirectX normal format (Y-flipped)
- Smoothness instead of roughness (inverted)
- Different channel packing
---
Unity URP (Lit)
Context: starter_assets Name: Unity URP (Lit) Use for: Unity Universal Render Pipeline
Output textures:
$textureSet_BaseMap.png- Base color + alpha$textureSet_Normal.png- Normal map$textureSet_MetallicSmoothness.png- Packed:- RGB: Metallic
- Alpha: Smoothness
---
Unreal Engine
Context: starter_assets Name: Unreal Engine 4/5 (Packed) Use for: Unreal Engine 4 and 5
Output textures:
$textureSet_BaseColor.png- Base color$textureSet_Normal.png- Normal map (DirectX)$textureSet_OcclusionRoughnessMetallic.png- Packed:- Red: Ambient Occlusion
- Green: Roughness
- Blue: Metallic
$textureSet_Emissive.png- Emissive
Note: Unreal's packed ORM format is widely used and efficient.
---
Arnold (AiStandard)
Context: starter_assets Name: Arnold (AiStandard) Use for: Arnold renderer (Maya, Cinema 4D, Houdini)
Output textures:
$textureSet_baseColor.exr- Base color (EXR 32-bit)$textureSet_normal.exr- Normal map$textureSet_specularRoughness.exr- Roughness$textureSet_metalness.exr- Metallic
Note: Uses EXR format for high dynamic range.
---
V-Ray
Context: starter_assets Name: V-Ray Use for: V-Ray renderer
Output textures:
- Separate files for each channel
- High bit-depth support (16/32-bit)
Web/glTF Presets
Optimized presets for web and real-time rendering.
glTF 2.0 Standard
Custom preset for glTF 2.0 compliance:
{
"exportPresets": [{
"name": "glTF_Standard",
"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"}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10 # 1024×1024
}
},
{
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_metallicRoughness",
"channels": [
{"destChannel": "G", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "roughness"},
{"destChannel": "B", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "metallic"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}
]
}]
}Matches glTF 2.0 specification:
- BaseColor in sRGB color space
- MetallicRoughness: Green = Roughness, Blue = Metallic
- Normal in OpenGL format (Y+)
---
Three.js Optimized
Preset optimized for Three.js MeshStandardMaterial:
{
"exportPresets": [{
"name": "ThreeJS_Standard",
"maps": [
{
"fileName": "$textureSet_color",
"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"}
],
"parameters": {"fileFormat": "jpeg", "bitDepth": "8", "sizeLog2": 10} # JPEG for smaller size
},
{
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_ao",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "ambientOcclusion"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_metalness",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "metallic"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_roughness",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "roughness"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}
]
}]
}Features:
- JPEG for baseColor (70-80% quality, 10× smaller)
- PNG for data textures (lossless required)
- Separate metalness and roughness for flexibility
- Separate AO map
Three.js usage:
const material = new THREE.MeshStandardMaterial({
map: new THREE.TextureLoader().load('Asset_color.jpg'),
normalMap: new THREE.TextureLoader().load('Asset_normal.png'),
aoMap: new THREE.TextureLoader().load('Asset_ao.png'),
metalnessMap: new THREE.TextureLoader().load('Asset_metalness.png'),
roughnessMap: new THREE.TextureLoader().load('Asset_roughness.png'),
});---
Babylon.js PBR
Preset for Babylon.js PBRMaterial:
{
"exportPresets": [{
"name": "BabylonJS_PBR",
"maps": [
{
"fileName": "$textureSet_albedo",
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_metallicRoughness",
"channels": [
{"destChannel": "G", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "roughness"},
{"destChannel": "B", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "metallic"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}
]
}]
}Babylon.js usage:
const pbr = new BABYLON.PBRMaterial("pbr", scene);
pbr.albedoTexture = new BABYLON.Texture("Asset_albedo.png", scene);
pbr.bumpTexture = new BABYLON.Texture("Asset_normal.png", scene);
pbr.metallicTexture = new BABYLON.Texture("Asset_metallicRoughness.png", scene);
pbr.useRoughnessFromMetallicTextureGreen = true;
pbr.useMetallnessFromMetallicTextureBlue = true;---
Mobile WebGL (Low Spec)
Aggressive compression for mobile:
{
"exportPresets": [{
"name": "Mobile_WebGL",
"maps": [
{
"fileName": "$textureSet_color",
"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"}
],
"parameters": {"fileFormat": "jpeg", "bitDepth": "8", "sizeLog2": 9} # 512×512
},
{
"fileName": "$textureSet_normal",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "normal"},
{"destChannel": "G", "srcChannel": "G", "srcMapType": "documentMap", "srcMapName": "normal"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 9} # Skip blue channel
},
{
"fileName": "$textureSet_packed",
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 9}
}
]
}]
}Optimizations:
- 512×512 maximum resolution
- JPEG for color (lossy but small)
- RG normal map only (reconstruct Z in shader)
- ORM packing (3 maps → 1 texture)
Total texture budget: ~3MB (vs. ~16MB standard)
Custom Preset Examples
ORM Packing (Occlusion-Roughness-Metallic)
Most efficient packing for web:
orm_preset = {
"exportPresets": [{
"name": "Web_ORM",
"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_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"}
]
}
]
}]
}Reduces texture count: 6 textures → 3 textures Shader setup (Three.js):
material.aoMap = ormTexture;
material.roughnessMap = ormTexture;
material.metalnessMap = ormTexture;
// Three.js automatically reads from R/G/B channels---
Height + Normal Combined
Pack height into normal alpha channel:
{
"fileName": "$textureSet_normalHeight",
"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"},
{"destChannel": "A", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "height"}
]
}Use case: Parallax occlusion mapping (height from alpha)
---
Emissive + Alpha
Pack emissive RGB + opacity:
{
"fileName": "$textureSet_emissiveAlpha",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "emissive"},
{"destChannel": "G", "srcChannel": "G", "srcMapType": "documentMap", "srcMapName": "emissive"},
{"destChannel": "B", "srcChannel": "B", "srcMapType": "documentMap", "srcMapName": "emissive"},
{"destChannel": "A", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "opacity"}
]
}---
Stylized/NPR Export
Non-photorealistic rendering (toon shading):
{
"exportPresets": [{
"name": "Stylized_Toon",
"maps": [
{
"fileName": "$textureSet_diffuse",
"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_shadowMask",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "ambientOcclusion"}
]
},
# No metallic/roughness for toon shaders
]
}]
}Channel Packing Strategies
Standard PBR Packing
glTF 2.0 / Khronos Standard:
- BaseColor: RGB (sRGB)
- Normal: RGB (OpenGL, linear)
- MetallicRoughness:
- R: Unused
- G: Roughness (linear)
- B: Metallic (linear)
Advantages:
- Industry standard
- Maximum compatibility
- glTF compliance
---
Unreal Engine Packing
Unreal's ORM format:
- BaseColor: RGB
- Normal: RGB (DirectX)
- ORM:
- R: Ambient Occlusion
- G: Roughness
- B: Metallic
Advantages:
- Includes AO without extra texture
- Widely used format
- Efficient for complex materials
---
Mobile Optimized Packing
Aggressive compression:
- BaseColor: JPEG (lossy)
- Normal: RG only (reconstruct Z)
- ORM: RGB (AO + Roughness + Metallic)
Advantages:
- Smallest file size
- Reduced texture memory
- Faster loading on mobile
Disadvantages:
- Lossy baseColor (visual artifacts)
- RG normal requires shader modification
---
High Quality / Archival
Separate channels, high bit depth:
- BaseColor: PNG-16 or EXR-32
- Normal: PNG-16
- Metallic: PNG-16 (separate)
- Roughness: PNG-16 (separate)
- AO: PNG-16 (separate)
- Height: PNG-16 (separate)
Advantages:
- Maximum quality
- Per-channel editability
- HDR support (EXR)
Disadvantages:
- Large file sizes
- More texture slots
- Overkill for real-time
Preset Selection Guide
By Platform
| Platform | Recommended Preset | Resolution | Format |
|---|---|---|---|
| Desktop Web | glTF_Standard / Web_ORM | 1024-2048 | PNG |
| Mobile Web | Mobile_WebGL | 512-1024 | JPEG+PNG |
| Unity HDRP | Unity HDRP (Lit) | 2048-4096 | PNG |
| Unreal Engine | Unreal Engine (Packed) | 2048-4096 | PNG |
| glTF Export | PBR Metallic Roughness | 1024-2048 | PNG |
| Three.js | ThreeJS_Standard | 1024-2048 | JPEG+PNG |
| Babylon.js | BabylonJS_PBR | 1024-2048 | PNG |
By Use Case
| Use Case | Preset | Notes |
|---|---|---|
| Quick web export | PBR Metallic Roughness | Built-in, universal |
| Smallest file size | Mobile_WebGL | JPEG color, ORM packing |
| Maximum compatibility | glTF_Standard | Strict glTF 2.0 spec |
| Maximum quality | High Quality / Archival | 16/32-bit, separate channels |
| Production pipeline | Web_ORM | Efficient, industry standard |
By Texture Count
| Texture Count | Packing Strategy | Maps |
|---|---|---|
| 3 textures | ORM packed | BaseColor, Normal, ORM |
| 4 textures | glTF standard | BaseColor, Normal, MetallicRoughness, Emissive |
| 5+ textures | Separate channels | Color, Normal, Metallic, Roughness, AO |
Best Practices
For Web Platforms
1. Use ORM packing - 3 textures instead of 6 2. JPEG for baseColor - 70-80% quality, 10× smaller 3. PNG for data maps - Normal, ORM require lossless 4. Start at 1024×1024 - Scale up only when needed 5. Test file sizes - Aim for <5MB total per asset
For Game Engines
1. Use engine-specific presets - Proper channel order/format 2. Match color space - sRGB for color, linear for data 3. Check normal format - DirectX vs. OpenGL (Y-flip) 4. Consider UDIM - For large characters (multiple tiles)
For glTF/GLB
1. Use "PBR Metallic Roughness" - Official standard 2. OpenGL normal maps - Y+ (green up) 3. Keep within spec - Avoid custom channels 4. Compress with Draco - gltf-pipeline for final export
Troubleshooting
Textures look washed out:
- BaseColor in wrong color space (should be sRGB)
- Check engine texture settings
Metallic looks rough / rough looks metallic:
- Channels swapped (check Green/Blue order)
- Verify MetallicRoughness packing: G=Roughness, B=Metallic
Normal map inverted:
- DirectX vs. OpenGL format
- Flip green channel in export or engine
Seams at UV borders:
- Use
"paddingAlgorithm": "infinite"in export parameters - Increase dilation distance
File sizes too large:
- Use JPEG for baseColor
- Reduce resolution (sizeLog2)
- Pack channels (ORM)
- Compress with external tools (pngquant, tinypng)
PBR Texture Channel Guide
Deep dive into Physically Based Rendering (PBR) texture channels, their purpose, authoring best practices, and technical specifications.
Table of Contents
PBR Fundamentals
What is PBR?
Physically Based Rendering (PBR) is a shading model that aims to accurately simulate how light interacts with surfaces in the real world. PBR workflows ensure materials look consistent under different lighting conditions.
Key principles: 1. Energy conservation - Reflected light never exceeds incoming light 2. Fresnel reflectance - Reflection intensity varies with viewing angle 3. Microsurface detail - Surface roughness affects light scattering
Metallic/Roughness vs. Specular/Glossiness
Substance 3D Painter uses the metallic/roughness workflow (also called metal/rough or met/rough).
Metallic/Roughness:
baseColor= Albedo color for dielectrics, reflectance tint for metalsmetallic= 0 for non-metals, 1 for metalsroughness= Surface roughness (0 = glossy, 1 = rough)
Advantages:
- Simpler (fewer maps)
- More physically accurate
- Industry standard (glTF, PBR games)
Alternative (Specular/Glossiness):
diffuse= Albedo colorspecular= RGB specular reflectanceglossiness= Surface smoothness (1 - roughness)
Note: Substance supports both, but metallic/roughness is recommended for web.
Core Channels
Base Color (Albedo)
What it is: The intrinsic color of the surface, without any lighting information.
Technical specs:
- Format: RGB, 8-bit per channel (24-bit total)
- Color space: sRGB (gamma ~2.2)
- Value range: 0-255 per channel
- File format: PNG or JPEG
Authoring guidelines:
For non-metals (dielectrics):
- Typical value range: sRGB 50-240 (linear 0.02-0.8)
- Dark materials: Charcoal ~50, Rubber ~60
- Mid-tone materials: Wood ~120, Leather ~100
- Light materials: Snow ~240, Paper ~200
- Never pure black (30 minimum for black materials)
- Never pure white (240 maximum for white materials)
For metals:
- Use actual metal reflectance values:
- Iron: sRGB(196, 198, 200)
- Gold: sRGB(255, 195, 86)
- Copper: sRGB(250, 155, 110)
- Aluminum: sRGB(245, 246, 246)
- Silver: sRGB(252, 250, 245)
- Bright, saturated colors for colored metals (gold, copper)
Common mistakes:
- ❌ Lighting baked in - Shadows or highlights in albedo
- ❌ Too dark - Black baseColor = no light reflection
- ❌ Too bright - Unrealistic energy conservation
- ❌ Specular details - Reflections should come from roughness, not albedo
Validation: Use the "no lighting" shader in Substance to check:
- Does the material look correct without lights?
- Are there any baked shadows or highlights?
- Are values within physically plausible ranges?
---
Normal Map
What it is: Encodes surface detail by perturbing surface normals, creating the illusion of depth without additional geometry.
Technical specs:
- Format: RGB, 8-bit per channel
- Color space: Linear (not sRGB!)
- Value range: 0-255 → maps to -1 to +1 normals
- File format: PNG (lossless required)
- Coordinate system: Tangent space
Normal format standards:
OpenGL (Y+) - Used by:
- glTF/GLB
- Three.js
- Babylon.js
- Substance 3D Painter (default export)
- WebGL/WebGPU
DirectX (Y-) - Used by:
- Unity
- Unreal Engine
- Direct3D engines
Visual difference:
- OpenGL: Green channel points "up" on the texture
- DirectX: Green channel points "down" (inverted Y)
Converting between formats:
- OpenGL → DirectX: Invert green channel (255 - G)
- DirectX → OpenGL: Invert green channel (255 - G)
Authoring guidelines:
Do:
- ✅ Bake high-poly details to normal map
- ✅ Use for micro-surface detail (scratches, dents, pores)
- ✅ Keep intensity realistic (don't over-exaggerate)
- ✅ Use 8-bit PNG (16-bit unnecessary for real-time)
Don't:
- ❌ Use JPEG (compression artifacts create shading errors)
- ❌ Apply to low-poly silhouettes (causes artifacts)
- ❌ Extreme details (creates aliasing/shimmering)
Validation:
- Flat surface = RGB(128, 128, 255) or #8080FF
- No black or white pixels (except extreme cases)
- Blue channel dominant (~255) for most pixels
---
Metallic
What it is: Defines whether a surface is metallic (conductor) or non-metallic (dielectric/insulator).
Technical specs:
- Format: Grayscale, 8-bit
- Color space: Linear
- Value range: 0-255 (0 = non-metal, 255 = metal)
- File format: PNG
Physical meaning:
- 0 (black) - Dielectric materials:
- Wood, plastic, fabric, skin, ceramic, stone
- Specular reflection ~4% (Fresnel F0 ≈ 0.04)
- BaseColor = diffuse color
- 255 (white) - Metallic materials:
- Iron, gold, copper, aluminum, silver, chrome
- Specular reflection ~60-100% (Fresnel F0 ≈ 0.6-1.0)
- BaseColor = specular tint (no diffuse)
Authoring guidelines:
Binary choice:
- Use only 0 or 255 (fully non-metal or fully metal)
- Avoid gray values (0-254) - not physically accurate
- Exception: Corroded/oxidized metals can have transition zones
Material examples:
- Metallic = 0: Wood, leather, cloth, rubber, plastic, glass, skin, concrete
- Metallic = 255: Iron, steel, aluminum, gold, silver, copper, brass, chrome
Common mistakes:
- ❌ Gradient metallic (0-255) - Not realistic
- ❌ Metallic = 255 with dark baseColor - Metals are reflective
- ❌ Metallic = 0 with bright baseColor + low roughness - Creates plastic look
Special cases:
Painted metal:
- Metallic = 0 (paint is dielectric)
- Only exposed metal areas = 255
Rusty metal:
- Clean metal = 255
- Rust (iron oxide) = 0 (rust is dielectric)
Validation:
- Histogram should show two spikes: at 0 and 255
- Very few pixels in 1-254 range
---
Roughness
What it is: Defines the microsurface roughness, controlling how light scatters when reflected.
Technical specs:
- Format: Grayscale, 8-bit
- Color space: Linear
- Value range: 0-255 (0 = smooth/glossy, 255 = rough/matte)
- File format: PNG
Physical meaning:
- 0 (black) - Mirror smooth:
- Perfect specular reflections
- Sharp, focused highlights
- Examples: Polished chrome, water, glass
- 255 (white) - Completely rough:
- Diffuse specular reflections
- Large, blurred highlights
- Examples: Concrete, fabric, unfinished wood
Authoring guidelines:
Typical value ranges:
- Mirror/chrome: 0-20
- Polished metal: 20-50
- Glossy plastic: 30-70
- Painted surfaces: 50-100
- Unfinished wood: 150-200
- Concrete/fabric: 200-255
Do:
- ✅ Use full range (0-255) across different materials
- ✅ Add variation (dirt, fingerprints, wear in crevices)
- ✅ Consider surface finish (polished vs. brushed vs. rough)
Don't:
- ❌ Uniform roughness across entire surface (boring, unrealistic)
- ❌ Pure white (255) for hard surfaces (leaves no specular)
- ❌ Pure black (0) for organic materials (too perfect)
Roughness variation examples:
Metal object:
- Base roughness: 40 (semi-polished)
- Scratches: 80 (exposed rough metal)
- Fingerprints: 30 (oil reduces roughness)
- Edges: 60 (worn from handling)
Wooden table:
- Base roughness: 100 (sanded wood)
- Glossy finish: 40 (varnish/polish)
- Worn areas: 150 (exposed grain)
Validation:
- Histogram should show variation (not single spike)
- Check for breakup in specular highlights (not uniform)
- Test under different lighting (rotating HDRI)
---
Additional Channels
Ambient Occlusion (AO)
What it is: Approximates how ambient light reaches each point on the surface. Darkens crevices and contact areas.
Technical specs:
- Format: Grayscale, 8-bit
- Color space: Linear
- Value range: 0-255 (0 = fully occluded, 255 = fully lit)
- File format: PNG
Purpose:
- Enhance depth perception
- Darken crevices, corners, contact points
- Fake indirect lighting occlusion
Authoring guidelines:
Do:
- ✅ Bake from high-poly mesh
- ✅ Subtle intensity (avoid pure black)
- ✅ Use for cavity detail (screws, seams, panel gaps)
Don't:
- ❌ Bake directional lighting (AO should be omnidirectional)
- ❌ Over-darken (reduces albedo contrast)
Web implementation:
Three.js:
material.aoMap = aoTexture;
material.aoMapIntensity = 1.0; // Adjust strength
geometry.attributes.uv2 = geometry.attributes.uv; // AO requires second UV channelNote: Many engines require a second UV channel for AO. Consider baking AO into baseColor as an alternative for simpler setups.
---
Height (Displacement)
What it is: Grayscale map encoding surface height, used for displacement mapping or parallax effects.
Technical specs:
- Format: Grayscale, 8-16 bit
- Color space: Linear
- Value range: 0-255 (0 = low, 255 = high)
- File format: PNG
Use cases: 1. Displacement mapping - Actual geometry modification (not common in real-time) 2. Parallax occlusion mapping (POM) - Shader-based fake depth 3. Normal map generation - Convert height → normal in Substance
Authoring guidelines:
- Smooth gradients (avoid sharp steps)
- Sufficient contrast for visible effect
- Usually lower resolution than other maps (512-1024)
Web usage: Limited support in real-time engines. Height is usually converted to normal maps in Substance before export.
---
Emissive (Self-Illumination)
What it is: RGB map defining areas that emit light (glow).
Technical specs:
- Format: RGB, 8-bit or 16-bit
- Color space: Linear (for HDR) or sRGB (for LDR)
- Value range: 0-255 (8-bit) or 0-65535 (16-bit)
- File format: PNG or EXR
Use cases:
- Glowing screens, LEDs, neon signs
- Bioluminescence
- Hot metal/lava
- Energy effects
Authoring guidelines:
LDR emissive (8-bit):
- Black (0,0,0) = no emission
- White (255,255,255) = full brightness
- Use
emissiveIntensityin engine to control brightness
HDR emissive (16/32-bit EXR):
- Values can exceed 1.0 for bloom effects
- Physically accurate for bright lights
Web implementation:
Three.js:
material.emissive = new THREE.Color(0xffffff);
material.emissiveMap = emissiveTexture;
material.emissiveIntensity = 2.0; // Boost for bloomCommon mistake: ❌ Using emissive for ambient occlusion (emissive should glow, not darken)
---
Opacity (Alpha)
What it is: Grayscale map controlling surface transparency.
Technical specs:
- Format: Grayscale, 8-bit
- Color space: Linear
- Value range: 0-255 (0 = transparent, 255 = opaque)
- File format: PNG (with alpha channel support)
Use cases:
- Glass, water, smoke
- Vegetation (leaves, grass)
- Decals, stickers
- Fading effects
Authoring guidelines:
Binary alpha (cutout):
- Use only 0 or 255 (fully transparent or opaque)
- Efficient for vegetation (no alpha blending)
- Set
alphaTestthreshold in engine
Gradient alpha (blended):
- Full 0-255 range for smooth transparency
- More expensive (requires alpha blending, sorting)
Web implementation:
Three.js:
material.transparent = true;
material.alphaMap = opacityTexture;
material.alphaTest = 0.5; // For binary alpha (cutout)
material.opacity = 1.0; // Overall opacity multiplierPerformance notes:
- Alpha blending is expensive (disable backface culling, requires sorting)
- Use
alphaTest(cutout) instead oftransparentwhen possible
Channel Best Practices
Color Space Management
sRGB (Gamma ~2.2):
- BaseColor
- Emissive (8-bit)
Linear (No Gamma):
- Normal
- Metallic
- Roughness
- AO
- Height
- Opacity
Why it matters: Incorrect color space causes incorrect lighting calculations. Always ensure:
- BaseColor textures have sRGB flag in engine
- Data textures (normal, roughness, etc.) use linear
Three.js example:
baseColorTexture.colorSpace = THREE.SRGBColorSpace; // ✅ Correct
normalTexture.colorSpace = THREE.NoColorSpace; // ✅ Correct (linear)---
Resolution Guidelines
By channel importance:
Highest detail (1024-2048):
- BaseColor (most visible)
- Normal (defines micro-detail)
Medium detail (512-1024):
- Roughness (specular breakup)
- Metallic (usually binary, less detail)
Lower detail (512-1024):
- AO (subtle effect)
- Emissive (often solid colors)
Lowest detail (256-512):
- Height (usually converted to normal)
Web optimization: All channels can share the same resolution (1024×1024) for simplicity, but consider per-channel resizing for advanced optimization.
---
File Format Selection
| Channel | Recommended Format | Reason |
|---|---|---|
| BaseColor | JPEG (70-80%) or PNG-8 | Large file, compression acceptable |
| Normal | PNG-8 | Lossless required, compression breaks lighting |
| Metallic | PNG-8 | Binary data, lossless required |
| Roughness | PNG-8 | Subtle details, lossless preferred |
| AO | PNG-8 or JPEG (80%) | Subtle, can tolerate some compression |
| Height | PNG-16 | Benefits from higher bit depth |
| Emissive | PNG-8 (LDR) or EXR-32 (HDR) | HDR for bloom effects |
| Opacity | PNG-8 (with alpha) | Requires alpha channel |
---
Texture Streaming & LOD
Mipmaps: Always generate mipmaps for all textures. Engines generate automatically, but you can bake custom mipmaps in Substance for specific filtering.
LOD strategy: Export multiple resolution sets:
- LOD0 (close-up): 2048×2048
- LOD1 (medium): 1024×1024
- LOD2 (distant): 512×512
Implementation (Three.js):
const lod = new THREE.LOD();
lod.addLevel(mesh_high_res, 0); // 0-10 units
lod.addLevel(mesh_med_res, 10); // 10-30 units
lod.addLevel(mesh_low_res, 30); // 30+ unitsCommon Material Types
Plastic
BaseColor:
- Saturated colors (RGB 180, 60, 40 for red plastic)
- Consistent across surface
Metallic: 0 (non-metal)
Roughness:
- Glossy plastic: 30-50
- Matte plastic: 150-200
Example (Three.js):
material.color = new THREE.Color(0xb43c28); // Orange plastic
material.metalness = 0.0;
material.roughness = 0.3; // Glossy---
Brushed Metal
BaseColor:
- Metal reflectance value (e.g., Aluminum: #F5F6F6)
Metallic: 255 (full metal)
Roughness:
- Directional variation (brush lines)
- 40-80 depending on finish
Special: Add anisotropic roughness for realistic brushed look (advanced shaders)
---
Wood
BaseColor:
- Natural wood colors (browns, tans)
- Wood grain texture
- RGB 90-150 range
Metallic: 0 (non-metal)
Roughness:
- Unfinished: 150-200
- Varnished: 40-80
- Add variation (grain roughness differs)
AO: Cavity detail in grain
---
Glass
BaseColor:
- Slight tint (RGB 245, 245, 245 for clear)
Metallic: 0
Roughness:
- Clean glass: 0-10
- Frosted glass: 100-200
Opacity: 20-50 (mostly transparent)
Special: Use refraction in engine (transmission in Three.js)
---
Fabric
BaseColor:
- Fabric color with weave detail
- Medium values (RGB 80-180)
Metallic: 0
Roughness: 180-220 (very rough)
Normal: Weave/fiber details
Special: Consider subsurface scattering for translucent fabrics
---
Rusty Metal
BaseColor:
- Clean metal: Metal reflectance
- Rust: Orange-brown (RGB 150, 80, 40)
- Use mask to blend
Metallic:
- Clean metal: 255
- Rust (iron oxide): 0
- Use same mask as baseColor
Roughness:
- Clean metal: 30-50
- Rust: 180-220
Example mask-based blend:
# In Substance, use rust mask to drive:
# - BaseColor: Metal color → Rust color
# - Metallic: 255 → 0
# - Roughness: 40 → 200Validation Checklist
Before exporting, verify:
BaseColor:
- [ ] No baked lighting/shadows
- [ ] Values within plausible range (30-240 for most materials)
- [ ] Metals use actual metal reflectance colors
Normal:
- [ ] Correct format (OpenGL vs. DirectX)
- [ ] No compression artifacts (PNG only)
- [ ] Blue channel dominant
Metallic:
- [ ] Binary values (0 or 255 only)
- [ ] Matches material type (0 for dielectrics)
Roughness:
- [ ] Has surface variation (not uniform)
- [ ] Within realistic range for material
All Maps:
- [ ] Correct color space (sRGB vs. linear)
- [ ] Appropriate resolution (not oversized)
- [ ] Proper file format (lossless for data, JPEG for color)
- [ ] Padding set to "infinite" (no seams)
Resources
- PBR Guide: https://academy.substance3d.com/courses/the-pbr-guide-part-1
- Material Values: https://docs.unrealengine.com/en-US/Resources/ContentExamples/MaterialProperties/
- Texture Naming: glTF 2.0 specification
Substance 3D Painter Python API Reference
Complete reference for automating Substance 3D Painter workflows with Python.
API Overview
The Substance 3D Painter Python API provides programmatic access to:
- Export System - Texture export configuration and execution
- Project Management - Open, save, close projects
- Texture Sets - Access and modify texture sets
- Resource Management - Access materials, brushes, presets
- Event System - React to application events
- UI System - Add custom menu items and dialogs
Core Modules
substance_painter.export
Texture export functionality.
Functions
export_project_textures(json_config: dict) → ExportResult
Exports project textures according to JSON configuration.
Parameters:
json_config(dict) - Export configuration object
Returns:
ExportResultwith status, message, and list of exported files
Raises:
ProjectError- No project is openValueError- Invalid configuration
Example:
import substance_painter.export
config = {
"exportPath": "C:/export",
"defaultExportPreset": "starter_assets://PBR Metallic Roughness",
"exportList": [{"rootPath": "TextureSetName"}],
"exportParameters": [{
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
}]
}
result = substance_painter.export.export_project_textures(config)
if result.status == substance_painter.export.ExportStatus.Success:
print(f"Exported {len(result.textures)} texture sets")---
list_project_textures(json_config: dict) → Dict[Tuple[str, str], List[str]]
Lists textures that would be exported without actually exporting.
Parameters:
json_config(dict) - Export configuration
Returns:
- Dictionary mapping (texture_set, stack) to list of file paths
Example:
texture_list = substance_painter.export.list_project_textures(config)
for stack, files in texture_list.items():
print(f"Stack {stack}:")
for file in files:
print(f" - {file}")Classes
ExportStatus (Enum)
Export operation status codes:
Success- Export completed successfullyError- Export failed
ExportResult
Result object returned by export_project_textures().
Attributes:
status(ExportStatus) - Status codemessage(str) - Human-readable status messagetextures(Dict[Tuple[str, str], List[str]]) - Exported files grouped by stack
Events
ExportTexturesAboutToStart
Triggered before export begins.
Attributes:
textures(Dict[Tuple[str, str], List[str]]) - Files to be exported
Example:
import substance_painter.event
def on_export_start(event):
print(f"Exporting {len(event.textures)} texture sets")
substance_painter.event.DISPATCHER.connect(
substance_painter.event.ExportTexturesAboutToStart,
on_export_start
)ExportTexturesEnded
Triggered after export completes.
Attributes:
status(ExportStatus) - Export statusmessage(str) - Status messagetextures(Dict[Tuple[str, str], List[str]]) - Exported files
substance_painter.project
Project management operations.
Functions
is_open() → bool
Check if a project is currently open.
Example:
if substance_painter.project.is_open():
print("Project is open")---
open(file_path: str) → None
Open an existing Substance project (.spp).
Parameters:
file_path(str) - Absolute path to .spp file
Example:
substance_painter.project.open("C:/projects/MyProject.spp")---
save() → None
Save the current project.
---
save_as(file_path: str) → None
Save project to a new file path.
---
close() → None
Close the current project.
---
file_path() → str
Get the current project's file path.
Returns:
- Absolute path to .spp file, or empty string if no project open
Example:
project_path = substance_painter.project.file_path()
export_path = project_path.replace('.spp', '_textures')---
name() → str
Get the current project's name (filename without extension).
Events
ProjectOpened
Triggered when a project is opened.
ProjectAboutToClose
Triggered before project closes.
ProjectAboutToSave
Triggered before project saves.
ProjectSaved
Triggered after project saves.
Example: Auto-export on save
import substance_painter.event
import substance_painter.project
import substance_painter.export
def auto_export(event):
if not substance_painter.project.is_open():
return
# Export textures when project saves
config = {
"exportPath": substance_painter.project.file_path().replace('.spp', '_export'),
# ... rest of config
}
substance_painter.export.export_project_textures(config)
substance_painter.event.DISPATCHER.connect(
substance_painter.event.ProjectSaved,
auto_export
)substance_painter.textureset
Access texture sets and UV tiles.
Functions
all_texture_sets() → List[TextureSet]
Get all texture sets in the project.
Returns:
- List of TextureSet objects
Example:
import substance_painter.textureset
for ts in substance_painter.textureset.all_texture_sets():
print(f"Texture Set: {ts.name()}")
print(f" Layered: {ts.is_layered_material()}")
print(f" Resolution: {ts.get_resolution()}")---
all_uv_tiles() → List[UVTile]
Get all UV tiles in the project (for UDIM workflows).
Returns:
- List of UVTile objects
Example:
for tile in substance_painter.textureset.all_uv_tiles():
print(f"Tile ({tile.u}, {tile.v}): {tile.get_resolution()}")---
set_uvtiles_resolution(resolutions: Dict[UVTile, Resolution]) → None
Set resolution for multiple UV tiles.
Parameters:
resolutions(dict) - Mapping of UVTile to Resolution
Example:
from substance_painter.textureset import Resolution
# Set all tiles in first row to 2K
row0 = [tile for tile in all_uv_tiles() if tile.v == 0]
resolution_2k = Resolution(2048, 2048)
substance_painter.textureset.set_uvtiles_resolution({
tile: resolution_2k for tile in row0
})Classes
TextureSet
Represents a texture set (material slot).
Methods:
name() → str- Get texture set nameis_layered_material() → bool- Check if uses layer stacksget_resolution() → Resolution- Get texture resolutionget_stack() → Stack- Get layer stack (if layered)
Stack
Represents a layer stack in a texture set.
Methods:
material() → TextureSet- Get parent texture setget_channel(channel_type: ChannelType) → Channel- Get specific channel
Channel
Represents a texture channel (baseColor, normal, etc.).
Methods:
is_color() → bool- Check if channel contains color data
ChannelType (Enum)
Available texture channels:
BaseColorNormalMetallicRoughnessHeightOpacityEmissiveAmbientOcclusion
Resolution
Texture resolution container.
Attributes:
width(int) - Width in pixelsheight(int) - Height in pixels
Constructor:
from substance_painter.textureset import Resolution
res = Resolution(2048, 2048) # 2KUVTile
Represents a UV tile in UDIM workflow.
Attributes:
u(int) - U coordinate (0-based)v(int) - V coordinate (0-based)
Methods:
get_resolution() → Resolution- Get tile resolutionset_resolution(resolution: Resolution) → None- Set tile resolution
substance_painter.resource
Access and manage resources (materials, brushes, presets).
Classes
ResourceID
Identifier for a resource.
Constructor:
resource = substance_painter.resource.ResourceID(
context="starter_assets", # Library name
name="PBR Metallic Roughness" # Resource name
)Methods:
url() → str- Get resource URL (e.g., "starter_assets://PBR Metallic Roughness")
Example: Use export preset
export_preset = substance_painter.resource.ResourceID(
context="starter_assets",
name="Unity HDRP (Lit)"
)
config = {
"defaultExportPreset": export_preset.url(),
# ...
}substance_painter.ui
UI customization and menu integration.
Enums
ApplicationMenu
Available application menus:
FileEditModeViewViewportShaderWindowHelp
Functions
add_action(menu: ApplicationMenu, action: QtWidgets.QAction) → None
Add menu item to application menu.
Parameters:
menu(ApplicationMenu) - Target menuaction(QAction) - Qt action to add
Example:
from PySide2 import QtWidgets
import substance_painter.ui
def my_function():
print("Menu item clicked!")
action = QtWidgets.QAction("My Custom Command", triggered=my_function)
substance_painter.ui.add_action(substance_painter.ui.ApplicationMenu.File, action)---
delete_ui_element(element) → None
Remove UI element (cleanup when plugin closes).
---
get_main_window() → QtWidgets.QMainWindow
Get main application window (for parenting dialogs).
Example: File dialog
from PySide2 import QtWidgets
export_path = QtWidgets.QFileDialog.getExistingDirectory(
substance_painter.ui.get_main_window(),
"Choose export directory"
)substance_painter.event
Event system for reacting to application events.
Event Dispatcher
DISPATCHER
Global event dispatcher object.
Methods:
connect(event_type, callback)- Register event handlerdisconnect(event_type, callback)- Unregister event handler
Example:
import substance_painter.event
def on_project_opened(event):
print("Project opened!")
substance_painter.event.DISPATCHER.connect(
substance_painter.event.ProjectOpened,
on_project_opened
)Available Events
Project Events:
ProjectOpenedProjectAboutToCloseProjectAboutToSaveProjectSaved
Export Events:
ExportTexturesAboutToStartExportTexturesEnded
Shader Events:
ShaderAddedShaderRemoved
Texture Set Events:
TextureSetAddedTextureSetRemoved
Export Configuration Schema
Complete JSON schema for export_project_textures() config parameter.
Root Configuration Object
{
"exportPath": str, # Required: Output directory path
"exportShaderParams": bool, # Optional: Export shader params to JSON (default: false)
"defaultExportPreset": str, # Optional: Default preset URL
"exportPresets": list, # Optional: Custom export presets
"exportList": list, # Required: Texture sets to export
"exportParameters": list # Optional: Export parameter overrides
}Export List Item
{
"rootPath": str, # Required: Texture set name (or "TextureSet/Stack" for layered)
"exportPreset": str, # Optional: Override preset for this item
"filter": { # Optional: Filter which maps/tiles to export
"outputMaps": list, # List of map names (e.g., ["$textureSet_baseColor"])
"uvTiles": list # List of [u, v] tile coordinates (e.g., [[1,1], [1,2]])
}
}Export Parameters Item
{
"filter": { # Optional: Match specific files
"dataPaths": list, # Texture set/stack names
"outputMaps": list, # Map name patterns
"uvTiles": list # UV tile coordinates
},
"parameters": { # Optional: Override export settings
"fileFormat": str, # "png", "jpeg", "tga", "exr", etc.
"bitDepth": str, # "8", "16", "32"
"dithering": bool, # Enable dithering
"sizeLog2": int, # Texture size: 9=512, 10=1024, 11=2048, 12=4096
"paddingAlgorithm": str, # "passthrough", "color", "transparent", "diffusion", "infinite"
"dilationDistance": int # Padding distance in pixels (required for some padding modes)
}
}Custom Export Preset
{
"name": str, # Preset name
"maps": [ # List of texture maps to export
{
"fileName": str, # Output filename pattern (e.g., "$textureSet_baseColor")
"channels": [ # Channel mapping
{
"destChannel": str, # "R", "G", "B", "A"
"srcChannel": str, # "R", "G", "B", "A"
"srcMapType": str, # "documentMap"
"srcMapName": str # "baseColor", "normal", "metallic", etc.
}
],
"parameters": { # Optional: Per-map export parameters
"fileFormat": str,
"bitDepth": str,
"sizeLog2": int
}
}
]
}Filename Patterns
Variables available in fileName patterns:
$textureSet- Texture set name$project- Project name$mesh- Mesh name$udim- UDIM tile number (e.g., 1001)
Examples:
$textureSet_baseColor→ "MyAsset_baseColor.png"$project_$textureSet_color→ "MyProject_MyAsset_color.png"
Complete Examples
Example 1: Basic Batch Export
Export all texture sets with standard settings:
import substance_painter.export
import substance_painter.resource
import substance_painter.textureset
# Define preset
preset = substance_painter.resource.ResourceID(
context="starter_assets",
name="PBR Metallic Roughness"
)
# Build config
config = {
"exportPath": "C:/export",
"defaultExportPreset": preset.url(),
"exportList": [
{"rootPath": ts.name()}
for ts in substance_painter.textureset.all_texture_sets()
],
"exportParameters": [{
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10,
"paddingAlgorithm": "infinite"
}
}]
}
# Export
result = substance_painter.export.export_project_textures(config)
print(f"Status: {result.status}")Example 2: Selective Export with Filters
Export only baseColor at high resolution, other maps at lower resolution:
config = {
"exportPath": "C:/export",
"defaultExportPreset": preset.url(),
"exportList": [{"rootPath": "CharacterBody"}],
"exportParameters": [
{
"filter": {"outputMaps": ["$textureSet_baseColor"]},
"parameters": {"sizeLog2": 12} # 4096 for baseColor
},
{
"filter": {"outputMaps": ["$textureSet_normal", "$textureSet_metallicRoughness"]},
"parameters": {"sizeLog2": 11} # 2048 for other maps
}
]
}Example 3: Custom Preset with Channel Packing
Create ORM (Occlusion-Roughness-Metallic) packed texture:
custom_preset = {
"exportPresets": [{
"name": "WebGL_ORM",
"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_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"}
]
}
]
}]
}
config = {
"exportPath": "C:/export",
"exportPresets": custom_preset["exportPresets"],
"exportList": [{"rootPath": "MyAsset", "exportPreset": "WebGL_ORM"}],
"exportParameters": [{
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}]
}Example 4: UDIM Export
Export specific UV tiles with different resolutions:
from substance_painter.textureset import Resolution
# Set tile 1001 to 4K, others to 2K
tile_1001 = next((t for t in all_uv_tiles() if t.u == 0 and t.v == 0), None)
if tile_1001:
tile_1001.set_resolution(Resolution(4096, 4096))
config = {
"exportPath": "C:/export",
"defaultExportPreset": preset.url(),
"exportList": [
{
"rootPath": "UDIMAsset",
"filter": {"uvTiles": [[0, 0], [1, 0], [2, 0]]} # Export first row
}
]
}Plugin Development
Creating a Plugin
Directory structure:
plugins/
└── my_plugin/
├── __init__.py
└── plugin.pyplugin.py:
import substance_painter.ui
from PySide2 import QtWidgets
plugin_widgets = []
def my_command():
print("Plugin command executed!")
def start_plugin():
"""Called when plugin loads"""
action = QtWidgets.QAction("My Command", triggered=my_command)
substance_painter.ui.add_action(substance_painter.ui.ApplicationMenu.File, action)
plugin_widgets.append(action)
def close_plugin():
"""Called when plugin unloads"""
for widget in plugin_widgets:
substance_painter.ui.delete_ui_element(widget)
plugin_widgets.clear()
if __name__ == "__main__":
start_plugin()Plugin Best Practices
1. Store UI elements in module-level list for cleanup 2. Check project state before operations:
if not substance_painter.project.is_open():
return3. Handle errors gracefully:
try:
result = substance_painter.export.export_project_textures(config)
except ValueError as e:
print(f"Export failed: {e}")4. Use absolute paths for file operations 5. Clean up resources in close_plugin()
Troubleshooting
Common Issues
Import Error: No module named 'substance_painter'
- API only available when running inside Substance 3D Painter
- Use File → Scripts → Run Script to execute Python code
ProjectError: No project is open
- Check
substance_painter.project.is_open()before API calls
ValueError: Invalid export configuration
- Verify all required fields in export config
- Check
exportPathexists and is writable - Ensure
exportListis not empty
ResourceID not found
- Verify
context(library name) is correct - Check resource
namematches exactly (case-sensitive) - Use Substance UI to find resource names
Debugging Tips
Print configuration:
import json
print(json.dumps(config, indent=2))List available texture sets:
for ts in substance_painter.textureset.all_texture_sets():
print(f"Texture Set: {ts.name()}")Preview export list without exporting:
preview = substance_painter.export.list_project_textures(config)
for stack, files in preview.items():
print(f"{stack}: {files}")Resources
- Official Python API Docs: https://helpx.adobe.com/substance-3d-painter-python/api/
- Substance 3D Community: https://community.adobe.com/t5/substance-3d-painter/ct-p/ct-substance-3d-painter
- Python Plugin Examples: Included in Substance installation under
python/examples/
#!/usr/bin/env python3
"""
Substance 3D Painter Batch Export Script
Exports all texture sets from the currently open Substance Painter project
using the PBR Metallic Roughness preset optimized for web.
Usage:
# Run from Substance Painter: File → Python → Execute Script
# Or load as plugin
Requirements:
- Substance 3D Painter must be running
- A project must be open
- substance_painter module (available in Substance environment)
Example:
import batch_export
batch_export.export_all_web()
# Or customize export path
batch_export.export_all_web(export_path="C:/MyExports")
"""
try:
import substance_painter.export
import substance_painter.resource
import substance_painter.textureset
import substance_painter.project
except ImportError:
print("ERROR: This script must be run inside Substance 3D Painter")
print("Go to: File → Python → Execute Script")
exit(1)
def export_all_web(export_path=None, resolution=1024):
"""
Export all texture sets with web-optimized settings.
Args:
export_path (str): Output directory. If None, exports next to .spp file.
resolution (int): Texture size (512, 1024, 2048, 4096).
Returns:
bool: True if export succeeded, False otherwise.
"""
# Verify project is open
if not substance_painter.project.is_open():
print("ERROR: No project is open. Open a project first.")
return False
# Determine export path
if export_path is None:
project_path = substance_painter.project.file_path()
if not project_path:
print("ERROR: Project has no file path (save the project first)")
return False
export_path = project_path.replace('.spp', '_textures')
print(f"Export directory: {export_path}")
# Map resolution to sizeLog2
size_map = {
512: 9,
1024: 10,
2048: 11,
4096: 12
}
if resolution not in size_map:
print(f"ERROR: Invalid resolution {resolution}. Use 512, 1024, 2048, or 4096.")
return False
size_log2 = size_map[resolution]
# Define export preset
try:
export_preset = substance_painter.resource.ResourceID(
context="starter_assets",
name="PBR Metallic Roughness"
)
except Exception as e:
print(f"ERROR: Could not load export preset: {e}")
return False
# Build export configuration
config = {
"exportShaderParams": False,
"exportPath": export_path,
"defaultExportPreset": export_preset.url(),
"exportList": [],
"exportParameters": [{
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"dithering": True,
"paddingAlgorithm": "infinite",
"sizeLog2": size_log2
}
}]
}
# Add all texture sets to export list
texture_sets = substance_painter.textureset.all_texture_sets()
if not texture_sets:
print("WARNING: No texture sets found in project")
return False
for texture_set in texture_sets:
config["exportList"].append({
"rootPath": texture_set.name()
})
print(f"Added to export queue: {texture_set.name()}")
print(f"\nExporting {len(texture_sets)} texture set(s) at {resolution}×{resolution}...")
# Execute export
try:
result = substance_painter.export.export_project_textures(config)
if result.status == substance_painter.export.ExportStatus.Success:
print("\n✓ Export completed successfully!")
print(f"\nExported textures:")
total_files = 0
for stack, files in result.textures.items():
print(f"\n {stack}:")
for file_path in files:
print(f" - {file_path}")
total_files += 1
print(f"\nTotal files exported: {total_files}")
return True
else:
print(f"\n✗ Export failed: {result.message}")
return False
except ValueError as e:
print(f"\n✗ Export configuration error: {e}")
return False
except Exception as e:
print(f"\n✗ Unexpected error during export: {e}")
return False
def export_mobile_optimized(export_path=None):
"""
Export with aggressive mobile optimization.
Features:
- 512×512 resolution
- JPEG for baseColor (smaller file size)
- PNG for data maps (lossless required)
Args:
export_path (str): Output directory. If None, exports to project_mobile/.
Returns:
bool: True if export succeeded, False otherwise.
"""
if not substance_painter.project.is_open():
print("ERROR: No project is open.")
return False
# Determine export path
if export_path is None:
project_path = substance_painter.project.file_path()
if not project_path:
print("ERROR: Project has no file path (save the project first)")
return False
export_path = project_path.replace('.spp', '_mobile')
print(f"Mobile export directory: {export_path}")
# Define export preset
export_preset = substance_painter.resource.ResourceID(
context="starter_assets",
name="PBR Metallic Roughness"
)
# Build export configuration with mobile optimizations
config = {
"exportShaderParams": False,
"exportPath": export_path,
"defaultExportPreset": export_preset.url(),
"exportList": [],
"exportParameters": [
{
# Default: JPEG for baseColor
"filter": {"outputMaps": ["$textureSet_baseColor"]},
"parameters": {
"fileFormat": "jpeg",
"bitDepth": "8",
"sizeLog2": 9, # 512×512
"paddingAlgorithm": "infinite"
}
},
{
# PNG for normal and metallicRoughness (data maps need lossless)
"filter": {"outputMaps": ["$textureSet_normal", "$textureSet_metallicRoughness"]},
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 9, # 512×512
"paddingAlgorithm": "infinite"
}
}
]
}
# Add all texture sets
for texture_set in substance_painter.textureset.all_texture_sets():
config["exportList"].append({"rootPath": texture_set.name()})
print(f"Exporting {len(substance_painter.textureset.all_texture_sets())} texture set(s) for mobile...")
# Execute export
try:
result = substance_painter.export.export_project_textures(config)
if result.status == substance_painter.export.ExportStatus.Success:
print("\n✓ Mobile export completed!")
return True
else:
print(f"\n✗ Export failed: {result.message}")
return False
except Exception as e:
print(f"\n✗ Error: {e}")
return False
def print_texture_sets():
"""Print all texture sets in the current project."""
if not substance_painter.project.is_open():
print("No project is open.")
return
texture_sets = substance_painter.textureset.all_texture_sets()
if not texture_sets:
print("No texture sets found in project.")
return
print(f"\nFound {len(texture_sets)} texture set(s):\n")
for i, ts in enumerate(texture_sets, 1):
print(f"{i}. {ts.name()}")
print(f" Layered: {ts.is_layered_material()}")
resolution = ts.get_resolution()
print(f" Resolution: {resolution.width}×{resolution.height}")
print()
def main():
"""Main entry point when script is executed directly."""
print("=" * 60)
print("Substance 3D Painter - Batch Export Script")
print("=" * 60)
if not substance_painter.project.is_open():
print("\nERROR: No project is open.")
print("Please open a Substance Painter project first.")
return
# Show project info
project_name = substance_painter.project.name()
print(f"\nProject: {project_name}")
# Show texture sets
print_texture_sets()
# Perform export
print("Starting web export (1024×1024)...")
success = export_all_web(resolution=1024)
if success:
print("\n" + "=" * 60)
print("Export complete! Check the output directory.")
print("=" * 60)
else:
print("\nExport failed. See errors above.")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Substance 3D Painter Export Preset Generator
Interactive tool for creating custom export preset JSON configurations.
Usage:
# Interactive mode
./generate_export_preset.py
# Quick presets
./generate_export_preset.py --preset gltf
./generate_export_preset.py --preset threejs
./generate_export_preset.py --preset orm
# Output to file
./generate_export_preset.py --preset gltf --output my_preset.json
Presets:
- gltf: glTF 2.0 standard (MetallicRoughness)
- threejs: Three.js optimized (separate channels)
- babylonjs: Babylon.js PBR format
- orm: Occlusion-Roughness-Metallic packed
- mobile: Mobile-optimized (512px, JPEG color)
Requirements:
- Python 3.6+
- Standard library only
"""
import json
import sys
import argparse
# Preset templates
PRESETS = {
'gltf': {
"exportPresets": [{
"name": "glTF_Standard",
"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"}
],
"parameters": {
"fileFormat": "png",
"bitDepth": "8",
"sizeLog2": 10
}
},
{
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_metallicRoughness",
"channels": [
{"destChannel": "G", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "roughness"},
{"destChannel": "B", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "metallic"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}
]
}]
},
'threejs': {
"exportPresets": [{
"name": "ThreeJS_Standard",
"maps": [
{
"fileName": "$textureSet_color",
"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"}
],
"parameters": {"fileFormat": "jpeg", "bitDepth": "8", "sizeLog2": 10}
},
{
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_ao",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "ambientOcclusion"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_metalness",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "metallic"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_roughness",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "roughness"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}
]
}]
},
'babylonjs': {
"exportPresets": [{
"name": "BabylonJS_PBR",
"maps": [
{
"fileName": "$textureSet_albedo",
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"fileName": "$textureSet_metallicRoughness",
"channels": [
{"destChannel": "G", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "roughness"},
{"destChannel": "B", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "metallic"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}
]
}]
},
'orm': {
"exportPresets": [{
"name": "Web_ORM",
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
},
{
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 10}
}
]
}]
},
'mobile': {
"exportPresets": [{
"name": "Mobile_WebGL",
"maps": [
{
"fileName": "$textureSet_color",
"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"}
],
"parameters": {"fileFormat": "jpeg", "bitDepth": "8", "sizeLog2": 9}
},
{
"fileName": "$textureSet_normal",
"channels": [
{"destChannel": "R", "srcChannel": "R", "srcMapType": "documentMap", "srcMapName": "normal"},
{"destChannel": "G", "srcChannel": "G", "srcMapType": "documentMap", "srcMapName": "normal"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 9}
},
{
"fileName": "$textureSet_packed",
"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"}
],
"parameters": {"fileFormat": "png", "bitDepth": "8", "sizeLog2": 9}
}
]
}]
}
}
def print_preset_info(preset_name):
"""Print information about a preset."""
descriptions = {
'gltf': "glTF 2.0 standard format with MetallicRoughness packing (OpenGL normals)",
'threejs': "Three.js optimized with separate channels and JPEG color",
'babylonjs': "Babylon.js PBR format with MetallicRoughness packing",
'orm': "Occlusion-Roughness-Metallic packed format (most efficient)",
'mobile': "Mobile-optimized with 512px resolution and aggressive compression"
}
if preset_name in descriptions:
print(f"\n{preset_name.upper()} Preset:")
print(f" {descriptions[preset_name]}")
def generate_preset_json(preset_name):
"""Generate preset JSON configuration."""
if preset_name not in PRESETS:
print(f"ERROR: Unknown preset '{preset_name}'")
print(f"Available presets: {', '.join(PRESETS.keys())}")
return None
return PRESETS[preset_name]
def print_usage_example(preset_name, output_file=None):
"""Print usage example for the generated preset."""
print("\n" + "=" * 60)
print("USAGE EXAMPLE (Python)")
print("=" * 60)
if output_file:
print(f"""
import json
import substance_painter.export
# Load custom preset
with open('{output_file}', 'r') as f:
custom_preset = json.load(f)
config = {{
"exportPath": "C:/export",
"exportPresets": custom_preset["exportPresets"],
"exportList": [{{"rootPath": "MyAsset", "exportPreset": "{PRESETS[preset_name]['exportPresets'][0]['name']}"}}],
"exportParameters": [{{
"parameters": {{"paddingAlgorithm": "infinite"}}
}}]
}}
result = substance_painter.export.export_project_textures(config)
""")
else:
print(f"""
import substance_painter.export
preset = {json.dumps(PRESETS[preset_name], indent=2)}
config = {{
"exportPath": "C:/export",
"exportPresets": preset["exportPresets"],
"exportList": [{{"rootPath": "MyAsset", "exportPreset": "{PRESETS[preset_name]['exportPresets'][0]['name']}"}}]
}}
result = substance_painter.export.export_project_textures(config)
""")
def interactive_mode():
"""Interactive preset builder."""
print("=" * 60)
print("Substance 3D Painter - Export Preset Generator")
print("=" * 60)
print("\nAvailable presets:\n")
for i, (name, _) in enumerate(PRESETS.items(), 1):
print(f"{i}. {name}")
print_preset_info(name)
print()
while True:
try:
choice = input("Select preset (1-5) or 'q' to quit: ").strip()
if choice.lower() == 'q':
sys.exit(0)
preset_index = int(choice) - 1
preset_names = list(PRESETS.keys())
if 0 <= preset_index < len(preset_names):
preset_name = preset_names[preset_index]
break
else:
print("Invalid selection. Try again.")
except ValueError:
print("Invalid input. Enter a number 1-5.")
except KeyboardInterrupt:
print("\nCancelled.")
sys.exit(0)
# Generate preset
preset_json = generate_preset_json(preset_name)
print("\n" + "=" * 60)
print(f"Generated {preset_name.upper()} Preset")
print("=" * 60)
print(json.dumps(preset_json, indent=2))
# Ask to save
save = input("\nSave to file? (y/n): ").strip().lower()
if save == 'y':
filename = input("Enter filename (default: export_preset.json): ").strip()
if not filename:
filename = "export_preset.json"
if not filename.endswith('.json'):
filename += '.json'
with open(filename, 'w') as f:
json.dump(preset_json, f, indent=2)
print(f"\n✓ Preset saved to: {filename}")
print_usage_example(preset_name, filename)
else:
print_usage_example(preset_name)
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Generate custom Substance 3D Painter export presets"
)
parser.add_argument(
'--preset',
choices=list(PRESETS.keys()),
help="Preset type (gltf, threejs, babylonjs, orm, mobile)"
)
parser.add_argument(
'-o', '--output',
dest='output_file',
help="Output JSON file path"
)
args = parser.parse_args()
# Interactive mode if no preset specified
if args.preset is None:
interactive_mode()
return
# Generate preset
preset_json = generate_preset_json(args.preset)
if preset_json is None:
sys.exit(1)
# Output to file or stdout
if args.output_file:
with open(args.output_file, 'w') as f:
json.dump(preset_json, f, indent=2)
print(f"✓ {args.preset.upper()} preset saved to: {args.output_file}")
print_usage_example(args.preset, args.output_file)
else:
print(json.dumps(preset_json, indent=2))
print_usage_example(args.preset)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Web Texture Optimizer
Post-processes exported textures from Substance 3D Painter for optimal web delivery.
Performs resizing, format conversion, and reports file size savings.
Features:
- Resize textures to target resolution
- Convert baseColor to JPEG (lossy compression)
- Keep data maps as PNG (lossless)
- Generate multiple LOD levels
- Report file size savings
Usage:
./web_optimizer.py <input_directory> [options]
./web_optimizer.py C:/exports/MyAsset --resolution 1024
./web_optimizer.py C:/exports/MyAsset --lod 3
./web_optimizer.py C:/exports/MyAsset --jpeg-quality 80
Requirements:
- Python 3.6+
- Pillow library (optional, for image processing)
Install: pip install Pillow
- If Pillow not available, provides instructions for manual optimization
Note:
This script uses only standard library when Pillow is not installed.
With Pillow, it can perform automatic image processing.
"""
import os
import sys
import argparse
from pathlib import Path
def check_pillow():
"""Check if Pillow is installed."""
try:
from PIL import Image
return True
except ImportError:
return False
def get_file_size_mb(file_path):
"""Get file size in MB."""
return os.path.getsize(file_path) / (1024 * 1024)
def resize_texture(input_path, output_path, target_size, quality=85):
"""
Resize texture and optionally convert format.
Args:
input_path (str): Input image path
output_path (str): Output image path
target_size (int): Target resolution (width = height)
quality (int): JPEG quality (1-100)
Returns:
bool: Success
"""
try:
from PIL import Image
img = Image.open(input_path)
# Resize with high-quality resampling
resized = img.resize((target_size, target_size), Image.Resampling.LANCZOS)
# Save with appropriate settings
if output_path.lower().endswith('.jpg') or output_path.lower().endswith('.jpeg'):
resized.save(output_path, 'JPEG', quality=quality, optimize=True)
elif output_path.lower().endswith('.png'):
resized.save(output_path, 'PNG', optimize=True)
else:
resized.save(output_path)
return True
except Exception as e:
print(f"Error processing {input_path}: {e}")
return False
def analyze_directory(input_dir):
"""
Analyze texture directory and categorize files.
Args:
input_dir (str): Input directory path
Returns:
dict: Categorized texture files
"""
textures = {
'baseColor': [],
'normal': [],
'metallicRoughness': [],
'emissive': [],
'other': []
}
for file_path in Path(input_dir).glob('*.png'):
file_name = file_path.name.lower()
if 'basecolor' in file_name or '_color' in file_name or '_diffuse' in file_name:
textures['baseColor'].append(file_path)
elif 'normal' in file_name:
textures['normal'].append(file_path)
elif 'metallic' in file_name or 'roughness' in file_name:
textures['metallicRoughness'].append(file_path)
elif 'emissive' in file_name:
textures['emissive'].append(file_path)
else:
textures['other'].append(file_path)
# Also check for existing JPEGs
for file_path in Path(input_dir).glob('*.jpg'):
textures['baseColor'].append(file_path)
return textures
def optimize_directory(input_dir, output_dir, target_resolution=1024, jpeg_quality=80):
"""
Optimize all textures in a directory.
Args:
input_dir (str): Input directory
output_dir (str): Output directory
target_resolution (int): Target texture size
jpeg_quality (int): JPEG quality (1-100)
Returns:
dict: Optimization statistics
"""
if not check_pillow():
print("ERROR: Pillow library not installed.")
print("Install with: pip install Pillow")
print("\nAlternatively, use manual optimization tools:")
print(" - ImageMagick: convert input.png -resize 1024x1024 output.png")
print(" - pngquant: pngquant --quality 80-100 input.png")
print(" - TinyPNG: https://tinypng.com/")
return None
from PIL import Image
# Create output directory
os.makedirs(output_dir, exist_ok=True)
# Analyze input directory
textures = analyze_directory(input_dir)
stats = {
'files_processed': 0,
'original_size_mb': 0,
'optimized_size_mb': 0
}
print(f"\nOptimizing textures to {target_resolution}×{target_resolution}...")
print(f"Input: {input_dir}")
print(f"Output: {output_dir}\n")
# Process baseColor textures (convert to JPEG)
for file_path in textures['baseColor']:
original_size = get_file_size_mb(file_path)
stats['original_size_mb'] += original_size
output_name = file_path.stem + '.jpg'
output_path = os.path.join(output_dir, output_name)
print(f"Processing {file_path.name} → {output_name} (JPEG {jpeg_quality}%)")
if resize_texture(str(file_path), output_path, target_resolution, jpeg_quality):
optimized_size = get_file_size_mb(output_path)
stats['optimized_size_mb'] += optimized_size
stats['files_processed'] += 1
savings = ((original_size - optimized_size) / original_size) * 100
print(f" {original_size:.2f} MB → {optimized_size:.2f} MB (-{savings:.1f}%)\n")
# Process data maps (keep as PNG)
for category in ['normal', 'metallicRoughness', 'emissive', 'other']:
for file_path in textures[category]:
original_size = get_file_size_mb(file_path)
stats['original_size_mb'] += original_size
output_name = file_path.name
output_path = os.path.join(output_dir, output_name)
print(f"Processing {file_path.name} (PNG)")
if resize_texture(str(file_path), output_path, target_resolution):
optimized_size = get_file_size_mb(output_path)
stats['optimized_size_mb'] += optimized_size
stats['files_processed'] += 1
savings = ((original_size - optimized_size) / original_size) * 100
print(f" {original_size:.2f} MB → {optimized_size:.2f} MB (-{savings:.1f}%)\n")
return stats
def generate_lod_levels(input_dir, output_base_dir, levels=3):
"""
Generate multiple LOD levels from input textures.
Args:
input_dir (str): Input directory with original textures
output_base_dir (str): Base output directory
levels (int): Number of LOD levels (1-4)
Returns:
bool: Success
"""
if not check_pillow():
print("ERROR: Pillow library required for LOD generation.")
return False
# LOD resolution mapping
lod_resolutions = {
0: 2048, # LOD0 - highest detail
1: 1024, # LOD1 - medium detail
2: 512, # LOD2 - low detail
3: 256 # LOD3 - very low detail
}
print(f"\nGenerating {levels} LOD level(s)...\n")
for lod in range(levels):
resolution = lod_resolutions[lod]
output_dir = os.path.join(output_base_dir, f'LOD{lod}')
print(f"LOD{lod}: {resolution}×{resolution}")
print(f"Output: {output_dir}\n")
stats = optimize_directory(input_dir, output_dir, resolution)
if stats:
print(f"LOD{lod} complete: {stats['files_processed']} files")
print(f"Total size: {stats['optimized_size_mb']:.2f} MB\n")
print("-" * 60 + "\n")
return True
def print_manual_instructions(input_dir):
"""Print manual optimization instructions when Pillow is not available."""
print("\n" + "=" * 60)
print("MANUAL OPTIMIZATION INSTRUCTIONS")
print("=" * 60)
print("\nSince Pillow is not installed, use these tools:\n")
print("1. ImageMagick (batch processing):")
print(" # Resize to 1024×1024")
print(f" for file in {input_dir}/*.png; do")
print(" convert \"$file\" -resize 1024x1024 \"optimized_$(basename $file)\"")
print(" done\n")
print("2. pngquant (PNG compression):")
print(" # Compress PNGs (lossy but smaller)")
print(f" pngquant --quality 80-100 {input_dir}/*.png\n")
print("3. cwebp (WebP conversion - best for web):")
print(" # Convert to WebP (90% smaller than PNG)")
print(f" for file in {input_dir}/*_baseColor.png; do")
print(" cwebp -q 80 \"$file\" -o \"$(basename $file .png).webp\"")
print(" done\n")
print("4. Online tools:")
print(" - TinyPNG: https://tinypng.com/")
print(" - Squoosh: https://squoosh.app/")
print("\n" + "=" * 60)
def main():
"""Main entry point."""
parser = argparse.ArgumentParser(
description="Optimize Substance 3D Painter textures for web delivery"
)
parser.add_argument(
'input_dir',
help="Input directory containing exported textures"
)
parser.add_argument(
'-o', '--output',
dest='output_dir',
help="Output directory (default: input_dir_optimized)"
)
parser.add_argument(
'-r', '--resolution',
type=int,
default=1024,
choices=[256, 512, 1024, 2048, 4096],
help="Target resolution (default: 1024)"
)
parser.add_argument(
'-q', '--jpeg-quality',
type=int,
default=80,
choices=range(1, 101),
metavar='1-100',
help="JPEG quality for baseColor (default: 80)"
)
parser.add_argument(
'--lod',
type=int,
choices=[1, 2, 3, 4],
help="Generate LOD levels (1-4)"
)
args = parser.parse_args()
# Validate input directory
if not os.path.isdir(args.input_dir):
print(f"ERROR: Input directory not found: {args.input_dir}")
sys.exit(1)
# Determine output directory
if args.output_dir is None:
args.output_dir = args.input_dir + '_optimized'
print("=" * 60)
print("Web Texture Optimizer")
print("=" * 60)
# Check if Pillow is available
if not check_pillow():
print("\nWARNING: Pillow library not installed.")
print_manual_instructions(args.input_dir)
sys.exit(1)
# Generate LOD levels or single optimization
if args.lod:
success = generate_lod_levels(args.input_dir, args.output_dir, args.lod)
if not success:
sys.exit(1)
else:
stats = optimize_directory(
args.input_dir,
args.output_dir,
args.resolution,
args.jpeg_quality
)
if stats is None:
sys.exit(1)
# Print summary
print("=" * 60)
print("OPTIMIZATION SUMMARY")
print("=" * 60)
print(f"Files processed: {stats['files_processed']}")
print(f"Original size: {stats['original_size_mb']:.2f} MB")
print(f"Optimized size: {stats['optimized_size_mb']:.2f} MB")
total_savings = stats['original_size_mb'] - stats['optimized_size_mb']
savings_percent = (total_savings / stats['original_size_mb']) * 100
print(f"Total savings: {total_savings:.2f} MB ({savings_percent:.1f}%)")
print("=" * 60)
print(f"\nOptimized textures saved to: {args.output_dir}")
if __name__ == "__main__":
main()
Related skills
FAQ
What does substance-3d-texturing do?
Comprehensive skill for Adobe Substance 3D Painter texturing and material creation workflow. Use this skill when creating PBR materials, exporting textures for web/game engines, optimizing 3D assets for real-time renderi
When should I use substance-3d-texturing?
Comprehensive skill for Adobe Substance 3D Painter texturing and material creation workflow. Use this skill when creating PBR materials, exporting textures for web/game engines, optimizing 3D assets for real-time renderi
What are common prerequisites?
--- name: substance-3d-texturing description: Comprehensive skill for Adobe Substance 3D Painter texturing and material creation workflow.
Is Substance 3d Texturing safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.