HoudiniのHDAやツール開発においてPythonを使わざるを得ない場合があるかと思います。
その際、HOMの速度だと問題になる場合もあるのではないでしょうか。
inlinecpp
と呼ばれるPythonコード内でC++コードを扱うモジュールがありますが環境を構築しなければ使えません。
ジオメトリであればカスタムSOPノードのverbを用いる方法もありますがその他の処理には用いることができせん。
そのような場合に使えそうな方法を見つけましたのでご紹介したいと思います。
ビルド準備
HDKの開発環境の構築等はすでに完了しているものとします。
自分の環境は以下になっています。
Windows 10
Visual Studio Community 2022
Houdini 20 (※HDKのビルドにライセンスは関係ないはずです)
説明が前後しますがのちに説明するpybind11をHDKのプロジェクトに追加するためサンプルSOP_StarのCMakeLists.txt
を流用し以下のように変更しています。
cmake_minimum_required( VERSION 3.6 )
project( project_name )
list( APPEND CMAKE_PREFIX_PATH "$ENV{HFS}/toolkit/cmake" )
find_package( Houdini REQUIRED )
set( library_name ${CMAKE_PROJECT_NAME} )
# pybind11のインクルードパスを設定
set( pybind_include_directory "~/site-packages/pybind11/include/" )
# Pythonのライブラリパスを設定
# Windowsでのみ動作
set( pybind_library_path "~/python310.lib" )
add_library( ${library_name} SHARED
${library_name}.cpp
)
target_link_libraries( ${library_name} Houdini ${pybind_library_path} )
target_include_directories( ${library_name} PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
${pybind_include_directory}
)
# 生成されるdllの拡張子をpydに変更する
# Windowsでのみ動作
set_target_properties( ${library_name} PROPERTIES SUFFIX ".pyd" )
set( output_dir_properties
LIBRARY_OUTPUT_DIRECTORY
RUNTIME_OUTPUT_DIRECTORY
ARCHIVE_OUTPUT_DIRECTORY
)
foreach ( output_dir_property ${output_dir_properties} )
set_target_properties( ${library_name}
PROPERTIES ${output_dir_property} "${CMAKE_CURRENT_BINARY_DIR}/Default/Out/" )
set_target_properties( ${library_name}
PROPERTIES ${output_dir_property}_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/Debug/Out/" )
set_target_properties( ${library_name}
PROPERTIES ${output_dir_property}_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/Release/Out/" )
set_target_properties( ${library_name}
PROPERTIES ${output_dir_property}_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/RelWithDebIinfo/Out/" )
endforeach ()
ご利用になられる場合はproject_name
やpybind_include_directory
、pybind_library_path
をご自身の環境に合わせて変更してお使いください。
変更したCMakeLists.txt
を$HFS/bin/hcmd.exe
を開きcmake
コマンドでビルドしプロジェクトファイル群を生成します。
その後は通常のVSでの開発と同様になるかと思います。
pybind11
HDKをPythonから扱えるようにするにはまずC++の機能をPythonから使えるようにしなければいけません。
これを解決するためにpybind11を用います。
pybind11とはc++の機能をPythonから扱うことができるC++のライブラリになります。
同様のことができる他のライブラリも存在しますがそれらと比較してヘッダーオンリーでシンプルに記述できる点で優れています。
こちらも調べればすぐ情報が出てきますしサンプルを見れば使い方もすぐわかると思うので詳しい説明は省きます。
とりあえず以下のコードをpybind_test
モジュールとしてビルドしテストします。
#include <pybind11/pybind11.h> // pybind11のヘッダ
// pybind11ネームスペースをpyに
namespace py = pybind11;
auto hello() -> void
{
py::print("Hello Houdini!");
}
PYBIND11_MODULE(pybind_test, m)
{
m.def("hello", &hello);
}
sys.path.append
でパスを通してインポートしてみます。
正しくC++で書いたコードが動作していることが確認できました。
インポートしたモジュールですが現状のPythonでは安全にアンロードする方法がないらしいのでビルドしなおすためには毎回Houdiniのセッションを落とす必要があります。
houモジュールについて
HoudiniでPythonを扱う際はほぼhou
モジュールを使うと思います。
このhou
モジュールの実態はC++のHOMをSWIGでラップし生成した_hou
モジュールをPythonでラップしたものです。
実際に以下のようにhou
で何かしらのオブジェクトを作りそのthis
アトリビュートを確認するとSWIGのオブジェクトが確認できるかと思います。
なぜthis
アトリビュートに格納されているのかというのはこちらの方1が詳しく説明されておりました。
このSwig Objectは以下のような構造体2として定義されています。
struct SwigPyObject
{
PyObject_HEAD
void* ptr; // これがラップ対象のクラスへのポインタ
void* ty;
int own;
PyObject* next;
PyObject* dict;
};
このptr
メンバがラップ対象のクラスインスタンスへのポインタになっており、hou
ではHOM_*
クラスへのポインタです。
このポインタをhou
側と対応するHOM
のクラスのポインタにキャストすることでC++側でhou
の機能にアクセスすることができます。
#include <HOM/HOM_ObjNode.h> // hou.ObjNodeに対応するHOMクラスのヘッダ
#include <pybind11/pybind11.h> // pybind11のヘッダ
struct SwigPyObject;
// pybind11ネームスペースをpyに
namespace py = pybind11;
auto getHomObj(py::handle hou_objnode/* hou.ObjNodeが渡される */) -> void
{
// 受け取ったPyObjectのthisアトリビュートをSwigPyObjectに変換
auto* hou_objnode_swigobj = reinterpret_cast<SwigPyObject*>(hou_objnode.attr("this").ptr());
// SwigPyObjectのptrメンバをHOMのクラスに変換
auto* hom_objnode = reinterpret_cast<HOM_ObjNode*>(hou_objnode_swigobj->ptr);
// ノードの名前を出力
py::print(hom_objnode->name());
}
PYBIND11_MODULE(py_HDK, m)
{
m.def("getHomObj", &getHomObj);
}
HOMからHDKのポインタを取り出す
ここから本題になります。
冒頭でinlinecpp
と呼ばれるモジュールがあるとお話ししました。
その一機能としてhou.Geometry
などを引数として渡すと対応したHDKのクラスに変換してくれるというものがあります。
このようなことができるということはHOM側のクラスで対応するHDK側のクラスへのポインタを持っているはずです。
そこでinlinecpp
で変換に対応しているクラスの親クラスであるHOM_OpNode
を確認すると_asVoidPointer
というメソッドが存在すると思います。
...
class HOM_API HOM_OpNode : public HOM_Node
{
public:
...
virtual void *_asVoidPointer() = 0;
...
}
...
これがHDKのクラスへのvoidポインタを戻り値に持つメソッドになります。
#include <OBJ/OBJ_Node.h> // HDKのObjノードクラスへのヘッダ
#include <HOM/HOM_ObjNode.h> // hou.ObjNodeに対応するHOMクラスのヘッダ
#include <pybind11/pybind11.h> // pybind11のヘッダ
struct SwigPyObject;
// pybind11ネームスペースをpyに
namespace py = pybind11;
auto getHdkObj(py::handle hou_objnode/* hou.ObjNodeのインスタンスが渡される */) -> void
{
// 受け取ったPyObjectのthisアトリビュートをSwigPyObjectに変換
auto* hou_objnode_swigobj = reinterpret_cast<SwigPyObject*>(hou_objnode.attr("this").ptr());
// SwigPyObjectのptrメンバをHOMのクラスに変換
auto* hom_objnode = reinterpret_cast<HOM_ObjNode*>(hou_objnode_swigobj->ptr);
// HOM_ObjNode._asVoidPointer()はOBJ_Node*へのポインタvoid*を返す
// 得られたvoid*をOBJ_Node*へ変換
auto* objnode = reinterpret_cast<OBJ_Node*>(hom_objnode->_asVoidPointer());
py::print(objnode->getName().c_str());
}
PYBIND11_MODULE(py_HDK, m)
{
m.def("getHdkObj", &getHdkObj);
}
HOM側とHDK側のクラスの対応はこちら3を参考にできるかと思います。
実例
実例としてHoudini 20から追加されたChannel primを指定したノードのチャンネルから作成する関数を作成してみました。
#include <string>
// HDK関連のヘッダ
#include <GA/GA_PrimitiveTypes.h>
#include <CH/CH_Channel.h>
#include <CL/CL_SimpleChannel.h>
#include <GU/GU_Detail.h>
#include <GU/GU_PrimChannel.h>
#include <SOP/SOP_Node.h>
// HOM関連のヘッダ
#include <HOM/HOM_SopNode.h>
#include <HOM/HOM_Geometry.h>
#include <HOM/HOM_GUDetailHandle.h>
// pybind11のヘッダ
#include <pybind11/pybind11.h>
// pybind11ネームスペースをpyに
namespace py = pybind11;
struct SwigPyObject
{
PyObject_HEAD
void* ptr;
void* ty;
int own;
PyObject* next;
PyObject* dict;
};
auto chprimFromChannel(py::handle hou_sopnode/* 検索対象のノード */,
std::string channel_name/* 検索対象のチャンネル名 */,
py::handle hou_geometry/* channelprimを追加するジオメトリ */) -> bool
{
// チャンネルを所持しているノード
auto* hou_sopnode_swigobj = reinterpret_cast<SwigPyObject*>(hou_sopnode.attr("this").ptr());
auto* hom_sopnode = reinterpret_cast<HOM_SopNode*>(hou_sopnode_swigobj->ptr);
auto* sopnode = reinterpret_cast<SOP_Node*>(hom_sopnode->_asVoidPointer());
// 出力先のジオメトリ
auto* hou_geometry_swigobj = reinterpret_cast<SwigPyObject*>(hou_geometry.attr("this").ptr());
auto* hom_geometry = reinterpret_cast<HOM_Geometry*>(hou_geometry_swigobj->ptr);
auto* geometry_detail = reinterpret_cast<GU_Detail*>(hom_geometry->_guDetailHandle()->_asVoidPointer());
// channel_nameにマッチするチャンネルを検索する
auto* channel = sopnode->findChannel(channel_name.c_str());
// チャンネルが存在しなければ抜ける
if (!channel)
{
return false;
}
// Channel Primを作成
auto* chprim = dynamic_cast<GU_PrimChannel*>(geometry_detail->appendPrimitive(GA_PRIMCHANNEL));
// チャンネルをコピー
auto chprim_write_handle = chprim->getChannelWriteHandle();
*chprim_write_handle.get() = channel->asSimpleChannel();
// Python SOPで全てのデータIDが更新されるのでこちらで何かする必要はなし
//geometry_detail->bumpDataIdsForAddOrRemove(false, false, true);
return true;
}
PYBIND11_MODULE(chprim_lib, m)
{
m.def("chprimFromChannel", &chprimFromChannel);
}
テスト用に以下のようなSOPネットワークのシーンを組んでみました。
ピッグヘッドにTransform SOPでX移動のアニメを付けPython SOPの2番目の入力につないでいます。
import chprim_lib
node = hou.pwd()
geo = node.geometry()
xform_node = node.input(1);
chprim_lib.chprimFromChannel(xform_node, 'tx', geo)
float tx = chprim_eval(1, 0, @Time);
@P.x += tx;
以下がビューポートのキャプチャgifです。
アニメーションを付けたチャンネルが正しくChannel Primにコピーされているのが確認できるかと思います。
注意点
この方法は渡されるポインタの型をC++側で判定することができないためツール等に組み込む際にPython側でチェックする等の仕組みを実装する必要があります。
また*_Node
のポインタはその親クラスのOP_Node
のポインタへのキャストはできませんでした。