この記事はHoudini Advent Calendar 2021 19日目です
はじめに
Houdini(3DCG)を今年の3月から始め、はや9か月が経ちました。楽しい時間はあっという間に過ぎ去ってしまいますね。私はHoudiniをモデリングをするための用途で利用していますが、PolyWire SOPやSweep SOPででた形状に対して、更にディテールを詰め込むような処理をしたことがないな。とふと思ったためこの記事を書き始めました。この記事では下の画像の左3つのWireのディテールについて紹介します。また作成したhipファイルはこちらから取得できますので、分かりづらい等があれば直接hipファイルを見ていただければと思います。
1本目: ボロノイを利用しWireに6角形状の模様をつける
ボロノイを利用し6角形状の模様を作るにために、6角形状に分割できるようなポイント群を作る必要があります。
今回はSweep SOPのUVとAttribute VOPのhextileを利用し、6角形状に分割できるようなポイントを求めます。
ボロノイ分割に自体には詳しく取り上げませんが、a_saitoさんのSIGGRAPH ASIA 2018の動画がわかりやすいため、不安な方はそちらに目を通してみるとよいかもしれません。
以下が6角形状に模様をつけるまでの流れです。(ピンク色のノードはパラメトリックに形状を変化させれるノードです。) ![Screenshot 2021-12-17 15-05-58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177955/24145d64-9c0f-b75f-a337-2ce8acfe4b9f.png)
まずSweep SOPで利用する、Wireの断面を適当に作ります。orient along curve SOPで円の外側に向けて法線が向くようにします。
上記の法線に従いポイントにノイズを加えることでCircleを歪ませます。
次にSweep SOPのUVs and Attributesオプションを利用しジオメトリにuvアトリビュートを付与します。このuvアトリビュートを利用しhextileや後の文字配置をおこないます。
hextileの精度を上げるためにremeshでハイポリのメッシュにし、hextileの入力に必要なuvを頂点からポイントへプロモートします。hextileを使うattribute vop内は次のようになっています。
やっていることとしては、先ほどプロモートしたポイントのuv属性をもとにhextileノードに入れています。hextileノードのアウトプットはhexuv
をデバッグ用にCd
に接続し、id
を__hex_id
として属性をポイントに付けています。この時に表面は次のようなカラーになっています。
6角形状上にuvがついているのが分かりますね、またidもそれぞれの6角形ごとに付くためcolor SOPで可視化した場合は以下のようになります。
hextile上に領域を分けられたので、extract centroid SOPを利用しそれぞれの中点を求めます。この中点をボロノイ分割に利用します。実際にこの点を利用しvoronoi fracture SOPをかけた後は次のように分割されています。
後は、polycut等を利用しながら良い感じにメッシュに起伏をつけることで6角形状の模様にすることができます。
2本目: Wireの表面に文字を配置する
Wireの表面に文字を配置する処理の流れは以下の図の通りです。こちらも1本目と同様に、Sweep SopのUVを利用します。
今回、UVを使って文字を配置するのは以下の理由です
- 3次元よりもUV空間で配置を考えたほうが楽だから
- 3次元 ⇔ UVの行き来をよく使い慣れているから
では文字を配置する手順について見ていきます。
文字を配置するに当たり、UVの縦横の長さと配置したい文字列の縦横の長さを知る必要があります。UVの縦横の長さに関してはattribute create SOPとattribute promote SOPを利用し、各ポイントのx,yのmaxを取ることで得ています。一方で文字列のほうはatribute create SOPでbbox(0, D_XSIZE), bbox(0, D_YSIZE)
のような感じでそれぞれの長さを取得しています。次は文字を綺麗に配置するための点とpscaleを求めます。
今回は以下の2種類の方法で配置を決めています。
配置する文字の数を元に点を配置する
変更可能な動的なパラメーターは以下の通りです
- y_num - y(v)方向の文字列の数
- y_padding - y(v)方向の文字列の配置間隔
- x_padding - x(u)方向の文字列の配置間隔
はじめに上記のパラメータとuv,文字列の長さを元にpscaleの値を求めることができます。
指定したパラメーターでuvに収まるようなpscaleを未知数とし方程式を立てると、解は下記のコードのようになると思います(間違ってたらごめんなさい)。
float uv_with = detail(0, "with"); // uvの横の長さ
float uv_height = detail(0, "height"); // uvの縦の長さ
float font_with = detail(1, "with"); // 文字列の横の長さ
float font_height = detail(1, "height"); // 文字列の縦の長さ
int y_num = ch("y_num");
float y_padding = ch("y_padding");
float x_padding = ch("x_padding");
float pscale = max((uv_height - y_num*y_padding), 0)/(y_num*font_height);
また、pscaleが定まったことでx方向への文字列の数が定まります。
int x_num = floor(uv_with/(pscale*font_with+x_padding));
次にそれぞれのパラメータを使いポイントを配置し、pscaleを設定します。
for(int j=0; j < x_num; j++){
for(int i=0; i<y_num; i++){
int pt = addpoint(0, set(j*font_with*pscale+j*x_padding, i*font_height*pscale+i*y_padding,0));
setpointattrib(0, "pscale", pt, pscale);
}
}
文字の大きさを元に点を配置する
文字の数を元にポイントを配置すのと同じような感じで方程式を立て解を求めることで、文字の大きさを元にポイントを配置することができます。
float font_with = detail(1, "with");
float font_height = detail(1, "height");
float scale = ch("scale");
float y_padding = ch("y_padding");
float x_padding = ch("x_padding");
int y_num = floor(uv_height / (font_height*scale + y_padding));
int x_num = floor(uv_with / (font_with*scale+x_padding));
for(int j=0; j < x_num; j++){
for(int i=0; i<y_num; i++){
int pt = addpoint(0, set(j*font_with*scale+j*x_padding, i*font_height*scale+i*y_padding,0));
setpointattrib(0, "pscale", pt, scale);
}
}
上記のpointとpscaleを元にCopy Stamp SOPで文字をコピーすることで、UV上に文字が配置されます。
最後にUV上に配置された文字を元の場所(3次元)へもどします。Houdiniには、UV座標を元に対象のgeometoryのアトリビュートを読む便利なvex関数uvsample
があります。今回はwrangleを利用し@P = uvsample(1, "P", "uv", @P);
のようなコードで元の形状に沿うようにポイント移動させています。
他にも精度良く配置する方法は沢山あると思うので、トライしてみてください🤗
3本目: WireからWireを生やす
WireからWireが生える表現について見ていきます。
まずベースのpolylineをorient along curve SOPで接線方向に法線を設定します。(法線を設定すると軸は下記のようになっているはず)
このベースのpolylineからwireが生える始点と終点のポイントをCarve SOPを使って取り出します。Carve SOPのExtract OptionでDivisionsで指定した分だけのポイントを抽出することができます。(今回は始点と終端なのでDivisionsを2にしている。)
始点と終端が取れたので、このポイントを元にwireを生やしていきます。
Wireの生やし方
先ほどの始点と終点には一番初めにorient along curve SOPでつけた法線があるので、それを元に回転したポイントを新しく作成します。以下がイメージ図です。
終点は始点と比べた時に逆向きに回転していることが分かります。このような法線を軸に回転したポイントをえるためにクォータニオンを利用します。始点側のクォータニオンを利用したコードは以下のようになっています。
vector4 rotN = dihedral(set(0,1,0), @N);
vector up = qrotate(rotN, set(0,0,1));
float move_outside_amount=ch("move_amount");
float yaw_angle_amount = ch("yaw_angle_amount");
float angle=radians(-chf("angle"));
for (int i=0; i<chi("num"); i++){
vector4 rot = quaternion(i*angle, @N);
vector qN = qrotate(rot, up);
vector nN = lerp(qN, @N, yaw_angle_amount);
int pt = addpoint(0, @P+normalize(qN)*move_outside_amount);
setpointattrib(0, "N", pt, nN);
setpointgroup(0, "p"+itoa(i), pt, 1);
}
removepoint(0, @ptnum);
y軸を法線方向に回転させるようなクォータニオンをもとにz軸を回転させupベクトルとしています。(本来upベクトルがy軸でz軸が法線なのがhoudiniの慣習で読み辛いと思いますがご容赦ください。)
次に法線をn度回転させるようなクォータニオンを元にupベクトルを回転させます。このupベクトルに沿ってpointを中心から指定分移動させた新しいポイントを作成しています。この新しいポイントに設定している法線はWireの生える角度を調整するために、upベクトルと元の法線とを線形補完したものを入れています。
終点側のコードは始点側とほとんどおなじなので、折り畳みでおいておきます。
\終点/
vector4 rotN = dihedral(set(0,1,0), @N);
vector up = qrotate(rotN, set(0,0,1));
float move_outside_amount=ch("move_amount");
float yaw_angle_amount = ch("yaw_angle_amount");
float angle=radians(chf("angle"));
for (int i=0; i<chi("num"); i++){
vector4 rot = quaternion(i*angle, -@N);
vector qN = qrotate(rot, up);
vector nN = lerp(qN, -@N, yaw_angle_amount);
int pt = addpoint(0, @P+normalize(qN)*move_outside_amount);
setpointattrib(0, "N", pt, nN);
setpointgroup(0, "p"+itoa(i), pt, 1);
}
removepoint(0, @ptnum);
上記で追加されたポイントを元にwireの支柱のベースになるpolylineを作成します。
int pt = addpoint(0, @P+@N*chf("length"));
int prim = addprim(0, "polyline");
addvertex(0, prim, pt);
addvertex(0, prim, @ptnum);
setpointgroup(0, "edge", pt, 1);
このpolylineをSweep SOPに繋げ支柱部分を作成します。
次に支柱から支柱へとをpolylineで結びます。始点側の支柱のedgeにあたる部分を元にcopy to pointしpolylineを生成します。それをnear pointを使い支柱のpolylineと結ぶことで支柱から支柱を結ぶようにしています。
おわりに
本記事ではwireにディテールを付け加える方法について紹介しました、この記事が誰かのお役に立てば幸いです。
今年も後残り僅かですが、良いHoudiniライフを...!