HDKでオリジナルのVEX関数を作る話


VEX関数を作る

Houdiniではノードに加えて、VEXやpython,HDKが使えます。
しかし、ネットの情報をあさってもHDKを使っている記事等が少ない...。
というより、HDKの日本語情報が少ない...!
そこで、この記事を書こうと思いました。

今回はオリジナルのVEX関数を作ることを通してHDKについて勉強することを目的とします。
(もっと楽な方法はたくさんあると思います。)

HDKについてはこちらが非常に参考になりました。
ありがとうございます。

はじめに

開発環境はlinux mintgcc9.2です。

学習リソース

公式のドキュメントとサンプルコードを読んで学習しました。
ここに詳しく書いてあります。

サンプルのコードはHoudiniと一緒にダウンロードされています。
場所は

$HFS/toolkit/samples/VEX

ここにカスタムVEX関数を作るためのコードがまとまっています。
ドキュメントのページにもあるのでブラウザでも見ることができます。

VEX関数プラグイン作成

HDKを使ってオリジナルのVEX関数を実装するには、VEX_VexOpクラスを使ってプラグインを作成します。
このVEX_VexOpクラスのオブジェクトを作成するには

  • 関数のSignature
  • コールバック関数

が必要になります。Signatureに関しては後ほど。
また、オプションとして、

  • 初期化関数と後処理関数
  • 最適化情報

を付加することもできます。

以上の要素を用意してVEX_VexOpクラスオブジェクトを作成するコードを書けば、オリジナルのVEX関数プラグインを作れるということです。

早速サンプルコードを見てみましょう。
たとえば、サンプルの中にあるVEX_Example.Cで書かれているtime関数に着目すると、

template <VEX_Precision PREC>
static void
time_Evaluate(int, void *argv[], void *)
{
    //処理
}

コールバック関数がtime_Evaluate()関数としてこのように書かれています。
さらにコードを辿っていくと、このコールバック関数を引数としてVEX_VexOpクラスがオブジェクト化されている箇所を見つけることができます。

void
newVEXOp(void *)
{
    //省略

    new VEX_VexOp("time@&I"_sh,     // Signature
        time_Evaluate<VEX_32>,      // Evaluator 32
        time_Evaluate<VEX_64>,      // Evaluator 64
        VEX_ALL_CONTEXT,    // Context mask
        nullptr,nullptr,    // init function 32, 64
        nullptr,nullptr,    // cleanup function 32, 64
        VEX_OPTIMIZE_1);    // Optimization level

    //省略
}

サンプルコードはいくつかの関数をまとめて書いているため、少しごちゃごちゃいています。
しかし、作成したいVEX関数それぞれについて同じことをしているだけです。
VEX_VexOpクラスのオブジェクト作成に必要なものを用意して、オブジェクトを作成する、これだけです。

VEX_VexOpクラス

ここでVEX_VexOpクラスのオブジェクト作成に必要な引数について詳しく見ていきます。
オブジェクト作成時の引数は以下をとれます。

  • Signature
  • コールバック関数()
  • 初期化関数()
  • 後処理関数()
  • Context mask
  • 最適化情報
  • (戻り値の有無)

これらの必要なものについてそれぞれ見ていきます。

Signature

SignatureはVEX関数の引数や戻り値を指定するために必要なもので、書き方に決まりがあります。
アルファベット・数字と2種類のprefixで構成されており、
アルファベット・数字は値の型を、&と*は読み書きの制限を指定しています。
対応は次のようになっています。

アルファベット・数字 値の型
I int
F float
V vector
P vector4
3 matrix3
4 matrix
S string

アルファベット・数字の前に [ をつけると配列として扱えます。
そして、prefixの対応は次のようになっています。

prefix 値の制限
何も無し 読み取りのみ可能
& 書き出しのみ可能
* 読み書き可能

また、いくつかルールがあります。

  1. 最初のアルファベット・数字は戻り値の指定となる
  2. 書き出しのみ可能(&)な変数が1つのときは戻り値として扱う
  3. 最初のアルファベットが読み書き可能(*)な場合、戻り値の指定とはならない。
  4. 書出しのみ可能(&)な変数が2つ以上あるとき、戻り値はvoid型となる(VEX_VexOpへの引数によって変更可能)

いくつか具体例を見てみましょう。
先程見たサンプルコードのtime()関数では

time@&I

となっています。1つ目のルールから

int time()

のSignatureだとわかります。

他にも例が公式のドキュメントに載っています。
ざっとまとめて書いておきます。

vector_length@&FV 
cross@&VVV
add_float@*FF
mread@&IS&4

これらは以下のSignatureです。

float vector_length(vector)
vector cross(vector, vector)
add_foat(float &, float)
void mread(int &, string, matrix &)
//mread関数についてはVEX_VexOpオブジェクト作成時の
//引数によって以下も可能
int mread(string, matrix &)

最後のmread()関数についてはVEX_VexOpの引数として最後にtrueを加えることで、4番目のルールを無視し、最初のアルファベット・数字を戻り値として指定できます。
つまり、以下のように引数を与えれば良いわけです。

VEX_VexOp("mread@&IS&4",            // Signature
            callback,               // VEX_VexOpCallback
            VEX_ALL_CONTEXT,        // VEX Contexts
            NULL,                   // VEX_VexOpInit
            NULL,                   // VEX_VexOpCleanup
            VEX_OPTIMIZE_2,         // Optimization level
            true);                  // Forced return code

コールバック関数

コールバック関数ではオリジナルのVEX関数の処理を記述します。
また、初期化関数・後処理関数は関数のためのデータを確保・開放するために使われ、void型のポインタを返します。そして、VEX関数が呼び出されるたびに実行されます。

コールバック関数には自分が行いたい処理を書けば良いだけですが、引数のとり方は決められています。
コールバック関数では以下の3つの引数を取ります。

  • argc  :関数に渡された引数の数
  • argv  :渡された引数データへのvoidポインタ
  • void* :初期化関数っから渡されたvoidポインタ

オリジナルのVEX関数を作るときには、引数から得られるvoidポインタから自分がほしい型へとコールバック関数内でキャストして使います。
つまり、以下のとおり。

// func@IF[V
void
Callback(int argc, void *argv[], void *data)
{
    const int                         *arg0 = (const int *)argv[0];
    const fpreal32                    *arg1 = (const fpreal32 *)argv[1];
    const UT_Array<UT_Vector3>        *arg2 = (const UT_Array<UT_Vector3> *)argv[2];
    ...
}

引数で配列を取る場合にはUT_Arrayオブジェクトにキャストする必要があります。

Context mask

Context maskでは関数が使えるコンテクストの範囲を指定できます。
基本VEX_ALL_CONTEXTだと思います。
(どういうときに使い分けるんでしょうか?わかる方教えてください。)

最適化

基本的にはVEXのランタイムエンジンによって最適化されるのですが、オリジナルの関数をどの程度最適化処理してよいのか伝える必要があります。
最適化情報としては

//最適化無し
VEX_OPTIMIZE_0 
//関数の計算結果が使用されていない場合、省略対象となる
VEX_OPTIMIZE_1 
//すべての引数が定数のとき、結果はキャッシュされる
VEX_OPTIMIZW_2 

の3種を引数にとれます。(番号だけでもよいです。)

より詳細な情報は...

こちらのヘッダファイルを読んでしまったほうが早いです...

コード

実際の作例に入ります。
今回は法線から接線と従法線を得る関数を作ります。
外積を使ったチュートリアルでよくあるものです。
こちらのようなものです。
普通にcrossを使って書けば済む話ですが、VEX関数にしてみます。(勉強のため)

書いたコードはこちらです。

MyVEX.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>

#include <UT/UT_DSOVersion.h>
#include <UT/UT_Thread.h>
#include <UT/UT_Vector3.h>
#include <VEX/VEX_VexOp.h>

using namespace UT::Literal;

namespace H_AdCaleSample {

//コールバック関数
template <VEX_Precision PREC>
static void
getright_Evaluate(int argc, void *argv[], void *)
{
    VEXvec3<PREC>   *result = (VEXvec3<PREC> *)argv[0];
    const UT_Vector3 *in_vec = (const UT_Vector3  *)argv[1];

    UT_Vector3 right;

    right.assign(in_vec->z() * (-1),0,in_vec->x());

    *result = right;
}

template <VEX_Precision PREC>
static void
getfront_Evaluate(int argc, void *argv[], void *)
{
    VEXvec3<PREC>   *result = (VEXvec3<PREC> *)argv[0];
    const UT_Vector3 *in_vec = (const UT_Vector3  *)argv[1];

    UT_Vector3 right;
    UT_Vector3 front;

    right.assign(in_vec->z() * (-1),0,in_vec->x());
    front.assign(in_vec->y()*right(2)-in_vec->z()*right(1),
                 in_vec->z()*right(0)-in_vec->x()*right(2),
                 in_vec->x()*right(1)-in_vec->y()*right(0));

    *result = front;
}

}

using namespace H_AdCaleSample;

//VEX_VexOpクラスのオブジェクト作成
void newVEXOp(void *)
{
    new VEX_VexOp(
        "getright@&VV"_sh,
        getright_Evaluate<VEX_32>,
        getright_Evaluate<VEX_64>,
        VEX_ALL_CONTEXT,
        nullptr,nullptr,
        nullptr,nullptr 
        );

    new VEX_VexOp(
        "getfront@&VV"_sh,
        getfront_Evaluate<VEX_32>,
        getfront_Evaluate<VEX_64>,
        VEX_ALL_CONTEXT,
        nullptr,nullptr,
        nullptr,nullptr
        );
}

getright()関数で接線を、getfront()関数で従法線を取れるようにしました。

コンパイルとインストール

コンパイルの前に...

ターミナルで以下を入力し、必要なものをインストールします。

sudo apt-get install tcsh g++ mesa-common-dev libglu1-mesa-dev libxi-dev

自分の環境はmintなので、apt-getを使いました。
Red Hat系OSでは、yumを使います。

yum install tcsh gcc-c++ mesa-libGL-devel mesa-libGLU-devel libXi-devel

コンパイル

はじめにターミナルでhcustomを使えるようにします。
ターミナルを開き、以下を入力します。

cd /opt/hfsXX.Y.ZZZ
source houdini_setup

これによって、hcustomでコンパイルを行ったり、houdiniと入力してHoudiniを起動させることができるようになります。

そして、コンパイルを行います。
コードを書いたディレクトリに移動し、hcustomでコンパイルです。

cd コードを書いたディレクトリ
hcustom MyVEX.cpp

以上でコンパイル完了です。
以外と簡単...!

ちなみに、hcustomでは複数のソースファイルは扱えません。
ソースファイルがいくつかある場合はMakefileを作ってコンパイルします。
コンパイルについての詳細はこのページ

インストール

自分で作成したVEX関数はコンパイルした後、インストールが必要です。
インストールする方法はターミナルで以下を入力し、houdiniが認識できるようにします。

echo コンパイルしてできた.soファイルのディレクトリ > /opt/hfsXX.Y.ZZZ/houdini/vex/VEXdso

まとめ

以上の手順でオリジナルのVEX関数を作成、導入しました。
他のC++で使える関数や自分で考えた関数を導入すると面白いと思います。
よかったら試してみてください!