Houdini Apprentice Advent Calendar 2025 の9日目の記事です
Houdini で 「途切れたカーブ同士をつなぐ」 基本的な考え方と作り方を、まとめています。
ここでは、2 本のカーブを例にして、
- どのポイント同士をつなぐのか
- そのためにどんな情報(アトリビュートやグループ)が必要なのか
を、VEX といくつかの SOP を使って決めていきます。
追記 2025/12/09
まずは難しく考えずにJoin SOPでやってみて、それで出来ない場合は今回のようなVEXを使って処理するほうが良いんじゃないかとご指摘頂きました。本当にそうですね。
既存のノードで、まずできるのか試してみて出来ないときに何が問題で出来ないのか考えて、その対処のためにVEXを書くのが筋だと思います。
そこで、この記事ではなにを伝えたいのかというと
PROJECT TITAN RAILS TOOLなどを試してみてポイント繋げてみようとしたときに交差などによって既存のノードだけでは処理ができないときにどういうアプローチで対処するのかということです。
https://www.sidefx.com/ja/tutorials/project-titan-rails-tool/
Houdiniにはどういうノードがあるのかを把握するのと一緒に、VEXではどう書くのかも意識出来ると良いと思います!
もう一つアドバイスとして
一つか二つの条件分岐と@なんとかでVEXが書けなさそうなときは、Run overは Detail にしてしまうことです。
pointやprimでなんとか出来ないかと悩むくらいなら並行処理のことは忘れて、for foreachでループしてしまうとあっけなく出来ることが多いです。
全体のゴール
- もともとカーブのインデックスが崩れている状態から...
- ルールにしたがって「つなぐべき端点(カーブの先端のポイント)」を見つけて...
- その 2 点のあいだに ポリライン(線)を 1 本引く ことで、カーブ同士をつなぐ。
このとき主に使うのは、次の 2 つです。
- Connectivity SOP ... カーブごとに ID(class アトリビュート)を振る
- Detail Wrangle(VEX) ... 端点を探したり、端点同士を結ぶ処理を書く
まずは「単純な 2 本のカーブをつなぐ」ケースから始めて、そのあとで「ランダムなインデックスを持つカーブ同士の一番近い端点をつなぐ」応用パターンを見ていきます。
ケース 1:インデックス順でシンプルにつなぐ
最初は、いちばんシンプルなケースです。
-
Connectivity SOP でカーブごとに
classアトリビュートを振る -
VEX(Detail Wrangle)で
-
class = 0のカーブの「最後のポイント」 -
class = 1のカーブの「最初のポイント」を探す
-
-
見つけた 2 つのポイントのあいだに、
addprimでポリラインを 1 本追加する
イメージとしては、「class=0 の終点」と「class=1 の始点」を 1 本の線でブリッジする感じです。
VEX コード(ケース 1)
以下は、その処理を行っている VEX です。
// -------------------------------------------------------------
// 目的:
// 2つの「class」グループの端点(始点と終点)を見つけて
// それらを結ぶポリラインを1本作成する。
// 実行モード:
// Detail (only once)
// -------------------------------------------------------------
// 全ポイント数を取得(デバッグ用)
int total_points = npoints(0);
// class アトリビュートに含まれるユニークな値(クラスID)を取得
int class_id_array[] = uniquevals(0, "point", "class");
// 始点と終点を格納する配列(後で addprim に渡す)
int startend[];
// 各クラスIDについて処理
foreach (int class_id; class_id_array) {
// 指定したクラスに属するポイント番号をすべて取得
int points_in_class[] = findattribval(0, "point", "class", class_id);
// class=0 の場合 → 最後のポイントを「終点」として取得
if (class_id == 0) {
int class0_end = max(points_in_class);
append(startend, class0_end);
setpointgroup(0, "class0_end", class0_end, 1);
}
// class=1 の場合 → 最初のポイントを「始点」として取得
if (class_id == 1) {
int class1_start = min(points_in_class);
append(startend, class1_start);
setpointgroup(0, "class1_start", class1_start, 1);
}
}
// 始点と終点を結ぶポリラインを作成
addprim(0, "polyline", startend[0], startend[1]);
このあと、VEX で接続に使った一時的なアトリビュートやグループを削除しています。
「接続に必要な処理はしたいが、それ以外の情報はできるだけ元のままにしておきたい」ためです。
ケース 2:ランダムなpointインデックスを持つカーブ同士で「一番近い端点」をつなぐ
ここからは、もう少し複雑なケースです。
- カーブごとに
classを振るところまでは同じ - ただし どのカーブ同士をつなぐか を、今回は「距離が一番近い端点ペア」で決めたい
つまり、
- それぞれのカーブの端点を拾う
- 端点同士の距離をすべて調べる
- いちばん距離が短い組み合わせだけをつなぐ
という流れで考えます。
ベースになるセットアップ
Connectivity SOP でカーブを分類するところまでは、ケース 1 と同じです。
そのうえで、いろいろな場面で使い回せるように、処理を小さなブロックに分けています。
頻繁に使うようであれば、HDA にまとめておくとさらに便利です。
汎用処理 1:アトリビュートの値ごとにポイントグループを作る
まずは、特定のポイントアトリビュート(class や id など)を見て、値ごとに自動でポイントグループを作る処理です。
たとえばアトリビュートを attr_name とすると、
-
attr_name = 0のポイント →grp_attrname_0 -
attr_name = 1のポイント →grp_attrname_1
のようなグループを、自動で全部作ってくれます。
// Run Over: Detail (only once)
int npt = npoints(0);
string attr_name = chs("target_attribute");
int var[] = uniquevals(0, "point", attr_name );
foreach(int i; var){
int pt_attr[] = findattribval(0, "point", attr_name, i);
foreach(int pt; pt_attr){
setpointgroup(0, sprintf("grp_\%s_\%s",attr_name, i), pt, 1);
}
}
この処理を使うと、
- アトリビュートが変わっても
- 値がいくつあっても
同じロジックでグループ分けできるので、セットアップの使い回しがしやすくなります。
場合によってアトリビュート名のパラメーターを変えることができます。
汎用処理 2:カーブの「端点」だけを拾う
次は、カーブの中で 端点になっているポイントだけを探してグループ化 する処理です。
-
neighbourcount(0, i) == 1になっているポイントは、「線の端」にあたるポイント - それらをすべて
grp_startEndというポイントグループに入れています
// Run Over: Detail (only once)
int npt = npoints(0);
string grp_class_array[] = detailintrinsic(0, "pointgroups");
int grp_startEnd_array[];
for(int i=0 ; i<npt; i++){
// 末端ポイントの取得
if(neighbourcount(0, i)==1){
int pt_grp = setpointgroup(0, "grp_startEnd", i, 1);
append(grp_startEnd_array, i);
}
}
このように、
- 「アトリビュートごとのグループ分け」
- 「カーブの端点だけを集める」
といった処理を部品として用意しておくと、
- どの端点同士をつなぐかを変える
- 別の条件でフィルタする
といった応用がやりやすくなります。
Scene Viewer 上で、ここまでの手順で作成したポイントグループを可視化し、正しくグルーピングできているか確認できます。
距離にもとづいて「一番近い端点ペア」をつなぐ
ここからが本題です。
- Group Combine SOP などで、
classごとに端点グループを作る(例:grp_class0_startEnd,grp_class1_startEnd) - それぞれの端点同士の距離をすべて計算する
- もっとも距離が短い組み合わせだけを選んで、その 2 点をポリラインで結ぶ
次の VEX では、その処理を実装しています。
// Run Over: Detail (only once)
int pts_grp_class0_startEnd[] = expandpointgroup(0, "grp_class0_startEnd");
int pts_grp_class1_startEnd[] = expandpointgroup(0, "grp_class1_startEnd");
float min_dist = 1e6; // 非常に大きな値で初期化(1,000,000)
int pt_min_pos0;
int pt_min_pos1;
// class0 の端点と class1 の端点を総当たりでチェック
foreach(int pt_class0; pts_grp_class0_startEnd)
{
vector pos0 = point(0, "P", pt_class0);
foreach(int pt_class1; pts_grp_class1_startEnd)
{
vector pos1 = point(0, "P", pt_class1);
float dist = distance(pos0, pos1);
if (dist < min_dist)
{
min_dist = dist;
pt_min_pos0 = pt_class0;
pt_min_pos1 = pt_class1;
}
}
}
// 見つけた「最短距離ペア」を結ぶポリラインを作成
addprim(0, "polyline", pt_min_pos0, pt_min_pos1);
ここでカーブ同士はポリラインでつながりますが、ポイント番号(インデックス)は元のままなので、
後続の処理によっては扱いにくい場合があります。
そのため、次に インデックスの整列 を行います。
一度不要なアトリビュートとグループを削除する
インデックスを整理する前に、
- もう使わない中間グループ
- 一時的なアトリビュート
などをいったん削除しておきます。
これは、後で自分や他の人がデータを見たときに「何が生きていて何が不要か」をわかりやすくするためです。
インデックス整列の準備:再び端点を取得
次に、インデックス整列に使うための端点をもう一度取得します。
ここでは、先ほど紹介した「端点だけを集める」処理を使い回し、
- 今回は
classは 1 つだけ - その中で端点を 2 つ拾う
という形にしています。
この 2 つの端点を、「始点」「終点」としてインデックス整列の基準にします。
インデックスの整列
端点のうち、
- インデックスが小さい方を「始点」
- インデックスが大きい方を「終点」
として、それぞれ別のポイントグループに分けます。
// Run Over: Detail (only once)
int npt = npoints(0);
int pts_grp_class0_startEnd[] = expandpointgroup(0, "grp_startEnd");
int start = min(pts_grp_class0_startEnd);
int end = max(pts_grp_class0_startEnd);
setpointgroup(0, "start", start, 1);
setpointgroup(0, "end", end, 1);
この start / end グループを、後続の Find Shortest Path に渡します。
- Start Points →
start - End Points →
end
という形で指定します。
こうすることで、カーブのポイントインデックスが整列された状態になり、
後続の処理で扱いやすくなります。
Find Shortest Path の注意点
ただし、ひとつ注意があります。
Find Shortest Path はカーブを再生成するため、
- 元のポイントが持っていたアトリビュート
- もともとのポイントグループ
が、そのままでは引き継がれません。
そのため、カーブの形は整ったけれど「大事なアトリビュートやグループが消えてしまう」という状態になります。
そこで、次のステップとして 距離にもとづいたアトリビュート・グループのコピー を行います。
距離にもとづくアトリビュートとグループのコピー
標準の Attribute Copy は「インデックスの一致」を前提にすることが多いため、
今回のようにインデックスが変わるケースではそのまま使いにくいです。
そこで、
- 入力 1 に「元のカーブ」
- 入力 0 に「整列後のカーブ」
を入れ、各ポイントの位置をもとに「いちばん近いポイントからアトリビュートやグループをコピー」するしくみを作ります。
copy_proximity_attribute(アトリビュートのコピー)
int string float vector2 vector3 vector4 matrix3 matrix4 のすべてのアトリビュートを対象にしています。
// Run Over: Detail (only once)
int npt = npoints(0);
// 入力1のpointアトリビュートをすべて取得
string attrs[] = detailintrinsic(1, "pointattributes");
// "P"を除外(位置はコピーしない)
int idxP = find(attrs, "P");
if (idxP >= 0)
removeindex(attrs, idxP);
for (int i = 0; i < npt; i++)
{
vector pos0 = point(0, "P", i);
int pt1 = nearpoint(1, pos0);
foreach (string attr; attrs)
{
int type = attribtype(1, "point", attr);
int size = attribsize(1, "point", attr);
// コピー先にアトリビュートがなければ作成(型を推測して一致させる)
if (!haspointattrib(0, attr))
{
if (type == 0 && size == 1)
addpointattrib(0, attr, 0); // int
else if (type == 1 && size == 1)
addpointattrib(0, attr, 0.0); // float
else if ((type == 1 && size == 2) || (type == 2 && size == 2))
addpointattrib(0, attr, {0, 0}); // vector2
else if ((type == 1 && size == 3) || (type == 2 && size == 3))
addpointattrib(0, attr, {0, 0, 0}); // vector3
else if ((type == 1 && size == 4) || (type == 2 && size == 4))
addpointattrib(0, attr, {0, 0, 0, 0}); // vector4
else if (type == 1 && size == 9)
addpointattrib(0, attr, matrix3(0)); // matrix3
else if (type == 1 && size == 16)
addpointattrib(0, attr, matrix(0)); // matrix4
else if (type == 3)
addpointattrib(0, attr, ""); // string
}
// 値を取得してコピー(同じく型を推測)
if (type == 0 && size == 1)
{
int v = point(1, attr, pt1);
setpointattrib(0, attr, i, v, "set");
}
else if (type == 1 && size == 1)
{
float v = point(1, attr, pt1);
setpointattrib(0, attr, i, v, "set");
}
else if ((type == 1 && size == 2) || (type == 2 && size == 2))
{
vector2 v = point(1, attr, pt1);
setpointattrib(0, attr, i, v, "set");
}
else if ((type == 1 && size == 3) || (type == 2 && size == 3))
{
vector v = point(1, attr, pt1);
setpointattrib(0, attr, i, v, "set");
}
else if ((type == 1 && size == 4) || (type == 2 && size == 4))
{
vector4 v = point(1, attr, pt1);
setpointattrib(0, attr, i, v, "set");
}
else if (type == 1 && size == 9)
{
matrix3 v = point(1, attr, pt1);
setpointattrib(0, attr, i, v, "set");
}
else if (type == 1 && size == 16)
{
matrix v = point(1, attr, pt1);
setpointattrib(0, attr, i, v, "set");
}
else if (type == 3)
{
string v = point(1, attr, pt1);
setpointattrib(0, attr, i, v, "set");
}
}
}
copy_proximity_group(ポイントグループのコピー)
// Run Over: Detail (only once)
int npt = npoints(0);
string ptgroups[] = detailintrinsic(1, "pointgroups");
foreach (string grp; ptgroups)
{
for (int i = 0; i < npt; i++)
{
vector pos0 = point(0, "P", i);
int pt1 = nearpoint(1, pos0);
if (inpointgroup(1, grp, pt1))
setpointgroup(0, grp, i, 1, "set"); // 自動でグループ作成
setpointgroup(0, grp, i, 1, "set"); // 必要なグループが自動で作成される
}
これで、
- 形状は Find Shortest Path で整ったカーブ に対して
- 元のカーブが持っていたアトリビュート・グループ を「位置の近さ」に基づいてコピーし直す
ことができます。
最終的に、
- 途切れたカーブ同士を「ルールにしたがって」つなぎつつ
- 後から使いたいアトリビュートやグループ情報も維持したまま
処理を進められるようになります。
















