株式会社デジタル・フロンティア-Digital Frontier

Header

Main

  • TOP
  • DF TALK
  • 【Houdini】のためのスタンドアローンなツールを作ろう。

【Houdini】のためのスタンドアローンなツールを作ろう。

2016/10/31

Tag: ,,,,

初めまして。
いよいよ僕の出番がやってきました。
FX室から移籍したTDのタカノリ[@auratus](岸川貴紀)です。
専ら、FX室付きのFX TDをやってます。

初めての記事に何を書こうかと、FX TDらしく表現に関するテクニカルネタを書くかとか、Compあたりも絡めて書こうかなとか、はたまた、最近やってるコミュニケーションパイプラインに関するシステム開発に関して書くかとか、色々趣向を巡らせてましたが、とりあえずはThe TD的な感じの話でも始めて、それから話を広げて行こうかと。

目次

はじめに

Houdiniというと、大方の皆さんはご存知の通りSideFXが開発しているノードベースというタイプのDCC(Digital Contents Creation)ツールです。

基本、ノードで何でも出来ます。モデリングやレンダリング、コンポジットまでなんでもです。

そして、この「何でも出来る」を実現するためにあるノードの種類は多岐に渡り、細分化すると、数学的な計算まで出来る細かいノードまであります。ですので、本来Houdiniのために何かしら特別なことをせずとも、プログラム言語をしらずとも複雑なモノでも何でも作れる!というのは大きな利点です。

しかしながら、大きなワークフロー、大きなパイプライン、大人数の中で自由を極めたHoudiniを使用する事は、時に大きな混乱とコストを要します。狙わずとも、そうなってしまいます。下手をすれば、MayaやMaxを使うよりも格段コストがかかるワークフローになりかねないです。

そこであらかじめ決められた仕様にそってワークフローのためのツール開発を行っていくのですが、アーティストの作業効率を突き詰めていくと、「そもそもDCCツール自体を起動しない。」という結論にも行き着きます。そっちの方が速く、誰でも出来て、確実な場合が多いのです。

さて、前置きはここまでにして、早速Houdiniのためにスタンドアローンツールを作るにはどうすればいいか。を、作例とともに学んでいきましょう。

 

実行環境

  • Windows 7
  • Houdini 15.0.459
  • Python 2.7.5
  • PySide 1.2.2

Houdini Object Model – Hython を確認する。

まずは、「HoudiniはPythonでコントロール出来る。」という事を確認しましょう。

Houdiniを立ち上げて、メインメニューのWindows > Python Shellを実行するか、パネルの上部にあるパネル追加アイコン「+」から、Python Shellを追加します。

まずはコレを入力してEnterキーを押してみましょう。

hou.node("/obj").createNode("geo").createNode("testgeometry_rubber")

すると、Boxジオメトリが一個生成され、ネットワークパネルを見ても、

hou.「/obj階層(ノード)に」.「geoノードが一個」.「作成したgeoノードの中にtestgeometry_rubberノードが一個」

という感じで作成されているのが分かります。
そして、この「hou」が Houdini Object Model、 通称 Hython(Houdini + Python) のモジュールオブジェクトです。

Houdiniでは、この「hou」モジュールを使って、ノードの作成が出来ます。もちろんパラメーターの変更も出来ます。それはつまり、Houdiniはノードベースで、すべてノードでオペレーションを行いますから…そう、「hou」モジュールでなんでも出来てしまうんですね。

Command Line Toolから、Hythonを実行してみる。

さてさてさて!
それでは、早速、この「hou」を、Houdiniを起動せずに使用してみます。

とりあえず、「テストジオメトリを作成して、保存する。」といったことをやってみます。

Houdiniを起動せずにHythonを使うには、Command Line Toolというものを使います。

色々やり方はあるのですが、とりあえず代表的な使い方は、Houdiniをインストールしているのであれば、

スタートメニューからCommand Line Toolを起動 or Houdiniのインストール先/bin/hcmd.exeを起動

hython_0002

そして、「hython」と入力し、Hythonを起動します。

Hythonを使って入力していきます。

geo = hou.node("/obj").createNode("geo")
hou.node("/obj/geo1/file1").destroy()
geo.createNode("testgeometry_rubbertoy")

# <hou.SopNode of type sphere at /obj/geo1/testgeometry_rubbertoy1>

hou.hipFile.save(file_name="保存先ディレクトリ/test.hip")

すると、指定したディレクトリに「test.hip」といった感じに保存されます。

さて、HoudiniでこのHIPファイルを開いてみましょう。こんな感じになります。
よく見たことのあるモデルが作成されてますね!

hython_0003

GUI上からアクションを起こしてみる。

それでは、いよいよスタンドアローンツールらしく、GUIを使用してHythonにアクセスしていきましょう。
「任意のHIPパスを入力して、『Create Test Geo』ボタンを押せば、テストジオメトリ入りのHIPファイルが作成される。」といったものを作ってみます。

こんな感じ。

hython_0004

まずは、ロードする用の.uiファイルをQtDesignerで作成します。 作成していくディレクトリに、「作業するディレクトリ/ui/gui.ui」 という感じに作成してください。(上記GIF画像の様な感じです。)
もしくは、サンプルファイルからgui.uiファイルをコピペ配置して下さい。

hython_0005

ここでは、QLineEditを「hipPathLineEdit」、QPushButtonを「pushButton」と名前を付けてます。

そして、Pythonファイルを作成します。

ハイライト部は「hou」モジュールを使用したところです。

createTestGeo.py

# -*- coding: utf-8 -*-
import os, sys

from PySide import QtCore, QtGui
from PySide.QtUiTools import QUiLoader

CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))

class GUI(QtGui.QMainWindow):
    """Main Window Class for Create Test Goemetry"""

    _defWidth  = 600
    _defHeight = 200
    _windowName  = "createTestModel"
    _windowTitle = "Create Test Model"

    UI_FILE = CURRENT_DIR + "/ui/gui.ui"

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

        loader = QUiLoader()
        self.UI = loader.load(self.UI_FILE)
        self.setWindowTitle(self._windowTitle)
        self.setObjectName(self._windowName)
        self.setCentralWidget(self.UI)

        self.resize(self._defWidth, self._defHeight)

        self.initUI()


    def initUI(self):
        self.UI.pushButton.setText("Create Test Geo")
        self.setSignals()


    def setSignals(self):
        self.UI.pushButton.clicked.connect(self.createTestHip)


    def createTestHip(self):
        filepath = self.UI.hipPathLineEdit.text().replace(os.path.sep, '/')

        geo = hou.node("/obj").createNode("geo")
        hou.node("/obj/geo1/file1").destroy()
        geo.createNode("testgeometry_rubbertoy")

        try:
            hou.hipFile.save(file_name=filepath)
        except:
            print "HIP File saving is failed. Check the path carefully."
      return


def main(*argv):
    app = QtGui.QApplication.instance()
    if app is None:
        app = QtGui.QApplication(sys.argv)

    app.setStyle('plastique')
    ui = GUI()
    ui.show()

    app.exec_()


if __name__ == '__main__':
    main(sys.argv[1:])

続いて、上記Pythonスクリプトと同じディレクトリに、実行用のコマンドファイルを書いていきます。

HYTHONDIRにHoudiniがインストールされているディレクトリの「bin」ディレクトリを、
SCRIPTDIRにcreateTestGeo.pyファイルが置いてあるディレクトリを記入します。

createTestGeo.bat

@echo off

set HYTHONDIR="Houdiniをインストールしているディレクトリパス\bin"
set SCRIPTDIR="スクリプトを置いているディレクトリパス"

echo.
echo ===============================================================================
echo.
echo                              Create Test HIP
echo.
echo ===============================================================================
echo.

%HYTHONDIR%\hython.exe %SCRIPTDIR%\createTestGeo.py

pause

それでは、実行ファイルを使用して、GUIを起動してみてください。

上記画像の様に、作成したいディレクトリとファイル名(ここでは「test.hip」)を続けて入力して「Create Test Geo」ボタンをクリックすると「test.hip」ファイルが指定のディレクトリに作成されたと思います。

そして、その「test.hip」を開いてみると、先ほどと同じくテストジオメトリが作成されていると思います。

実用的なものを作ってみる。

さて、いくつか試してきたので、そろそろ一個くらいは実用出来るもの欲しいですよね?
では、「インポート系ノードの配色をHoudiniを開かずにやってくれるツール」でも作ってみましょう。

なぜ、そのツールが必要か。
Houdiniで巨大なショットワークを処理したことがある人、そんなHIPファイルを他の人から受け継いだことがある人はご存知かもしれませんが、とにかくカオスです。
サンプルビジュアルを載っけてみると…こんなかんじ。

hython_0010

これは、とあるプロジェクトでの実際のショットワーク用ファイルです。表示しているのは氷山の一角だったりします。試行錯誤でやられてるので、なかなかにカオスですね。
そして、この作成者は自分が分かりやすい様に独自の配色を採用してます。
はい、分かり辛いです。
社内でルールに沿った配色スクリプトなどを配置してないと、こんな感じにカオスになります。

しかしながら、せめて、外部ファイルにアクセスしているノードだけでも分かれば…。なんて思いますよね。

さて、ツールを作って行きましょう。
使いやすいように、ウインドウにHIPファイルをドラッグ&ドロップでパスを入力してくれる機能も追加します。

こんなかんじ。

hython_0012

先ほどの「createTestHip.py」を複製して、「colorNodes.py」にリネームします。

そして、中身を開いて書き換えます。
ハイライト部が追記・編集したところ。

colorNodes.py

# -*- coding: utf-8 -*-
import os, sys

from PySide import QtCore, QtGui
from PySide.QtUiTools import QUiLoader

CURRENT_DIR = os.path.abspath(os.path.dirname(__file__))

class GUI(QtGui.QMainWindow):
    """Main Window Class to change node's color."""

    _defWidth  = 600
    _defHeight = 200
    _windowName  = "colorNodes"
    _windowTitle = "Color Nodes"

    UI_FILE = CURRENT_DIR + "/ui/gui.ui"

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

        loader = QUiLoader()
        self.UI = loader.load(self.UI_FILE)
        self.setWindowTitle(self._windowTitle)
        self.setObjectName(self._windowName)
        self.setCentralWidget(self.UI)

        self.resize(self._defWidth, self._defHeight)

        self.initUI()

        self.setAcceptDrops(True)

    def initUI(self):
        self.UI.pushButton.setText("Change Node's Color")
        self.setSignals()


    def setSignals(self):
        self.UI.pushButton.clicked.connect(self.createColoredHip)


    ## Drop Event
    def dropEvent(self, event):
        files = []
        for url in event.mimeData().urls():
            tgtfile = unicode(url.toLocalFile())
            ext     = tgtfile.lower().split('.')[-1]
            if ext in ['hip', 'hiplc', 'hipnc']:
                files.append(tgtfile)
            else:
                self.UI.hipPathLineEdit.setText("This is not hip file.")

        if files:
            tgtfile = files[0]
            basename = os.path.abspath(tgtfile)
            self.UI.hipPathLineEdit.setText(basename)


    ## Drag Enter Event
    def dragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            event.accept()
        else:
            event.ignore()


    def createColoredHip(self):
        filepath = self.UI.hipPathLineEdit.text().replace(os.path.sep, '/')

        default_color = (0.8, 0.8, 0.8)
        custom_color  = (1, 0.8, 0)

        import_nodes  = [
            "file",
            "filecache",
            "filemerge",
            "alembic",
            "alembicarchive",
        ]

        try:
            # HIPファイルをロード
            hou.hipFile.load(file_name=filepath)
        except:
            print "HIP File loading is failed. Check the hip file & path carefully."
            return

        # HIP内のすべてのノードを取得。
        nodes = hou.pwd().allSubChildren()

        for node in nodes:
            node_type_name = node.type().name().lower()
            if node_type_name in import_nodes:
                # インポート系ノードに色づけ
                node.setColor(hou.Color(custom_color))

            else:
                # インポート系以外のノードを通常の色に色づけ
                node.setColor(hou.Color(default_color))

        # "_colored"を追加リネームして保存。
        newBasename = hou.hipFile.basename().split('.')[0] + "_colored" + ".hip"
        newPath     = os.path.dirname(filepath) + '/' + newBasename
        hou.hipFile.save(file_name=newPath)


def main(*argv):
    app = QtGui.QApplication.instance()
    if app is None:
        app = QtGui.QApplication(sys.argv)

    app.setStyle('plastique')
    ui = GUI()
    ui.show()

    app.exec_()


if __name__ == '__main__':
    main(sys.argv[1:])


そんでもって、Batchファイルも複製・リネームして編集。

colorNodes.bat

@echo off

set HYTHONDIR="Houinidをインストールしているディレクトリパス\bin"
set SCRIPTDIR="スクリプトを置いているディレクトリパス"

echo.
echo ===============================================================================
echo.
echo                                Color Nodes
echo.
echo ===============================================================================
echo.

%HYTHONDIR%\hython.exe %SCRIPTDIR%\colorNodes.py

pause

実行して、HIPファイルをウインドウに放り投げて「Change Node’s Color」ボタンをクリックしてみましょう。
元のHIPファイルの場所に「_colored」がファイル名に追加されて保存されているのが分かります。

開いてみると…、この通り!
スケールアウトしても、どこで外部ファイルのインポートが行われているかが一目で分かる様になりましたね。

hython_0011

Command Line Toolを使わないでhouモジュールを使用する。

Command Line Toolやhython.exeは、最初からHythonとして使用するために必要なモジュールをimportしてきているので、取り扱いがとても楽です。

しかしながら、環境によっては既に用意されたPython Shellを使用するといった場合も考えられます。
例えば、yamlモジュールなんかをデフォルトでパスを通しているなどといった場合や社内ライブラリのパスを通してるといった感じだと、社内でのバージョン管理にはそちらのPython Shellで管理したほうが良いため、それを使用する場合が多いです。

そこで、「hou」モジュールをPythonスクリプト側で読み込む処理を追加する必要が出てきます。

まず、注意点としては、「source houdini_setup」 を設定していない場合、HFS(Houdiniがインストールされている場所)環境変数を登録する必要がある事。また、DLLのロードのため、さらに、PATH環境変数に$HFS\binを追加しておく必要があります。

なので、例えばこんな感じ。

set PYTHONDIR="Python Shellがあるパス"
set HFS=Houdiniをインストールしているパス
set PATH=%HFS%\bin;%PATH%;

set SCRIPTDIR="スクリプトを置いているディレクトリパス"

%PYTHONDIR%\python.exe %SCRIPTDIR%\importHou.py

hou」モジュールのインポート設定に関して、SideFXの公式ドキュメントでは、こんな感じに記されてます
こいつをスクリプトの先頭で読めばいいわけですね。

中間コメントアウトを省いて引用するとこんな感じ。

#!/usr/bin/python
def enableHouModule():
    '''Set up the environment so that "import hou" works.'''
    import sys, os

    if hasattr(sys, "setdlopenflags"):
        old_dlopen_flags = sys.getdlopenflags()
        import DLFCN
        sys.setdlopenflags(old_dlopen_flags | DLFCN.RTLD_GLOBAL)

    try:
        import hou
    except ImportError:
        sys.path.append(os.environ['HFS'] + "/houdini/python%d.%dlibs" % sys.version_info[:2])
        import hou
    finally:
        if hasattr(sys, "setdlopenflags"):
            sys.setdlopenflags(old_dlopen_flags)

enableHouModule()
import hou

ライセンスに関して。

この「hou」モジュールは、Houdini内で出来る事そのままが出来ます。
ということは、そこにはライセンスが発生してくるわけです。

hou」モジュールは、ここにも記載されている通り、

hou」モジュールをPython Shellがimportした時にライセンスを読みに行きます。

Houdiniのライセンスは、アクティブになっているマシン1台につき1ライセンスを使用する仕様ですので、同じマシンでHoudiniを起動しつつスタンドアローンのツールを起動する事は1ライセンスで出来ますが、

Houdiniを立ち上げていなくても、スタンドアローンのツールを立ち上げると1ライセンス使用します。

この点に注意して使用して下さい。

ちなみに、例えば、「GUIは常駐させておきたいので立ち上げておくが、ずっとライセンスを保持しておくとまずい。」といった節約志向がある場合、「機能の実行のたびにライセンスを使用し、その後手放す。」といった感じになるかと思います。
その場合、ライセンスを手放す場合には以下のスクリプトを機能終了後の処理に差し込めば良いです。

hou.releaseLicense()

まとめ

いかがでしたでしょうか。しょっぱなからこのボリュームっていうのも、なんだかな~とか言う感じもありますが。伝えたいことはある程度書いたかなと思ってます(笑)

スタンドアローンツールってなんだか、「これ俺が作ったぞ!改造したぞ!」って開発の達成感があるロマンだとも思っているので、是非色々試して頂いて、様々なツールを作っていってもらえれば、もしくは、その動機の一端にでもなれば幸いです!!

それから、それから、現在、弊社はエフェクトアーティストとFX TDを募集してます!
エフェクト室でエフェクトアーティストとしてエフェクトの絵作りをしたい方、ツールを作ったり、エフェクト表現のテクニカルアプローチをしたりして、エフェクトアーティストをサポートしたい方募集中です!

募集に関して、詳しくはこちらをどうぞ!!

※免責事項※
本記事内で公開している全ての手法・コードの有用性、安全性について、当方は一切の保証を与えるものではありません。
これらのコードを使用したことによって引き起こる直接的、間接的な損害に対し、当方は一切責任を負うものではありません。
自己責任でご使用ください。

コメント

コメントはありません

コメントフォーム

コメントは承認制ですので、即時に反映されません。ご了承ください。

CAPTCHA


 

*