教程 4:如何编写脚本插件

了解脚本插件以及如何编写自己的脚本插件。

下载示例脚本

跳转到示例脚本

下载教程 PDF

视频字幕:大家好,欢迎学习我们的系列教程“使用 Python 为 VRED Pro 编写脚本”。我叫 Christopher,今天我将介绍脚本插件以及如何编写自己的脚本插件。

我们在上一个教程中已经了解到,VRED 提供了许多不同的界面,您可以在这些界面中添加自己的 Python 脚本以自定义场景。我们可以通过几个简单的脚本实现可在生产期间为我们提供帮助的用户交互和工具。如果想要提供可供所有同事使用或交付给客户的专用工具,应考虑以脚本插件方式实现工具。

脚本插件是一种特殊的 Python 脚本。它们独立于 VRED 场景,并且必须安装在特殊脚本插件目录中才能使用。可以通过在 VRED 的菜单栏中打开“脚本”条目(此处列出每个已安装的脚本插件)访问脚本插件。可以通过单击相应条目启用和禁用每个插件。脚本插件通常会提供集成到 VRED 中的用户界面,此用户界面就像是 VRED 本身的一部分。这是因为 PySide 控件默认使用 VRED 窗口样式。在本教程中,我们将一起实现一个脚本插件,同时了解如何构建用户界面和实现新工具。

在本视频中,我想实现一个帮助我处理视点的简单插件。首先,我想实现一个工具来帮助我将所有视点渲染到一个可以指定的目录中。其次,我想随机选择一个视点,然后将摄影机设置为朝向它。

为了实现这些功能,首先必须创建插件类。对于您将开发的每个插件,此类设置基本上是相同的。首先在我的“用户文档\Autodesk\VRED13.3\ScriptPlugins”下创建一个新目录。此目录名为“ViewpointPlugin”,它将存放我们创建的任何插件文件。如果您的计算机上不存在此目录,可以直接创建它,VRED 将尝试从此处读取脚本插件。

对于以下示例,我使用安装了 Python 扩展的 Visual Studio Code。我可以在新创建的目录中打开 Visual Studio Code 并创建两个文件:viewpoint_plugin.pyviewpoint_plugin.ui。第一个文件将包含 Python 代码,第二个文件将包含我们的用户界面布局。在脚本顶部,我们从 PySide 名称空间导入所需的模块,即 QtCore、QtGui 和 QtWidgets。此外,还导入可帮助我们生成插件控件的 uiTools。

接下来,我们可以使用 uiTools 加载表格和控件库。此处,我们引用了刚刚创建的 ui 文件。然后,我们可以添加从刚刚生成的表格和库继承的实际插件类。init 构造函数的开头始终相同,您只需为您编写的每个插件复制它即可。

在类下面,我们可以将 VREDPluginWidget 作为构造函数的参数来实例化插件。如果这个进度有点太快以致您并未了解其中每个细节,不必担心。有许多始终相同的特殊插件代码。当您想要开发自己的插件时,可以从 VRED 的插件示例复制此插件结构。当然,您可以随时回放本教程或点击“暂停”按钮。

现在,我们完成了 Python 方面的工作,但还需要定义用户界面。这是在 viewpoint_plugin.ui 文件中完成的。布局文件使用 xml 表示法定义用户界面。在本教程中,我将只添加一些按钮,因此没有任何太复杂的操作。此处的重点在于我添加两个按钮并为它们命名。以后将在 Python 脚本中使用此名称来引用这些按钮。建议您了解“Qt Designer”,它提供了用于创建和导出此类 Qt 布局文件的编辑器。对于本教程,可以手动执行此操作,但对于较大的项目,手动操作非常繁琐。

保存文件后,我们可以切换回 VRED 并重新加载脚本插件。我们现在可以看到“脚本”菜单中有另一个条目,单击此条目时,会看到新的插件界面。我们已经有一个可以打开和关闭的 UI,但目前仍缺少功能。

下面实现用于渲染所有视点的第一个工具。我们定义一个名为 renderViewpoints 的新函数。首先,我们可以使用文件对话框模块要求用户提供目录。如果用户选择目录,没有问题。但是,如果他们取消该操作,我们必须捕捉到此情况,并从函数返回,且不执行任何操作。下一步,我们将遍历所有可用视点列表,并逐个激活每个视点。之后,将生成文件名并将视点渲染到文件。我们可以根据用户选择的目录和视点名称生成文件路径,以向 VRED 告知存储渲染的位置。当渲染完成后,还可以显示消息对话框并询问用户是否要打开保存文件的目录。这可以通过 QMessageBox 来实现,它可以为您返回用户选择。它具有“确定”和“取消”按钮,当用户单击“确定”时,我们使用默认的 Python 方法打开目录。

如果现在测试它,实际上什么都不会发生,因为我们忘记了脚本中至关重要的一个部分。我们仍需将渲染函数连接到按钮。这是通过连接到按钮的 clicked 信号完成的,此按钮通过使用在 UI 布局中为其提供的名称引用。现在,我们可以继续后续操作。我们可以通过重新加载脚本插件并启动新插件来测试所做的实现。正如预期的那样,所有视点均会渲染,并且在渲染完成后,还会询问是否要打开渲染目录。

接下来实现我们的第二个函数。这次,我们要向按钮添加图标,而不是文本。因此,我们从 UI 布局文件中的第二个按钮删除文本节点,然后返回 Python 文件。我们可以通过使用 setIcon 函数直接在按钮上设置图标来添加图标。此函数需要 QIcon 参数作为输入,我们要为此提供图标图像。我们的图标只是一个存储在插件目录中的 PNG 图像。

我们还必须设置图标的大小,使其不太大,并且适合我们的用户界面。当然,我们还必须将此按钮连接到某个函数。这次我们提前完成,并调用函数 selectRandomViewpoint。我们将此函数添加到插件类,然后开始为其编写实现。此工具要短得多,因为我们只需选择一个随机视点并将其设置为活动视点。重新加载插件时,我们看到文本按钮已更改为图像按钮,并且单击该按钮时,将会选择一个随机视点。很好!

从现在开始,您可以自由地实现您可以想到的任何功能。您可以设计自己的工具并为其设置独特的外观。您甚至可以实现使用屏幕板并以 HTML 用户界面作为输入的脚本插件。需要注意的是,VRED 将每个 Python 脚本视为单个脚本插件。这样就很难将插件拆分为多个文件。可以通过将使用的其他 Python 模块移动到内部 VRED Python 库目录来解决此问题。可以从 VRED 中的任何脚本加载此目录中的每个 Python 模块。

另一个注意事项:您可能已注意到,我们必须在脚本中显式导入一些 VRED 模块。对于 API v1 中的所有模块,需要执行此操作。对于 API v2 中的模块,VRED 本身会自动添加它们。

完成本教程后,您现在应该能够使用脚本插件实现自己的工具。如果您刚开始入门,建议您以 VRED 插件示例作为起点,并使用您自己的实现充实它。然后,您可以使用 Qt Designer 工具开始创建自己的用户界面。

今天就到这里!感谢您观看本视频,下次见!


Python 代码示例

下面是教程 4:如何编写脚本插件视频随附的 Python 脚本示例。

提示:

要下载这些压缩文件,请单击此处

简单插件

下面是 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()

视点插件

有三个示例文件:

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>