Houdiniでスケルトン天井のパイプのベースを自動生成する


Houdiniに手を付け始め、SOP&VEX本の7章までとりあえず読み進めてみたもののいざ自分で何か作ろうと思うとなんもわからん…ってことで練習に良さそうなテーマを選定してみた。

完成形

まずどんなものを作ったか伝えるために先にGIFを貼り付け。


何を作ったのか

本アセット random_pipe_generator は大きく分けて下記の2つからなる。

  1. グリッド分割された指定空間内で始点・終点を決め、最短経路までlineを伸ばすサブネット
  2. inputとして受け付けたlineに対して「パイプを這わす」サブネット

本記事で紹介するのは主に1つ目の部分。
2つ目のサブネットは下記の動画を元に作成している。

SideFX Tutorials | PROCEDURAL GENERATED PIPES
(「1: Generating Geometry」の動画)

モデル化

そもそも「スケルトン天井のパイプ」って何

こういうもの。
クロスなどで覆われておらず、打ちっ放しのコンクリートが剥き出しになっているような天井をスケルトン天井という。天井剥き出しのため、この写真のように天井を這っている配管が見えるのがかっこいい。


画像参考サイト

これをモデル化してみる

こんな感じで考えてみた。

  • 天井を這っているパイプの1本を選定
  • パイプが這う空間を3辺の長さが $(b_x, b_y, b_z)$ で示されるグリッド型直方体と考える
  • グリッド型直方体の対角線に位置する2頂点を選び、それらを始点/終点と考える
  • グリッド型直方体の始点/終点を結ぶための最短経路問題(高校の数学Aでやるやつ)をVEXで記述
  • せっかくなのでそれをアニメーションで表現

わかりにくいので図で補足。
ワイヤーフレームツールで書くのもしんどかったので、iPadにまとめた手書き図で。

実装

モデル化したらやりたいことの大半は完了、あとは書くだけ…なのだけど、書き慣れないのもあってここに1ヶ月ちょっと掛けてしまった。。

使っているオペレータはAttribute WranglePolyPathの2つだけ。

Attribute Wrangle内では下記のように実装している。

直方体の定義=経路ベクトルの定義

パラメータで受け付けた直方体の各辺の長さ(Integer)を$ b_x(1,0,0),b_y(0,1,0),b_z(0,0,1) $で分解しベクトルに格納する。


vector randVec[];

// パイプのルート用グリッドサイズ定義
int b_x = ch("op:../CONTROLLER/b_x");
int b_y = ch("op:../CONTROLLER/b_y");
int b_z = ch("op:../CONTROLLER/b_z");

// まずは全ての単位ベクトルを連結
for(int i=0; i<b_x; i++){
  push(randVec, {1,0,0});
}
for(int i=0; i<b_y; i++){
  push(randVec, {0,1,0});
}
for(int i=0; i<b_z; i++){
  push(randVec, {0,0,1});
}

経路ベクトルのシャッフル

経路をランダムに選択する部分をどう実現しようか悩んだ結果、「格納したベクトルの引数をシャッフルする」手法を選定することにした。

// 配列シャッフル
for (int i=0; i<len(randVec); i++) {
  int j = int(rand(i) * len(randVec));
  vector tmp = randVec[i];
  randVec[i] = randVec[j];
  randVec[j] = tmp;
}

因みに「0-1範囲で乱数(dice)を決め、その乱数に応じて1/3の確率でx,y,zいずれかの単位ベクトルを加算する」手法も書いてみたが、回数のうまい指定方法が浮かばず諦めた。。それになんかダサい。。

float dice = fit01(rand(i * chi("op:../CONTROLLER/seed")), 0, 1);

if (dice < 1.0/3){
    // X方向加算
}else if (1.0/3 <= dice && dice < 2.0/3){
    // Y方向加算
}else{
    // Z方向加算
}

pointの作成・連結

あとはシャッフルした経路ベクトルを順に加算し、point同士を連結していくのみ。

// 1つ目の座標は原点に配置
// それ以降のi番目の座標の位置は(i-1)番目の座標に加算して算出
addpoint(geoself(), 0);
setpointattrib(geoself(), "P", 0, {0,0,0}, "set");
vector pos = {0,0,0};

for(int i=1; i<len(randVec); i++){
  pos += randVec[i];
  addpoint(geoself(), i);
  setpointattrib(geoself(), "P", i, pos, "set");

  addprim(geoself(), "polyline", i-1, i);
}

この時点でランダム生成のlineが出来上がるので、Tutorialで作成した inputとして受け付けたlineに対して「パイプを這わす」サブネット に繋いであげると、昔のWindowsスクリーンセーバーで見たようなパイプが出来上がる。

遊び

せっかくなのでFor文の条件部に$Fを絡めると、冒頭の動画のようにシークバー?に応じてパイプが伸びる。

またせっかくなのでシャッフル時のrand()に外部パラメータseedを乗算してあげれば、seedの値を変えるたびにパイプのルートが変わる。

To be?

  • グリッド単位での別lineの「ぶつかり判定」をもたせる
  • ぶつかり判定を用いて別始点/終点から複数のパイプを這わせる
  • 「パイプを這わせたい天井空間」を与えて始点(=$V_s$)/終点(=$V_e$)自体ランダムに選定させ、 $(b_x, b_y, b_z) = V_e - V_s$ として上記サブネットに渡す

辺りだろうか。

この程度の作業でも相当ムダなことをしていそうなので、もっとラクにできそうな箇所などあればご指摘いただければ幸いです。