在场景中执行修改并从 Maya 中的 QThread 更新自定义窗口

Posted

技术标签:

【中文标题】在场景中执行修改并从 Maya 中的 QThread 更新自定义窗口【英文标题】:Perform modifications in the scene and update custom window from a QThread in Maya 【发布时间】:2020-07-24 16:14:51 【问题描述】:

上下文

我正在创建一个在 Maya 中运行的 PySide2 工具。该工具正在执行许多长任务,一些修改场景(清理任务),一些创建文件(导出任务)。

因为这是一项漫长的任务,我想在它运行时显示反馈(进度条)。

问题

很遗憾,到目前为止,整个 UI 在执行期间似乎没有更新。 另外,由于我在实际代码中有奇怪的行为(Maya 永远冻结),我猜这不是线程的安全使用。

示例代码

这是一段简化的代码,显示了我到目前为止的位置。这是使用 QThread 的正确方法吗?我来自 CG 艺术家背景,而不是专业程序员,所以我可能误用或误解了我正在尝试使用的概念(线程、PySide ......)

import time

from PySide2.QtGui import *
from PySide2.QtCore import *
from PySide2.QtWidgets import *

import maya.cmds as cmds


class Application(object):
    def __init__(self):
        self.view = View(self)

    def do_something(self, callback):
        start = int(cmds.playbackOptions(q=True, min=True))
        end = int(cmds.playbackOptions(q=True, max=True))

        # First operation
        for frame in xrange(start, end + 1):
            cmds.currentTime(frame, edit=True)
            # Export ...
        callback(33)
        time.sleep(1)

        # Second operation
        for frame in xrange(start, end + 1):
            cmds.currentTime(frame, edit=True)
            # Export ...
        callback(66)
        time.sleep(1)

        # Third operation
        for frame in xrange(start, end + 1):
            cmds.currentTime(frame, edit=True)
            # Export ...
        callback(100)
        time.sleep(1)


class View(QWidget):
    def __init__(self, controller):
        super(View, self).__init__()
        self.controller = controller
        self.thread = None
        
        self.setLayout(QVBoxLayout())
        
        self.progress = QLabel()
        self.layout().addWidget(self.progress)

        self.button = QPushButton('Do something')
        self.layout().addWidget(self.button)
        
        self.button.clicked.connect(self.do_something)
        
        self.show()
        
    def do_something(self):
        self.thread = DoSomethingThread(self.controller)
        self.thread.updated.connect(lambda progress: self.progress.setText(str(progress) + '%'))
        self.thread.run()
    
    
class DoSomethingThread(QThread):
    completed = Signal()
    updated = Signal(int)

    def __init__(self, controller, parent=None):
        super(DoSomethingThread, self).__init__(parent)
        self.controller = controller

    def run(self):
        self.controller.do_something(self.update_progress)
        self.completed.emit()
        
    def update_progress(self, progress):
        self.updated.emit(int(progress))
        
app = Application()

【问题讨论】:

Maya 根本不喜欢从另一个线程修改场景数据。如果您只进行计算然后在主线程中使用结果,它就可以正常工作,但除非文档中另有说明,否则大多数 maya api 函数都不是线程安全的。所以我担心最好的办法是批量完成这些任务。保存场景,开始批处理作业并执行您喜欢的任何修改/导出选项。您可以与此批处理作业进行通信并在当前 Maya 会话中显示进度。 我明白了,非常感谢。那么在我的情况下,线程更像是更新我的pyside UI的解决方法,所以在这种情况下,是否有另一种方法可以在执行长时间操作时更新qt ui?因为这实际上是主要目标。 【参考方案1】:

在 Maya Python 中很难正确使用线程(您可以从列出的问题数量中看到这一点here)

通常有两条硬性规则需要遵守:

    所有涉及 Maya 场景的工作(例如选择或移动对象)都必须在主线程中进行 所有涉及 Maya GUI 的工作也必须在主线程中进行。

这里的“主线程”是您从侦听器运行脚本时获得的线程,而不是您为自己创建的线程

这显然使很多事情变得难以做到。通常,解决方案将涉及在主线程上运行的控制操作,而其他不涉及 Maya GUI 或场景对象的工作正在其他地方进行。线程安全容器(如 python Queue 可用于将已完成的工作从工作线程移到主线程可以安全到达的地方,或者您可以使用 QT 信号安全地触发主线程中的工作线程....如果您的编程生涯不远,所有这些都有些棘手。

好消息是 - 如果您想在 Maya 中完成的所有工作都在场景中,那么您不会因为没有线程而损失太多。除非这项工作基本上是非 Maya 工作——比如使用 HTTP 请求抓取 Web 数据,或将非 Maya 文件写入磁盘,或其他不处理 Maya 特定数据的工作——否则添加线程将不会不会为您带来任何额外的性能。看起来您的示例正在推进时间线、工作,然后尝试更新 PySide GUI。为此,您根本不需要线程(您也不需要单独的 QApplication——Maya 已经是 QApplication)

这是一个非常愚蠢的例子。

from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
import maya.cmds as cmds

class DumbWindow(QWidget):

    def __init__(self):
        super(DumbWindow, self).__init__()
        
        #get the maya app
        maya_app = QCoreApplication.instance()
        
        # find the main window for a parent
        for widget in maya_app.topLevelWidgets():
            if 'TmainWindow' in widget.metaObject().className():
                self.setParent(widget)
                break
                
        self.setWindowTitle("Hello World")
        self.setWindowFlags(Qt.Window)
        
        self.layout = QVBoxLayout()
        self.setLayout(self.layout)
        
        start_button = QPushButton('Start', self)
        stop_button = QPushButton('Stop', self)
        self.layout.addWidget(start_button)
        self.layout.addWidget(stop_button)
        
        self.should_cancel = False
        self.operation = None
        self.job = None

        # hook up the buttons
        start_button.clicked.connect(self.start)
        stop_button.clicked.connect(self.stop)


    def start(self):
        '''kicks off the work in 'this_is_the_work''' 
        self.operation = self.this_is_the_work()
        self.should_cancel = False
        self.job = cmds.scriptJob(ie=self.this_makes_it_tick)
            
        
    def stop(self):
        ''' cancel before the next step'''
        self.should_cancel = True

    def this_is_the_work(self):
        print "--- started ---"        
        for frame in range(100):
            cmds.currentTime(frame, edit=True)
            yield "advanced", frame
        
        print "--- DONE ----"

    def bail(self):
        self.operation = None
        def kill_my_job():
            cmds.scriptJob(k=self.job)
            print "job killed"
        
        cmds.scriptJob(ie = kill_my_job, runOnce=True)

    def this_makes_it_tick(self):
        '''
        this is called whenever Maya is idle and thie
        '''

        # not started yet
        if not self.operation:
            return

        # user asked to cancel
        if self.should_cancel:
            print "cancelling"
            self.bail()
            return            

        try:
            # do one step.  Here's where you can update the 
            # gui if you need to 
            result =   next(self.operation)
            print result
            # example GUI update
            self.setWindowTitle("frame %i" % result[-1])
        except StopIteration:
            # no more stpes, we're done
            print "completed"
            self.bail()
        except Exception as e:
            print "oops", e
            self.bail()
 
         

test = DumbWindow()
test.show()

点击start 会创建一个maya scriptJob,它将尝试运行名为this_is_the_work() 的函数中的任何操作。它将运行到下一个yield 语句,然后检查以确保用户没有要求取消作业。在 yield 之间 Maya 会很忙(就像您在侦听器中输入一些行一样),但如果您在 yield 出现时与 Maya 交互,脚本将改为等待您。这允许在没有单独线程的情况下进行安全的用户交互,当然它也没有完全独立的线程那么流畅。

您会注意到这会在 bail() 方法中启动第二个 scriptJob - 这是因为 scriptJob 无法自行终止,因此我们创建另一个将在下一个空闲事件期间运行并终止我们的不想。

这个技巧基本上是 Maya 的大多数基于 MEL 的 UI 的底层工作方式 - 如果您在侦听器中运行 cmds.scriptJob(lj=True),您通常会看到许多表示 UI 元素的 scriptJobs 来跟踪事物。

【讨论】:

非常感谢,这正是我想要的。即使这仍然是一种解决方法,这也比我复杂的解决方案要优雅得多。

以上是关于在场景中执行修改并从 Maya 中的 QThread 更新自定义窗口的主要内容,如果未能解决你的问题,请参考以下文章

vue 结合h5+调用手机API该怎么用

如何在不启动 Maya 的情况下执行 Maya 脚本?

如何用maya 渲染论文彩图 (occulusion效果)

如何用maya 渲染论文彩图 (occulusion效果)

从并行线程在主 Maya 线程上执行代码

从 Maya 发送并执行 maxscript 或 python 到 3ds Max