古のバージョンから星のHDKは受け継がれています(少なくともHoudini9にはSOP_Star.Cファイルがありました。おそらくもっと前からあるはず)
HDK
Houdini Development Kit (HDK) は、C++ ライブラリの包括的なセットです。これらは、Side FXプログラマーが Houdini製品ファミリーの開発に使用するものと同じライブラリです。HDK を使用すると、Houdini インターフェイスのさまざまな領域をカスタマイズするプラグインを作成できます。例えばカスタム関数を追加したり、カスタムオペレータを追加 (SOP、COP、DOP、VOP、ROP、CHOP)することができます。
イントロダクションより
HDKの作り方は2014年のtakiさんのスライドからsatoruhigaさんのアドカレやtowazumiさんのアドカレ、tsuka0245さんのアドカレ、takavfxさんの記事、Yamabeさんのブログ、towazumiさんのpmxファイルを読み込むサンプルの記事で紹介されています。とはいえHDKの作り方もバージョンが新しくなるにつれて細かく変化していっています。
今回は公式のHDKのドキュメントを最初から見ていきながら、最新のHDKの作り方を探っていきます。
実行環境はWindows10、Houdini 19.5.303です。
HDKはどこ?
Houdini をインストールすると、HDK は$HFS/toolkitにあります。ツールキット ディレクトリ内には、次のフォルダーがあります。
フォルダ | コンテンツ |
---|---|
include | Houdini の包括的なライブラリ セットへのアクセスを可能にする C++ ヘッダー ファイル。 |
makefiles | HDK プロジェクトの Makefile のビルディング ブロックとして使用するための Makefile のコレクション。詳細については、 Makefile を使用したコンパイル を参照してください。 |
samples | HDK コード例の広範なセット。ドキュメント全体で、いくつかの HDK サンプルが参照されています。たとえば、standalone/geoisosurface.Cリンクは、$HFS/toolkit/samples/standalone/geoisosurface.C にあるコード サンプルを参照します。 |
Visual Studioをインストール
hcustomが起動時に、Microsoft Visual C++ ディストリビューションを自動検出します。
Visual Studio は少し古い2017や最新の2022などがありますが、Houdini19.5時点ではhoudini-19.5.303-win64-vc142、とあるようにvc142なので、バイナリ互換性を考えると2019が必要です。
ツールセットのバージョンは、Visual Studio 2015 の場合は v140、2017 の場合は v141、2019 の場合は v142、2022 の場合は v143 です。古いHoudiniの場合はv141を、将来的にHoudini20~以降になってv143になった場合はv143のMicrosoft C++ (MSVC) コンパイラツールセットが必要になるかと思います。
確認方法は
hcustom --output_msvcdir
です。人によって結果は違うかもしれませんが、僕の場合は、
MSVCDir = C:/Program Files (x86)/Microsoft Visual Studio/2019/Professional/VC/Tools/MSVC/14.28.29333
という結果が返ってきました。
初めての HDK プログラム
最初に入門で案内されるgeoisosurface.C はスタンドアロンなアプリです。
スタンドアロンとは何かというと、Houdini外で実行してHoudiniの実行結果を得るようなアプリです。
まずgeoisosurface.C をコピーして作業用のディレクトリにコピーします。
Houdiniのコマンドラインツールを立ち上げます。
All Programs > Side Effects Software > Houdini 19.5.303 > Command Line Tools
このコマンドツールは最初からHoudiniのパスが通っているのでhcustomコマンドが使用できます。
補足
ランチャーでアップデートしていると、もしかすると旧バージョンのコマンドラインツールがWindowsのStart Menuに残ったままになっているかもしれません。その場合はインストーラーから再インストールしてみてください。
以下のコマンドを打ち込んで、geoisosurface.Cをコピーします。手でコピーしても大丈夫です。
mkdir HDK
cd HDK
copy "C:/Program Files/Side Effects Software/Houdini 19.5.303/toolkit/samples/standalone/geoisosurface.C" .
-sコマンドはプラグインではなく、スタンドアローンのアプリケーションを作成する際のコマンドです。
hcustom -s geoisosurface.C
一通り実行すると実行ファイル(geoisosurface.exe)が作成されていることが確認できます。
補足
.oファイルはコンパイルが作成した中間データ(オブジェクトデータ)です
補足
このほかにもコマンドがあります。
https://www.sidefx.com/docs/hdk/_h_d_k__intro__tools.html使い方
hcustom [-g] [-s] [-e] [-L libdir] [-I incdir] [-l lname] [-i install_dir] source.C [source2.C]オプション
-d コンパイラに渡される CFLAGS 環境変数を表示します。Windowsの場合のみ。 -e このスクリプトが実行するコンパイラとリンカのコマンドをエコー表示します。 -g デバッグ情報付きでコンパイルします。 -I dir コンパイラのインクルード・ディレクトリを追加で指定します。 -i dir インストール・ディレクトリーを指定します。指定しない場合、LinuxとWindowsでは$HOME/houdiniX.Y/dso、Macでは$HOME/Library/Preferences/houdini/houdini/X.Y/dsoがデフォルトのインストール先ディレクトリとなります。 -L dir リンカーの追加ライブラリ・ディレクトリを指定します。 -l lname リンク先となる追加ライブラリを指定します。 -s プラグインではなく、スタンドアロンアプリケーションを作成します。 -t タグ情報の作成をオフにします。
補足
僕だけかもしれませんが、実行途中に以下のエラーが出ました。
C:\Program Files\Side Effects Software\Houdini 19.5.303\toolkit\include\UT\UT_StringHolder.h(1): warning C4819: ファイルは、現在のコード ページ (932) で表示できない文字を含んでいます。データの損失を防ぐために、ファイルを Unicode 形式で保存してください。
VSCodeのSaveWithEncodingでBOM付UTF-8にして保存しなおすと解決しました。
では、さっそく実行します。
するとsphere.bgeoが生成されました!ただの球です。
それもそのはず、geoisosurface.Cの中身を見てみるとISO サーフェスを生成してsphere.bgeoという名前のHoudiniジオメトリファイルに保存しているだけです。
また、GU_Detail.h のヘッダーを含めることで、Houdiniのジオメトリユーティリティライブラリに接続しています。
#include <GU/GU_Detail.h>
#include <UT/UT_Exit.h>
#include <stddef.h>
namespace HDK_Sample {
static float
densityFunction(const UT_Vector3 &P, void *data)
{
// Return the signed distance to the unit sphere
return 1 - P.length();
}
}
int
main(int argc, char *argv[])
{
GU_Detail gdp;
UT_BoundingBox bounds;
// Evaluate the iso-surface inside this bounding box
bounds.setBounds(-1, -1, -1, 1, 1, 1);
// Create an iso-surface
gdp.polyIsoSurface(HDK_Sample::densityFunction, NULL, bounds, 20, 20, 20);
// Save to sphere.bgeo
gdp.save("sphere.bgeo", NULL);
UT_Exit::exit(UT_Exit::EXIT_OK); // exit with proper tear down
return 0;
}
Houdini プラグイン
しかし、今回作りたいのはスタンドアロンではなく内部ツールのカスタムプラグイン。
ということで、SOPのHDKを作っていきます。ドキュメントを見るとSOP_Flatten.Cが題材となっています。これを進めていきましょう。
まずはドキュメントの文言を確認してみましょう
スタンドアロンアプリケーションの作成に加えて、HDK を使用して、Houdini インターフェイスにフックするカスタムプラグインを作成できます。最も単純な形式では、プラグインは単にライブラリファイル (Linux では .so ファイル、Windows では .dll、Mac OSX では .dylib) であり、起動時に Houdini によってロードされます。ライブラリには、カスタム ノード オペレータ、カスタム スクリプト コマンド、および Houdini によって認識されるその他のオブジェクトを含めることができます。
Houdini はどのようにプラグインをロードしますか?
実行時に、Houdini は Houdini Search Path によって決定されたフォルダーで .so (.dll、.dylib) プラグイン ファイルを検索します。デフォルトでは、Houdini は以下を検索します。./dso $HOME/houdiniX.Y/dso $HOME/Library/Preferences/houdini/X.Y/dso (Mac OSX のみ) /Users/Shared/houdini/X.Y/dso (Mac OSX のみ) $HSITE/houdiniX.Y/dso $HFS/houdini/dso
Houdini がプラグイン ライブラリを見つけると、カスタム HDK コードをインターフェイスに登録する役割を担うライブラリ内のフック関数を見つけて実行します。
カスタム プラグインを探すときに Houdini が検索するフォルダを変更するには、HOUDINI_DSO_PATH環境変数を使用します。
プラグインのビルド: SOP_Flatten.C の例
カレントディレクトリを移動して、さっそくFlatten.Cサンプルをビルドしてみましょう。$HFSはHoudiniのインストールフォルダです。Windowsなので%環境変数名%で参照します。
ドキュメントではProgramFile直下で作業していますが、権限の問題でファイルが開けないことがあるので、どこか別の場所にソースファイルをコピーします。
cd C:\Users\作業するディレクトリ
mkdir SOP_FlattenHDK
cd SOP_FlattenHDK
copy "%HFS%/toolkit/samples/SOP/SOP_Flatten.C" .
copy "%HFS%/toolkit/samples/SOP/SOP_Flatten.H" .
cd %HFS%/toolkit/samples/SOP
hcustom SOP_Flatten.C
すると$HOME/Houdini19.5/dso内にdllが生成されます。
これで完了!あとはHoudiniを立ち上げてSOP内のTabから「Flatten」を呼び出します。
まずは結果から。Orientationの軸に応じてぺったんこになります。
ではSOP_Flatten.Cのソースコードを見ていきます。
SOP_Flatten.C
#include "SOP_Flatten.h"
#include <SOP/SOP_Guide.h>
#include <GU/GU_Detail.h>
#include <GA/GA_Iterator.h>
#include <OP/OP_AutoLockInputs.h>
#include <OP/OP_Operator.h>
#include <OP/OP_OperatorTable.h>
#include <PRM/PRM_Include.h>
#include <UT/UT_DSOVersion.h>
#include <UT/UT_Interrupt.h>
#include <UT/UT_Matrix3.h>
#include <UT/UT_Matrix4.h>
#include <UT/UT_Vector3.h>
#include <SYS/SYS_Math.h>
#include <stddef.h>
using namespace HDK_Sample;
//★重要!!newSopOperatorはカスタム サーフェス オペレータを追加する関数です
void
newSopOperator(OP_OperatorTable *table)
{
table->addOperator(new OP_Operator(
"hdk_flatten", // 内部名
"Flatten",// UI 名
SOP_Flatten::myConstructor,// SOP の構築方法
SOP_Flatten::myTemplateList,// マイパラメータ
1,// ソースの最小数
1,// ソースの最大数
NULL));// ローカル変数
//もう一つ引数があるが省略している、OP_FLAG_GENERATOR などのフラグ
}
static PRM_Name names[] = {
PRM_Name("usedir", "Use Direction Vector"),
PRM_Name("dist", "Distance"),
};
PRM_Template
SOP_Flatten::myTemplateList[] = {
PRM_Template(PRM_STRING, 1, &PRMgroupName, 0, &SOP_Node::pointGroupMenu,0, 0, SOP_Node::getGroupSelectButton(GA_GROUP_POINT)),
PRM_Template(PRM_FLT_J, 1, &names[1], PRMzeroDefaults, 0, &PRMscaleRange),
PRM_Template(PRM_TOGGLE, 1, &names[0]),
PRM_Template(PRM_ORD, 1, &PRMorientName, 0, &PRMplaneMenu),
PRM_Template(PRM_DIRECTION, 3, &PRMdirectionName, PRMzaxisDefaults),
PRM_Template(),
};
//SOP_Flattenのコンストラクタ
OP_Node *
SOP_Flatten::myConstructor(OP_Network *net, const char *name, OP_Operator *op)
{
return new SOP_Flatten(net, name, op);
}
SOP_Flatten::SOP_Flatten(OP_Network *net, const char *name, OP_Operator *op)
: SOP_Node(net, name, op), myGroup(NULL)
{
//これは、このSOPがデータIDを手動で管理し、HoudiniがどのAttributeが変更されたかを識別できるようにすることを示します。
//例えば、ビューポートの作業を軽減したり、データIDが変更されたかどうかをチェックする他のSOPのためです。
//デフォルトでは(つまり、この行がない場合)、すべてのデータIDはSOP cookの後にバンプされ、すべてが変更された可能性があることを示すことになります。
//一部のデータ ID が適切にバンプされないと、ビューポートが更新されないことがあります。が正しくクックされないかもしれないので、十分注意してください。
mySopFlags.setManagesDataIDs(true);
// ガイドジオメトリを提供できる旨のフラグを立ててください。
mySopFlags.setNeedGuide1(true);
}
//SOP_Flattenのデストラクタ
SOP_Flatten::~SOP_Flatten() {}
//他のパラメータの値に基づいて、パラメータのdisableとhiddenの状態を更新する切り替え機能。
//Use Direction Vectorのチェックボックスが0のとき3番目のパラメータ(Orientation)が表示され4番目のパラメータ(Direction)が非表示になる
//逆にチェックボックスが1のとき4番目のパラメータが表示され3番目のパラメータが非表示になる
bool
SOP_Flatten::updateParmsFlags()
{
bool changed;
changed = enableParm(3, !DIRPOP());
changed |= enableParm(4, DIRPOP());
return changed;
}
OP_ERROR
SOP_Flatten::cookInputGroups(OP_Context &context, int alone)
{
// SOP_Node::cookInputPointGroups() は、pointの選択を処理するためのデフォルト実装を提供します。
return cookInputPointGroups(
context, // これはグループパラメータをcookするために必要で、単独の場合は入力をcookします。
myGroup, // 単独でない場合は、group(または NULL)が myGroup に書き込まれます。
alone, // これは、ハンドルを更新するためにcookMySopの外部で呼ばれた場合trueになります。
// true は、groupが入力ジオメトリに対応することを意味します。
// falseは、gdpのためのグループであることを意味します。 (the working/output geometry).
true, // (default) true は、単独でなく、ハイライトフラグがオンの場合に、選択範囲をグループに設定することを意味します。
0, // (default) グループフィールドのパラメータインデックス
-1, // (default) グループタイプフィールドのパラメータインデックス(存在しないので-1)。
true, // (default) true は、既存のグループへのポインタでもよいことを意味し、false は、グループが常に新規であることを意味します。
false, // (default) false は、新しいグループは順序付けされないことを意味し、true は、新しいグループは順序付けされることを意味します。
true, // (default) true は、すべての新しいグループがデタッチされることを意味し、detailに所有するものではありません。
// falseは、gdp上の新しいポイントおよびプリミティブグループがgdpによって所有されることを意味します。
0 // (default) グループが単独の場合、そのジオメトリのために作られる入力のインデックス。
);
}
OP_ERROR
SOP_Flatten::cookMySop(OP_Context &context)
{
// ジオメトリにアクセスする前に、入力をロックする必要があります。
// OP_AutoLockInputs は、returnするときに自動的に入力のロックを解除します。
// 注意: これを使用するときは、unlockInputs を自分で呼び出さないようにしましょう!
OP_AutoLockInputs inputs(this);
if (inputs.lock(context) >= UT_ERROR_ABORT)
return error();
fpreal now = context.getTime();
duplicateSource(0, context);
// 以下の3行でローカル変数のサポートが有効になります。
// これにより、たとえば $CR が赤い色を取得できるようになり、AttributeCreate SOP で作成された varmap もサポートされます。
// SOP_Star がそうであるように、自分自身のローカル変数のために evalVariableValue をオーバーライドする場合、
// SOP_Node::evalVariableValue を呼び出すことが不可欠であり、そうしないと内蔵のローカル変数の利点が全く得られないため注意が必要です。
// 変数の順序は、同じ名前の変数が複数の場所に現れる場合、どのAttributeが最初にバインドされるかの優先順位を制御します。
// この順序は、point Attributeが優先されることを保証します。
// 参考 setVariableOrder (int detail, int prim, int pt, int vtx)
setVariableOrder(3, 2, 0, 1);
//setCur*関数は、gdpのどの部分が現在処理されているかを追跡します。
//これは、evalVariableValueコールバックで現在のポイントとして使用されるものです。
//0は最初の入力の値で、2つの入力を持つことができるので、
//$CR2は2番目の入力の値を取得することになります。
setCurGdh(0, myGdpHandle);
// ローカル変数の属性と一致するルックアップテーブルを構築する。
setupLocalVars();
// ここで、どのグループに取り組むかを決定します。
// ここではポイントグループのみを扱います。
if (error() < UT_ERROR_ABORT && cookInputGroups(context) < UT_ERROR_ABORT &&
(!myGroup || !myGroup->isEmpty()))
{
UT_AutoInterrupt progress("Flattening Points");
// すべての位置、法線、およびベクトルAttributeを処理します。
// クォータニオンやトランスフォームのAttributeをどうするかは不明。
// すでにループしているため、変更するAttributeのデータIDをあらかじめバンプしておくが、
// 遅くなることを想定して、各ポイントごとにすべてをバンプすることは避けたい。
UT_Array<GA_RWHandleV3> positionattribs(1);
UT_Array<GA_RWHandleV3> normalattribs;
UT_Array<GA_RWHandleV3> vectorattribs;
GA_Attribute *attrib;
GA_FOR_ALL_POINT_ATTRIBUTES(gdp, attrib)
{
// non-transforming attributesをスキップ
if (!attrib->needsTransform())
continue;
GA_TypeInfo typeinfo = attrib->getTypeInfo();
if (typeinfo == GA_TYPE_POINT || typeinfo == GA_TYPE_HPOINT)
{
GA_RWHandleV3 handle(attrib);
if (handle.isValid())
{
positionattribs.append(handle);
attrib->bumpDataId();
}
}
else if (typeinfo == GA_TYPE_NORMAL)
{
GA_RWHandleV3 handle(attrib);
if (handle.isValid())
{
normalattribs.append(handle);
attrib->bumpDataId();
}
}
else if (typeinfo == GA_TYPE_VECTOR)
{
GA_RWHandleV3 handle(attrib);
if (handle.isValid())
{
vectorattribs.append(handle);
attrib->bumpDataId();
}
}
}
// blockAdvance を使用して、一度に GA_PAGE_SIZE までのポイントを反復処理します。
GA_Offset start;
GA_Offset end;
for (GA_Iterator it(gdp->getPointRange(myGroup)); it.blockAdvance(start, end);)
{
// ユーザが中止を要求したかどうかをチェックする
if (progress.wasInterrupted())
break;
for (GA_Offset ptoff = start; ptoff < end; ++ptoff)
{
// beint処理される現在のポイントをptoffに設定します。
// これは、パラメータを評価する際に発生するあらゆる ローカル変数に使用されることを意味します。
// 注意: ローカル変数とパラメータの繰り返し評価は、
// 値を使用するAttributeの名前を指定する文字列パラメータを持つよりも大幅に遅く、
// 時には複雑です。
// このパラメータは一度だけ評価される必要があり、Attributeは一度検索され、
// すぐにアクセスできます。
// しかし、ポイントごとに変化するプロパティごとに、個別のポイントAttributeが必要になります。
// ローカル変数の評価もスレッドセーフではありませんが、Attributeは複数のスレッドから
// 安全に読み取ることができます。
//
// 長い話です。*ローカル変数は恐ろしい。
myCurPtOff[0] = ptoff;
float dist = DIST(now);
UT_Vector3 normal;
if (!DIRPOP())
{
switch (ORIENT())
{
case 0 : // XY Plane
normal.assign(0, 0, 1);
break;
case 1 : // YZ Plane
normal.assign(1, 0, 0);
break;
case 2 : // XZ Plane
normal.assign(0, 1, 0);
break;
}
}
else
{
normal.assign(NX(now), NY(now), NZ(now));
normal.normalize();
}
// 法線成分の差し引きにより、平面上に位置を投影します。
for (exint i = 0; i < positionattribs.size(); ++i)
{
UT_Vector3 p = positionattribs(i).get(ptoff);
p -= normal * (dot(normal, p) - dist);
positionattribs(i).set(ptoff, p);
}
// 法線をすべてノーマルまたは負のノーマルにします
for (exint i = 0; i < normalattribs.size(); ++i)
{
UT_Vector3 n = normalattribs(i).get(ptoff);
if (dot(normal, n) < 0)
n = -normal;
else
n = normal;
normalattribs(i).set(ptoff, n);
}
// ベクトルを原点を通る平面に投影し、法線成分を差し引いたもの。
for (exint i = 0; i < vectorattribs.size(); ++i)
{
UT_Vector3 v = vectorattribs(i).get(ptoff);
v -= normal * dot(normal, v);
vectorattribs(i).set(ptoff, v);
}
}
}
}
// すべての myCur* 変数をクリアして、浮遊参照を持たないようにします。
//これにより、パラメータがこの cook パスの外側で評価される場合、
//古くなった可能性のあるポイント・ポインタを読もうとしないことが保証されます。
resetLocalVarRefs();
return error();
}
OP_ERROR
SOP_Flatten::cookMyGuide1(OP_Context &context)
{
const int divs = 5;
OP_AutoLockInputs inputs(this);
if (inputs.lock(context) >= UT_ERROR_ABORT)
return error();
float now = context.getTime();
myGuide1->clearAndDestroy();
float dist = DIST(now);
float nx = 0;
float ny = 0;
float nz = 1;
if (!DIRPOP())
{
switch (ORIENT())
{
case 0 : // XY Plane
nx = 0; ny = 0; nz = 1;
break;
case 1 : // YZ Plane
nx = 1; ny = 0; nz = 0;
break;
case 2 : // XZ Plane
nx = 0; ny = 1; nz = 0;
break;
}
}
else
{
nx = NX(now); ny = NY(now); nz = NZ(now);
}
if (error() >= UT_ERROR_ABORT)
return error();
UT_Vector3 normal(nx, ny, nz);
normal.normalize();
UT_BoundingBox bbox;
inputGeo(0, context)->getBBox(&bbox);
float sx = bbox.sizeX();
float sy = bbox.sizeY();
float sz = bbox.sizeZ();
float size = SYSsqrt(sx*sx + sy*sy + sz*sz);
float cx = normal.x() * dist;
float cy = normal.y() * dist;
float cz = normal.z() * dist;
myGuide1->meshGrid(divs, divs, size, size);
UT_Vector3 zaxis(0, 0, 1);
UT_Matrix3 mat3;
mat3.dihedral(zaxis, normal);
UT_Matrix4 xform;
xform = mat3;
xform.translate(cx, cy, cz);
myGuide1->transform(xform);
return error();
}
const char *
SOP_Flatten::inputLabel(unsigned) const
{
return "Geometry to Flatten";
}
SOP_Flatten.h
#ifndef __SOP_Flatten_h__
#define __SOP_Flatten_h__
#include <SOP/SOP_Node.h>
namespace HDK_Sample {
class SOP_Flatten : public SOP_Node
{
public:
SOP_Flatten(OP_Network *net, const char *name, OP_Operator *op);
~SOP_Flatten() override;
/// このメソッドは、ハンドルで呼び出せるように作成されています。
/// このSOPの入力グループのみをクックする。
/// このグループのジオメトリは、この SOP で操作される唯一のジオメトリ
OP_ERROR cookInputGroups(OP_Context &context,
int alone = 0) override;
static PRM_Template myTemplateList[];
static OP_Node *myConstructor(OP_Network*, const char *,
OP_Operator *);
protected:
/// 他のパラメータの値に基づいて、パラメータのdisableとhiddenの状態を更新する。
bool updateParmsFlags() override;
const char *inputLabel(unsigned idx) const override;
/// SOPのジオメトリをcookするメソッド
OP_ERROR cookMySop(OP_Context &context) override;
/// このメソッドは、「ガイド」のジオメトリを生成するために使用されます。
/// これは定義する必要はありません。
OP_ERROR cookMyGuide1(OP_Context &context) override;
private:
void getGroups(UT_String &str){ evalString(str, "group", 0, 0); }
fpreal DIST(fpreal t) { return evalFloat("dist", 0, t); }
int DIRPOP() { return evalInt("usedir", 0, 0); }
int ORIENT() { return evalInt("orient", 0, 0); }
fpreal NX(fpreal t) { return evalFloat("dir", 0, t); }
fpreal NY(fpreal t) { return evalFloat("dir", 1, t); }
fpreal NZ(fpreal t) { return evalFloat("dir", 2, t); }
/// この SOP で操作され、"cookInputGroups "メソッドでcookされるジオメトリのグループである。
const GA_PointGroup *myGroup;
};
} // End HDK_Sample namespace
#endif
けっこう簡単そうな処理なのに長い!
もうちょっと抽象化して考えないとよくわかりません。
とはいえ、パラメータTemplateの設定を独自に入れるは大変です。パラメータのタイプによって全然入力する項目が違うからです。
これを解決するのがDsFileです。説明は次の節で。
HDKコードのコンパイル
ドキュメントはここを参照しています。
今回はWindows環境なので、hcustom を使用した Windows (MSVC) でのコンパイルで進めていきます。
コンパイルの仕方は色んな種類がありますが、hcustomコマンドライン ツール(上記でやってきたCommand-line Toolsプログラムのことです)を使うのが一番簡単な方法です。
他にもMakefileやCmakeでのコンパイルがあります。hcustomは下準備がほぼ不要なのが良い点なのですが、hcustomでのビルドでは単一のファイルしか扱うことが出来ません。HDKの規模が少しでも大きくなると想定される場合は、素直にCMakeなどでのビルドを検討する必要があるでしょう。
MakefileやCmakeでのやり方はHDKドキュメントや、VimeoのSideFX公式チュートリアルを参照
補足cmakeでビルド
私の環境は以下のバージョンです。
cmake version 3.23.2
SOP_Fooまわりがきちんと記述されている前提で以下の流れで実行します。
mkdir build
cd build
hython %HHP%/generate_proto.py SOP_Foo.C SOP_Foo.proto.h
cmake ..
cmake --build . --clean-first
SOP_StarHDKをもう一度
過去のsatoruhigaさんのアドカレでSOP_Starを作ろうとした際に、以下のエラーでつまずいたことはないでしょうか?僕はつまずきました。
SOP_Star.C(34): fatal error C1083: include ファイルを開けません。'SOP_Star.proto.h':No such file or directory
SOP_Star.proto.hって何?!そんなファイルHoudiniディレクトリ内検索しても、どこにもないんだが!?
そこで.Cソースコード内のincludeの記述前にあるコメントを確認します。
SOP_StarVerb::cook からのパラメータ値に正しい型で簡単にアクセスできる SOP_StarParms を提供するために、以下の DsFile をベースに自動生成されたヘッダーファイルです。
DsFile(DialogScript)ファイルは.Cファイル内にある以下のような記述です。
/// この SOP のパラメータインタフェースを指定する複数行の文字列
static const char *theDsFile = R"THEDSFILE(
{
name parameters
parm {
name "divs" // 内部パラメータ名
label "Divisions" // ユーザーインターフェイスのための説明的なパラメータ名
type integer
default { "5" } // 新規ノードでのこのパラメータのデフォルト
range { 2! 50 } // 値が 2 より下になりません。
// UIのスライダーは 50 までですが、値はそれ以上にすることもできます。
export all // これにより、ノードの状態のときに、ビューポートの上のツールボックスに
// パラメータが表示されるようになります。
}
parm {
name "rad"
label "Radius"
type vector2
size 2 // 2つの成分を持つvector2
default { "1" "0.3" } // 外側と内側の半径のデフォルト
}
parm {
name "nradius"
label "Allow Negative Radius"
type toggle
default { "0" }
}
parm {
name "t"
label "Center"
type vector
size 3 // ベクトル内の3つのコンポーネント
default { "0" "0" "0" }
}
parm {
name "orient"
label "Orientation"
type ordinal
default { "0" } // メニューの最初の項目 "xy "をデフォルトとする
menu {
"xy" "XY Plane"
"yz" "YZ Plane"
"zx" "ZX Plane"
}
}
}
)THEDSFILE";
おお!確かに先ほどのflatten.Cのパラメータテンプレートより圧倒的に分かりやすいし、シンプルに記述できる!、、で、DsFileのドキュメントは??
どうやらドキュメントは無いようです。。。
ドキュメントが無いのに、どうやって調べるのかというと、
https://www.technical-artist.net/?p=2586
で紹介されていたDialog Scriptを出力するHOMを利用する方法があります。
HOM (Houdini Object Model:Pythonスクリプト言語を使ってHoudiniから情報を取得して制御することができるAPI)
- パラメータ定義を抽出するためHDAを作成
- HDAに、カスタムノードに持たせたいパラメータを作成
- HDAのhou.ParmTemplateGroup.asDialogScript()でDialog Scriptのコードを取得
asDialogScriptはHOMの機能のひとつで、parmテンプレートグループに相当するダイアログスクリプトファイルの内容を含んだ文字列を返してくれる便利な関数です。
https://www.sidefx.com/ja/docs/houdini/hom/hou/ParmTemplateGroup.html#asDialogScript
まずHDAをcreate new assetで作成します。
必要なパラメータを設定したら、PythonShellを立ち上げます。
対象となるHDAを選択した状態で、以下のコマンドを入力します。
※パスの入力が面倒なので、hou.selectedNodes()[0]としていますが、絶対パスでも問題ありません。
print(hou.selectedNodes()[0].type().definition().parmTemplateGroup().asDialogScript())
すると、コピペ用のDsFile文が入手できます。
{
name parameters
parm {
name "newparameter"
label "Label"
type button
default { "0" }
parmtag { "script_callback_language" "python" }
}
parm {
name "newparameter2"
label "Label"
type vector
size 3
default { "0" "0" "0" }
range { -1 1 }
parmtag { "script_callback_language" "python" }
}
parm {
name "newparameter3"
label "Label"
type toggle
default { "0" }
parmtag { "script_callback_language" "python" }
}
}
DsFileの準備ができたら、proto.hファイルに変換します。
Hython(Houdini + Python)を利用して、generate_proto.pyがソースコード(.Cファイル)に内包されたDsFileからproto.hファイルへの変換を行います。
このスクリプトは、プロトコルバッファを定型的なコードに変換、つまり.dsファイルを解析し、C++でコンパイルするために必要なパラメータテンプレートに変換します。
引数によって挙動は異なりますが、今回のように.Cファイルを引数として渡した場合、.Cファイルから埋め込まれたDSファイルを抽出する処理が走ります。
抽出範囲は、 R"THEDSFILE から THEDSFILE" までです。
使い方は、以下の通り。引数は基本的に入力ファイル名と、出力ファイル名です。
Usage: generate_proto.py [options] [infile [outfile]]
今回は、入力ファイルが「SOP_Star.C」、出力ファイルが「SOP_Star.proto.h」として指定します。
早速実行していきます。
cd C:\Users\作業するディレクトリ
mkdir SOP_StarHDK
cd SOP_StarHDK
copy "%HFS%/toolkit/samples/SOP/SOP_Star" .
hython %HHP%/generate_proto.py SOP_Star.C SOP_Star.proto.h
hcustom SOP_Star.C
.dllファイルは先ほどと同じくHoudini19.5/dso直下に作成されます。
SOP_Star.Cは何をしているか
改めてSOP_Starが何をしているのか整理してみましょう。
入力されるパラメータがこれで、
出力はこれです。
独自のノードを作る場合、そのプロセスは作成対象のネットワークタイプによって異なります。今回はSOPノードを作成するためSOP_Nodeをルートとしてサブクラス化します。親クラスに応じて適切な仮想関数(SOP_NodeサブクラスはSOP_Node::cookMySop())をオーバーライドします。
SOPノードでジオメトリを生成する形態は2つあります。入力を必要とせず、与えたパラメータに基づいてジオメトリを作成するジェネレータノードと、入力したデータを加工して出力するフィルタノードです。上の例でいうと、SOP_Flattenがフィルタノード、SOP_Starがジェネレータノードになります。
今回は入力なしでジオメトリを生成するためジェネレータノードに該当します。
この区分に意味があるかというと、最初の処理が少し異なります。ジェネレータノードでは一番最初にまっさらな状態で始めますが、フィルタノードでは入力データがあるため、そのデータを一番最初にロックする必要があります。
実行の流れとして
- SOP_Nodeをサブクラス化
- HoudiniがSOP_Star.DLLから情報を取得し、該当のSOPを登録するために呼び出すフックとなるnewSopOperatorを作成
- SOP_NodeクラスのOP_Operatorを作成してOP_OperatorTableに追加
- 登録の際にPRM_TemplateListが参照される。PRM_Templateは作るのが面倒なので、DSFILEを利用して生成する
- SOP_Nodeのインターフェイスが何であるかをHoudiniに知らせます。
- cookMySopメソッドで実処理。
の流れになります。
では次にcookMySopで何をしているのかを見ていきます。
このコードはSOP_Star.Cの実行部です。
/// これは実際の作業を行う機能です。
void
SOP_StarVerb::cook(const SOP_NodeVerb::CookParms &cookparms) const
{
auto &&sopparms = cookparms.parms<SOP_StarParms>();
GU_Detail *detail = cookparms.gdh().gdpNC();
// 1divisionにつき2点必要
exint npoints = sopparms.getDivs()*2;
// npointsが4以下の場合、強制的に4
if (npoints < 4)
{
// 除算にある範囲制限で、これは実際には不可能です。(整数のオーバーフローを除いて)
//しかし、これはSOPにエラーメッセージや警告を追加する方法を示しています。
cookparms.sopAddWarning(SOP_MESSAGE, "There must be at least 2 divisions; defaulting to 2.");
npoints = 4;
}
// この SOP が以前にクックしたことがあり、キャッシュから追い出されなかった場合、その出力の詳細には、最後にクックしたときのジオメトリが含まれる。
// cookしていない場合、またはキャッシュから削除された場合、出力詳細は空になります。
// 例えば、このcookのポイントの数が最後のcookと同じであれば、ポイントを移動するだけでよく(つまり、Pを修正する)、これはビューポートの労力を軽減することにもなります。
GA_Offset start_ptoff;
if (detail->getNumPoints() != npoints)
{
// SOPがクックされていないか、ディテールがキャッシュから追い出されたか、あるいは最後のクック以降にポイント数が変更されたかのいずれかである。
// これにより、空のPとトポロジー属性以外はすべて破壊されます。
detail->clearAndDestroy();
// 閉じた多角形(曲線とは異なる)、すなわち閉じたフラグが真に設定され、正しい数の頂点を持つ、頂点オフセットの連続したブロックとして構築します。
GA_Offset start_vtxoff;
detail->appendPrimitivesAndVertices(GA_PRIMPOLY, 1, npoints, start_vtxoff, true);
// 正しい数のポイントを、ポイントオフセットの連続したブロックとして作成します。
start_ptoff = detail->appendPointBlock(npoints);
// 頂点と点を結線する。
for (exint i = 0; i < npoints; ++i)
{
detail->setVertexPoint(start_vtxoff+i,start_ptoff+i);
}
// pointとvertexとprimitiveを追加したので、すべてのトポロジーAttributeデータID、PのデータID、プリミティブリストのデータIDをぶつけることになります。
detail->bumpDataIdsForAddOrRemove(true, true, true);
}
else
{
// 前回のcookと同じpoint数で、前回は点オフセットの連続したブロックを作成したことが分かっているので、最初の1点を取得するだけでいいのです。
start_ptoff = detail->pointOffset(GA_Index(0));
// Pを修正するだけなので、PのデータIDをぶつけるだけでいいのです。
detail->getP()->bumpDataId();
}
// これ以降はすべて、Pに何を書くかを考えて書くだけです。
const SOP_StarParms::Orient plane = sopparms.getOrient();
const bool allow_negative_radius = sopparms.getNradius();
UT_Vector3 center = sopparms.getT();
int xcoord, ycoord, zcoord;
switch (plane)
{
case SOP_StarParms::Orient::XY: // XY Plane
xcoord = 0;
ycoord = 1;
zcoord = 2;
break;
case SOP_StarParms::Orient::YZ: // YZ Plane
xcoord = 1;
ycoord = 2;
zcoord = 0;
break;
case SOP_StarParms::Orient::ZX: // XZ Plane
xcoord = 0;
ycoord = 2;
zcoord = 1;
break;
}
// 割り込みスコープの開始
UT_AutoInterrupt boss("Building Star");
if (boss.wasInterrupted())
return;
float tinc = M_PI*2 / (float)npoints;
float outer_radius = sopparms.getRad().x();
float inner_radius = sopparms.getRad().y();
// 次に、ポリゴンのすべてのpointを設定します。
for (exint i = 0; i < npoints; i++)
{
// ユーザーが割り込んできたかどうかを確認する...
if (boss.wasInterrupted())
break;
float angle = (float)i * tinc;
bool odd = (i & 1);
float rad = odd ? inner_radius : outer_radius;
if (!allow_negative_radius && rad < 0)
rad = 0;
UT_Vector3 pos(SYScos(angle)*rad, SYSsin(angle)*rad, 0);
// 円を正しい平面上に置く。
pos = UT_Vector3(pos(xcoord), pos(ycoord), pos(zcoord));
//円を動かして、正しい位置に中心を合わせます。
pos += center;
// ポイントオフセットの連続ブロックを作成したので、start_ptoffにiを追加するだけで、このポイントオフセットを見つけることができます。
GA_Offset ptoff = start_ptoff + i;
detail->setPos3(ptoff, pos);
}
}
cookMySop
まず最初の
void
SOP_StarVerb::cook(const SOP_NodeVerb::CookParms &cookparms) const
{
auto &&sopparms = cookparms.parms<SOP_StarParms>();
GU_Detail *detail = cookparms.gdh().gdpNC();
については、ドキュメントを参照するとDetailハンドラを渡しているようです。これはGU_Detailへのアクセス制御を提供しています。
このGU_Detailはジオメトリのコンテナであり、primitives、points、verticesなどすべてのAttributeが含まれています。また、ジオメトリを操作するための便利なメソッドのいくつかを提供しています。
virtual void SOP_NodeVerb::cook ( const CookParms cookparms ) const
cookparms.sopparmsにはSOP_StarParmsで定義されたパラメータが格納されています。&&の右辺値参照型なので、型は参照した性質を持ちます。
以下のパラメータにアクセスするための前準備です。
GU_Detail型ポインタ変数detailにSOP_NodeVerbのGU_Detailへのアクセス制御を提供しているアドレスcookparmsを渡し、gdh().gdpNC()で読み書き可能なポインタとして準備します。
パラメータの取得
指定したparameterからの参照はSOP_Star.proto.hを参照しています、
しょっぱな実行しているSOP_StarParmsは、
SOP_StarParms()
{
myDivs = 5;
myRad = UT_Vector2D(1,0.3);
myNradius = false;
myT = UT_Vector3D(0,0,0);
myOrient = 0;
}
です。ここで例えばパラメータに存在するdivを参照しようとしたい場合、SOP_StarParms()で定義している変数myDivsをgetDivs()で取得します。
ここでポイントとなるのがmyDivesとgetDivsです。この変数と関数は.py変換時に生成されます。Parmsに格納されるものは接頭辞にmyが付与されるローワーキャメルケース(先頭を小文字で書き始め、複合語のスペースをなくし要素の先頭は大文字にする)に。取得時の関数はgetの接頭辞が付くローワーキャメルケースに変換されるようです。getDivsはSOP_Star.proto.hにおいて、以下のように指定されます。
int64 getDivs() const { return myDivs; }
先に挙げたように、このカスタムノードはジェネレータノードのため
detail->clearAndDestroy();
で一度すべてのデータを破棄しています。
そのあとは星を作るためにいろいろやっていますが、ポイントは
GA_Offset start_ptoff;
と
start_ptoff = detail->appendPointBlock(npoints);
です。appendPointBlockメソッドは新しいPointを追加し、連続するブロックのオフセットを返します。
npoints個のポイント分For文でループさせ最終的に
detail->setPos3();でパラメータから星の位置に配置させていきます。
簡単ですが、SOP_Starの説明は以上です。
ヘルプ
ヘルプは "wiki "スタイルのテキストファイルに格納されています。
#type: node
#context: sop
#internal: star
#largeicon: /nodes/sop/SOP_hdk_star.png
#tags: shape
= Star =
""" Builds an n-pointed star """
@parameters
Divisions:
#channels: /divs
Number of points on the star
Radius:
#channels: /rad
The radius of the star
NegRadius:
#channels: /nradius
Allows the radius to be negative. This may result in bowtie
polygons.
Center:
#channels: /tx /ty /tz
The center of the star
Orientation:
#channels: /orient
The orientation of the star
@locals
PT:
current point number
NPT:
number of points in the star
@related
- [Node:sop/circle]
このテキストファイルを、以下の場所にコピーすることで、ヘルプが表示されます。
// $HOUDINI_PATH/help/nodes/sop/hdk_star.txt
ヘルプ
カスタム・オペレータのドキュメントを提供するには、OP_Operator::getHDKHelp() メソッドをオーバーロードする方法もあります。
このメソッドは、ヘルプ・ドキュメント・テキストを送信引数にコピーして、ヘルプが提供されたことを示す true を返します。この方法で提供するヘルプは通常ハードコードされていますが、オペレータがテキスト・ファイルから読み取ることも可能です。
bool
OP_MyNodeOperator::getHDKHelp(UT_String &str) const
{
str.harden("This is some help for OP_MyNodeOperator\n");
return true;
}
あるいは、 operator_name.txt ファイルを別に作成し、 オペレータのヘルプを指定することもできます。例えば、SOP_Star.Cのサンプルでは、そのオペレータ名はhdk_starなので、それに対応するヘルプファイルの名前はhdk_star.txtです。このファイルは、HOUDINI_PATH/helpの検索パスのどこかにインストールする必要があります。これは通常、次の場所にあります。
HOME/houdiniX.Y/help/nodes/OPTYPEになります。
アイコン
最後の作業です!GUIアイコンを作成しましょう。ファイルの形式は、Houdiniがサポートする画像形式も可能ですが、解像度に依存しないSVGファイルが推奨されています。
アイコンのファイル名は、新しい演算子のテーブルと名前に対応しています。次に、Houdini はデフォルトでそのアイコンをオペレータに使用します。たとえば、 hdk_lampという名前の新しいオブジェクト オペレータがある場合、対応するアイコンはOBJ_hdk_lamp.svgになります。
アイコンが完成したら、HOUDINI_UI_ICON_PATH検索パスのどこかにインストールする必要があります。
$HOME/houdiniX.Y/config/Icons
関連用語
用語 | 意味 | 補足 | リンク |
---|---|---|---|
GA | Geometry Attributesライブラリ | Houdiniのすべてのジオメトリの基本クラスが含まれる。Houdini 11.1以前のジオメトリライブラリでGBというクラスがあったが12以降はGAに完全に置き換わっている | docs |
GD | GAライブラリの2Dサブクラス | NURBS、ベジェなどカーブの2Dジオメトリを定義 | docs |
GEO | GAライブラリの3Dサブクラス | 3Dデータ構造を定義するライブラリ | docs |
GU | GEOライブラリのサブクラス | GU_Detail::cube()、 GU_Detail::polyIsoSurface()、 GU_Detail::lsystem()などの高レベルツールがここに実装されている。 | docs |
GA_Detail | すべてのジオメトリのコンテナ | すべてのHoudiniジオメトリはGA_Detailクラスに格納されています。 Detailには、GA_AttributeSetコレクションで管理されるAttribute(GA_Attribute)のリストが格納されています。 |
docs code |
GA_Attribute | すべてのAttribute型実装 (ATI) の基本クラス | このAttributeは、インターフェイス (AIF) の提供を担当します。 |
docs code |
ATI | Attribute Type Implementation | Attributeのデータの配列を管理 | docs |
GU_Detail | ジオメトリのコンテナ | primitives、points、verticesなどすべてのAttributeが含まれています。また、ジオメトリを操作するための便利なメソッドのいくつかを提供します。 |
docs code |
gdp | GU_Detailのローカルメンバ変数 | cook後のジオメトリの外観を反映するように更新する必要があります。 | docs |
GOP_Guide | GU_Detailを特殊化したガイド用の型 |
docs code |
|
SOP_Node | すべてのSOP オペレーターの基本クラス |
docs code |
HDK作成まとめ
ここまで長々と書いてきましたが、あまりにも長すぎて じゃあ実際に実装するには何するんだっけと混乱してきました。
なので、一度これから行う手順についてまとめます。
- どのようなHDKを作るのか決め、どのようなパラメータが必要なのか決定する
- コピペ用のDsFile(ダイアログスクリプトファイル)文を作成するために必要なパラメータを付与したダミーのHDAを作成する
- PythonShellを立ち上げ、対象となるHDAを選択した状態で、以下のコマンドを入力し コピペ用のDsFile文を入手する
- print(hou.selectedNodes()[0].type().definition().parmTemplateGroup().asDialogScript())
- .CファイルにDsFile文をコピペする
- Hython(generate_proto.py)を利用して、ソースコード(.Cファイル)に内包されたDsFileからproto.hファイルへの変換を実行する
- 例: hython %HHP%/generate_proto.py SOP_Star.C SOP_Star.proto.h
- C++でHDKとして使用するメイン処理を記述する
- hcustomもしくはCmake・Makefileなどでビルドする
- アイコンやwikiの細かい設定を行う(任意)
- Houdiniで実行テストを行う
一見複雑ですが、cookMySop以外の場所はある程度書くことが決まっているので、簡単に生成するスクリプトを作成しました。
ここからダウロードして使ってください。
中にあるのは、基本となるSOP_HDK_Templateとtemplate.pyです。やっていることはHDK作成時に書き換えなければならない箇所をHogehogeから置換して書き換えているだけのものです。
template.py
import os
import shutil
# HDK名を入力する
hdk_name = input("Enter the name of the HDK.(example:Hoge) = ")
theSOPTypeName = input("Enter the name of the SOP Type Name.(example:hdk_hoge) = ")
dir_name = "SOP_" + hdk_name
template_dir_name = "SOP_HDK_Template"
h_file_name = "SOP_" + hdk_name + ".h"
c_file_name = "SOP_" + hdk_name + ".C"
custom_sop_class_name = "SOP_" + hdk_name
# 新規ディレクトリ作成
if not os.path.exists(dir_name):
os.mkdir(dir_name)
######## CMakeLists.txt ########
# ファイルを読み込む
cmake_lists = open(template_dir_name + "/CMakeLists.txt", "r")
# ファイルを1行ずつ読み込む
cmake_lists_lines = cmake_lists.readlines()
# star.cのすべての行を読み込む
for i in range(len(cmake_lists_lines)):
# 行内の"Hogehoge"を"Hugahuga"に置換する
cmake_lists_lines[i] = cmake_lists_lines[i].replace("SOP_Hogehoge", custom_sop_class_name)
new_cmake_lists = open(dir_name +"/CMakeLists.txt", "w")
new_cmake_lists.writelines(cmake_lists_lines)
print("create new CMakeLists.txt")
######## SOP_Hogehoge.h ########
# ファイルを読み込む
hoge_h = open(template_dir_name + "/SOP_Hogehoge.h", "r")
hoge_h_lines = hoge_h.readlines()
for i in range(len(hoge_h_lines)):
hoge_h_lines[i] = hoge_h_lines[i].replace("SOP_Hogehoge", custom_sop_class_name)
new_hoge_h = open(dir_name + "/" + h_file_name, "w")
new_hoge_h.writelines(hoge_h_lines)
print("create "+h_file_name)
######## SOP_Hogehoge.c ########
# ファイルを読み込む
hoge_c = open(template_dir_name + "/SOP_Hogehoge.C", "r")
hoge_c_lines = hoge_c.readlines()
for i in range(len(hoge_c_lines)):
hoge_c_lines[i] = hoge_c_lines[i].replace("UI_HogeHoge", hdk_name)
hoge_c_lines[i] = hoge_c_lines[i].replace("hdk_hogehoge", theSOPTypeName)
hoge_c_lines[i] = hoge_c_lines[i].replace("SOP_Hogehoge", custom_sop_class_name)
new_hoge_c = open(dir_name + "/" + c_file_name, "w")
new_hoge_c.writelines(hoge_c_lines)
new_hoge_c.close
print("create "+c_file_name)
#### help
shutil.copy(template_dir_name+"/hdk_hogehoge.txt", dir_name+"/"+theSOPTypeName+".txt")
#### png
shutil.copy(template_dir_name+"/SOP_hdk_hogehoge.png", dir_name+"/SOP_"+theSOPTypeName+".png")
この記事がHDK作成のお役に立てれば幸いです。