Maya Python PySide

スタンドアロン / Maya / Houdini / Blender で共有できる UI を作る【クロスプラットフォーム】

PySide2 を利用して、スタンドアロンでも Maya でも Houdini でも Blender でも共通のコードを利用できる UI を組んでいきます。
(果たしてこれをクロスプラットフォームと呼んでいいのだろうか)

基本的にはコマンドや作り方が違うんで全く違うツール構成になるんですが、
パイプラインツールなどは自然と共通のものを利用することが多くなります。

各環境による起動コードの違い

それぞれの環境で起動するためのコードが違います。まずは各コードの違いを見ていきます。

'''
------------------------------------------------------------------------------------------------------------------------

    共有したいコード

------------------------------------------------------------------------------------------------------------------------
'''
class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.setGeometry(650, 200, 500, 200)
        self.setWindowTitle('共有ウィンドウ')

        self.set_UI()


    def set_UI(self):

        wrapper = QtWidgets.QWidget()
        self.setCentralWidget(wrapper)

        root_Area = QtWidgets.QVBoxLayout()
        wrapper.setLayout(root_Area)

        # ラベル
        lbl = QtWidgets.QLabel('Hello World')
        lbl.setStyleSheet(
            'font-size:26px;'
            'font-weight:bold;'
        )
        lbl.setAlignment(Qt.AlignCenter)
        root_Area.addWidget(lbl)

このコードを使って同じウィンドウを各環境で立ち上げられるようにしていきます。

スタンドアロン

# スタンドアロン用の起動
def launch_from_standalone():

    app = QtWidgets.QApplication()
    window = MainWindow()
    window.show()
    app.exec_()

スタンドアロンで起動するには上記のコードになります。

Maya

def get_maya_window():
    OpenMayaUI.MQtUtil.mainWindow()
    ptr = OpenMayaUI.MQtUtil.mainWindow()
    widget = wrapInstance(int(ptr), QtWidgets.QWidget)
    return widget

def launch_from_maya():
    maya_window = get_maya_window()
    ui = MainWindow(parent=maya_window)
    ui.show()

こちらが Maya で起動する場合の文です。

Maya のウィンドウを取得し、
その子要素として上記のウィンドウを立ち上げる必要があります。

また、from maya import OpenMayaUIfrom shiboken2 import wrapInstance
のインポートが必要です。

ウィンドウのコードも若干変わります。
parent の引数を受けとれるようにします。

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, parent):
        super(MainWindow, self).__init__(parent)


Maya の PySide は Maya 側で QSS が設定されており、ダークモードで起動します。
後述しますが、見た目も共通のものにするには自前で QSS を当てていく必要があります。

Houdini

Houdini も Maya と同様親のウィンドウを渡します。

def launch_from_houdini():
    window = MainWindow(parent=hou.ui.mainQtWindow())
    window.show()

Houdini だと簡単にメインウィンドウを取得できます。
import hou で hou モジュールのインポートも必要です。

Houdini も Maya と同じくダークテーマで起動しますが、色味やフォントが違います。

Blender

Blender 用のウィンドウは下記のように書きます。

def launch_from_blender():
    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.setWindowFlags(Qt.WindowStaysOnTopHint)
    window.show()
    app.exec_()

.setWindowFlags(Qt.WindowStaysOnTopHint) というのは
ウィンドウを常に最前面に出すメソッドです。
このコードは Blender のウィンドウに対して
自作したウィンドウを子要素にするための書き方ではありません。

下記に正しい書き方ありそうです。

面倒そうなので今回はこのまま最前面に出すコードのまま進めます。
(そもそも私はほとんど Blender を扱えません)
そのため、MainWindow に parent の設定が必要ありません。

class MainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()

Blender の PySide は pip で手動でインストールするため
ソフトウェア側の手が加わっていません。
そのため、スタンドアロンと同じ見た目で起動します。

Blender に PySIde を入れる

Blender に pip や PySide が入っていない方は SENA さんの動画を置いておきますので参考にしてください。

補足

Blender のインストール方法には

  1. インストーラからインストール
  2. ZIP ファイルをダウンロードして展開

の2種類があり、動画で紹介されているのは
1 のインストーラを利用したインストールのものです。
ZIP 版 で Blender を使用されている方は自由な場所に展開できるため、
パスが異なるはずです。

それでも最後の方のパスは同じです。

例えば Dドライブの system という名前のフォルダに Blender を入れている場合は

cd D:\system\blender\blender-3.0.1-windows-x64\3.0\python\bin

とします。
Blender のバージョン名のフォルダの中にある python > bin が cURL を実行するパスです。

また、Cドライブ以外のドライブへの移動は少し違います。
上記のコードだけでは移動できません。

cd D:\system\blender\blender-3.0.1-windows-x64\3.0\python\bin
cd d:

とします。
一度指定したパスで cd コマンドを実行した後にcd d:を実行してください。

全てを1つにまとめる

これらすべてを一つのコードにし、それぞれの環境でエラーを出させずに動かしていきます。

行ったことは3点です。

  1. モジュールのインポートを各関数内に入れる
  2. MainWindow の __init__ にデフォルトで parent=None を指定する。
  3. このコードをモジュール化して各環境から読み込む
    あるいは 一番下の適当なコメントを外して実行

# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function, unicode_literals)

import os
import sys
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt
from PySide2.QtGui import QPalette


'''
------------------------------------------------------------------------------------------------------------------------

    共有したいコード

------------------------------------------------------------------------------------------------------------------------
'''
class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setGeometry(650, 200, 500, 200)
        self.setWindowTitle('共有ウィンドウ')

        self.set_UI()


    def set_UI(self):

        wrapper = QtWidgets.QWidget()
        self.setCentralWidget(wrapper)

        root_Area = QtWidgets.QVBoxLayout()
        wrapper.setLayout(root_Area)

        # ラベル
        lbl = QtWidgets.QLabel('Hello World')
        lbl.setStyleSheet(
            'font-size:26px;'
            'font-weight:bold;'
        )
        lbl.setAlignment(Qt.AlignCenter)
        root_Area.addWidget(lbl)



'''
------------------------------------------------------------------------------------------------------------------------

    起動

------------------------------------------------------------------------------------------------------------------------
'''


# スタンドアロン用の起動
def launch_from_standalone():
    app = QtWidgets.QApplication()
    window = MainWindow()
    window.show()
    app.exec_()


# Maya のウィンドウ取得
def get_maya_window():
    from maya import OpenMayaUI
    from shiboken2 import wrapInstance
    OpenMayaUI.MQtUtil.mainWindow()
    ptr = OpenMayaUI.MQtUtil.mainWindow()
    widget = wrapInstance(int(ptr), QtWidgets.QWidget)
    return widget

# Maya 用の起動
def launch_from_maya():

    import maya.cmds as cmds
    maya_window = get_maya_window()
    window = MainWindow(parent=maya_window)
    window.show()


# Houdini 用の起動
def launch_from_houdini():
    import hou
    window = MainWindow(parent=hou.ui.mainQtWindow())
    window.show()


# Blender 用の起動
def launch_from_blender():
    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.setWindowFlags(Qt.WindowStaysOnTopHint)
    window.show()
    app.exec_()


# 呼び出し
# launch_from_standalone()
# launch_from_maya()
# launch_from_houdini()
# launch_from_blender()

Maya の cmds コマンドなど、各環境で共有できないモジュールは
それぞれの起動用の関数の中で記載していきます。

モジュール化する場合は
上記のコードをモジュールにしてパスを通したフォルダに入れ、
読み込んだ後に下記のコードで呼び出します。

スタンドアロンファイル名.launch_from_standalone()
Mayaファイル名. launch_from_maya()
Houdiniファイル名. launch_from_houdini()
Blenderファイル名. launch_from_blender()

これで同じ内容のウィンドウを各環境で起動させることができます。

関数は読み込んだ時にしかコードが走らないため、各環境でしか使えないモジュールは
関数内でインポートすることで他の環境でエラーが起きなくなります。

見た目を同じにする

見た目を同じにするには強制的に QSS を当てていきます。

ウィンドウは背景色を#3a3a3aに設定します。これは Houdini の背景色です。
Maya であれば#444444が背景色になります。

self.setStyleSheet(
    'background-color:#3a3a3a;'
)

ラベルはフォントを Arial で指定し、フォントカラーを#ccccccで設定します。
こちらも Houdini の色です。Maya であれば#bbbbbbがフォントカラーになります。

lbl = QtWidgets.QLabel('Hello World')
lbl.setFont(QtGui.QFont('Arial'))
lbl.setStyleSheet(
    'font-size:26px;'
    'font-weight:bold;'
    'color:#cccccc;'
)

私は使用する全てのウィジェットに対して QSS を当てたものを
ライブラリ化して使用しています。
組むのは面倒ですが、その後は全ての環境で使いまわせるので便利です。

各環境で個別の表示にする

環境ごとに表示を切り替えるには グローバル変数を代入することで分岐させています。

ENVIRONMENT = ''

最初にグローバル変数を作っておき、その後起動時の関数内でこの変数を上書きします。

# スタンドアロン用の起動
def launch_from_standalone():
    global ENVIRONMENT
    ENVIRONMENT = 'standalone'

# Maya 用の起動
def launch_from_maya():
    global ENVIRONMENT
    ENVIRONMENT = 'maya'

# Houdini 用の起動
def launch_from_houdini():
    global ENVIRONMENT
    ENVIRONMENT = 'houdini'

# Blender 用の起動
def launch_from_blender():
    global ENVIRONMENT
    ENVIRONMENT = 'blender'

今回はラベルにそのまま表示させています。

# ラベル
lbl = QtWidgets.QLabel(ENVIRONMENT + ' 環境')

並べてみて気づきましたが、Houdini だけ日本語がやや太いんですね。
どれもfont-weight:bold;を設定したものです。
何が原因なんだろう?

条件分岐

例えば Maya でのみ実行させたいものがある場合、下記のように書きます。

if ENVIRONMENT == 'maya':
    hoge

ENVIRONMENT には各環境の文字列が入っているため、ここで分岐させられます。
グローバル変数のため他のモジュールから参照して分岐させることもできます。

今回はわかりやすいように文字列型で値を作っていますが、
普段は数値で指定しています。

以上、PySIde2 のクロスプラットフォーム化でした。
この感じで作っていくと 3dsMAX とかでも全く同じものを使えるはずです。


追記

Substance Designer

Substance Designer 追記しました。

# Substance Designer
def launch_from_substance_designer():
    global ENVIRONMENT
    ENVIRONMENT = 'substance_designer'

    import sd
    app = sd.getContext().getSDApplication().getQtForPythonUIMgr()
    window = MainWindow(parent=app.getMainWindow())
    window.show()

-Maya Python, PySide