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

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: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`モジュールとしてビルドしテストします。

```cpp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/6757132e-cb60-893e-6d24-e7148cfeb333.png)

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

# houモジュールについて

HoudiniでPythonを扱う際はほぼ`hou`モジュールを使うと思います。
この`hou`モジュールの実態はC++のHOMをSWIGでラップし生成した`_hou`モジュールをPythonでラップしたものです。
実際に以下のように`hou`で何かしらのオブジェクトを作りその`this`アトリビュートを確認するとSWIGのオブジェクトが確認できるかと思います。
![hou_swig.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/c0e92148-dd43-25ff-2690-09589c048fc2.png)
なぜ`this`アトリビュートに格納されているのかというのはこちらの方[^1]が詳しく説明されておりました。
このSwig Objectは以下のような構造体[^2]として定義されています。

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

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

```cpp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/2f61ec77-f09f-cd68-0b3d-1e50556fc2a3.png)



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

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

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



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

```cpp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/38c4d45d-aee9-8e99-8b4e-5a939e463577.png)

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

# 実例

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

```cpp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/617ba3ed-d874-ef3a-63ec-8926d7d11d41.png)

```python:Python SOP
import chprim_lib

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

xform_node = node.input(1);

chprim_lib.chprimFromChannel(xform_node, 'tx', geo)
```

```c:Point Wrangle
float tx = chprim_eval(1, 0, @Time);

@P.x += tx;
```

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

![screencapture.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3628763/c1c99307-6c34-2a11-d27a-049160c72e46.gif)

# 注意点

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


[^1]:[SWIGオブジェクトから生のポインタを取り出すには](https://dothiko.hatenablog.com/entry/2017/11/28/233628)
[^2]:[SWIG and Python](https://www.swig.org/Doc2.0/Python.html)
[^3]:[Extending the hou Module Using C++](https://www.sidefx.com/docs/houdini/hom/extendingwithcpp.html)