5
0

pybind11を使ってPythonからHDKにアクセスする

Last updated at Posted at 2023-12-11

HoudiniのHDAやツール開発においてPythonを使わざるを得ない場合があるかと思います。
その際、HOMの速度だと問題になる場合もあるのではないでしょうか。
inlinecppと呼ばれるPythonコード内でC++コードを扱うモジュールがありますが環境を構築しなければ使えません。
ジオメトリであればカスタムSOPノードのverbを用いる方法もありますがその他の処理には用いることができせん。
そのような場合に使えそうな方法を見つけましたのでご紹介したいと思います。

ビルド準備

HDKの開発環境の構築等はすでに完了しているものとします。
自分の環境は以下になっています。

Windows 10
Visual Studio Community 2022
Houdini 20 (※HDKのビルドにライセンスは関係ないはずです)

説明が前後しますがのちに説明するpybind11をHDKのプロジェクトに追加するためサンプルSOP_StarCMakeLists.txtを流用し以下のように変更しています。

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_namepybind_include_directorypybind_library_pathをご自身の環境に合わせて変更してお使いください。
変更したCMakeLists.txt$HFS/bin/hcmd.exeを開きcmakeコマンドでビルドしプロジェクトファイル群を生成します。
その後は通常のVSでの開発と同様になるかと思います。

pybind11

HDKをPythonから扱えるようにするにはまずC++の機能をPythonから使えるようにしなければいけません。
これを解決するためにpybind11を用います。
pybind11とはc++の機能をPythonから扱うことができるC++のライブラリになります。
同様のことができる他のライブラリも存在しますがそれらと比較してヘッダーオンリーでシンプルに記述できる点で優れています。
こちらも調べればすぐ情報が出てきますしサンプルを見れば使い方もすぐわかると思うので詳しい説明は省きます。
とりあえず以下のコードをpybind_testモジュールとしてビルドしテストします。

pybind_test.cpp
#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でパスを通してインポートしてみます。

pybind_test.png

正しくC++で書いたコードが動作していることが確認できました。
インポートしたモジュールですが現状のPythonでは安全にアンロードする方法がないらしいのでビルドしなおすためには毎回Houdiniのセッションを落とす必要があります。

houモジュールについて

HoudiniでPythonを扱う際はほぼhouモジュールを使うと思います。
このhouモジュールの実態はC++のHOMをSWIGでラップし生成した_houモジュールをPythonでラップしたものです。
実際に以下のようにhouで何かしらのオブジェクトを作りそのthisアトリビュートを確認するとSWIGのオブジェクトが確認できるかと思います。
hou_swig.png
なぜthisアトリビュートに格納されているのかというのはこちらの方1が詳しく説明されておりました。
このSwig Objectは以下のような構造体2として定義されています。

SwigPyObject
struct SwigPyObject
{
    PyObject_HEAD
    void* ptr; // これがラップ対象のクラスへのポインタ
    void* ty;
    int own;
    PyObject* next;
    PyObject* dict;
};

このptrメンバがラップ対象のクラスインスタンスへのポインタになっており、houではHOM_*クラスへのポインタです。
このポインタをhou側と対応するHOMのクラスのポインタにキャストすることでC++側でhouの機能にアクセスすることができます。

hou_to_HOM.cpp
#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);
}

hou_to_HOM.png

HOMからHDKのポインタを取り出す

ここから本題になります。
冒頭でinlinecppと呼ばれるモジュールがあるとお話ししました。
その一機能としてhou.Geometryなどを引数として渡すと対応したHDKのクラスに変換してくれるというものがあります。
このようなことができるということはHOM側のクラスで対応するHDK側のクラスへのポインタを持っているはずです。
そこでinlinecppで変換に対応しているクラスの親クラスであるHOM_OpNodeを確認すると_asVoidPointerというメソッドが存在すると思います。

HOM/HOM_OpNode.h
...
class HOM_API HOM_OpNode : public HOM_Node
{
public:
...
    virtual void *_asVoidPointer() = 0;
...
}
...

これがHDKのクラスへのvoidポインタを戻り値に持つメソッドになります。

HOM_to_HDK.cpp
#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_to_HDK.png

HOM側とHDK側のクラスの対応はこちら3を参考にできるかと思います。

実例

実例としてHoudini 20から追加されたChannel primを指定したノードのチャンネルから作成する関数を作成してみました。

chprim_lib.cpp
#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番目の入力につないでいます。

network.png

Python SOP
import chprim_lib

node = hou.pwd()
geo = node.geometry()

xform_node = node.input(1);

chprim_lib.chprimFromChannel(xform_node, 'tx', geo)
Point Wrangle
float tx = chprim_eval(1, 0, @Time);

@P.x += tx;

以下がビューポートのキャプチャgifです。
アニメーションを付けたチャンネルが正しくChannel Primにコピーされているのが確認できるかと思います。

screencapture.gif

注意点

この方法は渡されるポインタの型をC++側で判定することができないためツール等に組み込む際にPython側でチェックする等の仕組みを実装する必要があります。
また*_Nodeのポインタはその親クラスのOP_Nodeのポインタへのキャストはできませんでした。

  1. SWIGオブジェクトから生のポインタを取り出すには

  2. SWIG and Python

  3. Extending the hou Module Using C++

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0