前言

  好久没有碰 Houdini 了,因为各种原因,原计划 2020 年 10 月份要学起来的 Houdini ,一直耽搁到最近才开始渐渐捡起来。
  虽然一直好想认认真真地把 Houdini 学透,但是却又无时无刻地意识到自己在逃避,硬骨头就是难啃(:з」∠)
  所以之前一直上传 Houdini 的教程来麻痹自己,然而 B 站已经停更了 2 个月了,堆积如山的教程让我无所适从。

  Anyway,最近趁元旦回家,总算肝了一些 Houdini 的教程,然后又遇到我以前也遇到过却不知道怎么解决的问题。
  Houdini 默认打开文件保存文件的窗口用了 Houdini 特殊修改过的 File Dialog
  这样就无法嵌入 listary 插件,用起来让我倍感不爽。 listary使用总结
  几个月前遇到这个问题,我以为 Houdini 也可以像 Maya 一样切换为 OS native 的文件窗口,然而查了一通并不可以。
  于是最近又遇到了这个让我很是头疼的问题,以下是我解决它研究的一些方案

利用 PySide2 框架获取软件所有的组件进行魔改

  那么还有一个方法就是遍历 Houdini 所有的组件,然后将 Menu 的 action 替换为系统内置的窗口。
  以前自己搞 mpdb 的时候在 Maya 里面使用过这个骚操作,利用 PySide2 可以获取 Maya 几乎绝大多数的组件。

打印 Maya 组件树的代码 链接
这个脚本里面我以前写的 traverseChildren 方法可以通过递归打印出 Maya 的组件树,只是代码加了额外不必要的功能,而且当时写的代码比较稚嫩,规范也不太好(:з」∠)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from __future__ import print_function
from maya import OpenMayaUI
from PySide2 import QtWidgets
from shiboken2 import wrapInstance

def print_widget_tree(parent,indent=4,prefix=""):

print(prefix,parent,parent.objectName())

if not hasattr(parent,"children"):
return

prefix = " " * indent + prefix
for child in parent.children():
print_widget_tree(child,indent=indent,prefix=prefix)

window = OpenMayaUI.MQtUtil.mainWindow()
window = wrapInstance(long(window), QtWidgets.QMainWindow)
print_widget_tree(window)

  上面代码经过我精简过,在 Maya2017 下输出的结果如下 数据链接
  从打印出来信息可以看到, Maya 可以获取到几乎所有的组件信息,包括主窗口上面的菜单也是可以获取到的。
  所以我有理由相信,通过 Houdini 的 PySide2 可以同样的方法获取到顶部的菜单信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from __future__ import print_function
import hou
from hutil.Qt import QtWidgets

def print_widget_tree(parent,indent=4,prefix=""):

print(prefix,parent,parent.objectName())

if not hasattr(parent,"children"):
return

prefix = " " * indent + prefix
for child in parent.children():
print_widget_tree(child,indent=indent,prefix=prefix)

window = hou.qt.mainWindow()
print_widget_tree(window)

  Houdini 获取主窗口的文档链接
  于是将上面的代码在 Houdini 下面执行,输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<PySide2.QtWidgets.QWidget object at 0x000000007500E048> RE_Window
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E088>
<PySide2.QtWidgets.QWidget object at 0x000000007500E0C8> RE_GLDrawableWrapper
<PySide2.QtWidgets.QWidget object at 0x000000007500E148> RE_GLDrawable
<PySide2.QtWidgets.QWidget object at 0x000000007500E1C8> RE_Window
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E288>
<PySide2.QtWidgets.QWidget object at 0x000000007500E2C8> RE_GLDrawableWrapper
<PySide2.QtWidgets.QWidget object at 0x000000007500E308> RE_GLDrawable
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E348>
<PySide2.QtWidgets.QWidget object at 0x000000007500E208> RE_Window
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E2C8>
<PySide2.QtWidgets.QWidget object at 0x000000007500E288> RE_GLDrawableWrapper
<PySide2.QtWidgets.QWidget object at 0x000000007500E348> RE_GLDrawable
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E308>
<PySide2.QtWidgets.QWidget object at 0x000000007500E248> RE_Window
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E288>
<PySide2.QtWidgets.QWidget object at 0x000000007500E2C8> RE_GLDrawableWrapper
<PySide2.QtWidgets.QWidget object at 0x000000007500E308> RE_GLDrawable
<PySide2.QtWidgets.QWidget object at 0x000000007500E3C8> RE_Window
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E488>
<PySide2.QtWidgets.QWidget object at 0x000000007500E4C8> RE_GLDrawableWrapper
<PySide2.QtWidgets.QWidget object at 0x000000007500E508> RE_GLDrawable
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E548>
<PySide2.QtWidgets.QWidget object at 0x000000007500E408> RE_Window
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E448>
<PySide2.QtWidgets.QWidget object at 0x000000007500E488> RE_GLDrawableWrapper
<PySide2.QtWidgets.QWidget object at 0x000000007500E548> RE_GLDrawable
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E508>
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E348>
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E188>
<PySide2.QtWidgets.QWidget object at 0x000000007500E108> RE_Window
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E188>
<PySide2.QtWidgets.QWidget object at 0x000000007500E148> RE_GLDrawableWrapper
<PySide2.QtWidgets.QWidget object at 0x000000007500E248> RE_GLDrawable
<PySide2.QtWidgets.QVBoxLayout object at 0x000000007500E1C8>

  结果真的是啪啪打脸……
  看来 Houdini 做得没有 Maya 灵活,只能获取到这些的组件了,这样也比较安全。
  我猜测 Maya 之所以可以获取所有的组件很可能是因为所有的 UI 元素都是用 Mel 语言构建的。
  可以通过修改 Maya 启动的 Mel 脚本来直接修改 UI 。
  因此 Maya 大部分的 UI 都是动态生成的,也就导致几乎所有组件都可以被访问到。
  Houdini 底层的 UI 估计是 C++ 的 Widget 写的, 无法通过 Python Binding 获取。
  以上也只是我的个人推断,毕竟没有深入接触过 Qt C++ 的东西…

修改 Houdini 菜单 xml 配置

  一计不成,再生一计。
  难道 Houdini 的菜单无法扩展吗,按照 DCC 软件的尿性,不至于这么不灵活。
  所以简单搜一下 houdini add custom menu 就可以找到解决方案 文档链接

alt

  通过上面的链接可以知道 Houdini 是通过 xml 来管理菜单的。
  在 Houdini 的安装目录可以找到 houdini/MainMenuCommon.xml 配置文件
  里面有关于菜单配置的所有信息。

alt

  根据上面文档链接最后的部分,可以通过添加 <scriptcode> 标记来执行 Python 代码。

alt

  这样就好办了,我可以通过 QFileDialog 调用本地的文件浏览器,然后再用代码来让 Houdini 打开文件。
  缺点就是这个 xml 配置需要重启 Houdini 才能生效,就无法动态修改菜单了。


  使用 OS Native 的文件预览窗口可以用 Qt 内置的 QFileDialog 来调用。
  然后调用 hou.hipFile 的 API 来读取和保存 文件 文档链接

alt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

<scriptItem id="h.native_open">
<label>Native Open...</label>
<scriptCode><![CDATA[
import hou
from PySide2 import QtWidgets
path, _ = QtWidgets.QFileDialog.getOpenFileName(
None, caption=u"读取 hip 文件", filter="*;;*.hip;;*.hip*;;*.hiplc;;*.hipnc",selectedFilter ="*.hip*"
)
if path:
hou.hipFile.load(path)
]]></scriptCode>
</scriptItem>

<!-- ...省略... -->

<scriptItem id="h.native_save">
<label>Native Save As...</label>
<scriptCode><![CDATA[
import hou
from PySide2 import QtWidgets
path, _ = QtWidgets.QFileDialog.getSaveFileName(
None, caption=u"保存 hip 文件", filter="*;;*.hip;;*.hip*;;*.hiplc;;*.hipnc",selectedFilter ="*.hip*"
)
if path:
hou.hipFile.save(path)
]]></scriptCode>
</scriptItem>

  为了代码运行不出错,需要确保代码前面没有缩进。
  如果想让 xml 看起来更整洁,可以用 exec 运行字符串的方式,字符串的多余缩进可以用 from textwrap import dedent 来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<scriptItem id="h.native_save">
<label>Native Save As...</label>
<scriptCode><![CDATA[from textwrap import dedent;exec(dedent(
ur"""
import hou
from PySide2 import QtWidgets
path, _ = QtWidgets.QFileDialog.getSaveFileName(
None, caption=u"保存 hip 文件", filter="*;;*.hip;;*.hip*;;*.hiplc;;*.hipnc",selectedFilter ="*.hip*"
)
if path:
hou.hipFile.save(path)
"""))
]]></scriptCode>
</scriptItem>

  通过 ; 也可以解决 Python 的换行,只是官方不推荐,因为代码挤在一起会破坏可读性,这里用了一个分号来换行 可以避免 xml 缩进被破坏。
  字符串前加了 ur ,其中 u 代表 unicode 字符串,确保中文显示不会变乱码, r 代表 raw ,确保不会对字符串进行转义,避免代码有 \ 之类的路径导致转义出错,这样直接贴代码也不用担心出问题。
  使用效果如下↓↓↓

alt


  通过这个方法也可以嵌入 Python 代码实现一些特殊效果,比如官方的菜单里面无法直接打开一个悬浮的渲染窗口,需要先打开一个渲染窗口再切换。
  可以加一个菜单项让这个动作一步到位。 文档链接
  渲染窗口的类型是 hou.paneTabType.IPRViewer

1
2
3
4
5
6
7
8
9
<scriptItem id="h.floatIPRViewer">
<label>Open Render Panel</label>
<scriptCode><![CDATA[from textwrap import dedent;exec(dedent(
ur"""
import hou
hou.ui.curDesktop().createFloatingPaneTab(hou.paneTabType.IPRViewer)
"""))
]]></scriptCode>
</scriptItem>

alt

总结

  以上就是 Houdini 自定义菜单修改。
  最近看 Houdini 教程萌生了一个想法,想要去做一个 HoudiniWiki 的网站将教程的思路和节点全部集中式管理成文档。
  看教程的时候就一直惦记着要做这个网站将知识归类,所以没法子,教程又给搁置了。

2021-2-6 更新

  很久以前刚开始学习 Houdini 的时候有配置过一款插件,可以将 Vex 代码自动同步到 VScode 进行编写。

http://cgtoolbox.com/houdini-expression-editor/
https://github.com/cgtoolbox/HoudiniExprEditor

  最近我重新配置了这个工具,发现菜单扩展可以通过我的文档 C:\Users\%USERNAME%\Documents\houdini##.#\ 里面进行添加。
  如此就不用改动官方默认的文件了。

  这么写的话需要注意 parentinsertAfter 标签描述来定义添加的选项的位置。