Substance 3D Painter PythonAPI をつかってみよう

Substance 3D Painterをつかってみよう

はじめまして。COYOTE 3DCG STUDIO TAチーム所属 柴犬好きの藤井です。

今回は、Substance 3D Painter Python APIをこれから使ってみたいと思ってる人向けの記事です。そんなに難しくありません。
Substance 3D Painterで効率化を試みたい人は今回の記事をきっかけに少し使ってみてはいかがでしょうか。

■ そもそもSubstance 3D Painter Python APIとは?

Substance 3D PainterをPythonで操作できる公式配布パッケージです。
非常に機能がコンパクトなので使い方をすぐ把握出来るのが良いところです。ソースコードも気楽に読めます。手軽に触ってみることができるAPIです。

● Python APIで出来ること

例えば、以下のようなことはPython APIで自動化や効率化ができます。

  • 表示に関する設定(環境マップ/トーンマッピング/カラープロファイル)
  • テクスチャセットの解像度/チャンネル等の設定
  • テクスチャセットのエクスポート
  • プロジェクト作成・保存等

また、自作ウィンドウやメニューを追加することも可能です。

● Python API本体インストール場所

Python APIの各モジュールは以下のパスにインストールされています。
C:\Program Files\Adobe\Adobe Substance 3D Painter\resources\python\modules\substance_painter

手軽にソースコードを見ることが出来ます。

● Python API関連ドキュメント

スクリプトドキュメント

スクリプトドキュメントは下記のローカルパスにインストールされます。

C:\Program Files\Adobe\Adobe Substance 3D Painter\resources\python-doc\index.html

Substance 3D Painter.exeの画面上部のメニュー[ヘルプ]>[スクリプトのドキュメント]>[PythonAPI]からもこのドキュメントを開くことができます。
スクリプトドキュメントの内容はソースコードに記述されているdocstringとほぼ同じです。

公式Webサイト

Substance 3Dの公式ドキュメントWebサイトではPythonプラグインの基本的な作成手順が簡単に紹介されています。

https://substance3d.adobe.com/documentation/spdoc/creating-a-python-plugin-197427756.html


■ 実際にプラグインを作ってみよう

これから、テクスチャセットを出力するプラグインを実際に作成してみます。
作成工程を順を追って説明してみようと思います。

以下のようになります。

1.前提の確認
2.環境変数の設定
3.プラグインテンプレートの作成
4.テクスチャセットの出力パス定義の記述
5.テクスチャセットの出力設定の記述
6.出力実行処理の記述
7.UIの記述
8.実行確認


1.前提確認

● 想定読者

  • python初心者以上
  • Substance 3D Painterの使用経験がある人

● 使用環境

Windows 10 Pro
Adobe Substance 3D Painter 8.1.3
Adobe Substance 3D Painter Python API 0.2.6

● 注意事項

本記事の内容はSubstanceがAdobe製品に統合される前のバージョンでは使用できない内容が一部含まれています。旧バージョンのSubstancePainterを使用されてる方はご留意ください。

ver 7.1以前 Substance Painter (Substance Painter2021.1以前)
ver 7.1以降 Adobe Substance 3D Painter  


2.環境変数の設定

最初に、スクリプトを書き始める前にプラグインパスの環境変数を設定します。
Substance 3D Painterにスクリプトファイルをプラグインとして認識させるには、スクリプトファイルが置かれているディレクトリにプラグインパスが設定されている必要があります。

既定ではユーザーフォルダのドキュメントにプラグインパスが設定されています。
%userprofile%\Documents\Adobe\Adobe Substance 3D Painter\python

今回の記事では上記の既定プラグインパスを使用しません。
ユーザーフォルダを含むパスを使用すると「絶対パスの記述がしにくい」「GitやSVNでの管理がしにくい」等の不都合があるためです。
新規にディレクトリを作成して新しくプラグインパスを設定する方法を紹介します。

● プラグインディレクトリ作成

まず、プラグインを配置するための専用ディレクトリを任意の場所に作成します。(フォルダ名は全角非推奨です)
このディレクトリの直下には「startup」「plugins」「modules」の3つのフォルダを新規作成しておきます。

file

各フォルダには以下のような役割があります。
「startup」 : Substance 3D Painter.exe起動時に読み込むモジュールを置くフォルダ
「plugins」 : オプションとして読み込むモジュールを置くフォルダ
「modules」 : プラグイン間で共有するモジュールを置くフォルダ

環境変数「SUBSTANCE_PAINTER_PLUGINS_PATH」に上記のフォルダが置かれた親ディレクトリパスを設定すると、Substance 3D Painter.exeが各フォルダ内に置かれたスクリプトファイルを読み込んでくれるようになります。

● 起動bat作成

次に、作成したディレクトリにプラグインパスを設定する方法を紹介します。あくまでも一例です。
アプリケーション起動時に環境変数を設定する方法が既に分かる方は読み飛ばしていただいて構いません。

テキストエディタに以下の内容をコピーします。
拡張子を「.bat」、文字コードを「Shift JIS」に設定して、プラグインディレクトリと同階層に保存します。

rem batが置かれているディレクトリパスを環境変数に設定
set current_dir=%~dp0
set SUBSTANCE_PAINTER_PLUGINS_PATH=%current_dir%

rem exeを起動
start "" "C:/Program Files/Adobe/Adobe Substance 3D Painter/Adobe Substance 3D Painter.exe"

rem コマンドウィンドウを閉じる
exit 

file

上記.batファイルを実行すると、一時的に環境変数を設定した状態でsubstance 3D Painter.exeを起動できます。
他の人にプラグインファイルを配布するときはこの.batファイルと一緒にリリースします。
.batのショートカット(.lnkファイル)をデスクトップに作成しておくと起動しやすくなります。

● 余談

上記で紹介した内容はあくまでもごく簡易的な方法です。
幣スタジオの実際の業務では、COYOTE Launcherというツールを使って各種DCCソフトの環境変数の設定を管理しています。
このツールの運用により、各プロジェクトの仕様に合わせて環境変数の設定を切り替える仕組みが整備されています。


3.プラグインテンプレートの作成

プラグインとして認識される必要最小限のテンプレートを作成します。
以下の内容をテキストエディタにコピーして、プラグインパスを設定したディレクトリ直下の「plugins」フォルダ内に、任意の名前で保存します。拡張子は「.py」です。

import substance_painter 

def start_plugin():
    print("test")

def close_plugin():
    pass

if __name__ == "__main__":
    start_plugin()

● ポイント

substance 3D Painterのプラグインではstart_plugin()とclose_plugin()を定義することが推奨されています。
プラグイン初回読み込み・リロード時にstart_plugin()が、
プラグイン終了時にclose_plugin()が実行される仕組みになっているようです。
以下のソースコードを読むとどのように処理されているかが分かります。
C:\Program Files\Adobe\Adobe Substance 3D Painter\resources\python\modules\substance_painter_plugins.py

● 読み込み確認

次に、作成したプラグインファイルをSubstance 3D Painter.exeで読み込めるか確認しましょう。

前述のbatファイルからSubstance 3D Painter.exeを起動します。
※前述のbatファイルじゃなくても、何かしらの方法でプラグインパスが指定出来ていれば問題ありません。

Substance 3D Painter.exeの画面上部の[Python]メニューに作成したプラグインファイル名が表示されていることを確認してください。

file

もしメニューに表示されていない場合はエラーの可能性があります。ログを確認します。

画面上部のメニュー[ウィンドウ]>[ビュー]>[ログ]を開いてください。

file

正常に読み込まれた場合にはprint()の内容がログウィンドウに表示されます。
file

.pyファイル内の記述を変更した後は
画面上部のメニュー[python]>[プラグインフォルダーをリロード]を実行して変更を反映させることができます。

以上でプラグインの最低限の体裁は出来ました。
次から実際に、テクスチャセットの出力に必要な処理を書いていきます。


4.テクスチャセットの出力パス定義の記述

テクスチャセットの出力パスを定義する処理を書いてみます。
出力パスの仕様は「出力先フォルダ名とインポートしたメッシュ名が一致している」という想定で書いてみます。

インポートしたメッシュ(.fbx や.obj)のパスはprojectモジュールのlast_imported_mesh_path()というメソッドで取得できます。

あらかじめ、テクスチャセットを出力するフォルダと使用するプロジェクトデータを用意しておいてください。

テンプレートを以下のように書きかえます。

# -*- coding: utf-8 -*-
import substance_painter 
import pathlib

#出力先の基底パス※あくまでも一例です 自分で用意した出力フォルダ階層のパスに変更してください
SAMPLE_EXPORT_BASEPATH = r"D:\SVN\projects\SampleProject\assets"

def get_export_path():
    # インポートしたメッシュのパスを取得
    mesh_path = substance_painter.project.last_imported_mesh_path()
    # パスからメッシュ名を取得    
    mesh_name = pathlib.Path(mesh_path).stem
    # 基底パスと取得した名前を連結
    export_path = pathlib.Path(SAMPLE_EXPORT_BASEPATH, mesh_name)
    return str(export_path)

def start_plugin():
    print(get_export_path())

def close_plugin():
    pass

if __name__ == "__main__":
    start_plugin()

基底となるパスと取得したメッシュ名を連結させました。パスの連結にはPathlibというPython標準ライブラリを使用してます。

Substance 3D Painter.exeの画面に戻り、任意のプロジェクトデータを開いてください。

画面上部のメニュー[python]の一覧から作成したプラグインのチェックのオンオフを切り替えると、スクリプトファイルが再読み込みされてログウィンドウに定義したパスが表示されるかと思います。

file


5.テクスチャセットの出力設定の記述

次に、テクスチャセットの出力設定を指定する処理を書いてみます。
「テクスチャの書き出し」ウィンドウに相当する部分の処理になります。
出力設定は基本的にjson形式(辞書型)で記述します。
以下の二通りの方法があります。

  • 「設定を一から記述する」
  • 「出力プリセットファイル(書き出しテンプレート)を併用する」

「設定を一から記述する」のはなかなか大変なので、今回は「出力プリセットファイル(書き出しテンプレート)を併用する」方法を採用します。

「設定を一から記述する」方法についてはドキュメントの以下の部分を参考にします。
file:///C:/Program%20Files/Adobe/Adobe%20Substance%203D%20Painter/resources/python-doc/substance_painter/export.html#full-json-config-dict-possibilities

● 出力プリセットファイルの作り方

まず、出力プリセットファイル(書き出しテンプレート)を作成します。
画面上部のメニュー[ファイル]から[テクスチャの書き出し]を開きます。[テクスチャの書き出し]ウィンドウのタブ[出力テンプレート]の画面で出力プリセットファイルの作成が出来ます。
以下のような感じで簡単に作成出来ます(以下gif)。

file

出力マップ名の横の「$」のボタンを押すと、「$mesh」「$textureSet」などの変数を挿入することができます。

file

これらの変数はそれぞれプロジェクトデータに応じてメッシュ名やテクスチャセット名を代入してくれます。

詳細は以下の公式ドキュメントページを見てください。
https://substance3d.adobe.com/documentation/spdoc/creating-export-presets-98959398.html

任意の設定で出力プリセットを作りましょう。[設定の保存]は不要です。

作成・保存した出力プリセットデータは、以下のユーザーフォルダのパスに.spexpファイルとして作成されます。
%userprofile%\Documents\Adobe\Adobe Substance 3D Painter\assets\export-presets
※ver7.1以前の場合は
%userprofile%\Documents\Allegorithmic\Substance Painter\shelf\export-presets

export_preset_foloder-1.png

今回作成した出力プリセットファイルをプラグインで使用します。バージョン管理等がしやすい任意の場所に移動させておきます。パスも控えておきます。

● 出力設定(json_config)記述

次は、先程の出力プリセットファイルを使って出力設定(json_config)を記述します。

先程定義したget_export_path()とstart_plugin()の間に以下の内容をコピーしてください


# 作成した出力プリセットファイルパス※あくまでも一例です 自分が作成した出力プリセットファイルのパスに変更してください
SAMPLE_PRESET_PATH = r"D:\work\Substanc3DPainter\sample_preset.spexp"

def get_export_config(export_path):
    # 指定パスの出力プリセットファイルを「出力プリセット」形式のリソースとして現在のセッションにインポート
    export_preset = substance_painter.resource.import_session_resource(SAMPLE_PRESET_PATH, substance_painter.resource.Usage.EXPORT)

    # インポートした出力プリセットのリソースURLを取得
    export_preset_url = export_preset.identifier().url()

    # 出力設定を作成
    export_config = {
                    # 出力パスの設定
                    "exportPath":export_path,
                    # 作成したプリセットファイルをデフォルト出力プリセットに設定
                    "defaultExportPreset": export_preset_url,
                    # デフォルト出力プリセットを使用
                    "exportPresets":[ { "name" : "default", "maps" : [] } ],
                    #
                    "exportList":[],
                    #「パディング:テクスチャの境界線まで」設定
                    "exportParameters":[{"parameters" : {"paddingAlgorithm": "infinite" }}], 
                    #「シェーダーパラメーターを書き出し」オフ
                    "exportShaderParams":False
                    }

    # プロジェクト内の全テクスチャセット名を取得して"exportList"に設定する
    export_list = export_config.get("exportList")
    for texture_set in substance_painter.textureset.all_texture_sets():
        export_list.append({"rootPath": texture_set.name()})

    return export_config

export_configの内容は「テクスチャを書き出し」の「設定」タブと対応しています。

「テクスチャを書き出し」の設定ウィンドウ

● texturesetモジュール all_texture_sets() の説明

for texture_set in substance_painter.textureset.all_texture_sets():
texturesetモジュールのall_texture_sets()は現在のプロジェクトの全テクスチャセット一覧
を取得します。「TextureSet」オブジェクトを配列にして返すメソッドです。「TextureSet」オブジェクトにはテクスチャセットに関する情報を取得・設定するメソッドが備わっています。
今回はテクスチャセットの出力リストを設定するため、テクスチャセット名の取得に使用しました。


6.出力実行処理の記述

次に、「4.テクスチャセットの出力パス定義の記述」と「5.テクスチャセットの出力設定の記述」を組み合わせて出力実行部分を書いていきます。
以下のようになります。
先程定義したget_export_config()とstart_plugin()の間にコピーしてください。


def export_textures():
    #プロジェクトが開いてない場合はログを出して処理が止まるように
    if not substance_painter.project.is_open():
        substance_painter.logging.log(
            substance_painter.logging.INFO,
            "samplePlugin",
            "Please open project or save project.")
        return
    # 出力先パスを定義
    export_path = get_export_path()
    # 出力設定(json_config)作成
    export_config = get_export_config(export_path)
    # json_configをexport.export_project_texturesの引数に与える
    export_result = substance_painter.export.export_project_textures(export_config)
    # 出力の成否をログ表示
    substance_painter.logging.log(
        substance_painter.logging.INFO,
        "samplePlugin",
        export_result.message)
    return  
● projectモジュール is_open() の説明

if not substance_painter.project.is_open():
projectモジュールのis_open()は、プロジェクトが開いていない場合もしくは空のプロジェクトの場合にFalseを返します。今回はプロジェクトを開いていない場合に処理が止まるようにしています。
projectモジュールにはプロジェクトの状態をbool型で返すメソッドがいくつか用意されています。(未保存の変更があるかどうか、ベイク等のプロセスの途中であるどうか、等)。条件分岐の記述に使用できます。

● loggingモジュール の説明

substance_painter.logging.log(substance_painter.logging.INFO,"samplePlugin","Please open project or save project.")
PythonAPIにはloggingモジュールが用意されています。使用方法はPython標準モジュールloggingとほぼ同じで、引数にはstring型で表示したいログメッセージを与えます。

● exportモジュール export_project_textures() の説明

export_result = substance_painter.export.export_project_textures(export_config)
exportモジュールの.export_project_textures()の引数に、json_configを与えるとテクスチャの出力ができます。
返り値の型は「TextureExportResult」というオブジェクトになっていて、status/message/texturesというアトリビュートを持っています。いずれかのアトリビュートをつかうことで出力結果を確認出来ます。(今回はexport_resultという変数に代入して「export_result.message」としてログに表示させています)


7.UIの記述

次に、プラグインに専用のウィンドウを付けてみましょう。

pythonAPIでuiに関わる操作をしたい場合、PySide2(Qt for Python)というライブラリを使う必要があります。

uiモジュールは基本的にPySide2.QtWidgetsというオブジェクトのuiパーツを引数に受け取るメソッドで構成されています。

今回はPyside2の使い方自体については紹介しません。DCCソフト用のツール作成スクリプティングする際にはよく使用されるライブラリです。PySide2というライブラリ自体についてはご自身で少し調べてみるといいかなと思います。
以下、個人的にお勧めするPySideに関するブログサイトです。
https://unpyside.com/

今回は説明を省くため最小構成で書きます。
以下を最初の4行目当たりにコピーします。

from PySide2 import QtWidgets

ウィンドウ作成のクラス(設計図)を以下のように追加します。
start_plugin()の前にコピーします。


#QtWidgets.QMainWindowを継承してウィンドウを作ります
class Widget(QtWidgets.QMainWindow):
    def __init__(self):
        super(Widget, self).__init__()
        #ウィンドウタイトル設定
        self.setWindowTitle("sample")
        #ボタン作成
        self.button = QtWidgets.QPushButton("export")
        #ボタンをクリックしたときに実行する関数を設定
        self.button.clicked.connect(export_textures)
        #ウィンドウにボタンを配置
        self.setCentralWidget(self.button)

● ポイント

作成したプラグインに専用のUIを作った場合、プラグインを読み込んだときにUIが作成されて、プラグインを終了させたときにUIが削除されるように動作させる必要があります。

sample_plugin_widgets を start_plugin()の前に定義します。
start_plugin()とclose_plugin()の中身を以下のように変更します。


#UIオブジェクト保存用のリスト
sample_plugin_widgets = []

def start_plugin():
    #UIのインスタンスを作成してSubstance 3D Painter のメインウィンドウにドックする
    widget = Widget()
    substance_painter.ui.add_dock_widget(widget)

    #プラグイン停止時に呼び出す為に、あらかじめグローバル変数に保存しておく
    sample_plugin_widgets.append(widget)

def close_plugin():
    #プラグインを停止する際にグローバル変数に保存しておいたUIのインスタンスを削除します
    for widget in sample_plugin_widgets:
        substance_painter.ui.delete_ui_element(widget)
    sample_plugin_widgets.clear()

if __name__ == "__main__":
    start_plugin()

8.実行確認

ここまでの内容で以下のようなスクリプトになっているかと思います。確認してください。

グローバル変数(関数外で定義されている変数)は上部にまとめ直しました。
書き方を整理していますが内容は同じです。
SAMPLE_EXPORT_BASEPATH(出力先の基底パス), SAMPLE_PRESET_PATH(出力プリセットパス)は、自分の環境に合わせて値を変更してください。


# -*- coding: utf-8 -*-
import substance_painter 
import pathlib
from PySide2 import QtWidgets

SAMPLE_EXPORT_BASEPATH = r"D:\SVN\projects\SampleProject\assets"
SAMPLE_PRESET_PATH = r"D:\work\Substanc3DPainter\sample_preset.spexp"
sample_plugin_widgets = []

def get_export_path()-> str:
    mesh_path = substance_painter.project.last_imported_mesh_path()
    mesh_name = pathlib.Path(mesh_path).stem
    export_path = pathlib.Path(SAMPLE_EXPORT_BASEPATH, mesh_name)
    return str(export_path)

def get_export_config(export_path: str)-> dict:
    export_preset = substance_painter.resource.import_session_resource(SAMPLE_PRESET_PATH, substance_painter.resource.Usage.EXPORT)
    export_preset_url = export_preset.identifier().url()
    export_config = {
                    "exportPath":export_path,
                    "defaultExportPreset": export_preset_url,
                    "exportPresets":[ { "name" : "default", "maps" : [] } ],
                    "exportList":[],
                    "exportParameters":[{"parameters" : { "paddingAlgorithm": "infinite" }}],
                    "exportShaderParams":False
                    }
    export_list = export_config.get("exportList")
    for texture_set in substance_painter.textureset.all_texture_sets():
        export_list.append({"rootPath": texture_set.name()})
    return export_config

def export_textures():
    if not substance_painter.project.is_open():
        substance_painter.logging.log(
            substance_painter.logging.INFO,
            "samplePlugin",
            "Please open project or save project.")
        return
    export_path = get_export_path()
    export_config = get_export_config(export_path)
    export_result = substance_painter.export.export_project_textures(export_config)
    substance_painter.logging.log(
        substance_painter.logging.INFO,
        "samplePlugin",
        export_result.message)
    return  

class Widget(QtWidgets.QMainWindow):
    def __init__(self):
        super(Widget, self).__init__()
        self.setWindowTitle("sample")
        self.button = QtWidgets.QPushButton("export")
        self.button.clicked.connect(export_textures)
        self.setCentralWidget(self.button)

def start_plugin():
    widget = Widget()
    substance_painter.ui.add_dock_widget(widget)
    sample_plugin_widgets.append(widget)

def close_plugin():
    for widget in sample_plugin_widgets:
        substance_painter.ui.delete_ui_element(widget)
    sample_plugin_widgets.clear()

if __name__ == "__main__":
    start_plugin()

これでプラグインのウィンドウが表示してテクスチャセットの出力を実行出来るようになりました。

Substance 3D Painter.exeでプラグインをリロードしてもウィンドウが表示されない場合、ウィンドウが非表示になっている可能性があります。画面上部のメニュー[ウィンドウ]>[ビュー]から作成したウィンドウタイトルを探し、チェックを入れてください。

表示されたウィンドウのボタンを押すと指定したフォルダにテクスチャセットが出力できます。

これで一通り完成です。


■ まとめ

今回の手順で作成したスクリプトは説明に不要な部分を全て削ぎ落した未熟な状態です。実際に人に使ってもらう場合は、デバックを行い、適切な設計で書きましょう。
例えば、以下のように拡張することが考えられます。

  • 出力先にフォルダが存在するか探索して、無い場合は新規にフォルダを作成する等の条件分岐を追加する
  • Python標準ライブラリ(subprocess.Popenなど)を使って出力先フォルダをエクスプローラで開くようにする
  • PySide2.QtWidgetsで出力サイズを切り替えられるコンボボックスを追加する
  • projectモジュールのMetaDateでプロジェクトに前回のツール情報を保存するようにする

Substance 3D Painterはモデラーが頻繁に使用するソフトなのでちょっとしたスクリプトでも意外と作業の効率化に役立つのではないかと思います。
初めてチャレンジしてみる方の参考になれば幸いです。読んでいただきありがとうございました。

Author: fujii

テクニカルアーティスト 柴犬が好き