Help us understand the problem. What is going on with this article?

【Maya Python】スクリプトの中身を噛み砕く1 ~カメラスピードエディタ編

この記事の目的

自作ツールの中身を一つ一つ確認しながら理解を深め、今後のツール開発に役立てる。
今までなんとなくコピペで動いてたからいいやとスルーしてきたことを徹底理解する。
同じようにスクリプトを書いていてなんとなく動いてるからいっか、で終わってる人への一助になればと思います。

ツール概要

Mayaでのカメラ移動スピードを遅くし、カメラの微調整を可能にする。
Mayaのカメラは微妙に動きが早い時があるので、繊細なカメラワークの時に少し面倒です。
普通はTool Settingsで調整するが、カメラツールごとに設定があったりして使いにくいので一元化した。
ダイアルを左に回すとスピードを遅く、右に回すと早く、各カメラツールの切り替えなどができます。

cameraSpeedEditor.gif

動作環境:Maya2019.3,Maya2020.1
Pyside2を使用しているので2016以前では動作しません。

コード全文

cameraSpeedEditor.py
# -*- coding: utf-8 -*-
from maya.app.general.mayaMixin import MayaQWidgetBaseMixin
from PySide2 import QtWidgets
from PySide2.QtCore import Qt
from maya import cmds

class Widget(QtWidgets.QWidget):
    def __init__(self):
        # 親メソッドの呼び出し
        super(Widget, self).__init__()

        # グリッドレイアウトを配置
        layout = QtWidgets.QGridLayout(self)

        # 説明ラベル
        labelWidget = QtWidgets.QLabel(u'カメラのスピードを調整します\n\n←遅 カメラスピード調整 速→')
        labelWidget.setAlignment(Qt.AlignCenter)
        layout.addWidget(labelWidget,0,0,1,-1)

        # ダイアルウィジェット
        self.camDial = QtWidgets.QDial()
        self.camDial.setRange(0, 100)
        self.camDial.setValue(50)
        self.camDial.setNotchesVisible(True)
        self.camDial.sliderReleased.connect(self.setCamSpeed)
        layout.addWidget(self.camDial,1,0,1,-1)

        # スーパースローモードのチェック
        self.checkWidget = QtWidgets.QCheckBox(u'スーパースロー')
        self.checkWidget.setChecked(False)
        self.checkWidget.stateChanged.connect(self.setCamSpeed)
        layout.addWidget(self.checkWidget,2,0)

        # カメラツールコンボボックス
        setCamLabel = QtWidgets.QLabel('set Camera tool:')
        setCamLabel.setAlignment(Qt.AlignRight)
        layout.addWidget(setCamLabel,3,0)
        selCamItem = QtWidgets.QComboBox(self)
        selCamItem.addItem('tumble')
        selCamItem.addItem('track')
        selCamItem.addItem('dolly')
        selCamItem.addItem('boxZoomSuper')
        selCamItem.addItem('roll')
        selCamItem.addItem('yawPitch')
        selCamItem.activated[str].connect(self.setTool)
        layout.addWidget(selCamItem,3,1)

        # リセットボタン
        resetButton = QtWidgets.QPushButton(u'リセット')
        resetButton.clicked.connect(self.resetCamSpeed)
        layout.addWidget(resetButton,4,0,1,-1)

    # カメラスピードセット
    def setCamSpeed(self):
        # スピードの数値を計算
        value = float(self.camDial.value()) **3*0.0000072+0.1
        if self.checkWidget.isChecked():
            value = value /10

        # 計算した数値をセット
        cmds.tumbleCtx( 'tumbleContext'             ,e=True ,ts=value)
        cmds.trackCtx(  'trackContext'              ,e=True ,ts=value)
        cmds.dollyCtx(  'dollyContext'              ,e=True ,s =value)
        cmds.boxZoomCtx('boxZoomContext'            ,e=True ,zs=value)
        cmds.rollCtx(   'rollContext'               ,e=True ,rs=value)
        cmds.orbitCtx(  'azimuthElevationContext'   ,e=True ,orbitScale=value)
        cmds.orbitCtx(  'yawPitchContext'           ,e=True ,orbitScale=value)

        # 設定したことを表示
        print ( '# Camera speed is '+str(value)+' !\n'),

    # カメラツールセット
    def setTool(self, selectTool):
        cmds.setToolTo('%sContext' %(selectTool))
        print ( 'Set %s Tool !\n' %(selectTool)),

    # カメラスピードリセット
    def resetCamSpeed(self):
        # 各カメラの数値をデフォルトに戻す
        cmds.tumbleCtx( 'tumbleContext'             ,e=True ,ts=1)
        cmds.trackCtx(  'trackContext'              ,e=True ,ts=1)
        cmds.dollyCtx(  'dollyContext'              ,e=True ,s =1)
        cmds.boxZoomCtx('boxZoomContext'            ,e=True ,zs=1) 
        cmds.rollCtx(   'rollContext'               ,e=True ,rs=1)
        cmds.orbitCtx(  'azimuthElevationContext'   ,e=True ,orbitScale=1)
        cmds.orbitCtx(  'yawPitchContext'           ,e=True ,orbitScale=1)
        self.camDial.setValue(50)
        self.checkWidget.setChecked(False)
        print ( '# Camera speed is Default !\n'),


class MainWindow(MayaQWidgetBaseMixin, QtWidgets.QMainWindow):
    def __init__(self):
        # 親メソッドの呼び出し
        super(MainWindow, self).__init__()

        # ウィンドウの名前とサイズを指定
        self.setWindowTitle('Camera Speed editor')
        self.resize(200, 250)

        # Widgetをインスタンスしてウィンドウに配置
        widget = Widget()
        self.setCentralWidget(widget)

    def closeEvent(self, event):
        # ウィンドウを閉じたときにカメラスピードリセットを実行
        widget = Widget()
        widget.resetCamSpeed()


def main():
    # ウィンドウの表示
    window = MainWindow()
    window.show()

# メインで呼び出されたときの処理
if __name__ == "__main__":
    main()

詳細をみていく

ここから、頭から順番に一つ一つそれぞれのコードがどういう動作をしているのか、自分で理解しながら説明していければと思います。

頭に必ず書くおまじない

# -*- coding: utf-8 -*-

コードに日本語を使うときは入れるといいみたい。詳細はこちらを参照。
Python3ではむしろ非推奨らしい。Mayaはまだ2.7なので念のためつけとく。

Python で文頭に記載する文字コードの「アレ」の名称(なんちゃら UTF-8 みたいなやつ)

ライブラリのインポート

from maya.app.general.mayaMixin import MayaQWidgetBaseMixin
from PySide2 import QtWidgets
from PySide2.QtCore import Qt
from maya import cmds

使用する各ライブラリをインポート。
Pythonはライブラリが豊富なのでそれを活用することで色んなことができるようになる。MELではなくPythonを使うメリットの一つじゃないかな。

from 〇〇 import △△ で〇〇にある△△を持ってこいという意味。
各ライブラリの機能。
MayaQWidgetBaseMixin:ウィンドウの親子関係設定に使う
QtWidgets,Qt:PySide、UI関係の機能
cmds:mayaコマンド

4行目のfrom maya import cmdsはmayaのリファレンスなんかでは、
import maya.cmds as cmdsのように記述されている。
なんとなくfromで揃ってるほうが美しいなあということでこのようにしている。やってることは多分同じ。

Pythonのモジュールとimportとfrom入門

ウィジェットの設定

class Widget(QtWidgets.QWidget):
    def __init__(self):
        # 親メソッドの呼び出し
        super(Widget, self).__init__()

        ...

class。
classの説明についてはここでは端折ります。ややこしいので書籍で理解してください。Pythonやってて一番つまずいたところ。
使いながら慣れてくといいのかな。

あと自分でも勉強してて思ったことですが、
ネット記事だけだと限界があるので書籍を読むことをおすすめします。
個人的にはオライリージャパンの入門Python3が非常にわかりやすいです。今も読んでます。初~中級向けだと思います。

オライリージャパンの入門Python3
【図解】オブジェクト指向とは?(クラス・メソッド・インスタンスの意味)

さらに一行ずつ見てく

class Widget(QtWidgets.QWidget):

ライブラリからインポートしてきたQtWidgets.QWidgetWidgetに継承する。
PySide2のベースを流用して自分用にカスタムする下準備みたいなもの。

    def __init__(self):

__init__
特殊メソッド。インスタンスを作成した際に最初に呼び出される部分で初期化を行う。
classのコードを見ると一番よく目にする。

self
第一引数。
self以外でも一応動作するが、慣例的にselfを必ず使用する。
こういったルールはPEP 8というドキュメントにまとめられているのでそれを参考に。
たぶんネット独学だとこういうルールにたどり着かず独自ルールが出来上がるんだろうな(自戒

Pythonのselfとかinitを理解する
Pythonのselfとは?使い方や注意点について解説
[Pythonコーディング規約]PEP8を読み解く

        # 親メソッドの呼び出し
        super(Widget, self).__init__()

またしてもややこしい記述。最初訳わからなかったけど理解すればなんてことない。
QtWidgets.QWidgetWidgetに継承して、
__init__で初期化メソッドを作ると、親(QtWidgets.QWidget)の初期化メソッドが呼び出されなくなる。
なので、この記述を入れて、親の初期化メソッドを呼び出す。
ちなみにpython2系ではこの記述が必要だったけど3系では、super().__init__()でよくなる。シンプル。
Mayaがいつ3系に切り替えるか気になるところではありますが。。

[Python]クラス継承(super)
Python の super() 関数の使い方

ここまでの動きを図解してみた

クラスの動きがどうなってるか理解を深めるために、どうなってるのか図解してみます。
cameraSpeedEditor__class.jpg
2.from~でPySide2.QtWidgetsをインポートしてくる
7.インポートしたQtWidgetsのQWidgetをWidgetという名前で継承
8.Widgetクラスで初期化メソッドを作成
10.インポートしたQtWidgets.QWidgetの初期化メソッドを呼び出し
13以降 UIの配置やら機能を作成

まだ文字が多いので簡単なイラストにしてみた。
class_explain.jpg
このようにもととなる設計図をインスタンスコピー(Mayaではおなじみ)して、腕やら足、色をオーバーライド(上書き)して自分の思うような形にするといった使い方ができるというわけだね。
だから、__init__で上書きしちゃうともとの__init__が呼ばれなくなるからsuperが必要になると。

UIの作成

さて、classの説明が長くなりましたが、これからようやくUIを作成していきます。

        # グリッドレイアウトを配置
        layout = QtWidgets.QGridLayout(self)

QGridLayoutを作成。このグリッドレイアウトの中に、ボタンやらラベルやらを配置していく。

        # 説明ラベル
        labelWidget = QtWidgets.QLabel(u'カメラのスピードを調整します\n\n←遅 カメラスピード調整 速→')

ラベルをグリッドレイアウトに追加。
u'文字'というように日本語を使うときは頭にuを付ける。
\nで改行ができる。

        labelWidget.setAlignment(Qt.AlignCenter)

作成したラベルウィジェットをセンターに配置。

        layout.addWidget(labelWidget,0,0,1,-1)

ラベルウィジェットをグリッドレイアウトの0行目、0列目、1行の長さ、-1列の長さ
指定した場所から最後まで使用する場合は-1を使うといい。

grid_explain.jpg
このように、行、列のどのポジションからどのくらいの長さかを指定できる。

        # ダイアルウィジェット
        self.camDial = QtWidgets.QDial()
        self.camDial.setRange(0, 100)
        self.camDial.setValue(50)
        self.camDial.setNotchesVisible(True)
        self.camDial.sliderReleased.connect(self.setCamSpeed)
        layout.addWidget(self.camDial,1,0,1,-1)

ダイアルウィジェットを作成。
今回、self.camDial とselfを付けているのは、別のところから数値を引っ張りたいため。
レンジを0~100に指定。デフォルト値を50に変更。setNotchesVisibleで目盛りを追加。
self.camDial.sliderReleased.connect(self.setCamSpeed) で、
マウスのボタンを離したときにself.setCamSpeedが実行されるように設定。

        # スーパースローモードのチェック
        self.checkWidget = QtWidgets.QCheckBox(u'スーパースロー')
        self.checkWidget.setChecked(False)
        self.checkWidget.stateChanged.connect(self.setCamSpeed)
        layout.addWidget(self.checkWidget,2,0)

チェックウィジェットを作成。
デフォルトをチェック無し状態に、チェック状態が変更されたときに、self.setCamSpeedを実行するように指定。
レイアウト配置は2行目、0列目。長さが両方1でよければ未記入でよい。

        # カメラツールコンボボックス
        setCamLabel = QtWidgets.QLabel('set Camera tool:')
        setCamLabel.setAlignment(Qt.AlignRight)
        layout.addWidget(setCamLabel,3,0)
        selCamItem = QtWidgets.QComboBox(self)
        selCamItem.addItem('tumble')
        selCamItem.addItem('track')
        selCamItem.addItem('dolly')
        selCamItem.addItem('boxZoomSuper')
        selCamItem.addItem('roll')
        selCamItem.addItem('yawPitch')
        selCamItem.activated[str].connect(self.setTool)
        layout.addWidget(selCamItem,3,1)

selCamItem = QtWidgets.QComboBox(self) コンボボックスを作成。
addItemでカメラツールのアイテムを複数追加。名前はそのまま使いたいため、Mayaのカメラツールの記述に準拠。
selCamItem.activated[str].connect(self.setTool) アクティブが変更されたときに、
self.setToolにaddItemのstringを渡して実行。

        # リセットボタン
        resetButton = QtWidgets.QPushButton(u'リセット')
        resetButton.clicked.connect(self.resetCamSpeed)
        layout.addWidget(resetButton,4,0,1,-1)

リセットボタンの作成。
動作設定や、配置は他と同じように設定する。

以上でUIの作成は完了です。

UIを操作したときの動作を作成する

UI作成時に出てきた~.connectの動作内容を作成していく。

    # カメラスピードセット
    def setCamSpeed(self):
        # スピードの数値を計算
        value = float(self.camDial.value()) **3*0.0000072+0.1
        if self.checkWidget.isChecked():
            value = value /10

        # 計算した数値をセット
        cmds.tumbleCtx( 'tumbleContext'             ,e=True ,ts=value)
        cmds.trackCtx(  'trackContext'              ,e=True ,ts=value)
        cmds.dollyCtx(  'dollyContext'              ,e=True ,s =value)
        cmds.boxZoomCtx('boxZoomContext'            ,e=True ,zs=value)
        cmds.rollCtx(   'rollContext'               ,e=True ,rs=value)
        cmds.orbitCtx(  'azimuthElevationContext'   ,e=True ,orbitScale=value)
        cmds.orbitCtx(  'yawPitchContext'           ,e=True ,orbitScale=value)

        # 設定したことを表示
        print ( '# Camera speed is '+str(value)+' !\n'),

ダイアルや、チェックボックスの状態を取得し、
計算した数値を各カメラツールに反映する。

value = float(self.camDial.value()) **3*0.0000072+0.1 ダイアルの値からカメラスピードの値へ変換。
なんでこんなヘンテコな計算式だけど、、ちょうどいい感じに動くよう調整してたらこうなった。

if~ はスーパースローモードがチェックされている場合はvalueを10分の1にする。

# 計算した数値をセット 以下
計算した数値をそれぞれのカメラツールの値に反映する
無駄な余白が多いのは列をそろえて見やすくするため。
PEP8だと非推奨らしいけどあえて無視。実際のところどうなんだろう。

print ('hogehoge'),
最後の,(カンマ)はMayaのコマンドラインに表示するためつけてる。
warningとかだと色付きでちょっと仰々しいから。
今の所なんでカンマつけるとコマンドラインに表示できるのか謎。Mayaの仕様なのかなあ。

    # カメラツールセット
    def setTool(self, selectTool):
        cmds.setToolTo('%sContext' %(selectTool))
        print ( 'Set %s Tool !\n' %(selectTool)),

selCamItem.activated[str].connect(self.setTool) から受け取った[str]を、
cmds.setToolTo('%sContext' %(selectTool)) このコマンドで切り替える。

%s は%(ほにゃらら) を置き換えてくれる
例:
'%sContext' %('tumble')'tumbleContext'

    # カメラスピードリセット
    def resetCamSpeed(self):
        # 各カメラの数値をデフォルトに戻す
        cmds.tumbleCtx( 'tumbleContext'             ,e=True ,ts=1)
        cmds.trackCtx(  'trackContext'              ,e=True ,ts=1)
        cmds.dollyCtx(  'dollyContext'              ,e=True ,s =1)
        cmds.boxZoomCtx('boxZoomContext'            ,e=True ,zs=1) 
        cmds.rollCtx(   'rollContext'               ,e=True ,rs=1)
        cmds.orbitCtx(  'azimuthElevationContext'   ,e=True ,orbitScale=1)
        cmds.orbitCtx(  'yawPitchContext'           ,e=True ,orbitScale=1)
        self.camDial.setValue(50)
        self.checkWidget.setChecked(False)
        print ( '# Camera speed is Default !\n'),

カメラの設定とUIをデフォルトに戻す。

以上でウィジェットを操作したときの動作を作成できた。

ウィンドウの作成

class MainWindow(MayaQWidgetBaseMixin, QtWidgets.QMainWindow):
    def __init__(self):
        # 親メソッドの呼び出し
        super(MainWindow, self).__init__()

        # ウィンドウの名前とサイズを指定
        self.setWindowTitle('Camera Speed editor')
        self.resize(200, 250)

        # Widgetをインスタンスしてウィンドウに配置
        widget = Widget()
        self.setCentralWidget(widget)

MayaQWidgetBaseMixinQtWidgets.QMainWindowの二つを継承
MayaQWidgetBaseMixinはMayaのメインウィンドウの下にウィンドウが行かないようにするための機能。
以前はshibokenなどを使っていたがずいぶんシンプルになっていい感じ。

新!Mayaウインドウの親子関係MayaQWidgetBaseMixin!

ウィンドウタイトルと、ウィンドウサイズを指定。
先ほど作ったウェイジェットをウィンドウの中央に配置。

    def closeEvent(self, event):
        # ウィンドウを閉じたときにカメラスピードリセットを実行
        widget = Widget()
        widget.resetCamSpeed()

ウィンドウを閉じたときにカメラスピードをリセットする。
def closeEvent(self, event):を使えばウィンドウを閉じたときに追加の動作を加えることができる。

以上でUIとメインの機能を作成できた。

ウィンドウの表示

def main():
    # ウィンドウの表示
    window = MainWindow()
    window.show()

main()とコマンドをたたけばウィンドウが表示され、プログラムが実行できる。

メイン実行時の動作

# メインで呼び出されたときの処理
if __name__ == "__main__":
    main()

これもおまじないのようなもの。
インポートの際にプログラムを実行しないようにする。
コード全文をスクリプトエディタにコピペして実行であればプログラムが起動する。

【python】if name == 'main':とは?

おわりに

最初はメモ書き程度と思っていたが、結構長くなってしまった。
所々書きながら理解できてないところを追加してたらこんなことに。

毎回ツール書くたびにこういう記事を残しておくと自分のためにもよさそうだ。
やはり勉強は 教わる→やってみる→教える で身についていくものだなと思う。

至らないところが多々あるかもしれませんが、ご容赦ください。
間違っている部分あればご教示頂ければ幸いです。

参考一覧

下記、参考にさせて頂いたサイトリンクです。

Python で文頭に記載する文字コードの「アレ」の名称(なんちゃら UTF-8 みたいなやつ)
Pythonのモジュールとimportとfrom入門
【図解】オブジェクト指向とは?(クラス・メソッド・インスタンスの意味)
Pythonのselfとかinitを理解する
Pythonのselfとは?使い方や注意点について解説
[Pythonコーディング規約]PEP8を読み解く
[Python]クラス継承(super)
Python の super() 関数の使い方
新!Mayaウインドウの親子関係MayaQWidgetBaseMixin!
【python】if name == 'main':とは?

elloneil
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした