Substance 3D Painterで20体ぶんのテクスチャを書き出す——Export windowを20回開くのかと思うと気が重い。調べると「Pythonでバッチ書き出しできる」と出てくるのに、コマンドラインからスクリプトを渡す方法がどこにも書いていない。その方法は無いからだ。Painterの自動化は「閉じたまま回す」ものではなく「開いた状態で、その中のPythonに仕事をさせる」ものになる。
検証環境:Substance 3D Painter 12.1.3(Windows 11)/同梱Pythonインタプリタ 3.13.9/Python API 0.3.5。実測した範囲=コマンドラインオプションの抽出・インタプリタの版・アプリ外からのimport結果・設定JSONの構築。実測していない範囲=起動して実際に書き出すところ。仕様は同梱のPython APIドキュメントに拠る。なおオプションの抽出は網羅の保証ではないので、以下は「抽出できた範囲では」と読んでほしい。
Painterを起動せずにバッチ書き出しできる?
できない。12.1.3の実行ファイルから抽出できたオプション(アプリ自身が「隠しオプションを含む全件」として持っている一覧)に、「起動せずに書き出して終了する」ものは含まれていなかった。自動化に関わるのは次の4つだ。
| オプション | アプリ自身のヘルプ文(原文) |
|---|---|
| (位置引数) | Path of the project to open or create. |
--export-path |
Default export path where the project maps will be exported. |
--enable-remote-scripting |
Enable remote scripting. Implies --disable-modal-popup – experimental. |
--disable-modal-popup |
Disable modal popup. Usefull for automation. |
--export-path は書き出しコマンドではない。ヘルプ文が言っているのは default export path=Export windowの初期値だ。「付けたのに何も出てこない」の原因はたいていこれになる。
なお headless という語自体は実行ファイル内にあるが、ライセンス基盤の状態名とVulkanのAPI名でオプションではなかった。
自動化の実体は2経路。普通のPythonからは呼べない
| A:アプリ内でPythonを動かす | B:リモートスクリプティング | |
|---|---|---|
| 起動 | いつもどおり | --enable-remote-scripting を付ける |
| 通信 | なし | HTTPで localhost:60041・本文はbase64 |
Bのポート番号はアプリ側が jsonServerPort: 60041 と持っていて指定不要(無効時は -1)。ただし公式ドキュメントが例示する通信ライブラリ lib_remote.py は同梱されない。「人が触らない運転」はこの経路と --disable-modal-popup で作れるが、プロセスが立たない運転にはならない。
では「モジュールを入れて外から回す」は。できない。
> python.exe -c "import substance_painter"
ModuleNotFoundError: No module named '_substance_painter'
substance_painter 自体は普通のPythonファイルだが、_substance_painter というネイティブモジュールに依存している。そしてこの名前のファイルはインストールフォルダ全体を探しても無い。同梱の .pyd 45個はどれも別物で、名前はアプリ本体の実行ファイルの中にだけ現れる。組み込み済みで、プロセスの外には出てこないということだ。一方で環境自体は素直で、Python 3.13.9 に pip 25.2 が同梱され、site-packagesにPySide6が入っている。
一括書き出しの設定JSONはどう書く?
中核は export_project_textures() 一つ。渡すdictは4ブロックだ。exportPath(保存先)、exportPresets(任意・プリセット定義)、exportList(対象のテクスチャセット)、exportParameters(上書きルール)。
import substance_painter.export, substance_painter.resource
import substance_painter.textureset
def build_export_config(out_dir):
preset = substance_painter.resource.ResourceID(
context="starter_assets", name="PBR Metallic Roughness")
return {
"exportPath": out_dir,
"exportShaderParams": False,
"defaultExportPreset": preset.url(),
"exportList": [
{"rootPath": ts.name()}
for ts in substance_painter.textureset.all_texture_sets()
],
"exportParameters": [
{"parameters": {
"fileFormat": "png", "bitDepth": "8",
"dithering": False, "sizeLog2": 11,
"paddingAlgorithm": "infinite"}},
],
}
テクスチャセット名を手打ちせず列挙しているのが1点目。2点目が最後の exportParameters を省略しないことだ。パラメータはプリセット側の値で初期化され、ルールが上から順に照合されて一致分だけ上書きされる。そして1つでも未定義が残ると設定全体が無効になる。これは公式ドキュメントに明記された挙動で、同ドキュメントは全パラメータに既定値を与えるルールを1つ置くことを good practice として挙げている。逆に後ろに書いた広いルールは前の個別指定を潰すので、サイズが指定どおりに出ないときはまずここを疑う。
指定できるのは fileFormat / bitDepth / dithering / sizeLog2 / paddingAlgorithm / dilationDistance の6つ。sizeLog2 は2の指数で11=2048、12=4096だ。ノーマルマップの規格は virtualMap の Normal_OpenGL / Normal_DirectX で切り替わり、UE5向けとUnity向けの出し分けはここに効く(書き出し設定とテクスチャ解像度の記事で整理している)。
事故を防ぐ2つの作法:ドライランとstatusの罠
まとめて回す処理で怖いのは、間違った設定で大量に上書きすることだ。書き出さずに結果だけ返す関数がある。
listing = substance_painter.export.list_project_textures(config)
for (texture_set, stack), files in listing.items():
print(texture_set, stack, files)
返るのは(テクスチャセット名, スタック名) をキーにしたファイル名の一覧=書き出し本体と同じ形だ。本番前に一度通せば、どこに何という名前で何個できるかを1ファイルも書かずに確認できる。意図しないテクスチャセットの混入やUDIMの件数違いは、この時点で気づける。
戻り値の status でエラーを判定してはいけない。仕様上、export_project_textures() の status は決して Error にならない。失敗の原因はすべて例外として送出されるからだ。しかもイベント(ExportTexturesEnded)側には Error が渡る。戻り値とイベントで取れる状態が違う。
キャンセル時は Cancelled になり、そこまでに書けたファイルの一覧が入って返る。中途半端な出力が残るので後続処理をそのまま走らせない。ValueError(設定不正)と ProjectError(未オープン)を捕まえ、status は Success かで見るのが素直だ。
スクリプトを常設する
毎回コンソールに貼るのは現実的でない。Painterはドキュメントフォルダ配下を自動で読む。
ドキュメント\Adobe\Adobe Substance 3D Painter\python\
├ modules\ … 共通モジュール
├ plugins\ … プラグイン本体
└ startup\ … 起動時に読み込むもの
この3つはインストール直後から在る。plugins にファイルを置き start_plugin() と close_plugin() を定義すれば認識され、反映は再起動不要で、メニューの「Python」→「プラグインフォルダーをリロード」で読み直せる。試すモデルが無くても resources/samples/ に MeetMat.spp が同梱されている。
よくある質問
Painterを起動せずにバッチ書き出しできますか?
--export-path は保存先の初期値を決めるだけです。ただし人が操作しない運転は可能で、--enable-remote-scripting と --disable-modal-popup を併用すればダイアログで止まらずに外部から流せます。プロセス自体は立ち上がります。同梱のPythonにpipで外部ライブラリを追加できますか?
python\modules に置くほうが安全です。実際に入れて動かすところまでは検証していません。リモートスクリプティングは常用して問題ありませんか?
lib_remote.py も同梱されず自分で用意します。待ち受けはローカルホストのポート60041なので外部に開放しない前提で。手元で完結する処理ならアプリ内でPythonを動かすほうが依存が少なくて済みます。まとめ:入口を間違えない
- プロセスを起動しない書き出しは無い。
--export-pathは保存先の初期値を決めるだけ - 経路はアプリ内Pythonとリモートスクリプティング(localhost:60041)の2つ
- 設定JSONはフィルタなしの全体ルールを1つ置く。未定義が残ると設定ごと無効になる
- 本番前に
list_project_textures()でドライランし、失敗は例外で捕まえる
「開かずに回す」ではなく「開いたまま回す」——ここさえ押さえれば、あとはJSONを組むだけだ。まずは同梱のサンプルで list_project_textures() を1回叩いてみるといい。

コメント