前回、VEXとエクスプレッションそのものの解説をしたので、家の制作の続きを行う前に第一回と第二回の記事で記述した VEX やエクスプレッションの解説を行います。
まずは distance と名付けた attribute wrangle SOP のコードです。
f@distance = length(v@centroid);
ここには @
の記法が登場しています。
f@distance
というアトリビュートに length(v@centroid)
を代入しています。
length
関数はベクトルの長さを返す関数です。
引数にはベクトルを入れます。
v@centroid
で上流の extract centroid SOP (重心を計算するノード)で作成した centroid アトリビュートを取得しています。
この attribute wrangle SOP の Run over (パラメータ)は Primitive に設定されているので、この VEX コードはそれぞれの Primitive で実行されます。
したがって、ジオメトリスプレッドシートを確認すると、それぞれの Primitive の distance アトリビュートに重心の原点からの距離が格納されています。
※上の画像のようにスキップフラグをオンオフすると、変化が分かりやすいです。
次に、ひとつ下の delete SOP の Group の記述です。
@distance>3
以前にも紹介しましたが、これは VEX でもエクスプレッションでもありません。
Group パラメータにだけ指定できる独自の記述方法です。
比較演算子こそ使えるものの、 VEX やエクスプレッションと比べても制限が多く、特殊な記法です。
詳しく知りたい方はこちらをご覧ください。
個人的には汎用性が低く読みずらいため、グループ名の直接指定(スペース区切り)や *
, !
や比較演算子などの割と一般的な表記以外はあまり使用していません。
次に、 rift_ridge と改名した attribute wrangle SOP の VEX です。
v@P.y += @edgedist;
このコードは簡単ですが、 P アトリビュートの y 座標(つまり高さ)に @edgedist を加算しています。
※ P をベクトルとして取り出すため、 @
の前に v
をつけています。
これにより棟の部分の高さが上がり、屋根のシルエットが浮かび上がります。
次に、 y_diff と改名した attribute wrangle SOP の VEX です。
f@y_diff = v@P.y - v@rest.y;
これは、 rest アトリビュートに保存した元々の高さ v@P.y
と、 ray SOP によって移動した移動後の高さ v@P.y
の差を計算して y_diff アトリビュートに保存しています。
y_diff アトリビュートはもともとは存在しませんが、 @
で呼び出したアトリビュートは存在しない場合は作成されます。
次に、 transform1 ノードの Translate パラメータの y に入れたエクスプレッションです。
-point(-1, 0, "y_diff", 0)
1つ目の引数の -1
で Spare Input からの入力(1つ目)を指定しています。( y_diff ノードにつながっています。)
また、2つ目の引数の 0
は 0 番目のアトリビュートか(ジオメトリスプレッドシートの行)を指定しています。(何番目のアトリビュートにも同じ値が入っています。)
3つ目の引数の "y_diff" ではアトリビュート名(ジオメトリスプレッドシートの列)を指定しています。
4つ目の引数の "0" では、アトリビュートの中の何番目の値かを(0 始まりで)指定しています。
P などでは x, y, z と 3 つ値があるので重要ですが、今回の y_diff の場合は1つしか値が無い(1次元)ので 0 を指定しておきます。
ここからまた家の制作に戻ります。
屋根の角度を指定できるようにするため、前回作成した left ridge (棟を上げる point wrangle )のコードを編集します。
以下のように編集しましょう。
v@P.y += tan(radians(ch("roof_angle")))*@edgedist;
// 元のコード
// v@P.y += @edgedist;
※下2行は //
を行頭に付けることでコメントアウト(無効化)しているので、入力しても挙動は変わりません。
コードを理解する前に、 VEX 入力欄の右上のボタンを押しましょう。
すると、 "Roof Angle" というパラメータが現れます。
このパラメータを 30 に設定すると屋根の傾斜が 30 度になり、 50 にすると 50 度になります。
屋根の角度を指定できるようになりました。
コードを説明します。
まず、カッコがたくさんあり混乱するかもしれませんが以下の部分は関数が3重になっています。
v@P.y += tan(radians(ch("roof_angle"))) * @edgedist;
一番内側が ch (チャンネル)関数です。
チャンネル関数は、パラメータの値を取得します。
ch("roof_angle")
の部分では roof_angle というパラメータを指定していますが、この名前のパラメータははじめ存在しません。
※もちろん存在するパラメータを指定することもできます。
その場合、先ほどのボタンを押すと Spare Parameter といって、 wrangle SOP にその名前のパラメータが作成されます。
少しマイナーな機能ですが、便利なので是非覚えておきましょう。
radians(ch("roof_angle"))
では ch 関数で取得した Roof Angle パラメータの値をラジアンに変換しています。
学生時代の復習になりますが、ラジアンとは0~360度で表す角度(degree)に対して、0~2πで表す角度です。
tan(radians(ch("roof_angle")))
ではラジアンに変換した角度をさらにタンジェントに変換しています。
これを @edgedist
にかける( *
)ことで、上げたい高さ(下の図の A )が計算できます。
それを +=
で v@P.y
に加算して棟の部分の高さを上げています。(edgedist が 0 のポイントでは A も 0 なので変化はありません。)
最後に、追加した Roof Angle パラメータの値の範囲を設定しておきましょう。
wrangle SOP の右上の歯車ボタンを押し、 Edit Parameter Interface をクリックします。
そして現れるパラメータの設定画面で、先ほど追加した Roof Angle パラメータを探してクリックします。
右の画面に Range という項目があるので、こちらを 1 から 89 に設定しておきます。
そして、両端のロックボタンを有効にしておきます。
このロックボタンを有効にするとこれ以上(または以下)の値の手入力を許可しないようになります。
最後に右下の Accept ボタンを押し、確定します。
すると、 Roof Angle パラメータのスライダーの最大値と最小値が 1 から 89 になってスライダーによる操作が行いやすくなりました。
また、範囲外の数値を手入力しても範囲内の数値に自動的に変更されます。
Roof Angle は 50 に戻しておきましょう。
VEX にも少し慣れたところで、家の UV 展開を行っていきます。
UV 展開ももちろんプロシージャルで行うため、 SOP を使って組み立てていきます。
UV 展開はテクスチャをモデルに張り付けるための重要な工程ですが、嫌われがちな地味な作業です。
しかし、だからこそプロシージャルな UV 展開を実装できるようになる価値は大きいので頑張りましょう。
まずは、屋根と壁を分離して別々に処理を行っておきます。
これまで、ほぼ一直線にノードを構成し、部分部分に処理を行いたい時(例えば屋根だけに処理をしたいとき)は Group で処理範囲を指定してきました。
それに対して、 Houdini ではあらかじめ根元でノードを分岐させて処理を視覚的に分割することもできるのでその方法も使ってみましょう。
最後の方に置いた transform1 SOP の下に、 split SOP を配置します。
split SOP が分岐させるためのノードです。
パラメータは次のように設定します。
roof
そして、 split SOP の表示フラグを付けると屋根だけが表示されます。
このノードには出力が 2 つ存在し、 Group パラメータで選択した部分が左(0 番目の出力)に、それ以外の部分が右(1 番目の出力)に仕分けられます。
Houdini では基本的に表示フラグを有効にしているとき、一番左の出力が Scene View に表示されるので、屋根のみが表示されています。
まずは壁から UV 展開を行っていきたいので、右の出力に壁が仕分けられていることを確認しましょう。
右の出力に null SOP をつなぎます。
null SOP は何もしないノードです。
null SOP の表示フラグを有効にすると壁だけが表示されることが分かります。
null SOP は今回のようにとりあえず出力を確認したい場合などに使われます。
※ほかの用途として、名札的に一連の処理の終端に配置してその処理の出力に名前を付ける際などにも使用されます。(この場合は null SOP の名前を変更して名前を付けます。)
次に、それぞれの壁を UV 展開したいので foreach primitive ブロックを繋げます。
null SOP は不要なので消しておきます。
そして、uv unwrap SOP を foreach primitive ブロックの中につなげます。
すると、壁がこのように UV 展開されます。
(市松模様のテクスチャが現れていない場合は画像の中心あたりにあるボタンを押せば表示されます)
UV展開でよく使用されるノードが uv unwrap SOP です。
UV展開をしたい際には一度このノードを試してみましょう。
Houdini での UV 展開の基本的な指針として、まずはSOPをいくつか試してみてそれでもうまくいかない個所を自分で実装していくのが良いと思います。
しかしよく見ると、1階と2回の切れ目の部分で UV が切れてしまっています。
これは、foreach primitive を使ってそれぞれの Primitive ごとにUV展開を行ったためです。
1階と2階の部分をつなげて展開するためには、foreach primitive ブロックのまえに dissolve flat edges SOP をつなぎ、一度1階と2階をそれぞれ一体化してからUV展開を行うように変更します。
すると、上の画像のように上下の切れ目が無くUV展開ができます。
メッシュ的な分割は復活させたいので、 uv transfer SOP を画像のように配置します。
すると、メッシュの分割が元に戻った状態でUV展開は切れ目なく行われた状態になります。
UV transfer SOP も使用頻度の高いノードで、左の入力(input 0)のUVに右の入力のUVを転写するノードです。
どのようにUV展開が行われているか確認するため、Scene View を調整しましょう。
まず、画像の手順で画面を分割します。
画面が分割されたら、片方のビューを UV ビューポートに変更します。
すると、UV展開された展開図が現れます。
各 Primitive がどのように配置されているかを確認するためには、 foreach ブロックの最後のノード(foreach end)の Single Pass パラメータにチェックを入れます。
そして、画像のように数字を変更するとどの Primitive がどこにどのように展開されいるかを確認できます。
Single Pass パラメータを有効にすると、 foreach ブロック反復して実行された処理のうちで指定した回数目の処理だけを行うことができます。(主にこのように確認などの用途です。)
ちなみにUV展開のやり方(配置)は用途によって変わってきますが、今回は Unreal Engine 5 でループテクスチャを貼り付けることを想定してUV展開を行っています。(したがってUVが被っていても問題ありません。)
今回のここからの内容は数学的な内容を含むので、人によっては難しいと思います。
ただ、扱えるととても役に立つ考え方を紹介しています。
あまり理解できない場合は、なんとなくこういったことができるということを学んでいただければと思いますので、気にせずコピペで先に進んでみてください。
また、講座全体に言えることですが、便利な知識をできるだけたくさん紹介するのが目的なので敢えて遠回りをして実装している個所がありますのでご了承ください。
次に、屋根のUV展開を行います。
先ほど配置した split SOP の左側に UV unwrap SOP をつなげます。
すると画像のように展開されます。
これでも用途によっては十分なのですが、 今回は後で紹介するちょっとした理由で下の画像の水色の部分が同じ島になっている(くっついて展開されている)のが不都合です。
なので、uv unwarp SOP の上に primitive split SOP を挟みます。
primitive split SOP の Attributes パラメータのチェックを外し、uv unwrap SOP の表示フラグを有効にすると、先ほどの部分が分離されて展開されることが分かります。
次に、この現状抱えている問題があるので、それを確認します。
uv unwrap SOP の表示フラグを有効にしたまま、上流の lift_ridge ノードの Roof Angle を 45 度以上の値に設定します。
屋根の向き(雨水の流れる方向)に数字が向かっていて、屋根の上下とテクスチャの上下が一致していて、問題ありません。
ただ、45度より小さい値に設定すると向きが揃ってしまい、屋根の上下とテクスチャの上下が揃っていません。
これだとテクスチャを貼った際に向きがめちゃくちゃになってしまいます。
なので、 Roof Angle が45より小さい値の場合のみUVを回転させる必要があります。
このような場合、ノードの条件分岐が必要なので switch if SOP を使います。
switch if SOP と null SOP を画像のようにつないでみましょう。
すると、画像のように switch if SOP の Enable パラメータの 1 と 0 を変更すると点線と実線が切り替わると思います。
(切り替わらない場合は、 switch if SOP の表示フラグを有効にしてください。)
つまり、switch if SOP は 1 の場合のみ右側を参照します。
この 0 と 1 を Roof Angle によって切り替えるためにまず、lift ridge ノードの Roof Angle パラメータを右クリックし、 Copy Parameter をクリックします。
そして、画像の手順で Enable パラメータに入力を行います。
<45
と付け足すすると、パラメータの内容は以下のようになります。
ch("../lift_ridge/roof_angle")<45
ch 関数は先ほども出て来ましたが、パラメータの値を取得するエクスプレッション関数です。
このように Copy Reference → Paste Relative References とすることで相対パスも込みでコピペして使うことができます。
これで、Roof Angle によって処理を変える仕組みを作ることができました。
Roof Angle を変更すると 45 を境目に点線と実線が切り替わっているのが分かります。
Roof Angle が 45 度より小さい場合に処理をする必要があるので、一度 45 より小さい値を設定しておきましょう。
まず、画像からどう回転させたいのかを整理する必要があります。
それぞれの面の向き(N:Normal)は赤色の矢印で、一方今画像が向いている方向は +Z 方向です。(方向を知るためにはビューポートの左下の座標軸を見ます。)
つまり、それぞれの Primitive のUVが回転してほしい方向は以下の画像のようになります。
これを実現するために、まず Normal SOP を右側につなぎます。
パラメータは Add Normals to を Primitive に設定します。
これで、 Primitive のアトリビュートとしてそれぞれの面の向き(N:Normal)が格納されました。
そして、primitive wrangle SOP (Run Over パラメータが Primitives に設定された wrangle SOP)をつなげて VEX を記入します。
入力する VEX は以下です。
vector roof_direction = v@N;
roof_direction.y = 0;
roof_direction = normalize(roof_direction);
vector z = {0, 0, 1};
f@angle = degrees(acos(dot(z, roof_direction)));
if(cross(roof_direction, z).y < 0){
f@angle = -f@angle;
}
今回は少し長めの VEX になりましたが、上から解説していきます。
まず上の3行、
vector roof_direction = v@N;
roof_direction.y = 0;
roof_direction = normalize(roof_direction);
この部分は、 1 行目で先ほど normal SOP で作った N アトリビュートを roof_direction 変数に格納し、
2 行目で y 座標に 0 を代入して、 x と z の二次元ベクトルの状態にします。
3 行目でベクトルの大きさが 1 以外になってしまっているので、 normalize 関数でベクトルの大きさをが 1 になるように再計算します。(ベクトルの向きは変わりません。)
つまり、 roof_direction 変数の中に屋根を上から見た際の二次元的な方向を格納しました。
次の5, 6行目、
vector z = {0, 0, 1};
f@angle = degrees(acos(dot(z, roof_direction)));
この部分では、+Z方向のベクトルを作り angle アトリビュートに roof_direction 変数との角度を計算して格納しています。
acos(dot(ベクトルA, ベクトルB))
の部分はベクトルA, Bがなす角度(ラジアン)を計算する式ですが、これはよく使用するので覚えておくと便利です。
数式の説明に入ります。
dot 関数は内積を計算する関数で、内積の式は以下の通りです。
ここで、二つのベクトルの大きさが 1 の場合、内積を計算するとコサインの値になります。
そして、 acos はアークコサイン関数で、コサインの逆関数です。
逆関数とは入出力が逆になった関数のことです。
コサインが角度からコサイン値(辺の比率)を計算する関数であるのに対し、コサイン値から角度を計算することができるのがアークコサインです。
図にすると以下のような形です。
degrees 関数は単にラジアンの角度を単位が「度」の角度に変換する関数です。
VEX の三角関数系の関数は基本的に角度の単位がラジアンなので「度」に変換するには degrees 関数を使います。
次に、最後の3行です。
if(cross(roof_direction, z).y < 0){
f@angle = -f@angle;
}
ここでは、 外積( cross 関数)を用いて角度が右回りか左回りかを判定し、左回りの場合はマイナス値にしています。
この用途での外積は図で理解すると早いです。
ベクトル A, B の外積は、AとBそれぞれに直角なベクトルになります。
ベクトル A, B のそれぞれに直角な(正規)ベクトルは2つ存在しますが、いわゆる右ねじの方向の方のベクトルが外積です。
右ねじの方向とは右回りのねじを締めたときにねじが進む方向です。(ベクトルAからBへの回転の方向がねじを締める方向です。)
別の説明をするとフレミングの左手を作った時に中指がA、人差し指がBとすると外積の方向は親指の方向です。
したがって、 Θ が逆回転になると外積の方向が真逆になります。
この性質を利用して、今回のように 2 つのベクトルがXZ平面上に存在するときに外積の向きから右回りか左回りかが判定できます。
もう一度最後の 3 行を見てみましょう。
if(cross(roof_direction, z).y < 0){
f@angle = -f@angle;
}
cross(roof_direction, z).y < 0
の部分で roof_direction
と z
ベクトルの外積の y 座標の正負を判定しています。
そして、負の場合は @angle = -f@angle;
として角度をマイナスにしています。
これで、角度が右回りか左回りか分かるようになりました。
ちなみに、if 文を使わずに以下のように書いても同じです。
vector roof_direction = v@N;
roof_direction.y = 0;
roof_direction = normalize(roof_direction);
vector z = {0, 0, 1};
f@angle = degrees(acos(dot(z, roof_direction))) * sign(cross(roof_direction, z).y);
※sign 関数は数値の符号を 1 か -1 で返します。
angle アトリビュートが正しく各 Primitive に入っているか確認しましょう。
ジオメトリスプレッドシートで Primitive の表を表示します。
どの Primitive にどの値が入っているかを可視化するため、 Visualize SOP を使うこともできます。
画像のように設定すると、 angle アトリビュートの値が Scene View で各 Primitive の上に表示されます。
次に、この値を使用して UV を回転させます。
Foreach Primitive ブロックをつなげます。(パラメータが画像のようになっているか確認してください。)
そして、uv transform SOP をつなげ、 Rotate パラメータの 3 つ目に以下のエクスプレッションを記述します。
prim(0, 0, "angle", 0)
この Rotate パラメータの 3 つの入力欄はそれぞれ U, V, W 軸の回転値で、W 軸は UV 平面に垂直な方向の(仮想的な)軸です。
今回は W 軸周りに回転をさせたいので 3 つ目のパラメータにエクスプレッションを記述しました。
そして、 switch SOP に表示フラグを立てたまま lift_ridge ノードの Roof Angle を変更して確認してみると、 45 度より小さい角度の場合でも UV の向きが正しい向きになっていることが確認できます。
次に気になる点が、 UV が上下に少し伸びていることです。
これは、 uv unwrap SOP の仕組みによるものです。
uv unwrap SOP は(デフォルトのパラメータで使用した場合)X, Y, Z 軸それぞれの方向にモデルを投影させるような形で UV 展開を行います。
Roof Angle が 45 度より小さい場合は Y 軸の方向に、45 度以上の場合は X 軸の方向に投影が行われますが、どちらの場合でも角度が付いているので歪みが生じてしまいます。
この歪みを修正するためには以下のように考えることができます。
まず、 45 度より小さい場合は下の図のようになります。
現在の歪んだ状態の比率 B から歪んでいない状態の比率 A に復元するためには、Roof Angle (図の Θ )のコサイン値の逆数を乗算すれば良いことが分かります。
45度以上の場合は以下のようになります。
コサイン値を計算する際の角度が 90 - Roof Angle に変わります。
UV の V 方向にこれらの値を乗算すれば治すことができます。
実際に実装していきましょう。
まず switch if SOP の下に foreach Primitive ブロックをつなぎます。
そして、その中に switch if SOP を新たにつなげ、条件式( Enable パラメータ)には以下のエクスプレッションを記述します。
ch("../lift_ridge/roof_angle")<45
先ほど switch if SOP に記述したものと同じです。
そして、左側に uv transform SOP をはさみ、 Scale の 2 つ目のパラメータ(V 座標)に 1/cos(90-ch("../lift_ridge/roof_angle"))
というエクスプレッションを記入します。
右側にも uv transform SOP をはさみ、同じ場所に 1/cos(ch("../lift_ridge/roof_angle"))
というエクスプレッションを記入します。
すると、以下のように UV の歪みが角度によらず修正されていることが確認できます。
バラバラの場所に UV 展開がされている状態になったので、これを整列します。
つぎに、 foreach ブロックの最後に uv layout SOP をつなぎます。
パラメータは以下の通りに変更します。
すると、 UV 展開された Primitive が左下( UV の原点)のほうに移動しました。
foreach end ノードに表示フラグを立てると、UV 上のすべての Primitive が左下に移動していることが分かります。
これで全ての壁と屋根の両方の UV 展開が完了したので、 Merge SOP で別れていた処理を統合します。
すると以下のようにすべての Primitive が UV 展開された家が Scene View に表示されます。
先述の通り今回の家にはループテクスチャ(タイル状にループするテクスチャ)を貼る予定で、その際にテクスチャにあわせて UV のスケールを変更できるようになっていると便利です。
例えば、2m×2mのテクスチャであれば 2 とパラメータを設定すれば UV の縮尺が変更されるような仕組みを作ります。
まずは、 merge SOP の下に foreach Primitive ブロックをつなぎます。
そしてその中に、 point wrangle SOP を挟み、以下の VEX を記述します。
int p0 = @ptnum;
int v0 = pointvertex(0, p0);
vector pos0 = v@P;
vector uv0 = vertex(0, "uv", v0);
int p1 = neighbour(0, p0, 0);
int v1 = pointvertex(0, p1);
vector pos1 = point(0, "P", p1);
vector uv1 = vertex(0, "uv", v1);
float P_dist = distance(pos0, pos1);
float uv_dist = distance(uv0, uv1);
f@uv_scale = P_dist/uv_dist;
このコードはざっくり説明すると、 3D (XYZ)空間上での Primitive の大きさと UV 空間上での Primitive の大きさの比率を求めるコードです。
上から順に説明していきます。
int p0 = @ptnum;
int v0 = pointvertex(0, p0);
vector pos0 = v@P;
vector uv0 = vertex(0, "uv", v0);
最初の 4 行では、対象の点を p0
とし、その 3D 空間上での位置 pos0
と UV 空間上での位置 uv0
を取得しています。
対象の点というのは、この wrangle SOP は point wrangle SOP (Run Over パラメータが Points に設定されている wrangle SOP)なので、それぞれの点です。
int p0 = @ptnum;
の部分でポイントの番号を取得し、 int v0 = pointvertex(0, p0)
で取得したポイントに対応する vertex の番号を取得します。
vector pos0 = v@P;
は v@P
の値(位置)を変数に格納しているだけです。(分かりやすさのため名前のついている変数に格納しています)
vector uv0 = vertex(0, "uv", v0);
は先ほど取得した vertex 番号を使って uv 座標を取得しています。
次の 4 行、
int p1 = neighbour(0, p0, 0);
int v1 = pointvertex(0, p1);
vector pos1 = point(0, "P", p1);
vector uv1 = vertex(0, "uv", v1);
こちらでは、最初の 4 行で取得したもの(3D空間上での位置とUV空間上での位置)を別の点で取得しています。
int p1 = neighbour(0, p0, 0);
が別の点の番号を取得している部分で、 neighbour 関数を使用しています。
neighbour 関数もよく使用する関数ですが、この関数はある点の隣にある点を取得します。
今回は p0
の隣にある点を取得するのに使用しています。
そして最後の部分、
float P_dist = distance(pos0, pos1);
float uv_dist = distance(uv0, uv1);
f@uv_scale = P_dist/uv_dist;
この部分で、 pos0 と pos1 の距離と uv0 と uv1 の距離を計算して、その比率を uv_scale
アトリビュートに保存しています。(ジオメトリスプレッドシートで確認することもできます)
次に、uv_scale
アトリビュートを point から detail に移します。(後で説明しますが、 detail というのはモデル全体を表しています。)
以前も使用した attribute promote SOP を使用します。
今回はパラメータは以下のように設定します。
最初の 3 パラメータはどのアトリビュートをどこからどこに移すかの設定です。
最後のパラメータは、アトリビュートの複数の値を集計するときの方法です。
ここで言う集計とは何かというと、複数の値をひとつの値にまとめるということです。
detail は point, vertex, primitive とは違って要素数の概念が存在せず、アトリビュートをひとつ設定するとその中に入っている値は 1 つの値です。(ここではベクトルや配列など複数の値がセットになっている値も 1 つの値とみなしています。)
なので、 uv_scale アトリビュートを point から detail に移動させる際、複数の値を 1 つの値にまとめる必要があります。
その方法に Average(平均) や Sum(合計)など様々な方法が用意されていますが、今回使用する Mode は最頻値です。(最もたくさん登場した値です)
※ジオメトリスプレッドシートで値が detail に移っているか確認してみてください。
次に、 uv_scale
を用いてテクスチャのサイズから uv スケールを変更できるようにします。
foreach ブロックの最後に uv_transform SOP を挟み、scale パラメータのすべてに以下のエクスプレッションを追加します。
detail(0, "uv_scale", 0)/ch("texture_size")
この ch 関数ではこの SOP 内に存在しないパラメータ texture_size
を参照しているので、パラメータを増設します。
右上の歯車ボタンを押し、 Edit Parameter Interface ボタンを押します。
すると次のような画面が出てくるので、画像の手順で texture_size パラメータを作成しましょう。
すると、 uv transform SOP の一番下にパラメータを追加することができました。
foreach end ノードに表示フラグを立てると、 UV スケールが統一されていることが分かります。
また、 texture_size パラメータを変更してみると、 UV のスケールが変更されることが確認できます。
UV 展開が完了しました。
最後に fuse SOP を付けましょう。
foreach Primitive ブロックで処理した際に、 Primitive 同士の結合が外れてしまうので、 fuse SOP で再度結合しておきます。
そして、 Output の方につなげるため、 Normal SOP につながっている線を fuse SOP につなげます。
最後にノードの整理をしていきます。
wrangle SOP には適切な名前を付けます。
そして、最後に今回作成した部分に色を付けて、 UV というラベルを付けておきましょう。
Project と HDA を忘れずに保存すれば、 #4 はこれで終わりです。
お疲れ様でした。