SETUP 18: SUBSTANCE PAINTER LIVE-LINK
Организовать повторяемый цикл Blender ↔ Substance 3D Painter: вытащить из Blender меш с корректной геометрией (для карт ID/curvature/curvature/AO), забрать из Painter готовые PBR карты и автоматически подставить их в материал Blender, включая UDIM тайлы. Отдельная цель — сделать так, чтобы повторный
ID: SETUP-18 | Type: Texturing Pro | Difficulty: ⭐⭐ | Time: 40-60 min RU/EN Bilingual
Когда применять: когда сцена уже в Blender, а текстуры пишет художник в Substance 3D Painter и результат должен возвращаться в материал без ручного «экспорт-импорт-потерялся». Для однократного прогона достаточно обычного экспорта; здесь — схема, где цикл можно повторять.
🎯 ЦЕЛЬ
Организовать повторяемый цикл Blender ↔ Substance 3D Painter: вытащить из Blender меш с корректной геометрией (для карт ID/curvature/curvature/AO), забрать из Painter готовые PBR-карты и автоматически подставить их в материал Blender, включая UDIM-тайлы. Отдельная цель — сделать так, чтобы повторный прогон после правок в Painter не требовал ручных кликов в Blender.
📋 ЧТО ПОНАДОБИТСЯ
| Компонент | Версия | Зачем |
|---|---|---|
| Substance 3D Painter | 9.x / 10.x (ранее Substance Painter) | текстурирование, smart materials, экспорт карт |
| Substance 3D Integration для Blender | см. примечание ниже | живая связь, если она есть для твоей версии |
| Blender | 4.2 LTS+ | сборка материала и приём карт |
| Разрешение текстур | 2K на тест, 4K в продакшене | Painter жрёт память пропорционально размеру карт |
| Расшаренная папка обмена | ASCII-путь | канал между Painter и Blender |
[!NOTE] Официальные плагины Substance 3D Integration заявлены для 3ds Max, Maya и Unreal, и набор поддерживаемых DCC менялся от версии к версии. Проверь на сайте Adobe, есть ли поддержка Blender в твоей версии Substance 3D — если её нет, статья ниже описывает файловый roundtrip с автоподхватом, который работает всегда. Не выдумывай имя аддона: сначала посмотри, что предлагает Adobe.
🛠️ НАСТРОЙКА
1. Из Blender в Painter: меш, на котором считаются карты
# Batch-экспорт из Blender без GUI — удобно, когда скрипт гоняется по расписанию
blender --background scene.blend \
--python-expr "import bpy; bpy.ops.object.select_all(action='SELECT'); \
bpy.ops.export_scene.fbx(filepath='C:/sp/asset.fbx', use_selection=True, \
path_mode='COPY', embed_textures=False, mesh_smooth_type='FACE')"
Ключевое: mesh_smooth_type='FACE' и развёртка. Painter строит карты ID/curvature/AO из геометрии; если в FBX приедут усреднённые нормали, карта curvature станет плоской и запечется пустой.
Внутри Blender, чтобы не собирать длинную строку:
# export_for_painter.py — Blender: Text Editor → Run Script
import bpy, os
OUT = r"C:\sp"
os.makedirs(OUT, exist_ok=True)
bpy.ops.object.select_all(action='DESELECT')
for ob in bpy.context.scene.objects:
if ob.type == 'MESH':
ob.select_set(True)
bpy.context.view_layer.objects.active = bpy.context.scene.objects[0]
bpy.ops.export_scene.fbx(
filepath=os.path.join(OUT, "asset.fbx"),
use_selection=True,
path_mode='COPY',
object_types={'MESH'},
mesh_smooth_type='FACE', # усреднённые нормали убивают карту curvature
use_mesh_modifiers=True, # subdivision должен быть применён ДО экспорта
apply_unit_scale=True,
axis_forward='-Z', axis_up='Y', # согласование осей с Painter — главный источник «перевёрнутого» меша
)
print("FBX:", os.path.join(OUT, "asset.fbx"))
2. В Painter: export preset под Blender
Готовый пресет — это JSON-файл с расширением .spexp. Надёжный путь: в UI File → Export Texture Preset → Export, потом правишь полученный файл и закидываешь обратно в Documents/Adobe/Adobe Substance 3D Painter/*/resources/presets/.
{
"exportPresets": [
{
"name": "Blender_PBR_UDIM",
"maps": [
{ "fileName": "$textureSet_BaseColor", "channels": [ { "destChannel": "R", "srcChannel": "R" },
{ "destChannel": "G", "srcChannel": "G" },
{ "destChannel": "B", "srcChannel": "B" } ],
"parameters": { "fileFormat": "png", "bitDepth": "8", "dithering": false, "paddingAlgorithm": "infinite" } },
{ "fileName": "$textureSet_Metallic", "channels": [ { "destChannel": "R", "srcChannel": "L" } ],
"parameters": { "fileFormat": "png", "bitDepth": "8", "dithering": false, "paddingAlgorithm": "infinite" } },
{ "fileName": "$textureSet_Roughness", "channels": [ { "destChannel": "R", "srcChannel": "R" } ],
"parameters": { "fileFormat": "png", "bitDepth": "8", "dithering": false, "paddingAlgorithm": "infinite" } },
{ "fileName": "$textureSet_Normal", "channels": [ { "destChannel": "RGB", "srcChannel": "Normal" } ],
"parameters": { "fileFormat": "png", "bitDepth": "16", "dithering": false, "ditheringIntensity": 0 } },
{ "fileName": "$textureSet_AO", "channels": [ { "destChannel": "R", "srcChannel": "AO" } ],
"parameters": { "fileFormat": "png", "bitDepth": "8", "dithering": false, "paddingAlgorithm": "infinite" } },
{ "fileName": "$textureSet_Height", "channels": [ { "destChannel": "R", "srcChannel": "Height" } ],
"parameters": { "fileFormat": "exr", "bitDepth": "32" } }
],
"exportList": [ { "rootPath": "$textureSet" } ],
"exportPresets": [ { "name": "Default" } ],
"parameters": { "paddingAlgorithm": "infinite", "dithering": true, "sizeLog2": 11, "fileFormat": "png" }
}
]
}
[!NOTE] Схема
.spexp— это то, что Painter сохраняет сам. Не собирай пресет вручную по памяти: выгрузи пресет из UI, открой его в текстовом редакторе и правь только те поля, которые реально нужно поменять (размер, формат,paddingAlgorithm). Точные имена токенов$textureSet_BaseColorу каждой карты свои — сверяйся с выгрузкой.
Три решения, которые чаще всего отличаются от «ожидаемого»:
- Metallic в R, Roughness в G — как ожидает Principled BSDF; отдельная карта metallic только в R, roughness только в R, если используешь отдельные ноды.
- Normal в PNG 16-bit — 8 бит на нормали дают заметный бандинг на гладких поверхностях.
- Height в 32-bit EXR — 8-битный height ломает Bump/Displace при сильном смещении.
3. В Painter: батч-экспорт по всем texture set
# substance_export.py — запускается ВНУТРИ Painter: Python → Run Script,
# либо из меню Python в панели Scripting. Не из командной строки ОС.
import substance_painter.export
import substance_painter.project
import substance_painter.textures
CONFIG = substance_painter.export.ExportUASConfig(
export_project_path="C:/sp/export", # куда класть результат
export_list=[substance_painter.export.ExportOutputConfig(
output_name="Blender_PBR_UDIM",
export_shader_params={}
)],
)
def do_export():
substance_painter.export.export_project_textures(CONFIG)
print("exported", len(substance_painter.textures.all_texture_sets()), "texture sets")
# Экспорт нельзя запускать, пока Painter занят — отсюда обёртка
substance_painter.project.execute_when_not_busy(do_export)
[!NOTE] Имена классов конфигурации экспорта в Painter менялись между мажорными версиями (например,
ExportUASConfig/ExportOutputConfigпротив более ранних конструкторов). Сверься сhelp(substance_painter.export)в Python-консоли Painter — это надёжнее любой статьи.
4. В Blender: собрать материал из карт
# build_pbr_material.py — Blender: Text Editor → Run Script
import bpy, os
EXPORT_DIR = r"C:\sp\export"
def _clear_and_build(mat, basecolor="BaseColor.png", metallic="Metallic.png",
roughness="Roughness.png", normal="Normal.png", ao="AO.png",
height="Height.exr"):
mat.use_nodes = True
tree = mat.node_tree
tree.nodes.clear()
nodes, links = tree.nodes, tree.links
out = nodes.new("ShaderNodeOutputMaterial"); out.location = (700, 0)
bsdf = nodes.new("ShaderNodeBsdfPrincipled"); bsdf.location = (400, 0)
def tex(filename, non_color=False):
n = nodes.new("ShaderNodeTexImage"); n.location = (0, 0)
path = os.path.join(EXPORT_DIR, filename)
if not os.path.exists(path):
print("нет карты:", filename); return None
n.image = bpy.data.images.load(path, check_existing=False)
n.image.colorspace_settings.name = 'Non-Color' if non_color else 'sRGB'
return n
# sRGB только у BaseColor; Metallic/Roughness/AO — это данные
for filename, key, non_color in [
(basecolor, "Base Color", False),
(metallic, "Metallic", True),
(roughness, "Roughness", True),
]:
n = tex(filename, non_color)
if n:
links.new(n.outputs["Color"], bsdf.inputs[key])
n = tex(normal, non_color=True)
if n:
nm = nodes.new("ShaderNodeNormalMap"); nm.location = (200, -300)
nm.inputs["Strength"].default_value = 1.0
links.new(n.outputs["Color"], nm.inputs["Color"])
links.new(nm.outputs["Normal"], bsdf.inputs["Normal"])
n = tex(ao, non_color=True)
if n:
# Мультипликативное наложение AO на Base Color.
mix = nodes.new("ShaderNodeMixRGB"); mix.location = (200, 250)
mix.blend_type = 'MULTIPLY'; mix.inputs["Fac"].default_value = 1.0
links.new(n.outputs["Color"], mix.inputs["Color2"])
base = tex(basecolor)
if base:
links.new(base.outputs["Color"], mix.inputs["Color1"])
links.new(mix.outputs["Color"], bsdf.inputs["Base Color"])
links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
return mat
_clear_and_build(bpy.data.materials.get("M_Asset") or bpy.data.materials.new("M_Asset"))
#> [!NOTE]
ShaderNodeMixRGB— это legacy-идентификатор: в Blender 4.x нода Mix в меню Add переехала вShaderNodeMixс полемdata_type, и старый тип оставлен для совместимости скриптов. Если в твоей версииnodes.new("ShaderNodeMixRGB")бросаетTypeError, замени наShaderNodeMixсdata_type='RGBA'иblend_type='MULTIPLY'; входы у новой ноды продублированы по типам, так что обращайся к ним по индексам, а не по именам.
5. UDIM: импорт тайлов в Blender
Painter пишет тайлы отдельными файлами (<Set>_1001.png и далее). Blender читает их как одну текстуру, если при импорте указать номера тайлов.
Blender → Image Editor → Image → Open
→ в файловом диалоге выбрать НЕСКОЛЬКО файлов с именами вида 1001.png, 1002.png …
→ Blender соберёт их в один tile-датаблок
→ в ноде Image Texture ничего менять не нужно: проекция уже UDIM
Программно через bpy.data:
import bpy, os
EXPORT_DIR = r"C:\sp\export"
tiles = sorted(
f for f in os.listdir(EXPORT_DIR)
if f.endswith(".png") and f.rsplit("_", 1)[-1].split(".")[0].isdigit()
)
print("тайлов найдено:", len(tiles), tiles[:4])
# Важно: bpy.data.images.load() на каждый файл по отдельности НЕ собирает
# тайлы — получится N отдельных датаблоков. Тайлы собирает оператор
# image.open, когда ему передают список файлов.
bpy.ops.image.open(
directory=EXPORT_DIR + os.sep,
files=[bpy.types.FileSelectEntry(name=n) for n in tiles],
)
# Проверка: у собра изображения должен появиться список тайлов
for img in bpy.data.images:
if getattr(img, "source", None) == 'TILED':
print("UDIM-датаблок:", img.name, "тайлов:", len(img.tiles))
Через UI это делается ровно так же: Image Editor → Image → Open, выбрать в диалоге все файлы *_1001.png … *_1004.png сразу — и Blender соберёт их в один датаблок. Отдельно тайлы раскладываются сеткой оператором UV-редактора:
# Разложить UV по сетке тайлов. Оператор живёт в контексте UV-редактора,
# поэтому включи контекст явно — из Text Editor он может не сработать.
bpy.ops.uv.smart_project(angle_limit=1.15, island_margin=0.02) # на случай, если развёртки нет
with bpy.context.temp_override(area=bpy.context.screen.areas[0]):
bpy.ops.uv.uv_layout_generate_tile_grid(image=bpy.data.images["BaseColor"], grid_x=4, grid_y=4)
[!NOTE]
uv_layout_generate_tile_grid— оператор UV-редактора, и его сигнатура (а также то, обязателен ли для него активный объект и открытый UV-редактор) менялась между версиями Blender. Проверьbpy.ops.uv.uv_layout_generate_tile_grid.get_rna_type().propertiesв своей версии; если оператора нет — разложи тайлы вручную через Image Editor.
6. «Живой» цикл: Blender сам подхватывает новые файлы
# watcher.py — следит за папкой экспорта и перезагружает текстуры
import bpy, os
EXPORT_DIR = r"C:\sp\export"
_seen = {}
def _poll():
if not os.path.isdir(EXPORT_DIR):
return 2.0
for img in list(bpy.data.images):
if not img.filepath_raw or not os.path.isabs(bpy.path.abspath(img.filepath_raw)):
continue
path = bpy.path.abspath(img.filepath_raw)
if os.path.dirname(path).lower() != os.path.normcase(EXPORT_DIR).lower():
continue
mtime = os.path.getmtime(path)
if _seen.get(img.name) != mtime:
_seen[img.name] = mtime
img.reload()
img.update()
print("[painter] обновлено:", os.path.basename(path))
return 1.5
if _poll not in bpy.app.timers:
bpy.app.timers.register(_poll, first_interval=1.5)
Чтобы цикл не ждал перерисовки вручную, добавь перерисовку вьюпорта:
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
⚙️ КЛЮЧЕВЫЕ КОМАНДЫ И ПАРАМЕТРЫ
# Painter: повторный батч-экспорт всех texture set с одним пресетом
# (через встроенную Python-консоль Painter, не через системный Python)
# substance_painter.project.execute_when_not_busy(do_export)
# Blender: headless-пересборка материалов после экспорта — для CI
blender --background scene.blend --python build_pbr_material.py -- --out C:/sp/export
# Проверка, что все карты на месте, до того как начнётся сборка нод
import os
EXPORT_DIR = r"C:\sp\export"
required = ["BaseColor.png", "Metallic.png", "Roughness.png", "Normal.png", "Height.exr"]
for f in required:
p = os.path.join(EXPORT_DIR, f)
print(f"{'OK ' if os.path.exists(p) else 'MISS'} {f}"
+ (f" {os.path.getsize(p)//1024} KB" if os.path.exists(p) else ""))
| Параметр | Значение | Когда менять |
|---|---|---|
sizeLog2 / размер карт |
11 = 2K, 12 = 4K | 4K на 4GB VRAM — уже тесно, считай окно Painter |
| Normal: bitDepth | 16 | 8 бит даёт бандинг на больших гладких поверхностях |
| Height: fileFormat | exr, 32 bit |
если используешь Displace, а не Bump |
paddingAlgorithm |
infinite для тайлов, finite для отдельных карт |
UDIM требует бесшовности по краю |
dithering |
8-бит карты — да, 16-бит normal — нет | dithering ломает карты, считываемые как данные |
use_mesh_modifiers при экспорте FBX |
True |
выключи, если не хочешь применять модификаторы |
| Таймер опроса | 1.0–2.0 с | меньше 0.5 — нагрузка на диск без пользы |
| colorspace карты | BaseColor sRGB, остальные Non-Color |
ошибка здесь не видна в viewport, но видна в рендере |
Про smart materials и экспорт
Smart materials (и вообще весь процедурный слой Painter) не экспортируются. Выход — только разрешённые каналы: Base Color, Metallic, Roughness, Normal, Height, AO, Opacity, Emissive. Если в сцене нужен процедурный слой, вариантов два: оставить Artist Tool поверх карты в Blender, или генерировать процедурный слой в Substance Designer (см. SETUP 19).
⚠️ ТИПОВЫЕ ОШИБКИ
| Симптом | Причина | Решение |
|---|---|---|
| Текстура плоская, curvature/AO пустые | В FBX усреднённые нормали или не применён Subdivision | mesh_smooth_type='FACE', use_mesh_modifiers=True; в Painter проверь карту curvature на глаз |
| Меш в Painter лежит/повёрнут не так | Несовпадение систем осей | Правь axis_forward/axis_up в экспорте; для FBX в Painter есть переключатель оси |
| Меш в Blender после импорта вверх ногами | Обратная задача | Импортируй FBX с теми же axis_forward/axis_up, что и экспортировал |
| Карты с чёрными рамками по краям | Не задан paddingAlgorithm |
Для UDIM — infinite; переэкспортируй |
| Нормали «пластиковые» / полосатые | 8-битный PNG для normal | 16 бит, и dithering выключить |
| Металл выглядит как пластик | Metallic/Roughness подключены как sRGB или AO перемножен в Base Color с инверсией | Обе карты — Non-Color; AO только MULTIPLY, Fac = 1.0 |
| UDIM-текстура не занимает весь меш | При импорте указаны не все тайлы | Переимпортируй, выбрав все файлы *_10xx.png разом |
| Новые файлы из Painter не подхватываются | Таймер не зарегистрирован или сцена открыта в background | Проверь bpy.app.timers.is_registered(_poll); в headless это не сработает |
| Painter молча ничего не экспортирует | Экспорт вызвался в занятом состоянии | Оборачивай в execute_when_not_busy |
| Долгая пауза перед появлением карты | Painter считает карты и dithering одновременно | Считай по одной карте, dithering — только на цветовых |
🔗 СВЯЗИ
- Прочие в категории «Сетапы»: SETUP 19: SUBSTANCE DESIGNER SBSAR, SETUP 01: BLENDER-MCP BASIC, SETUP 02: GENESISCORE LOCAL (OLLAMA), SETUP 03: DREAM TEXTURES (STABLE DIFFUSION IN BLENDER), SETUP 04: HEADLESS RENDER (CLI / CI/CD)
- По темам: P26: MCP BASICS (BLENDER-MCP COMMANDS), SETUP 05: RENDER FARM (FLAMENCO), SETUP 06: CUSTOM MCP TOOLS
- Теги: #setup, #substance
Связанные материалы
Подобраны по тегам и категории.