On this page |
概要 ¶
Houdiniにはinlinecpp
というモジュールがあり、これを使うとC++で関数を記述することができ、そしてその関数にはPythonからアクセス可能です。C++ソースコードは、Pythonソースコードの中でインラインとして記述します。このコードは自動的にあなた用のライブラリにコンパイルされます。コンパイルされたライブラリは自動的に読み込まれ、ライブラリが既に存在すれば、そのコードがコンパイルされず、ライブラリが既に読み込まれていれば、そのライブラリを読み込みし直さないといった長所があります。
inlinecpp
モジュールを使えば、Pythonから必要最低限のコードだけでC++ HDK(Houdini Development Kit)に簡単にアクセスすることができます。以下のサンプルでは、PythonからUT_String
のmultiMatch
メソッドをコールできていることがわかります:
>>> import inlinecpp >>> mymodule = inlinecpp.createLibrary( ... name="cpp_string_library", ... includes="#include <UT/UT_String.h>", ... function_sources=[ ... """ ... bool matchesPattern(const char *str, const char *pattern) ... { ... return UT_String(str).multiMatch(pattern); ... } ... """]) ... >>> string = "one" >>> for pattern in "o*", "x*", "^o*": ... print repr(string), "matches", repr(pattern), ":", ... print mymodule.matchesPattern(string, pattern) ... 'one' matches 'o*' : True 'one' matches 'x*' : False 'one' matches '^o*' : False
このモジュールは、houモジュールのPythonオブジェクトとそれに該当するC++ HDKオブジェクト間を変換することも可能で、HDKで利用可能なメソッドにアクセスすることができます。ただし、houのメソッドにはアクセスできません。このサンプルでは、hou.Geometryオブジェクトが自動的にconst GU_Detail *
に変換されています:
>>> geomodule = inlinecpp.createLibrary("cpp_geo_methods", ... includes="#include <GU/GU_Detail.h>", ... function_sources=[""" ... int numPoints(const GU_Detail *gdp) ... { return gdp->getNumPoints(); }"""]) ... >>> geo = hou.node("/obj").createNode("geo").displayNode().geometry() >>> geomodule.numPoints(geo) 80
このモジュールは、C++を使ってhouクラスを拡張することも可能です:
>>> inlinecpp.extendClass(hou.Geometry, "cpp_geo_methods", function_sources=[""" ... int numPoints(const GU_Detail *gdp) ... { return gdp->getNumPoints(); }"""]) ... >>> geo = hou.node("/obj").createNode("geo").displayNode().geometry() >>> geo.numPoints() 80
Tip
houクラスの拡張には注意してください。その拡張が原因でHoudiniで用意されたメソッドとあなたが拡張したメソッドをユーザが区別できなくなります。そのため、一般的な接頭辞をメソッドの名前に付けることで、違いをはっきりとさせることができます。
使用方法 ¶
以下の関数は、inlinecpp
モジュールで利用可能です:
createLibrary(name, includes="", function_sources=(), debug=False, catch_crashes=None, acquire_hom_lock=False, structs=(), include_dirs=(), link_dirs=(), link_libs=())
これは、モジュールのようなオブジェクトを返します。
C++ソースコードからC++関数のライブラリを作成します。ライブラリが既に読み込まれていれば、そのライブラリを返します。まだコンパイルされていなければ、そのソースコードをコンパイルします。
name
共有するオブジェクトファイルの名前を設定するための固有な名前。同じ名前を再利用しないように注意してください。さもないとinlinecppは、同じ名前で他のライブラリを作成するPythonコードに遭遇した時、不必要な再コンパイルによる無駄時間を減らすための処置として、そのライブラリを削除してしまいます。
includes
関数の前に記述する#includeの行を含んだ文字列。この文字列には、あなたの関数からコールできてもPythonからは見えないというヘルパー関数を記述することもできます。
function_sources
文字列のシーケンスであり、シーケンス内の各文字列には、あなたが作成するいくつかの関数の内の1つの関数のソースコードを記述します。その文字列は解読できるように、あなたの関数のシグネーチャから始めなければなりません。
debug
Trueにすると、コードはデバッグ情報付きでコンパイルされます。True且つcatch_crashesの値を指定しなかった場合、HoudiniはC++コード内のクラッシュをPythonのRuntimeError例外に変換しようとします。
catch_crashes
Trueにすると、HoudiniはC++コード内のクラッシュをキャッチして、C++スタックトレースを含めてPythonのRuntimeError例外に変換しようとします。 Houdiniが常にC++コードからクラッシュをリカバリできるという保証はありません。つまり、このパラメータをTrueに設定しても、Houdiniがクラッシュする可能性があるということです。 このパラメータをNone(デフォルト)に設定すれば、デバッグパラメータと同じことになります。
acquire_hom_lock
Trueにすると、コードがHOM_AutoLockを使用し、Pythonコードが別スレッドで実行された時にC++オブジェクトにスレッドセーフでアクセスするように自動的に修正されます。コードがHoudiniの内部状態を修正する場合は、このパラメータをTrueに設定してください。
structs
戻り値として使うことが可能なC 構造の説明のシーケンス。各構造の説明は、1組になっていて、1番目のエレメントが構造の名前で、2番目のエレメントがメンバーの説明のシーケンスです。各メンバーの説明はメンバーの名前とタイプを含む1組の文字列です。
メンバーの説明のシーケンスの代わりに、構造の説明の2番目のエレメントに文字列を設定しても構いません。その場合、タイプはTypedefになります。そのTypedefは値の配列用のタイプ名を作成するのに役に立ちます。
このパラメータの詳細は、以下を参照してください。
include_dirs
includeファイルを検索するディレクトリパスのシーケンス。これらのパスはC++コードをコンパイルする時に、-Iオプションとしてhcustomコマンドに渡されます。
link_dirs
C++コードがリンクする共有または静的ライブラリを検索するディレクトリパスのシーケンス。これらのパスは-Lオプションとしてhcustomコマンドに渡されます。
link_libs
C++コードがリンクする必要のあるライブラリの名前のシーケンス。これらのライブラリの名前は-lオプションとしてhcustomコマンドに渡されます。
extendClass(cls, library_name, includes="", function_sources=(), debug=False, catch_crashes=None, structs=(), include_dirs=(), link_dirs=(), link_libs=())
C++で実装したメソッドを追加することでhouクラスを拡張します。
clsは、拡張したいhouモジュールクラスです。残りの引数は、acquire_hom_lock
が常にTrue
であること以外は、createLibraryと同じです。この関数は自動的にベースとなるC++クラス用に#include
の行を追加することを知っておいてください。
C++関数の最初のパラメータは、houオブジェクトに相当するC++オブジェクトへのポインタでなければなりません。
コンパイラのエラーと警告 ¶
inlinecpp
はhcustom
HDKツールを使ってC++コードのコンパイルとリンクをします。C++コードがコンパイルに失敗すると、inlinecppはhcustom
からのフル出力とコンパイルエラーを含んだinlinecpp.CompilerError
例外を起こします。
inlinecpp
はhcustom
を使用しているので、HDKが利用できる環境をセットアップしなければなりません。Windowsでは、Microsoft Visual Studio C++ (ExpressかProfessional Editionのどちらでも可)をインストールする必要があります。任意ですが、MSVCDir
環境変数を設定すれば、inlinecpp
が使用するコンパイラを指定することができます。この環境変数を設定しなくても、inlinecpp
は自動的に使用するコンパイラを設定してくれます。LinuxとMacでは、コンパイラの環境をセットアップする必要はありません。
コードをコンパイルすれば、createLibrary
が返したオブジェクトの_compiler_output
メソッドをコールすることで、何かコンパイラの警告が出ていないか確認することができます。例:
>>> mymodule = inlinecpp.createLibrary("test", function_sources=[""" ... int doubleIt(int value) ... { ... int x; ... return value * 2; ... } ... """]) ... >>> print mymodule._compiler_output() Making /home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.o and /home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.so from /home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.C /home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.C: In function int doubleIt(int): /home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_10.5.313_PM3a6t3irKe+jdb113yHpw.C:8: warning: unused variable x
既にコードをコンパイルしているためにcreateLibrary
がライブラリをコンパイルする必要がない場合、_compiler_output
をコールするとライブラリが強制的にコンパイルされることに注意してください。
可能なパラメータタイプ ¶
C++関数は、指定されたパラメータタイプのみ使用可能です。有効なパラメータタイプを以下に載せます:
int
Python intに変換可能な何かを渡します
float
Python floatに変換可能な何かを渡します
double
Python floatに変換可能な何かを渡します
const char *
Python strオブジェクトを渡します
bool
Python boolに変換可能な何かを渡します
GU_Detail *
Python SOP内のhou.Geometryを渡します
const GU_Detail *
hou.Geometryオブジェクトを渡します
OP_Node *
hou.OpNodeオブジェクトを渡します
CHOP_Node *
hou.ChopNodeオブジェクトを渡します
COP2_Node *
hou.Cop2Nodeオブジェクトを渡します
DOP_Node *
hou.DopNodeオブジェクトを渡します
LOP_Node *
hou.LopNodeオブジェクトを渡します
OBJ_Node *
hou.ObjNodeオブジェクトを渡します
ROP_Node *
hou.RopNodeオブジェクトを渡します
SHOP_Node *
hou.ShopNodeオブジェクトを渡します
SOP_Node *
hou.SopNodeオブジェクトを渡します
VOP_Node *
hou.VopNodeオブジェクトを渡します
VOPNET_Node *
hou.VopNetNodeオブジェクトを渡します
OP_Operator *
hou.NodeTypeオブジェクトを渡します
OP_OperatorTable *
hou.NodeTypeCategoryオブジェクトを渡します
PRM_Tuple *
hou.ParmTupleオブジェクトを渡します
CL_Track *
hou.Trackオブジェクトを渡します
SIM_Data *
hou.DopDataオブジェクトを渡します
UT_Vector2D *
hou.Vector2オブジェクトを渡します
UT_Vector3D *
hou.Vector3オブジェクトを渡します
UT_Vector4D *
hou.Vector4オブジェクトを渡します
UT_DMatrix3 *
hou.Matrix3オブジェクトを渡します
UT_DMatrix4 *
hou.Matrix4オブジェクトを渡します
UT_BoundingBox *
hou.BoundingBoxオブジェクトを渡します
UT_Color *
hou.Colorオブジェクトを渡します
UT_QuaternionD *
hou.Quaternionオブジェクトを渡します
UT_Ramp *
hou.Rampオブジェクトを渡します
以下に注記すべき重要事項を載せています:
-
関数が
GU_Detail *
を受け取る場合、読み取り専用ではないhou.Geometryオブジェクトに渡さなければなりません。そうでない場合に関数をコールするとinlinecpp
はhou.GeometryPermissionError例外を起こします。言い換えると、ジオメトリを修正可能なPython SOPからコールされる関数に対してだけGU_Detail *
を使用してください。ジオメトリを修正しない関数に対しては、const GU_Detail *
パラメータを使用してください。 -
上記のリスト以外のパラメータタイプを使った場合、inlinecppは渡されたPythonオブジェクトを
void *
に変換します。例えば、int *
のパラメータタイプを使用する場合、整数配列のアドレスを持ったPython integerに渡されます。 -
C++コードから
char *
として文字列を受取、そのデータを変更することで、Python文字列の内容を修正しないでください。Pythonの文字列はimmutable(変更不可)なので、文字列の内容を変更すると、Pythonの内部状態が無効になりHoudiniがクラッシュする危険性があります。
可能な戻り値のタイプ ¶
デフォルトで用意されている戻り値のタイプを以下に載せます:
void
None
に変換されます
int
Python int
に変換されます
float
Python float
に変換されます
double
Python float
に変換されます
const char *
Python str
(解放されないNull終了文字列を返します)に変換されます
char *
Python str
(free()
で解放されるNull終了文字列を返します)に変換されます
bool
True
またはFalse
に変換されます
inlinecpp::String
Python3ではstr
、Python2ではunicode
に変換されます(std::string
でコンストラクト)
inlinecpp::BinaryString
Python3ではbytes
、Python2ではstr
に変換されます(std::string
でコンストラクト)
以下に注記すべき重要事項を載せています:
-
あなたの関数が文字列を返す最善の方法は、inlinecpp::BinaryString (詳細は以下参照)を返すことです。しかし、あなたの関数は、ポインタをNull終了文字配列に返すことで文字列を返すこともできます。あなたの関数の戻り値のタイプは、それをPython文字列に変換した後に配列を解放するかどうか決めます。戻り値のタイプが
const char *
なら、配列は解放されませんが、char *
ならfree()
を使って配列が解放されます。delete []
で文字配列を解放することができず、strdup
の結果を返し、あなたの関数から配列をdelete
します。
独自の戻り値のタイプを簡単に作成することができます。詳細は、以下を参照してください。
文字列を返す ¶
文字列を返す一番簡単な方法は、C++関数でinlinecpp::Stringを返して、std::stringからそのStringをコンストラクトすることです。以下のサンプルでは、ノードの削除スクリプトを含む文字列を返します:
node_utilities = inlinecpp.createLibrary("node_utilities", acquire_hom_lock=True, includes="#include <OP/OP_Node.h>", function_sources=[""" inlinecpp::String deletion_script(OP_Node *node) { return node->getDelScript().toStdString(); } """])
時々、C++コードがNull終了のCスタイル文字列を返す必要があります。このCスタイルの文字列が関数内のローカル変数にあり、関数が戻ると解放される場合、文字列のコピーを返す必要があります。コピーを返す方法が2通りあります。1つ目は単に、Cスタイル文字列からstd::stringをコンストラクトし、inlinecpp::Stringを返すことです。2つ目は、以下に説明しているとおり、inlinecpp::Stringを返し、inlinecpp::as_string関数を使ってそれをコンストラクトすることで、std::stringのコンストラクトを回避します:
node_utilities = inlinecpp.createLibrary("node_utilities", acquire_hom_lock=True, includes="#include <OP/OP_Node.h>", function_sources=[""" inlinecpp::String deletion_script(OP_Node *node) { return inlinecpp::as_string(node->getDelScript().nonNullBuffer()); } """])
C++コードが、関数が戻り、且つそれをコール元に戻さない時に解放しないCスタイルの文字列を返す必要があるなら、単に、const char *
(char *
ではなく)の戻り値タイプを使えばよいです。膨大な文字列を返すと、この手法は、inlinecpp::Stringを返すよりも効率的です。なぜなら、データの不要なコピーを回避できるからです。Pythonが文字列のコピーを作成するので、Cスタイルの文字列の内容は、関数が戻った後も利用可能であることに注目してください。サンプルを以下に載せます:
example_lib = inlinecpp.createLibrary("example_lib", includes="#include <unistd.h>", function_sources=[""" const char *user_name() { return getlogin(); } """])
最後に、C++コードが解放を必要としないCスタイルの文字列を返す必要があれば、char *
(const char *
ではなく)の戻り値タイプを使用してください。inlinecppはdelete []
ではなくデータに対してfree()
をコールし、そのコピーをstd::stringに作成し、配列を削除し、inlinecpp::Stringとしてstd::stringを返します。以下のサンプルでは、解放される文字列を返す方法を説明しています:
example_lib = inlinecpp.createLibrary("example_lib", function_sources=[""" char *simple_string() { return strdup("hello world"); } """])
バイナリデータを返す ¶
C++コードが任意のバイナリデータを返す必要がある場合、Pythonのbytes
オブジェクトに変換されるinlinecpp::BinaryString
を返します。例えば、Null文字を含む文字列を返したい場合は、inlinecpp::BinaryString
オブジェクトのset
メソッドをコールして、const char *
ポインタにバイト単位でデータとサイズを渡すことができます。以下にサンプルを載せます:
example_lib = inlinecpp.createLibrary("example_lib", function_sources=[""" inlinecpp::BinaryString build_binary_data() { char binary_data[] = "embedded\0null\0characters"; inlinecpp::BinaryString result; result.set(binary_data, 24); return result; } """])
inlinecpp::BinaryString
を使ってint、float、doubleの配列のバイナリ表現を返すことも可能です。単に値を適切なタイプのstd::vector
に保存し、以下のサンプルのようにinlinecpp::as_binary_string
をコールしてstd::vector
からinlinecpp::BinaryString
をコンストラクトします。Pythonのarray
モジュールを使えば、簡単に文字列を適切なタイプの配列に戻すことができます(配列の戻り値タイプを使えば、inlinecpp::BinaryString
を使用することなく配列を返すことが可能であることに注目してください)。
import array example_lib = inlinecpp.createLibrary("example_lib", function_sources=[""" inlinecpp::BinaryString build_int_array() { std::vector<int> values(10); for (int i=0; i<10; ++i) values[i] = i; return inlinecpp::as_binary_string(values); } """]) >>> data = example_lib.build_int_array() >>> int_array = array.array("i", data) >>> for value in int_array: ... print value
最後に、動的に割り当てたint、float、doubleの配列を返す必要があるなら、単に、配列の最初のエレメントへポインタとバイト単位の配列のサイズを使ってinlinecpp::BinaryStringのset
メソッドをコールします(配列の戻り値タイプを使えば、手法が単純になることに注目してください)。
example_lib = inlinecpp.createLibrary("example_lib", function_sources=[""" inlinecpp::BinaryString build_int_array() { int *values = new int[10]; for (int i=0; i<10; ++i) values[i] = i; inlinecpp::BinaryString result; result.set((const char *)values, 10 * sizeof(int)); delete [] values; return result; } """])
カスタム戻り値タイプ ¶
上記のデフォルトの戻り値タイプと文字列の戻り値タイプに加えて、うまくPythonにオブジェクトとシーケンスに変換される構造体と配列を返すことが可能です。
そのような戻り値のタイプを使用するには、structs
パラメータをライブラリの関数から返したいC++の構造体の記述を持つcreateLibraryに渡します。このパラメータをシーケンス内の各エレメントが1組の値になっているシーケンスに設定します。1組の1番目のエレメントが構造体の名前で、2番目のエレメントがメンバーのシーケンスです。各メンバーは、メンバー名とタイプを含んだ1組の文字列です。メンバータイプは、Pythonのstructモジュールで使われるフォーマット文字の1つです(例:i
はinteger、d
はdouble、f
はfloat、c
はcharなど)。
以下のサンプルは、double型のx
とy
を持つPosition2D
という名前の構造体を作成し、ノードの位置を返します。
>>> node_utilities = inlinecpp.createLibrary( ... acquire_hom_lock=True, ... name="node_utilities", ... includes="#include <OP/OP_Node.h>", ... structs=[("Position2D", ( ... ("x", "d"), ... ("y", "d"), ... ))], ... function_sources=[""" ... Position2D node_position(OP_Node *node) ... { ... Position2D result; ... result.x = node->getX(); ... result.y = node->getY(); ... return result; ... } ... """]) ... >>> geo_node = hou.node("/obj").createNode("geo") >>> geo_node.setPosition((3.5, 4.5)) >>> print geo_node.position() [3.5, 4.5] >>> position = node_utilities.node_position(geo_node) >>> print position <inlinecpp.Position2D object at 0x7f10e7cf0d90> >>> print position.x 3.5 >>> print position.y 4.5
前のサンプルから、構造体は構造体インスタンスを作成し個々のメンバーに割り当てることでC++で初期化されます。
配列メンバーを持つ構造体を返す ¶
メンバータイプの文字列の頭に*
が付いていれば、それらのエレメントの配列を意味します。例えば、*i
はintegerの配列です。以下のサンプルでは、2つの数値の配列を返す方法を載せています:
example_lib = inlinecpp.createLibrary( "example_lib", structs=[("ArrayPair", ( ("ints", "*i"), ("doubles", "*d"), ))], function_sources=[""" ArrayPair build_array_pair() { std::vector<int> ints; ints.push_back(2); ints.push_back(4); std::vector<double> doubles; doubles.push_back(1.5); doubles.push_back(3.5); ArrayPair result; result.ints.set(ints); result.doubles.set(doubles); return result; } """]) >>> array_pair = example_lib.build_array_pair() >>> print array_pair.ints[0] 2 >>> print array_pair.doubles[1] 3.5 >>> print zip(array_pair.ints, array_pair.doubles) [(2, 1.5), (4, 3.5)]
配列メンバーのsetメソッドをコールする上記のサンプルは、std::vectorに渡します。以下の値をsetメソッドに以下の値を渡すことができます:
-
std::vector
-
ポインタとエレメントの数
-
std::string (配列が文字の配列の場合のみ)
-
std::vector of std::vectors (配列が配列の配列の場合のみ)
-
std::vector<std::string> (文字の配列の配列の場合のみ)
配列を返す ¶
メンバー記述のシーケンスの代わりに、構造体記述の組の2番目のエレメントも文字列です。この場合、inlinecppはtypedefを作成し、そのtypedefは値の配列を返すときに役に立ちます。以下のサンプルでは、グローバルノード選択のノードIDに相当するintegerの配列を返す方法を載せています:
node_utilities = inlinecpp.createLibrary( "node_utilities", acquire_hom_lock=True, includes=""" #include <OP/OP_Director.h> #include <UT/UT_StdUtil.h> """, structs=[("IntArray", "*i")], function_sources=[""" IntArray ids_of_selected_nodes() { std::vector<int> ids; UTarrayToStdVector(OPgetDirector()->getPickedItemIds(), ids); return ids; } """]) def selected_nodes(): return [hou.nodeBySessionId(node_id) for node_id in node_utilities.ids_of_selected_nodes()]
上記のサンプルは、IntArrayオブジェクトを作成せず、setメソッドをコールしてintegerの配列を返しています。代わりに、単純にstd::vectorからIntArrayオブジェクトをコンストラクトすることができます。配列のsetメソッドに渡すことが可能なパラメータもコンストラクターに渡されます(配列でない構造体は、コンストラクタを持たないので、構造体の各エレメントに割り当てなければなりません)。
配列オブジェクトに対してset
メソッドをコールすることもでき、1番目のエレメントへのポインタとエレメントの数を渡します:
example_lib = inlinecpp.createLibrary("example_lib", structs=[("IntArray", "*i")], function_sources=[""" IntArray build_int_array() { int *values = new int[10]; for (int i=0; i<10; ++i) values[i] = i; IntArray result; result.set(values, 10); delete [] values; return result; } """])
以下のサンプルは、文字の配列の配列を返すことで、文字列の配列を返す方法を載せています:
example_lib = inlinecpp.createLibrary("example_lib", structs=[("StringArray", "**c")], function_sources=[""" StringArray build_string_array() { std::vector<std::string> result; result.push_back("one"); result.push_back("two"); return result; } """]) >>> example_lib.build_string_array() ('one', 'two')
構造体と配列のネスト化 ¶
フォーマット文字に加えて、メンバータイプは構造体のシーケンス内の初期のタイプの名前またはそのようなタイプの配列を使用します。この手法で、他の構造体内の構造体または構造体の配列をネストすることができます。
例えば、以下のシーケンスは2つのintegerメンバーx
とy
そしてtolerance
(double)、single_point
(Point)、points
(Pointの配列。C++関数は、自動的にPythonオブジェクトに変換されるData
値を返します)という名前を含むData
構造体を持つPoint
構造体を作成します。
example_lib = inlinecpp.createLibrary( "example_lib", structs=( ("Point", ( ("x", "i"), ("y", "i"), )), ("Data", ( ("tolerance", "d"), ("single_point", "Point"), ("points", "*Point"), )), ), function_sources=[""" Data build_nested_struct() { std::vector<Point> points; for (int i=0; i<5; ++i) { Point point; point.x = i; point.y = i + 1; points.push_back(point); } Data result; result.tolerance = 0.01; result.single_point.x = 4; result.single_point.y = 6; result.points.set(points); return result; } """]) >>> result = example_lib.build_nested_struct() >>> print result.tolerance 0.01 >>> print result.single_point.x 4 >>> print result.points[2].y 3
構造体の配列や構造体の配列の配列であるtypedefを作成することもできます。以下のサンプルでは、構造体の配列の配列を返す方法を載せています。各構造体には、prim_id
とvertex_id
という名前の2つのintegerを含んでいます。このサンプルは、hou.Geometryオブジェクトを受け取る関数を作成し、各ポイントに対して、そのポイントを参照している頂点(プリミティブと頂点のIDとして)を返します。
point_ref_utils = inlinecpp.createLibrary( "point_ref_utils", acquire_hom_lock=True, structs=( ("ReferencingVertex", ( ("prim_id", "i"), ("vertex_id", "i"), )), ("ReferencesToPoint", "*ReferencingVertex"), ("ReferencesToAllPoints", "*ReferencesToPoint"), ), includes=""" #include <GU/GU_Detail.h> #include <GB/GB_PointRef.h> int vertex_index(GB_Vertex &vertex, GB_Primitive &prim) { GEO_Vertex &geo_vertex = dynamic_cast<GEO_Vertex &>(vertex); GEO_Primitive &geo_prim = dynamic_cast<GEO_Primitive &>(prim); int num_vertices = geo_prim.getVertexCount(); for (int i=0; i<num_vertices; ++i) { if (geo_prim.getDetail().getVertexMap() == geo_vertex.getIndexMap() && geo_prim.getVertexOffset(i) == geo_vertex.getMapOffset()) { return i; } } return -1; } """, function_sources=[""" ReferencesToAllPoints vertices_referencing_points(const GU_Detail *gdp) { GB_PointRefArray point_ref_array(gdp, /*group=*/NULL); std::vector<ReferencesToPoint> references_to_points; for (int i=0; i<point_ref_array.entries(); ++i) { std::vector<ReferencingVertex> referencing_vertices; for (const GB_PointRef *ref = point_ref_array(i); ref; ref=ref->next) { ReferencingVertex referencing_vertex; referencing_vertex.prim_id = ref->prim->getNum(); referencing_vertex.vertex_id = vertex_index(*ref->vtx, *ref->prim); referencing_vertices.push_back(referencing_vertex); } references_to_points.push_back(referencing_vertices); } return references_to_points; } """]) def vertices_referencing_points(geo): """ジオメトリ内のポイント毎に1エントリーの値のリストを返します。 そのリスト内の各エントリーは、頂点のリストで、各頂点はそれに相当するポイントを参照します。 """ vertices_for_each_point = [] for references_to_point in point_ref_utils.vertices_referencing_points(geo): vertices = [] for reference_to_point in references_to_point: prim = geo.iterPrims()[reference_to_point.prim_id] vertices.append(prim.vertex(reference_to_point.vertex_id)) vertices_for_each_point.append(vertices) return vertices_for_each_point >>> geo = hou.node("/obj").createNode("geo").createNode("box").geometry() >>> for point, vertices in zip(geo.points(), vertices_referencing_points(geo)): ... print point ... for vertex in vertices: ... print " ", vertex
配列パラメータ ¶
配列を埋める ¶
inlinecppを使用する時、後でPythonで使用するためにC++コードを使って配列の中身を埋めることがよくあります。例えば、Pythonを使ったgenerator COPはC++を使って、すべてのピクセルの内容を計算し、Pythonを使ってノードのパラメータを評価して、ピクセルをCOPに保存します。
cpp_lib = inlinecpp.createLibrary("example", function_sources=[""" void fill_array(float *array, int length) { for (int i=0; i<length; ++i) array[i] = i; } """]) length = 10 # 以下のコードは、Pythonと一緒に既に入っている配列モジュールを使用して、 # 配列を作成してから、それをC++関数に渡す方法を説明しています。 import array a = array.array("f", [0] * length) cpp_lib.fill_array(a.buffer_info()[0], len(a)) print a # 以下のコードは、Pythonのnumpyモジュールを使用して # 上記と同じ事をする方法を説明しています: import numpy a = numpy.zeros(length, dtype=numpy.float32) cpp_lib.fill_array(a.ctypes.data, len(a)) print a # C++コードは配列の内容を初期化する必要がないので、 # 非常に大きな配列を扱う時はnumpy.zeroの代わりにnumpy.emptyを使用した方が速いです: a = numpy.empty(length, dtype=numpy.float32) cpp_lib.fill_array(a.ctypes.data, len(a)) print a
Note
-
numpyモジュールはarrayモジュールよりも機能がたくさんあります。しかしarrayモジュールはすべてのPythonディストリビューションに入っているのに対してnumpyは入っていないのでインストールする必要があります。
-
ほとんどの処理では、numpyを使って配列の内容を修正することができるのでinlinecppをまったく使う必要がありません。
-
numpy.zerosは配列をすべてゼロに初期化し、numpy.emptyは配列を初期化しません。
-
"f"
はarrayモジュールで32ビットのfloatの配列を作成します。同様に、numpy.float32
(または"f4"
)はnumpyモジュールで32ビットのfloatの配列を作成します。"d"
はarrayモジュールで64ビットのdoubleを使用します。numpy.float64
(または"f8"
)はnumpyで同じく64ビットのdoubleを使用します。 -
array.arrayのbuffer_infoメソッドは2つのintegerのタプル(配列のアドレスとバイト単位のサイズ )を返すので、C++関数にraw配列のアドレスであるそのタプルの1番目の値を渡します。同様にnumpy.arrayオブジェクトのctypes.dataアトリビュートには、numpy配列のrawデータへのポインタが入っています。
-
C++ 関数はポインタだけでは配列の長さを判断できないので、パラメータとして長さを渡さなければなりません。
-
大きい配列に対しては、numpyモジュールがarryaモジュールよりも効率的です。その理由は、arrayモジュールはゼロの配列や初期化しない配列を効率的に作成する方法がないからです。代わりに、arrayモジュールはまず最初に
[0] * length
で一時的なリストを記述し、リストの内容で配列を初期化します。Pythonは小さい整数(例えばゼロ)のリストを作成するように最適化されていますが、非常に大きい配列を扱う時はarrayオブジェクトの作成に時間がかかります。
Tip
LinuxとMac OSでは、Houdiniはコンピュータで利用可能なPythonディストリビューションを使おうとします。つまり、numpyをシステムのPythonディストリビューションにインストールするだけで、Houdiniがそのモジュールを拾います。しかし、Windowsでは、HoudiniはHoudiniと一緒にインストールされたPythonディストリビューションを使用します。Houdiniからnumpyを使用するには、それを$HFS内の適切なPythonディレクトリ(例:$HFS/python27)にインストールしなければなりません。
array.arraysとnumpy.arraysの両方がPythonシーケンスのように動作します。つまり、Pythonから配列を繰り返してエレメントを取り出すことができます。必要であれば、配列をバイナリ文字列に変換することもできます。array.arraysを使った場合は、以下のコードを記述します。
a.tostring()
numpy.arraysを使った場合は、以下のコードを記述します。
str(a.data)
これらの文字列は、hou.Cop2Node.setPixelsOfCookingPlaneFromString、hou.Geometry.setPointFloatAttribValuesFromString、hou.Volume.setAllVoxelsFromStringなどのメソッドに渡すことができます。しかし、array.arrayやnumpy.arrayオブジェクトのメソッドに文字列に変換する必要なく直接渡すこともできます。そうする方がメモリの割り当てやデータのコピーが必要ないので効率的です。
バイナリデータから配列を構築する ¶
Houdiniで返されたバイナリ文字列データから配列を簡単に構築することができます。例えば、array.array関数を使う場合は、以下のコードを記述します:
a = array.array("f", geo.pointFloatAttribValuesAsString("P"))
numpy.frombuffer関数の場合は以下の通りです:
a = numpy.frombuffer(geo.pointFloatAttribValuesAsString("P"), dtype=numpy.float32) # aは読み取り専用です。
または
a = numpy.frombuffer(geo.pointFloatAttribValuesAsString("P"), dtype=numpy.float32).copy() # aの内容を修正することができます。
Note
-
arrayモジュールで作成した配列は常に読み書き可能です。しかし、numpyで作成した配列は場合によって読み書き可能な時や読み取り専用の時があります。numpy.zerosやnumpy.emptyで配列を作成する時、または既存の配列に対してcopyメソッドをコールした時は、返される配列は常に書き込み可能です。しかし、frombufferでnumpy配列を作成すると、numpyはデータのコピーを作成せず、配列は読み取り専用になります。
配列の修正 ¶
以下のPython SOPのサンプルでは、Houdiniが返したバイナリデータから配列を作成し、C++で配列の内容を修正し、そのデータをHoudiniに戻す方法を載せています:
import inlinecpp cpp_module = inlinecpp.createLibrary("example", function_sources=[""" void modify_point_positions(float *point_positions, int num_points) { // 各ポイントのYコンポーネントに0.5を加算します。 for (int point_num=0; point_num < num_points; ++point_num) { point_positions[1] += 0.5; point_positions += 3; } } """]) geo = hou.pwd().geometry() num_points = len(geo.iterPoints()) # 以下のコードは、位置を修正するための配列モジュールを使用しています: import array positions = array.array("f", geo.pointFloatAttribValuesAsString("P")) cpp_module.modify_point_positions(positions.buffer_info()[0], num_points) geo.setPointFloatAttribValuesFromString("P", positions) # 以下のコードは、numpyを使用して位置を再度修正しています: import numpy positions = numpy.frombuffer(geo.pointFloatAttribValuesAsString("P"), dtype=numpy.float32).copy() cpp_module.modify_point_positions(positions.ctypes.data, num_points) geo.setPointFloatAttribValuesFromString("P", positions)
構造体の配列 ¶
Numpyの構造体の配列には、その構造体の配列をC++関数に渡す仕組みがあります。以下のサンプルでは、すべてのポイントのP (position)とCd (diffuse color)アトリビュートを2つのnumpy配列に格納し、その配列を1つに結合し、raw配列データをC++関数に渡します。C++関数は、そのデータを、構造体の配列として受取り、その配列の各エレメントにはx,y,z,red,green,blueのfloat値が入っています。
import numpy import inlinecpp geo = hou.pwd().geometry() num_points = len(geo.iterPoints()) positions_array = numpy.frombuffer( geo.pointFloatAttribValuesAsString("P"), dtype="f4,f4,f4") positions_array.dtype.names = ("x", "y", "z") colors_array = numpy.frombuffer( geo.pointFloatAttribValuesAsString("Cd"), dtype="f4,f4,f4") colors_array.dtype.names = ("red", "green", "blue") attribs_array = numpy.empty(num_points, dtype="f4,f4,f4,f4,f4,f4") attribs_array.dtype.names = ("x", "y", "z", "red", "green", "blue") for component in "x", "y", "z": attribs_array[component] = positions_array[component] for component in "red", "green", "blue": attribs_array[component] = colors_array[component] cpp_lib = inlinecpp.createLibrary("example", includes=""" #pragma pack(push, 1) struct AttribValues { float x; float y; float z; float red; float green; float blue; }; #pragma pack(pop) """, function_sources=[""" void process_attribs(AttribValues *attrib_values_array, int length) { for (int i=0; i<length; ++i) { AttribValues &values = attrib_values_array[i]; // アトリビュート値を解析するために、ここで何かを実行します。 cout << values.x << ", " << values.y << ", " << values.z << " " << values.red << ", " << values.green << ", " << values.blue << endl; } } """]) cpp_lib.process_attribs(attribs_array.ctypes.data, len(attribs_array))
Note
-
numpy.frombufferに渡すdtypeパラメータがカンマ区切りの文字列の場合、numpyは構造体の配列を作成します。
"f4"
はnumpy.float32
と同じです。 -
numpy.emptyは正しいサイズと正しいフィールドの配列を割り当てますが、データを初期化しません。
-
フィールド名を配列のインデックスとして使用することで、配列のフィールド毎にビューを作成することができます。これらのビューを使うことで、フィールドのすべての値を含む配列から目的の配列のフィールドに割り当てることができます。
-
不明なパラメータタイプを持つinlinecpp C++関数を作成すると、 そのタイプはポインタとして扱われます。つまり、エレメント毎に24バイトを含むnumpy配列データのアドレスに渡すことで、ポインタから、24バイトサイズでnumpyフィールドが揃っているC++構造体を作成することで、それらのエレメントのC++配列を受け取ることができます。
-
C++コンパイラは構造体エレメント間を桁埋めすることで、指定したバイトサイズに揃えることができます。上記の#pragma packマクロを宣言すると、構造体を桁埋めしません。
-
Numpyの構造体配列には、フィールド値別にエレメントを並べ替える仕組みがあります。
Python SOP ¶
inlinecpp
はPython SOPを記述する時に非常に役に立ちます。これを使うことでC++ GU_Detailクラスで利用可能な高レベル処理にアクセスすることができます。さらに、C++でコードを実装することで遅いループの処理を速くすることができます。この手法は、 Type Propertiesダイアログでパラメータを作成し (C++ PRM_Templates
を使う必要がありません)、 C++でパフォーマンスを出すという組み合わせができるメリットがあります。
以下のサンプルでは、平面を基準にジオメトリをクリップするPython SOP内のコードを載せています。法線用パラメータ(normal
という3つのfloatを持つベクトル)と法線沿いの距離用のパラメータ(distance
というfloat)があることを前提に、その法線と原点からの距離を使って平面を作成し、その平面でジオメトリをクリップします。
import inlinecpp geofuncs = inlinecpp.createLibrary( "example_clip_sop_library", acquire_hom_lock=True, includes=""" #include <GU/GU_Detail.h> #include <GQ/GQ_Detail.h> """, function_sources=[""" void clip(GU_Detail *gdp, float nx, float ny, float nz, float distance) { GQ_Detail *gqd = new GQ_Detail(gdp); UT_Vector3 normal(nx, ny, nz); gqd->clip(normal, distance, 0); delete gqd; } """]) nx, ny, nz = hou.parmTuple("normal").eval() geofuncs.clip(hou.pwd().geometry(), nx, ny, nz, hou.ch("distance"))
inlinecpp
を使ったPythonとHDKを使ったC++を比較してみてください。これらのサンプルでは、inlinecpp
バージョンはHDKバージョンと同じ速さですが、自動コンパイルを使用しているためコードが短いです。
以下の別のサンプルでは、destroyMe
メソッドをhou.Attrib(hou.Attrib.destroyが既に存在していることに注目してください)に追加しています:
import types geomodule = inlinecpp.createLibrary("geomodule", acquire_hom_lock=True, includes=""" #include <GU/GU_Detail.h> template <typename T> bool delete_attribute(GU_Detail *gdp, T &attrib_dict, const char *name) { GB_Attribute *attrib = attrib_dict.find(name); if (!attrib) return false; gdp.destroyAttribute(attrib->getOwner(), attrib->getName()); return true; } """, function_sources=[ """ int delete_point_attribute(GU_Detail *gdp, const char *name) { return delete_attribute(gdp, gdp->pointAttribs(), name); } """, """ int delete_prim_attribute(GU_Detail *gdp, const char *name) { return delete_attribute(gdp, gdp->primitiveAttribs(), name); } """, """ int delete_vertex_attribute(GU_Detail *gdp, const char *name) { return delete_attribute(gdp, gdp->vertexAttribs(), name); } """, """ int delete_global_attribute(GU_Detail *gdp, const char *name) { return delete_attribute(gdp, gdp->attribs(), name); } """]) attrib_type_to_delete_function = { hou.attribType.Point: geomodule.delete_point_attribute, hou.attribType.Prim: geomodule.delete_prim_attribute, hou.attribType.Vertex: geomodule.delete_vertex_attribute, hou.attribType.Global: geomodule.delete_global_attribute, } def destroyAttrib(attrib): return attrib_type_to_delete_function[attrib.type()](attrib.geometry(), attrib.name()) hou.Attrib.destroyMe = types.MethodType(destroyAttrib, None, hou.Attrib)
hou.Nodeクラスの拡張 ¶
以下のサンプルでは、hou.OpNodeクラスやそのサブクラスを拡張する方法を載せています:
この1番目のサンプルでは、パターン文字列を受け取るexpandGroupPatternというメソッドを追加し、そのパターンに一致するグループ内の子ノードの名前をカンマ区切りのリストで返します。
>>> inlinecpp.extendClass(hou.OpNode, "cpp_node_methods", function_sources=[""" ... inlinecpp::BinaryString expandGroupPattern(OP_Node *node, const char *pattern) ... { ... UT_String result; ... node->expandGroupPattern(pattern, result); ... return result.toStdString(); ... }"""]) ... >>> hou.node("/obj").expandGroupPattern("@group1") 'geo1,geo2,geo3'
この2番目のサンプルでは、オブジェクトノードオブジェクトにsetSelectableメソッドを追加しています(このメソッドは既にhou.ObjNode.setSelectableInViewportとして利用可能であることを覚えておいてください)。
inlinecpp.extendClass( hou.ObjNode, "node_methods", includes="#include <UT/UT_UndoManager.h>", function_sources=[""" void setSelectable(OBJ_Node *obj_node, bool selectable) { if (!obj_node->canAccess(PRM_WRITE_OK)) return; UT_AutoUndoBlock undo_block("Setting selectable flag", ANYLEVEL); obj_node->setPickable(selectable); } """])
Python COP ¶
Python COPでinlinecppの使い方のサンプルに関しては、Python COPの章でCOPの一部をC++で記述するを参照してください。
例外を発生させる ¶
C++ ソースコードではPythonへ例外を発生させることができません。しかし、例外を発生させるテクニックが1つあり、C++関数がコードを返すようにして、そのコードをチェックするwrapperのPython関数を作成して、必要に応じて例外を発生させることです。以下のサンプルは上記と同じですが、hou.PermissionError例外を発生させます。
import types cpp_node_methods = inlinecpp.createLibrary( "node_methods", acquire_hom_lock=True, includes=""" #include <OBJ/OBJ_Node.h> #include <UT/UT_UndoManager.h> """, function_sources=[""" bool setObjectSelectable(OBJ_Node *obj_node, bool selectable) { if (!obj_node->canAccess(PRM_WRITE_OK)) return false; UT_AutoUndoBlock undo_block("Setting selectable flag", ANYLEVEL); obj_node->setPickable(selectable); return true; } """]) def setObjectSelectable(obj_node, selectable): if not cpp_node_methods.setObjectSelectable(obj_node, selectable): raise hou.PermissionError() hou.ObjNode.setSelectable = types.MethodType(setObjectSelectable, None, hou.ObjNode)
houオブジェクトを返す ¶
C++関数が返すものを返すことはできません。C++関数が返すものは、自動的にそれに該当するhouオブジェクトに変換されます。しかしPython wrapper関数を使えば、変換することができます。
例えば、OP_Node::getParmsThatReference
はパラメータ名を受け取り、PRM_Parm
ポイントのUT_PtrArray
を返します。以下のように、hou.Parmオブジェクトを受け取り、hou.ParmTupleオブジェクトのシーケンスを返すPython関数を作成することができます:
parm_reference_lib = inlinecpp.createLibrary( "parm_reference_lib", acquire_hom_lock=True, structs=[("StringArray", "**c")], includes=""" #include <OP/OP_Node.h> #include <PRM/PRM_Parm.h> // このヘルパー関数は、Pythonから直接コールされません: static std::string get_parm_tuple_path(PRM_Parm &parm_tuple) { UT_String result; parm_tuple.getParmOwner()->getFullPath(result); result += "/"; result += parm_tuple.getToken(); return result.toStdString(); } """, function_sources=[""" StringArray get_parm_tuples_referencing(OP_Node *node, const char *parm_name) { std::vector<std::string> result; UT_PtrArray<PRM_Parm *> parm_tuples; UT_IntArray component_indices; node->getParmsThatReference(parm_name, parm_tuples, component_indices); // たとえコンポーネントインデックスの配列を返しても、ほとんど-1になるので、 // これらを使用することができません。 for (int i=0; i<parm_tuples.entries(); ++i) result.push_back(get_parm_tuple_path(*parm_tuples(i))); return result; } """]) def parm_tuples_referencing(parm): return [hou.parmTuple(parm_tuple_path) for parm_tuple_path in parm_reference_lib.get_parm_tuples_referencing( parm.node(), parm.name())]
このサンプルのC++関数は、スペース区切りの文字列を返します。それを文字列のリストに分割し、hou.ParmTupleオブジェクトのリストに変換します。
このサンプルでは、includes
パラメータ内にソースコードを記述することで、Python側に表示されないヘルパー関数を作成していることに注目してください。
コンパイルしたライブラリの配布 ¶
ユーザが適切なコンパイラ環境をセットアップする必要がないように、コンパイルしたライブラリを配布したいのであれば、単にinlinecpp
が作成した$HOME/houdiniX.Y/inlinecpp
ディレクトリ下にある共有オブジェクト(.so
/.dll
/.dylib
)を配布すればいいだけです。ユーザがC++ソースコードを修正しない限り、チェックサムは同じで共有オブジェクトのファイル名も同じにします。
Houdiniは共有オブジェクトを検索する時に$HOUDINI_PATH
にあるinlinecppサブディレクトリをすべて調べます。つまり、$HOUDINI_PATH
以下のどこかの場所に共有ライブラリを配置することができます。
共有ファイルのパスを調べるには、モジュールのようなオブジェクトの_shared_object_path
メソッドをコールします。 例:
>>> mymodule = inlinecpp.createLibrary("test", function_sources=[""" ... int doubleIt(int value) { return value * 2; }"""]) ... >>> mymodule._shared_object_path() '/home/user/houdiniX.Y/inlinecpp/test_Linux_x86_64_11.0.313_3ypEJvodbs1QT4FjWEVmmg.so'
ライブラリはプラットフォームとアーキテクチャ別に名前が異なります。つまり、共有オブジェクト名の干渉を気にすることなく複数のマルチプラットフォームに対応できるように複数の共有オブジェクトを配布することができます。
デジタルアセット外へのコードの保存 ¶
バージョン管理システムでソースコードの編集と管理を簡単にするために、デジタルアセット外にPythonソースコードを保存する方法のサンプルは、Python SOPのドキュメントを参照してください。
クラッシュの修復 ¶
C++コードはHoudiniプロセス内で実行されるので、コード内にバグがあればHoudiniを簡単にクラッシュさせてしまいます。ライブラリを作成する時にdebug=True
を渡すことで、Houdiniはコードを最適化なしでデバッグ情報を含めてコンパイルするので、標準のデバッガーをHoudiniに追加することでコードをデバッグするのが簡単になります。
デフォルトでは、debug=True
を渡してcatch_crashes
パラメータの値を指定しなかった場合、HoudiniはC++ソースコードのクラッシュをリカバリーしようとし、それをC++スタックトレースを含むPython RuntimeError
例外に変換します。例えば、以下のコードはNullポインタの参照先を取得しているのでクラッシュします:
>>> mymodule = inlinecpp.createLibrary("crasher", debug=True, function_sources=[""" ... void crash() ... { ... int *p = 0; ... *p = 3; ... } ... """]) >>> mymodule.crash() Traceback (most recent call last): File "<console>", line 1, in <module> File "/opt/hfs/houdini/python3.9libs/inlinecpp.py", line 620, in __call__ return self._call_c_function(*args) File "/opt/hfs/houdini/python3.9libs/inlinecpp.py", line 589, in call_c_function_and_catch_crashes return hou.runCallbackAndCatchCrashes(lambda: c_function(*args)) RuntimeError: Traceback from crash: pyHandleCrash UT_Signal::UT_ComboSignalHandler::operator()(int, siginfo*, void*) const UT_Signal::processSignal(int, siginfo*, void*) _L_unlock_15 (??:0) crash (crasher_Linux_x86_64_10.5.313_fpk9yUhcRwBJ8R5BAVtGwQ.C:9) ffi_call_unix64+0x4c ffi_call+0x214 _CallProc+0x352 <unknown>
トレースバックのPythonの部分には、例外が発生する直前のコールとC++スタックトレースのコールを含む例外が入っています。
そのトレースで注目すべき行は、crash(crasher_Linux_x86_64_10.5.313_fpk9yUhcRwBJ8R5BAVtGwQ.C:9)
です。これは、$HOME/houdiniX.Y/inlinecpp/crasher_Linux_x86_64_10.5.313_fpk9yUhcRwBJ8R5BAVtGwQ.C
の9行目に問題があることを意味しています。そのファイルを探すと、以下の内容を確認することができます:
#include <UT/UT_DSOVersion.h> #include <HOM/HOM_Module.h> extern "C" { void crash() { int *p = 0; *p = 3; } }
9行目は、Nullポインタの参照先を取得するステートメントです。
デフォルトでは、debug=True
の場合のみ、クラッシュのハンドリングが有効になります。
catch_crashes
パラメータに値を設定すれば、debug
設定に関係なく、クラッシュのハンドリングを有効にするかどうか制御することができます。