※この記事ではHoudini 17.0.416 Apprenticeバージョンを使用しています。
※Houdiniの基本的な操作やウィンドウの役割等は他のチュートリアルで既に分かっている前提です。
この記事の目的
カプセル型のポリゴンモデルを作りたいなぁと思ったのですが、カプセル作るノードはHoudiniではデフォルトで有りませんでした。
そこでカプセルを作るノードを作成し、SphereやTubeなどのようにSceneView上で形状を微調整可能に出来るように実装をしたのでその方法のメモです。
この記事を見た人はVEXでカプセル形状のポリゴンモデルの作り方と、ハンドルを追加してSceneView上でサイズなどのパラメータを調整出来るように出来るようになります。
ノード構成
はい、その通りです。VEXで全部やってしまうのが一番手っ取り早かったのです…
他にもSphereを半分に分けて間にTubeを挟み込んでfuseしたり、sphereの半分をグループ化してPolyExtrudeで押し出したりでも出来るのですが、頂点の並び順が気に入らず、それを修正するようにすると余計な処理が増えてしまって非効率になってしまい、結局最初からスクリプトできれいに並べてあげるのが手っ取り早かったのです。
これについてはこだわりポイントのところで軽く説明します。
ノードの設定とVEXpression
とにかくスクリプトだけのなので、サクッとコードだけ載せてしまいます。
以下の通り、"Run Over"は"Detail(only once)"に設定します。
コードは以下の通りです。
#include "math.h"
// parameters
float hsheight = 0.5;//chf("../hsheight");
vector size = {1, 2, 1};//chv("../size");
vector center = {0, 0, 0};//chv("../center");
int hsrows = 6;//chi("../hsrows");
int tubesplitnum = 6;//chi("../tubesplitnum");
int cols = 24;//chi("../cols");
// constant values
int geohandle = geoself();
vector pos_offset = center + set(0, size.y * 0.5, 0);
float radx = size.x * 0.5;
float radz = size.z * 0.5;
// ===================
// create points
// ===================
// polar points
addpoint(geohandle, set(0, 0, 0) + pos_offset);
addpoint(geohandle, set(0, -size.y, 0) + pos_offset);
// upper hemisphere
for (int row = 0; row < hsrows; row++)
{
float rt = M_PI * 0.5 * ((float)(row+1) / hsrows);
float cy = 1 - cos(rt);
float sy = sin(rt);
for (int col = 0; col < cols; col++)
{
float ct = M_PI * 2.0 * ((float)col / cols);
vector pos = {0, 0, 0};
pos.x = radx * cos(ct) * sy;
pos.y = -hsheight * cy;
pos.z = radz * -sin(ct) * sy;
pos += pos_offset;
addpoint(geohandle, pos);
}
}
// tube
for (int row = 0; row < tubesplitnum; row++)
{
float rt = ((float)(row+1) / (tubesplitnum+1));
float cy = (size.y - hsheight*2) * rt;
for (int col = 0; col < cols; col++)
{
float ct = M_PI * 2.0 * ((float)col / cols);
vector pos = {0, 0, 0};
pos.x = radx * cos(ct);
pos.y = -cy - hsheight;
pos.z = radz * -sin(ct);
pos += pos_offset;
addpoint(geohandle, pos);
}
}
// lower hemisphere
for (int row = 0; row < hsrows; row++)
{
float rt = M_PI * 0.5 * (1 - ((float)row / hsrows));
float cy = 1 - cos(rt);
float sy = sin(rt);
for (int col = 0; col < cols; col++)
{
float ct = M_PI * 2.0 * ((float)col / cols);
vector pos = {0, 0, 0};
pos.x = radx * cos(ct) * sy;
pos.y = hsheight * cy - size.y;
pos.z = radz * -sin(ct) * sy;
pos += pos_offset;
addpoint(geohandle, pos);
}
}
// =======================
// create primitives
// =======================
// upper polar triangles
for (int col = 0; col < cols; col++)
{
int p1 = 0;
int p2 = 2 + ((col + 1) % cols);
int p3 = 2 + col;
addprim(geohandle, "poly", p1, p2, p3);
}
// quads
int primrows = hsrows + hsrows + tubesplitnum - 1;
for (int row = 0; row < primrows; row++)
{
for (int col = 0; col < cols; col++)
{
int ofs = row * cols;
int p1 = 2 + col + ofs;
int p2 = 2 + ((col + 1) % cols) + ofs;
int p3 = p2 + cols;
int p4 = p1 + cols;
addprim(geohandle, "poly", p1, p2, p3, p4);
}
}
// lower polar triangles
int ofs = primrows * cols;
for (int col = 0; col < cols; col++)
{
int p1 = 1;
int p2 = 2 + col + ofs;
int p3 = 2 + ((col + 1) % cols) + ofs;
addprim(geohandle, "poly", p1, p2, p3);
}
結果の確認
うまく行けばSceneView上に以下のようなポリゴンモデルが作成出来ます。
こだわりポイント
先に説明した通り、他にも方法はあるのですが、既存のノードを組み合わせて作る方法はあまり気に入らないデキになるので却下しました。
具体的に何が"気に入らない"のかを軽く説明しておきます。
"気に入らない"の例
Sphereの上半分をグループ化してから"PolyExtrude"の"Front Transform"で押し出した場合です。
ノード構成は以下のようにして、上半分をboundingboxなどでグループ化して押し出します。
結果は以下のようになります。
このように頂点の順番が飛んでしまっていてキレイになっていません。
元のSphereにあった両極は0と1である事も崩れてしまっています。
そのため、これを修正するためにsortノードなどを使って並び替える必要がありますが、大量の頂点をソートするのは理屈上負荷が大きそうなので出来れば避けたいのです。(気にしすぎかもしれませんが…)
理想の状態(+簡単なコード解説)
理想としては以下の要件を満たすようにしたいと考えました。
- Sphereのように両極が0と1になっている。
- pointとprimitiveが上から順番に並んでいる。
そこでとりあえず最初に両極にpointを追加します。
頂点を追加するには**addpoint**関数を使います。
...
// polar points
addpoint(geohandle, set(0, 0, 0) + pos_offset);
addpoint(geohandle, set(0, -size.y, 0) + pos_offset);
...
Sphereノードで作った球体と同じように、両極の頂点の番号が0と1になっています。
そして、あとはsinとcosで円を作りながらカプセルの形にしていくだけです。
上部の半球→チューブ状の部分→下部の半球という順番でpointをきれいに並べていきます。
最初に上部の半球です。
// upper hemisphere
for (int row = 0; row < hsrows; row++)
{
float rt = M_PI * 0.5 * ((float)(row+1) / hsrows);
float cy = 1 - cos(rt);
float sy = sin(rt);
for (int col = 0; col < cols; col++)
{
float ct = M_PI * 2.0 * ((float)col / cols);
vector pos = {0, 0, 0};
pos.x = radx * cos(ct) * sy;
pos.y = -hsheight * cy;
pos.z = radz * -sin(ct) * sy;
pos += pos_offset;
addpoint(geohandle, pos);
}
}
変数rtとそこから計算されるcyでyを移動させ、syはyの高さに合わせてxとzをスケールします。
変数ctはxとzをそれぞれsin関数とcos関数とかけることでXZ平面に円の形に移動させます。
これで半球が出来ます。
次はチューブの部分です。
// tube
for (int row = 0; row < tubesplitnum; row++)
{
float rt = ((float)(row+1) / (tubesplitnum+1));
float cy = (size.y - hsheight*2) * rt;
for (int col = 0; col < cols; col++)
{
float ct = M_PI * 2.0 * ((float)col / cols);
vector pos = {0, 0, 0};
pos.x = radx * cos(ct);
pos.y = -cy - hsheight;
pos.z = radz * -sin(ct);
pos += pos_offset;
addpoint(geohandle, pos);
}
}
やっていることは半球を作るときとそんなに違いはありません。yに合わせてxzをスケールする必要はないので、指定数分だけ円を作るだけです。
最後に下部の半球です。
// lower hemisphere
for (int row = 0; row < hsrows; row++)
{
float rt = M_PI * 0.5 * (1 - ((float)row / hsrows));
float cy = 1 - cos(rt);
float sy = sin(rt);
for (int col = 0; col < cols; col++)
{
float ct = M_PI * 2.0 * ((float)col / cols);
vector pos = {0, 0, 0};
pos.x = radx * cos(ct) * sy;
pos.y = hsheight * cy - size.y;
pos.z = radz * -sin(ct) * sy;
pos += pos_offset;
addpoint(geohandle, pos);
}
}
これは上部の半球と反対の形になるようにrtが1から0になるようにしています。
これで頂点の配置が全て完了しました。
その後、各頂点を使ってprimitiveを作っていきます。
primitiveの追加には**addprim**を使います。
// =======================
// create primitives
// =======================
// upper polar triangles
for (int col = 0; col < cols; col++)
{
int p1 = 0;
int p2 = 2 + ((col + 1) % cols);
int p3 = 2 + col;
addprim(geohandle, "poly", p1, p2, p3);
}
// quads
int primrows = hsrows + hsrows + tubesplitnum - 1;
for (int row = 0; row < primrows; row++)
{
for (int col = 0; col < cols; col++)
{
int ofs = row * cols;
int p1 = 2 + col + ofs;
int p2 = 2 + ((col + 1) % cols) + ofs;
int p3 = p2 + cols;
int p4 = p1 + cols;
addprim(geohandle, "poly", p1, p2, p3, p4);
}
}
// lower polar triangles
int ofs = primrows * cols;
for (int col = 0; col < cols; col++)
{
int p1 = 1;
int p2 = 2 + col + ofs;
int p3 = 2 + ((col + 1) % cols) + ofs;
addprim(geohandle, "poly", p1, p2, p3);
}
これでスクリプトは終わりです。
とにかく順番に上から並べるという処理でした。
理想の頂点順にしようとするとスクリプトで最初からきれいに並べる方法が一番シンプルに実装が出来ました。
とりあえず、これで一旦完成とします。
HDA化して調整用のハンドル付ける
せっかく作ったのだからもっと形を調整出来るようにして、どこでも使い回せるようにしたいです。
そこでHDA化してしまいます。
調整用パラメータを追加してHDAにする
HDAの作成方法については、ドキュメントにも書いてあるしググれば沢山出てくるので細かい事は割愛します。
とりあえず、パラメータを調整出来るようにノードを全て選択(Ctrl+A)し、サブネット化(Shift+C)します。
調整用のパラメータの追加の仕方については以前投稿した記事を参考にして頂けるとうれしいです。
調整用のパラメータは以下の通り追加します。
これは全て、VEXpressionの最初の方に書いていたパラメータと同一です。
Name | Label | Type | 役割 |
---|---|---|---|
hsheight | HemiSphereHeight | Float | 半球の高さ |
size | Size | Float Vector 3 | 全体のサイズ |
center | Center | Float Vector 3 | 全体の中心位置 |
hsrows | HemiSphereRows | Integer | 半球の横分割数 |
tubesplitnum | TubeSplitNum | Integer | チューブ部分の分割数 |
cols | Columns | Integer | 縦の分割数(SphereやTubeのColumnsと同じ意味) |
以下のように追加しました。
パラメータはとりあえず、VEXpressionに書いたものと同じにしておきます。
次に、subnet1ノードの中に入ってcreate_capsuleノードを選び、コードのパラメータの部分のコメントアウトしてあるch関数と入れ替えます。
// parameters
float hsheight = 0.5;//chf("../hsheight");
vector size = {1, 2, 1};//chv("../size");
vector center = {0, 0, 0};//chv("../center");
int hsrows = 6;//chi("../hsrows");
int tubesplitnum = 6;//chi("../tubesplitnum");
int cols = 24;//chi("../cols");
// parameters
float hsheight = chf("../hsheight");
vector size = chv("../size");
vector center = chv("../center");
int hsrows = chi("../hsrows");
int tubesplitnum = chi("../tubesplitnum");
int cols = chi("../cols");
階層を一つ戻ってsubnet1ノードを選択、パラメータを調整して反映されているかを確認します。
問題がなければsubnet1ノードを右クリック、Create Digital Assetを選択、適当に名前を付けてHDAを作成します。
名前はとりあえず無難にcreate_capsuleとしておきました。
ハンドルを付ける
これだけでも十分使えるのですが、形状を微調整したい時に毎回パラメータに数値を入れて行くのは苦労します。
そこで、ハンドルを使ってSceneView上で形を調整出来るようにします。
ちなみにハンドルとは↓のようなものの事です。(マニピュレーターともいうかもしれない)
まず、HDAのノードを右クリックして**Type Properties...**を選択し、Edit Operator Type Propertiesのウィンドウを開きます。
その後、Handlesのタブを選び、Create Handleをクリックします。
色々出てきますが、今回は一番操作が近そうな**Bounding Box (boundingbox)**を選びました。
追加するハンドルを選ぶと、右側にどのパラメータを連動させるかを設定する項目が出てくるので、直接パラメータ名を打ち込むか、右側にある▼のボタンを押して選択します。
centerとsizeを設定し終わったらAcceptを押して反映させます。
SceneViewでEnterキーを押してハンドルを表示します。
これで形状の調整が簡単になりました。
最後にHDAの保存をするのを忘れずに。
ノードを右クリックしてSave Node Typeを選択してHDAを保存します。
以上で完了です。
ハンドルは他にも種類があるようなので便利な使い方があったら教えてください!
あとがき
以前のあとがきで、出来るだけWrangleノードは使わずに行きたいなと考えていたのですが結構難しいです。
ズバリの機能が無かったり単にアトリビュートを追加したい場合とかはWrangleノードでサクッとやってしまうのがベストなのかもしれません。
【追記】gistにサンプルコード掲載
もうちょっと洗練させたコードをgistにあげました。
https://gist.github.com/mamemame360/762151e83f67f90850b11dac270a1c48
【追記2】Sweep(2.0)を使ってやる方法(Houdini18から)
Houdini18で改良されたSweepを使うと、VEXを書かずに2ノードだけでカプセルを作る事が出来ます。
※Houdini 18.348 Apprenticeバージョンで確認。
- lineとsweepノードを作って、lineをsweepに繋ぎます。
- sweepのSurface ShapeをRound Tubeにします。
- 同じくsweepのEnd Caps>End Cap TypeをGridにします。
- Radiusを適当な大きさ(ここでは0.5)に設定します。
lineのLengthでカプセルの長さ、Pointsで、筒部分の分割数、sweepのColumnsで半球部分の縦方向分割数、Cap Divisionsで半球の横方向分割数が指定出来ます。
さらに、End Cap ScaleとEnd Cap Roundnessなどを調整すると、半球部分の形状も調整出来るのでとても便利です。