チュートリアル 4: スクリプト プラグインの記述方法

スクリプト プラグインと独自のプラグインの記述方法について説明します。

サンプル スクリプトをダウンロードする

サンプル スクリプトに移動する

チュートリアルの PDF をダウンロードする

ビデオのキャプション: VRED Pro の Python スクリプト作成に関するチュートリアル シリーズへようこそ。Christopher といいます。今日は、スクリプト プラグインとその作成方法についてお話ししたいと思います。

前回のチュートリアルで学習したように、VRED は、独自の Python スクリプトを追加してシーンをカスタマイズすることができる、さまざまなインタフェースを備えています。いくつかの簡単なスクリプトを使用して、制作に役立つユーザ インタラクションとツールを実装することができます。すべての同僚が利用したり、顧客に配布したりする必要がある特殊なツールを提供する場合は、ツールをスクリプト プラグインとして実装することを検討する必要があります。

スクリプト プラグインは特別な種類の Python スクリプトです。これらは VRED シーンから独立しており、使用するには特別なスクリプト プラグイン ディレクトリにインストールする必要があります。スクリプト プラグインにアクセスするには、VRED のメニュー バーで「Script」エントリを開きます。このエントリには、インストールされているすべてのスクリプト プラグインがリストされます。各プラグインは、対応するエントリをクリックすることで、有効と無効を切り替えることができます。通常、スクリプト プラグインは VRED に統合され、VRED の一部のように使用できるユーザ インタフェースを提供します。これは、PySide ウィジェットが既定で VRED のウィンドウ スタイルを使用しているためです。このチュートリアルでは、スクリプト プラグインを一緒に実装し、その過程で、ユーザ インタフェースを構築し、新しいツールを実装する方法を学習します。

このビデオでは、ビューポイントの操作に役立つ簡単なプラグインを実装します。まず、指定できるディレクトリにすべてのビューポイントをレンダリングするのに役立つツールを実装します。次に、ビューポイントをランダムに選択して、カメラを設定します。

これらの関数を実装するには、最初にプラグイン クラスを作成する必要があります。このクラス設定は、基本的に、開発するすべてのプラグインで同じになります。まず、「マイ ユーザ ドキュメント」、「Autodesk」、「VRED 13.3」、「ScriptPlugins」の下に新しいディレクトリを作成します。このディレクトリには「ViewpointPlugin」という名前を付け、作成するすべてのプラグイン ファイルを格納します。このディレクトリがコンピュータ上に存在しない場合は作成し、VRED がそこからスクリプト プラグインの読み込みを試みるようにできます。

次のサンプルでは、Python 拡張機能をインストールした Visual Studio Code を使用して作業します。新しく作成したディレクトリで Visual Studio Code を開き、viewpoint_plugin.pyviewpoint_plugin.ui の 2 つのファイルを作成することができます。最初のファイルには Python コードが含まれ、2 番目のファイルにはユーザ インタフェース レイアウトが含まれます。スクリプトの先頭で、PySide 名前空間から必要なモジュール、つまり Qt コア、Qt GUI、および Qt ウィジェットを読み込みます。プラグイン ウィジェットの生成に役立つ uiTools も読み込みます。

次に、UI ツールを使用して、フォームとウィジェット ベースをロードします。ここでは、先ほど作成した UI ファイルを参照します。次に、先ほど生成したフォームとベースから継承する実際のプラグイン クラスを追加します。init コンストラクタの開始は常に同じで、作成するすべてのプラグインにコピーすることができます。

クラスの下で、コンストラクタのパラメータとして「VRED プラグイン ウィジェット」を使用して、プラグインをインスタンス化できます。説明が速すぎてすべてを理解できなくても心配はいりません。これらは多くの特殊なプラグイン コードですが、常に同じです。プラグインを独自に開発する場合は、VRED のプラグインのサンプルからこのプラグイン構造をコピーすることができます。またこのチュートリアルは、いつでも巻き戻したり、[一時停止]ボタンをクリックして一時停止することができます。

Python 側での作業はこれで完了しましたが、ユーザ インタフェースも定義する必要があります。これは、viewpoint_plugin.ui ファイルで行います。レイアウト ファイルは、ユーザ インタフェースの定義に XML 表記を使用します。このチュートリアルでは、いくつかのボタンを追加します。複雑な操作はありません。ここで重要なのは、2 つの押しボタンを追加して名前を付けることです。この名前は、後でボタンを参照するために Python スクリプトで使用されます。このような QT レイアウト ファイルを作成して書き出すためのエディタを提供する「QT Designer」を参照することをお勧めします。このチュートリアルでは手動で行いますが、大規模なプロジェクトでは非常に手間がかかります。

ファイルを保存したら、VRED に戻ってスクリプト プラグインを再ロードします。[スクリプト]メニューに別のエントリが表示されています。このエントリをクリックすると、新しいプラグイン インタフェースが表示されます。開いたり閉じたりできる UI は既に存在しますが、現在はまだ機能がありません。

すべてのビューポイントをレンダリングする最初のツールを実装しましょう。「render viewpoints」という新しい関数を定義します。最初に、ファイル ダイアログ モジュールを使用して、ユーザにディレクトリを要求します。ユーザがディレクトリを選択する場合は問題ありません。ただし、ユーザが操作をキャンセルした場合は、そのことを確認し、何も行わずに関数から戻る必要があります。次のステップでは、使用可能なすべてのビューポイントのリストに対して処理を反復し、ビューポイントごとにアクティブにします。その後、ファイル名を生成し、ビューポイントをファイルにレンダリングします。ディレクトリ、ユーザの選択、およびビューポイントの名前から、レンダリングを保存する場所を VRED に指示するファイル パスを生成できます。レンダリングが完了したら、ファイルが保存されたディレクトリを開くかどうかをユーザに尋ねるメッセージ ダイアログを表示するのもよいアイデアです。これは、ユーザの選択を返すことができる「QMessageBox」を使用して実現できます。[OK]と[キャンセル]ボタンが表示され、ユーザが[OK]をクリックすると、既定の Python の機能を使用してディレクトリが開きます。

今すぐテストを行っても、スクリプトの重要な部分がないため、実際には何も起こりません。レンダリング関数を押しボタンに接続する必要があります。そのためには、UI レイアウトで指定した名前を使用して、参照するボタンの clicked シグナルに接続します。これで、準備が完了しました。スクリプト プラグインを再ロードして新しいプラグインを開始すると、実装をテストできます。予想したとおり、すべてのビューポイントがレンダリングされ、レンダリングが完了した後にレンダリング ディレクトリを開くかどうかを尋ねられます。

ここからは、2 番目の関数を実装します。今回は、テキストではなく、アイコンをボタンに追加します。そこで、UI レイアウト ファイルの 2 番目の押しボタンからテキスト ノードを削除し、Python ファイルに戻ります。setIcon 関数を使用してボタンにアイコンを直接設定すれば、アイコンを追加できます。この関数は、アイコン イメージを使用して作成する入力として QIcon パラメータを必要とします。アイコンは、プラグイン ディレクトリに格納される .PNG イメージです。

また、サイズが大きすぎてユーザ インタフェースに収まらないことがないよう、アイコンのサイズを設定する必要があります。もちろん、この押しボタンを関数に接続する必要もあります。今回は、事前に実行して関数 selectRandomViewpoint を呼び出します。この関数をプラグイン クラスに追加し、実装のための記述を開始します。このツールは、ランダムなビューポイントを選択してアクティブにするだけなので、実行時間は大幅に短くなります。プラグインを再ロードすると、テキスト ボタンがイメージ ボタンに変わり、クリックするとランダムなビューポイントが選択されます。うまくできました。

ここから、使用したい機能を自由に実装できます。独自のツールを設計し、独自のルック & フィールを与えることができます。HTML ユーザ インタフェースを入力とするスクリーンプレートを使用するスクリプト プラグインを実装することもできます。注意しなければならない点は、VRED がすべての Python スクリプトを単一のスクリプト プラグインとして扱うことです。そのため、プラグインを複数のファイルに分割することが難しくなります。この問題を回避するには、使用する追加の Python モジュールを内部の VRED Python ライブラリ ディレクトリに移動します。このディレクトリ内のすべての Python モジュールは、VRED 内の任意のスクリプトからロードできます。

もう 1 つの注意事項: スクリプトにいくつかの VRED モジュールを明示的に読み込む必要があることに気付いたかもしれません。この操作は、API バージョン 1 のすべてのモジュールに行う必要があります。API バージョン 2 のモジュールは、VRED 自身によって自動的に追加されます。

このチュートリアルを終えると、スクリプト プラグインを使用して独自のツールを実装できるようになります。初めての場合は、VRED プラグインのサンプルを開始点として使用し、独自の実装を入力することをお勧めします。次に、Qt Designer ツールを使用して、独自のユーザ インタフェースの作成を開始できます。

今日の説明はこれで終わりです。ご視聴いただき、ありがとうございました。またお会いしましょう。


Python サンプル コード

これは、「チュートリアル 4: スクリプト プラグインの記述方法」ビデオに付属する Python サンプル スクリプトです。

ヒント:

以下のファイルが含まれた ZIP ファイルをダウンロードするには、ここをクリックしてください。

簡単なプラグイン

ここに render_viewpoints_simple.py のサンプル コードがあります。

import vrFileIO
import vrFileDialog
import vrRenderSettings
from PySide2 import QtCore, QtWidgets
from shiboken2 import wrapInstance

def vredMainWindow(): 
    main_window_ptr = getMainWindow()
    return wrapInstance(int(main_window_ptr), QtWidgets.QMainWindow)

class CustomDialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super(MyDialog, self).__init__(parent)

        boxlayout = QtWidgets.QVBoxLayout(self)

        self.lineedit = QtWidgets.QLineEdit()
        boxlayout.addWidget(self.lineedit)

        self.button = QtWidgets.QPushButton("Set Label")
        self.button.clicked.connect(self.buttonClicked)
        boxlayout.addWidget(self.button)

        self.label = QtWidgets.QLabel()
        boxlayout.addWidget(self.label)

        self.setLayout(boxlayout)

    def buttonClicked(self):
        self.label.setText(self.lineedit.text())
        self.lineedit.setText("")


def renderViewpoints():
    renderDirectory = vrFileDialog.getExistingDirectory("Select a render directory:", vrFileIO.getFileIOBaseDir())

    if not renderDirectory:
        print("No directory where to save the renderings!")
        return

    viewpoints = vrCameraService.getAllViewpoints()
    for viewpoint in viewpoints:
        name = viewpoint.getName()
        vrRenderSettings.setRenderFilename("{}.jpg".format(name))
        vrRenderSettings.startRenderToFile(False)


dialog = CustomDialog(vredMainWindow())
dialog.show()

ビューポイント プラグイン

次の3つのサンプル ファイルがあります。

viewpoint_plugin.py

from PySide2 import QtCore, QtGui, QtWidgets

import os
import random
import threading

# Import vred modules in a try-catch block to prevent any errors
# Abort plugin initialization when an error occurs
importError = False
try:
    import vrController
    import vrFileIO
    import vrMovieExport
    import vrFileDialog
    import vrRenderSettings
except ImportError:
    importError = True
    pass

import uiTools

# Load a pyside form and the widget base from a ui file that describes the layout 
form, base = uiTools.loadUiType('viewpoint_plugin.ui')

class vrViewpointPlugin(form, base):
    """
    Main plugin class
    Inherits from fhe form and the widget base that was generated from the ui-file
    """

    def __init__(self, parent=None):
        """Setup and connect the plugins user interface"""

        super(vrViewpointPlugin, self).__init__(parent)
        parent.layout().addWidget(self)
        self.parent = parent
        self.setupUi(self)
        self.setupUserInterface()

        # Initialize some class variables that we need for our loop function
        self.loopCounter = 0
        self.loopRunning = False


    def setupUserInterface(self):
        """Setup and connect the plugins user interface"""

        self._render_all.clicked.connect(self.renderViewpoints)

        self._random_viewpoint.clicked.connect(self.selectRandomViewpoint)
        self._random_viewpoint.setIcon(QtGui.QIcon("icon_random_viewpoint.png"))
        self._random_viewpoint.setIconSize(QtCore.QSize(32,32))

        self._loop_viewpoints.clicked.connect(self.loopViewpoints)


    def renderViewpoints(self):
        """
        Open a directory dialog and then render all viewpoints to that directory
        """

        print("[Viewpoint Plugin] Render all viewpoints...")
        renderDirectory = vrFileDialog.getExistingDirectory("Select a render directory:", vrFileIO.getFileIOBaseDir())
        if not renderDirectory:
            print("No directory where to save the renderings!")
            return

        viewpoints = vrCameraService.getAllViewpoints()
        for viewpoint in viewpoints:
            name = viewpoint.getName()
            viewpoint.activate()
            print("{}/{}.jpg".format(renderDirectory, name))
            vrRenderSettings.setRenderFilename("{}/{}.jpg".format(renderDirectory, name))
            vrRenderSettings.startRenderToFile(False)

        msgBox = QtWidgets.QMessageBox()
        msgBox.setWindowTitle("Finished Rendering Viewpoints")
        msgBox.setInformativeText("Finished Rendering Viewpoints. Do you want to open the render directory?")
        msgBox.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
        ret = msgBox.exec_()

        if ret == QtWidgets.QMessageBox.Ok:
            os.startfile(renderDirectory)


    def selectRandomViewpoint(self):
        """ Select a random viewpoint from all viewpoints """

        print("[Viewpoint Plugin] Select a random viewpoint...")

        viewpoints = vrCameraService.getAllViewpoints()
        randomViewpoint = random.choice(viewpoints)
        randomViewpoint.activate()


    def loopViewpoints(self):
        """
        Loops through all viewpoints once. Stops looping when the button is pressed again
        """

        # When a loop is already running, then cancel the loop
        if self.loopRunning:
            print("[Viewpoint Plugin] Stop loop...")
            self.__setLoopViewpointLabel("Loop Viewpoints")

            self.loopRunning = False
            return

        # Otherwise start a new loop
        if self.loopCounter == 0 and not self.loopRunning:
            print("[Viewpoint Plugin] Loop all viewpoints...")
            self.__setLoopViewpointLabel("Stop Loop")

            viewpoints = vrCameraService.getAllViewpoints()
            self.loopCounter = len(viewpoints) - 1
            self.loopRunning = True
            threading.Timer(1.0, self.__loopNextViewpoint).start()


    def __loopNextViewpoint(self):
        """
        Loops through all viewpoints once. Stops looping when the button is pressed again
        """

        if self.loopCounter == 0 or not self.loopRunning:
            self.loopRunning = False
            self.loopCounter = 0
            return

        viewpoints = vrCameraService.getAllViewpoints()
        viewpoints[self.loopCounter].activate()
        self.loopCounter = self.loopCounter - 1
        threading.Timer(2.0, self.__loopNextViewpoint).start()


    def __setLoopViewpointLabel(self, labelText):
        """ Change the text of the "loop" button """

        self._loop_viewpoints.setText(labelText)

# Actually start the plugin
if not importError:
    viewpointPlugin = vrViewpointPlugin(VREDPluginWidget)

viewpoint_plugin.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>vrViewpointPlugin</class>
 <widget class="QWidget" name="vrViewpointPluginGUI">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>300</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Viewpoint Plugin</string>
  </property>

  <layout class="QVBoxLayout" name="verticalLayout">
   <item>
    <widget class="QPushButton" name="_render_all">
     <property name="text">
      <string>Render All Viewpoints</string>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QPushButton" name="_random_viewpoint">
    <property name="text">
      <string>Render All Viewpoints</string>
     </property>
    </widget>
   </item>
   <item>
    <widget class="QPushButton" name="_loop_viewpoints">
     <property name="text">
      <string>Loop Viewpoints</string>
     </property>
    </widget>
   </item>
  </layout>

 </widget>
 <resources/>
 <connections/>
</ui>