[Houdini] Houdini Technical Artist Procedural HDA関連 031 HDAのパラメータ設計とメッシュ交換システム

[Houdini] Houdini Technical Artist Procedural HDA Related 031 Parameters and Interchangeable Meshes

① attribpaint1 を選択

ノードをクリック
② ビューポートで
Enterキー押す
③ すると変わる
カーソルがブラシになる

Geometry Spreadsheet 

Scene View | Animation Editor | Render View | 👉 Geometry Spreadsheet 

Alt + Shift + G

vector Nn = normalize(@N);

// maskで生やす場所制御
if (@mask < 0.5) {
    removepoint(0, @ptnum);
    return;
}

// 急斜面を除外
if (Nn.y < 0.7) {
    removepoint(0, @ptnum);
    return;
}

if (s@name == "tree" && rand(@ptnum) < 0.7) {
    removepoint(0, @ptnum);
}
if (s@name == "tree" && @mask < 0.8) {
    removepoint(0, @ptnum);
}

// 傾き調整
vector worldUp = {0,1,0};
Nn = normalize(lerp(worldUp, Nn, 0.3));

// 向き
matrix3 m = dihedral({0,1,0}, Nn);
//p@orient = quaternion(m);

if (s@name == "tree") {
    // 木は基本まっすぐ
}
else if (s@name == "rock") {
    float angle = rand(@ptnum) * M_PI * 2;
    p@orient = qmultiply(p@orient, quaternion(angle, {0,1,0}));
}
else {
    // 草はちょい倒す
    vector tilt = normalize(Nn + rand(@ptnum));
    p@orient = quaternion(dihedral({0,1,0}, tilt));
}





// ひねり
float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

// サイズ
@pscale = fit01(rand(@ptnum), 0.5, 1.5);

float d = length(@P);
@pscale *= fit(d, 0, 10, 1.5, 0.3);

// 浮かせる
@P += Nn * (@pscale * 0.5);

// 種類切り替え
float r = rand(@ptnum * 17.123);

if (r < 0.33) {
    s@name = "tree";
    @pscale *= fit01(rand(@ptnum + 100.0), 0.9, 1.4);
}
else if (r < 0.66) {
    s@name = "rock";
    @pscale *= fit01(rand(@ptnum + 200.0), 0.05, 0.06);
}
else {
    s@name = "grass";
    @pscale *= fit01(rand(@ptnum + 300.0), 0.4, 0.9);
}

1.HDA最短ルート

今のネットワークをそのままHDAにするんじゃなくて、UEで触りたい入口だけ残して整理する。

やる順番はこれ。

1. HDA用の最終ノードを1本にまとめる

今は途中ノードが多いから、最後に Null を1個作って名前を

OUT_FOLIAGE

にする。

UEに持っていく出力はこれだけにする。

2. UEで触りたい値だけパラメータ化する

まず必要なのはこのへんだけでいい。

  • Scatterの数
  • Mountainの高さ
  • maskしきい値
  • tree / rock / grass の出現率
  • 各種類のスケール

たとえば Wrangle でベタ書きしてる数値を消して、チャンネル参照に変える。

今こういうのを

if (@mask < 0.5) {
removepoint(0, @ptnum);
return;
}

こうする。

float mask_threshold = ch("mask_threshold");
if (@mask < mask_threshold) {
removepoint(0, @ptnum);
return;
}

3. 種類の比率もパラメータ化

今のランダム分岐もUEで触れるようにする。

float tree_ratio = ch("tree_ratio");
float rock_ratio = ch("rock_ratio");float r = rand(@ptnum * 17.123);if (r < tree_ratio) {
s@name = "tree";
@pscale *= fit01(rand(@ptnum + 100.0), 0.9, 1.4);
}
else if (r < tree_ratio + rock_ratio) {
s@name = "rock";
@pscale *= fit01(rand(@ptnum + 200.0), 0.25, 0.6);
}
else {
s@name = "grass";
@pscale *= fit01(rand(@ptnum + 300.0), 0.4, 0.9);
}

これなら UE 側で tree_ratio と rock_ratio を触れる。

4. 地形入力を固定生成から「外部入力」に変える

UEで使うなら、Houdini内の grid1 をそのまま使うより、UEの地形やメッシュを入力できる形の方が強い。

最初の段階ではこうすると早い。

方法

grid1 の代わりに Object Merge を置く。
そのObject Mergeの元に、HDA入力を使う。

最初は難しく感じるから、超最短なら今はこうでもいい。

  • 今の grid1 → mountain1 構成のままHDA化
  • UE上では「生成ツール」として使う
  • その後、入力メッシュ対応に拡張する

まず持っていくことが優先なら、これで十分。

5. 不要な表示用ノードを外す

merge1 は確認用だから、HDA出力には入れない方が楽。

出力は基本、

copytopoints1 → OUT_FOLIAGE

だけでいい。

地面まで一緒に出したいなら別Nullにする。

OUT_INSTANCES
OUT_TERRAIN

でも最初は OUT_FOLIAGE だけでいい。

attribwrangle1 の完成版

vector Nn = normalize(@N);

float mask_threshold = ch("mask_threshold");
if (@mask < mask_threshold) {
    removepoint(0, @ptnum);
    return;
}

float slope_threshold = ch("slope_threshold");
if (Nn.y < slope_threshold) {
    removepoint(0, @ptnum);
    return;
}

vector worldUp = {0,1,0};
float upright_blend = ch("upright_blend");
Nn = normalize(lerp(worldUp, Nn, upright_blend));

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

float base_scale_min = ch("base_scale_min");
float base_scale_max = ch("base_scale_max");
@pscale = fit01(rand(@ptnum), base_scale_min, base_scale_max);

float tree_ratio = ch("tree_ratio");
float rock_ratio = ch("rock_ratio");
float r = rand(@ptnum * 17.123);

if (r < tree_ratio) {
    s@name = "tree";
    @pscale *= fit01(rand(@ptnum + 100.0), ch("tree_scale_min"), ch("tree_scale_max"));
}
else if (r < tree_ratio + rock_ratio) {
    s@name = "rock";
    @pscale *= fit01(rand(@ptnum + 200.0), ch("rock_scale_min"), ch("rock_scale_max"));
}
else {
    s@name = "grass";
    @pscale *= fit01(rand(@ptnum + 300.0), ch("grass_scale_min"), ch("grass_scale_max"));
}

float offset_mult = ch("offset_mult");
@P += Nn * (@pscale * offset_mult);

最初に作るパラメータ

この名前で作ればかなり使いやすい。

mask_threshold   = 0.5
slope_threshold = 0.7
upright_blend = 0.3

base_scale_min = 0.5
base_scale_max = 1.5

tree_ratio = 0.33
rock_ratio = 0.33

tree_scale_min = 0.9
tree_scale_max = 1.4

rock_scale_min = 0.05
rock_scale_max = 0.06

grass_scale_min = 0.4
grass_scale_max = 0.9

offset_mult = 0.5

HDA化の手順

1

最終的に使うノード群を全部選ぶ

2

サブネット化する

3

そのサブネットを右クリックして Create Digital Asset

4

名前を付ける
たとえば

furcraea_foliageScatter

5

Type Properties を開いて、今作ったパラメータを見えるようにする

UEに持っていくときの最初の形

最初は欲張らずこうするのが早い。

  • 入力なし
  • HDAを置く
  • パラメータを触る
  • インスタンスが更新される

これでまず成功体験を作る。

その後に

  • Static Mesh Input
  • Landscape Input
  • World Partition対応
  • bake

を足せばいい。

最終ノード構成

[VARIANTS]
box1
└→ attribwrangle2 // s@name="tree";
└→ pack1
└→ attribwrangle5 // s@name="tree";

sphere1
└→ attribwrangle3 // s@name="rock";
└→ pack2
└→ attribwrangle6 // s@name="rock";

tube1
└→ attribwrangle4 // s@name="grass";
└→ pack3
└→ attribwrangle7 // s@name="grass";

attribwrangle5 / 6 / 7
└→ merge_variants

[PLACEMENT]
grid1
└→ mountain1
└→ normal1
└→ attribpaint1
└→ scatter1
└→ attribwrangle1

[OUTPUT]
merge_variants ───────────────→ copytopoints1 (左)
attribwrangle1 ───────────────→ copytopoints1 (右)
copytopoints1 ────────────────→ OUT_FOLIAGE

HDAで外に出すパラメータ

まずはこれだけで十分。

地形

  • mountain_height
  • scatter_count

マスク・斜面

  • mask_threshold
  • slope_threshold
  • upright_blend

全体サイズ

  • base_scale_min
  • base_scale_max

出現率

  • tree_ratio
  • rock_ratio

種類別サイズ

  • tree_scale_min
  • tree_scale_max
  • rock_scale_min
  • rock_scale_max
  • grass_scale_min
  • grass_scale_max

浮かせ量

  • offset_mult

それぞれどこに仕込むか

mountain1

Height を promote
名前は mountain_height

scatter1

Force Total Count を promote
名前は scatter_count

attribwrangle1

今の ch("...") がそのまま出せる

1回HDAにしたあとの出力方法がわからない

① HDAとしてそのままUEで使う(←メイン)

手順

① OUTノードを確認

最後がこれになってるか👇

copytopoints1 → OUT_FOLIAGE

👉 Nullの名前が重要

OUT_FOLIAGE

② HDA化

  1. objレベルに戻る
  2. box_object1 を右クリック
  3. 👉 Create Digital Asset
FoliageScatter
  1. 保存:
.hda

③ 保存(超重要)

👉 HDAは「Save Node Type」しないと更新されない

右クリック → Save Node Type

④ Unreal Engine 側

  1. Houdini Engine Plugin 有効化
  2. .hda をContent Browserにドラッグ
  3. レベルに置く

⑤ 出てこない場合チェック

  • OUTノードが無い
  • OUTじゃないノードを表示してる
  • copytopointsの後じゃない

これ、エラーじゃなくて**「UIレイアウトどうする?」って確認ダイアログ**だから安心していい。

✅ 今回はこれ押す

👉 Revert Layout


UEのバージョンはどれにする?
Houdini 21.x
👉 UE5.6 / 5.7 が正解

① Houdini側にあるPluginを使う

C:\Program Files\Side Effects Software\Houdini 21.0.xxx\engine\unreal

C:\Program Files\Side Effects Software\Houdini 21.0.671\engineに

unreal

ない

① Houdini Launcher開く

あるなら

C:\Program Files\Side Effects Software\Houdini Engine\Unreal\21.0.671

BLANK

Plugins
にいれる

Houdini Engineにチェックいれる

HDAをドラッグ

正しいノード構成

Subnetwork Input #1

object_merge1 Object1=../Subnetwork Input #1
object_merge1 Object1=../subnetwork_input1
object_merge1 Object1=opinputpath("..", 0)

scatter1

attribwrangle

copytopoints1

OUT_FOLIAGE

いみない


attribwrangle8

vector Nn = {0,1,0};
if (len(@N) > 0.0) {
    Nn = normalize(@N);
}

if (haspointattrib(0, "mask")) {
    float mask_threshold = ch("mask_threshold");
    if (@mask < mask_threshold) {
        removepoint(0, @ptnum);
        return;
    }
}

float slope_threshold = ch("slope_threshold");
if (Nn.y < slope_threshold) {
    removepoint(0, @ptnum);
    return;
}

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

@pscale = 1.0;



これで見える状態

書き出して



UEでも


これでいい この△だらけのデータをビビらずにUEに持っていく。

UE OK

さらに normalノード復帰

vector Nn = {0,1,0};
if (len(@N) > 0.0) {
    Nn = normalize(@N);
}

if (haspointattrib(0, "mask")) {
    float mask_threshold = ch("mask_threshold");
    if (@mask < mask_threshold) {
        removepoint(0, @ptnum);
        return;
    }
}

float slope_threshold = ch("slope_threshold");
if (Nn.y < slope_threshold) {
    removepoint(0, @ptnum);
    return;
}

if (length(v@N) > 1e-6)
{
    Nn = normalize(v@N);
}

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

@pscale = 1.0;

HOUDINIで動作確認後

書き出し

Normalノード追加で復帰した

この状態

vector Nn = {0,1,0};
if (length(v@N) > 1e-6)
{
    Nn = normalize(v@N);
}

if (haspointattrib(0, "mask"))
{
    float mask_threshold = ch("mask_threshold");
    if (f@mask < mask_threshold)
    {
        removepoint(0, @ptnum);
        return;
    }
}

float slope_threshold = ch("slope_threshold");
if (Nn.y < slope_threshold)
{
    removepoint(0, @ptnum);
    return;
}

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

@pscale = fit01(rand(@ptnum), 0.5, 1.5);

@P += Nn * @pscale * 0.2;

ch(“slope_threshold”);のパラメータ化

① attribwrangle8 選択

② 右のパラメータ欄の上にある

👉 ⚙️(歯車) → Edit Parameter Interface


③ 左側から追加

  • Float をドラッグ
  • 名前を設定👇
Name: slope_threshold
Label: slope_threshold

④ Default値入れる

例:

0.3

⑤ Accept
👉 Save Node Type(超重要)

② 一番上のノード(HDA本体)を選択

👉 furcraea_foliagescatter(一番外のやつ)


③ 右クリック

👉 Type Properties


④ Parametersタブ

今の状態👇

  • slope_threshold は「Wrangleの中」
  • HDA側には無い

⑤ ここでやる

左側の一覧から👇

👉 attribwrangle8 の slope_threshold をドラッグ

👉 右側(HDAのUI)にドロップ

../ を追加

vector Nn = {0,1,0};
if (length(v@N) > 1e-6)
{
    Nn = normalize(v@N);
}

if (haspointattrib(0, "mask"))
{
    float mask_threshold = ch("../mask_threshold");
    if (f@mask < mask_threshold)
    {
        removepoint(0, @ptnum);
        return;
    }
}

float slope_threshold = ch("../slope_threshold");

if (Nn.y < slope_threshold)
{
    removepoint(0, @ptnum);
    return;
}

matrix3 m = dihedral({0,1,0}, Nn);
p@orient = quaternion(m);

float angle = rand(@ptnum + 10.0) * M_PI * 2.0;
p@orient = qmultiply(p@orient, quaternion(angle, Nn));

@pscale = fit01(rand(@ptnum), 0.5, 1.5);

@P += Nn * @pscale * 0.2;


ちゃんと 1.01で消えた

👉 UE → Houdini → Wrangle → パラメータ制御

全部つながった

これはかなりデカい


🧠 今の到達点

要素状態
入力
scatter
orient
pscale
offset
slope制御
UEパラメータ連動

👉 プロシージャルツールとして成立してる

つまり問題はここ

👉 法線の分布が単調すぎる

ノード構成

furcraea_foliagescatter3 内で

Subnetwork Input #1

normal_input

attribnoise1

normal1

attribwrangle9 (f@slope を作る)

scatter1

attribwrangle8 (slope_threshold で削除)

copytopoints1

attribwrangle9 に入れるコード (scatter 前に slope を保存するノード)

ノード名: attribwrangle9
役割: scatter 前に slope 属性を作る

VEX 全文をこれに置き換えてください。

vector Nn = {0, 1, 0};
if (length(v@N) > 1e-6)
{
    Nn = normalize(v@N);
}

// slope: 0 = 平地, 1 = 垂直
f@slope = 1.0 - clamp(Nn.y, 0.0, 1.0);

接続:

normal1 → attribwrangle9 → scatter1

attribwrangle8 に入れるコード(scatter 後に slope_threshold で消すノード

ノード名: attribwrangle8
役割: scatter 後のポイントを slope で削除する

VEX 全文をこれに置き換えてください。使いやすいレンジに圧縮します。

float ui_threshold = ch("../slope_threshold");

// UIの0.0~1.0を、実際の有効域に再マップ
float slope_threshold = fit(clamp(ui_threshold, 0.0, 1.0), 0.0, 1.0, 0.8, 1.0);

if (f@slope > slope_threshold)
{
    removepoint(0, @ptnum);
    return;
}

@pscale = 0.05;

接続:
scatter1 → attribwrangle8 → copytopoints1

0.0から増えて1.0から増えなくなった 完璧だね
今の挙動なら slope_threshold が UE 側で素直に使える状態 になっています。

パラメータ制御 + メッシュの入れ替えができてます

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です