HoudiniでBlenderのblend形式のファイルを読み込む(?)HDAについて

Houdini Advent Calendar 2022 23日目の記事です。

Blenderのblend形式のファイルを読み込む(ように見える)SOPのHDAを作りました。今回はこのHDAについて掻い摘んで解説してみようと思います。

作ったもの

File SOPとCharacter FBX Import SOPと対応する、Blend File SOPとCharacter Blend Import SOPを作成しました。

Blend File SOP

File SOPをラップしたHDAです。

Parameterに読み込みたいObjectが含まれたblendファイルのパスとObject名を指定します。 File SOPのようにReload Geometryボタンを押すことでBlenderのObjectがHoudiniのGeometryとして出力されます。 Geometryを読み込む処理がそこそこ時間がかかるので、hipファイルの読み込み時に毎回cookが走らないようStashしています。

Character Blend Import SOP

Character FBX Import SOPをラップしたHDAです。BlenderのArmatureを読み込む用途を想定しています 基本的な仕組みはBlend File SOPと同様ですが、出力はKineFXのCharacterとして扱えるよう、Skin MeshとRest Skelton、Pose Skeltonの3つを用意しています。そのため、Blend File SOPのようにGeometryをStashする部分では、一度Character Pack SOPでこれらの3つのGeometryを一つのGeometryにまとめてからStashしています。また、Shapekeyを保持したままModifierを適応できる機能がついています。

できるようになったこと

Blenderで編集を行って保存した後、ボタンひとつでHoudini上でblendファイルからモデルデータを読み込めるようになりました(キャラモデルだと数十秒ほど処理の時間がかかりますが)。Blender上でFBXの書き出し時に行わなければいけない設定を全てHoudini側に持っていけたので書き出しの手間がだいぶ減りました。

また、通常Blender上でのArmatureを使用したキャラクターモデリングでは、書き出し前に手動でModifierの適応やObjectのMerge等の破壊的な作業を行う必要があります。今回このフローが大きく改善されたのが嬉しい点でした。このHDAの特に強力な機能が、Shapekeyを保持したままModifierを適応できる点で、後工程のHoudini側で行うObjectの合成等の処理と組み合わせることで、今までBlender上で行っていた書き出しに伴う破壊的な作業をHoudini側で非破壊的に行えるようになりました。もっと早く作ればよかった。

読み込みの流れ

タイトルの疑問符部分の種明かしです。HoudiniでOperatorのReload Geometryボタンを押したときの処理ですが、HeadlessのBlenderを立ち上げてblendファイルを読み込ませ、FBXをエクスポート、Houdini側でFile SOP等でFBXを読み込んでいます。blendファイルをHoudiniで直接呼んでいるわけではないです。まあやろうと思えば自前でbgeo形式でBlenderからHoudiniにデータを渡せるような気もします(cf. satoruhiga/blender-houdini-geo-io)。

HeadlessでBlenderを立ち上げる

HoudiniのPython経由でReload Geometryボタンを押した際にBlenderを起動します。 Blenderはそこそこ充実したCLIが用意されているため、Houdini側でHDAのParameterを元に起動用のコマンドを組み立てていい感じにBlenderを動かしています。コマンドライン引数には-bオプションを指定してWindowを生成しないバックグラウンドモードで立ち上げています。また、起動した直後に実行するPythonスクリプトも指定できるので、FBXからの書き出し処理等を行うスクリプトと、この書き出しの挙動を指定するためのHoudiniのParameterの値も追加しています。

cf. Command Line Rendering — Blender Manual

コマンドができあがったらHoudiniのPythonでsubprocessを用いてコマンドライン経由でBlenderを立ち上げます。

Blender上で行う書き出し前の処理

コマンドライン経由で受け取ったパラメータを元に書き出し前の処理を行います。 書き出し前の処理は、書き出し対象になるObjectの選択と、ObjectのModifierの適応になります。

Objectの選択

選択はHoudini側でターゲットになるObjectを名前の文字列で指定すると、Blender側でそのObjectとその子になるObjectが選択されるようになっています。なお、Renderフラグが有効になっていないObjectについては選択しないようにしています。

Modifierの適応

BlenderからFBXとして書き出す前にそれぞれのObjectのModifierを適応します。一応FBXの書き出しオプションで自動でModifierを適応してくれる項目がありますが、これを使用すると適応時にObjectのBlendshapeが消えてしまうため、自前でどうにかしなければなりません。そこで今回はSKkeeperというBlenderのAddonをPython経由で呼び出し、Blendshapeを保持したままModifierを適応する処理を行っています。

github.com

ちなみにこのSKkeeperは後発のAddonということもあり、同じ機能を持ったApply Modifier等のAddonよりも概ね高速に動作するので、普通にBlenderモデリングする場合にもオススメのAddonです。

今回は、選択したObjectについて、UIでModifierを指定しShapekeyを保持したまま適応できるSKkeeperのOperator、sk.apply_mods_choice_skを使用します。

Blenderの任意のOperatorはPython上ではbpy.ops.<bl_idname >()(bl_idnameはOperator定義時のクラス変数bl_idnameを指す cf. Operator(bpy_struct) — Blender Python API)の形式で簡単に実行できます。

ただ今回動かしたいこのOperatorは、対象になるObjectと適応するModifierのListを状態(クラス変数)として持つOperatorであるため、上記の方法では実行できません。そこで今回はOperatorのインスタンスをでっちあげてexecute()メソッドを直接叩きにいくことにしました。 Operator(bpy_struct) — Blender Python API

とりあえず以下のような形でSKkeeperのOperatorを動かしています。対象となるModifierを格納できる適切なフィールドを持ったNamedTupleとしてDummyDefを用意し、Operatorのexecute()メソッドの第一引数にインスタンスとして突っ込んで実行しています。なおobjは対象となるObject、resource_listは適応する複数Modifierを指定します

DummyDef = namedtuple('DummyDef', ['obj', 'resource_list'])
...
SKkeeper.SK_OT_apply_mods_choice_SK.execute(DummyDef(object, resource_list), bpy.context)

ちょっと無理やりですがまあ動いているので良いでしょう。

ちなみにresource_listはCollectionPropertyと呼ばれるTypeで、直接扱いやすい形で読み込めないため一旦sceneに生やした後、context経由で取得しています。Sceneが汚染されますが今回の用途ではblendファイルを保存しないので問題無いです。

bpy.types.Scene.skkeeper_resource_list = CollectionProperty(name='Modifier List', type=SKkeeper.SK_TYPE_Resource)
resource_list = bpy.context.scene.skkeeper_resource_list

現状の課題ですが、「適切な順序」でModifierを適応することは現状できていません。そのためModifierがついているObjectをAttachしたModifierを持つObjectについては正しく処理を行えない場合があります。なお、Blender自体にはObject間のDependency cycleを検出する仕組みが存在するので、Python側から何らかのAPIを叩いて得られたDependencyを元にObjectをTopological Sortしてあげれば良いとは思うのですが、未だにこのあたりを触れるAPIを見つけられていない状況です。ご存知のかたは優しく教えて頂けると嬉しいです。

Blenderからの書き出し処理

bpy.ops.export_scene.fbx()でModifier適応済みの選択したObjectをFBXとして書き出しています。

cf. Export Scene Operators — Blender Python API

オプションが大量にあるので適切に設定を行っています。普段BlenderからFBXを書き出す際に毎回行う、Apply ScalingをFBX Allにする処理やApply Transformの設定もこのオプションで指定しています。

書き出したFBXをFile SOPで読み込む

Python側からOperatorのボタンを押しています。HoudiniのOperator上ではボタンも等しくParameterです。以下のような形でボタンのParameterをpressしてあげています。

node.node('file1').file.parm('reload').pressButton()

Stash SOPに読み込んだGeometryを突っ込む

ボタンを押してStashする使い方が一般的ですが、ボタンを押さずにPython経由で直接Geometryを保存することもできます。 そもそもでこのSOPがどこにGeometryを格納しているかの話になるのですが、実はOperatorのParameterにGeometryを格納しています。

これはGeometry Dataと呼ばれるTypeのParameterで、HoudiniのParameterには、普段よく使用されるスライダーやテキストボックスといった数値や文字列等を扱うParameterと同じように、Geometryを格納する種類のParameterです。

このGeometry Dataには以下の形でgeometryを設定しています。

node.node('stash1').parm('stash').set(file.geometry())

自明な余談ですが、GeometryはParameterに格納することができるので、Stash SOPのような機能をもつOperatorを手軽に自前で作ることもできます。 Geometry Dataの形式のParameterをInterfaceに追加してあげればOperatorがGeometryを持つことができるので、あとは入出力をPythonで記述してあげれば出来上がりです。入力はボタン等何らかの処理でこのParameterにGeometryを指定してあげればOKです。出力はPython SOPあたりでParameterから値を読み取りnode.geometryに代入することで、Operatorの出力としてGeometryが得られます。

まとめ

HoudiniのACでしたがBlenderに関する記述がそこそこ多めの分量になってしまいました。今回のように他のツールと連携する等、ワークフローを改善するために様々なアプローチができる点、また、HDAやHoudini Packageとしてパッケージングすることでその可用性を高めやすいところがHoudiniの良いところだと思います。

それではみなさん良いYak Shavingを。