リアルタイム向けにこんもりした山を作る


完成図

はじめに

参考画像
  • こんな風にこんもりしている山を作りたい!
  • UE4でなるべくリアルタイムに動くような方法がいい

という二点を満たすのが今回の目的です。

方法

UE4用にHDAを作ります。HDAの中でHeightFieldを作成し、木をインスタンス配置します。木は事前に作成したUE4のアセットです。

UE4用のHDAでインスタンスを配置する方法

FBXのアセット等をインスタンス化し、HDAでプロシージャルに配置したいことがあります。そういう時は@unreal_instanceという文字列型アトリビュートを点に持たせます。

このアトリビュートにはアセットのリファレンスを持たせます。リファレンスは、UE4でアセットを右クリックのメニューでリファレンスをコピーで取得できます。ちなみに、この方法で配置できるアセットはmeshだけではなく、Particleのエミッター等、UE4でシーンに配置できるものであればいろいろと指定することができます。

HDAで作成したmeshやheightfieldにマテリアルを張り付ける方法

@unreal_materialという文字列アトリビュートをPrimitiveにつけると、UE4のマテリアルやマテリアルインスタンスを張り付けることができます。アトリビュートの値は@ureal_instanceと同様にマテリアルアセットのリファレンスです。

環境

HoudiniとUE4のバージョンです。
- UE4.26.0
- Houdini 18.5.436

HDAの構成

  1. 山のベースを作成
  2. 山のディテールを追加
  3. 木の密度と大きさのマスクを作成
  4. 木の種類のマスクを作成
  5. 密度と種類に従って木を配置
  6. 地形にUE4のマテリアルを設定

1. 山のベースを作成

今回は山の形を決め打ちで作りたかったので、カーブを引いてそれに合わせて山ができるようにしました。自分で引いたカーブとちょうど重なっている所が一番高いところになり、離れるにしたがってだんだん標高が低くなってくるようにします。

まずこんな感じにカーブを引きます。

次に、各点の高さをアトリビュートに記録してから、高さを0にします。

そして、for loopノードを使いつつ各カーブに対して以下のvolume wrangleを実行します。

height_from_curve
int prim;
vector uv;
float distance = chf("distance");
xyzdist(1,@P,prim,uv,distance); //重要


if(prim!=-1)
{
    vector pos = primuv(1,"P",prim,uv);
    float height = primuv(1,"height",prim,uv);
    float trans = 1.0 - 
    length(@P - pos)/distance; //重要
    trans = chramp("ramp",trans);
    height = lerp(@height, height, trans);

    @height = max(@height,height);
}

この処理はheightfieldの各ピクセルに対して行われます。各ピクセルから見て一番近くにあるカーブの位置をxyzdistで取得します。そしてその地点における、先ほどカーブの各点に持たせたheightの値を取得します。最後に、取得したheightを距離に応じてスケールダウンさせていきます。

2. 山のディテールを追加

カーブから作成したベースにノイズと侵食を加えて自然な見た目にします。今回は山の表面が木で見えなくなってしまうため、あまり凝ったことはしませんでした。行ったことは以下の三点です。
1. Sparse Convolution ノイズを加算
2. HeightField Distort by NoiseをCurlで適用
3. erodeを実行
各パラメータはほとんど初期値のままです。Houdiniの侵食は他ソフトの侵食と比べて丸みがあって柔らかい形状になりやすく、これが今回出したいイメージにあっていたからです(同じ侵食表現でも使うソフトによってそれ特有の味が出ます)。

3. 木の大きさと密度を決める

ここでは木を配置する準備をします。何をするかというと、山の各場所での木の大きさと密度を制御するマスクをそれぞれ作ります。まずは大きさマスクを作ります。大きさマスクは、HeightField mask by Featureで作った諸々のマスクと、ノイズの組み合わせで作ります。任意枚のマスクを組み合わせる時に活躍するのが、最近SideFx Labsに追加された HeightField Combine Maksです。


HeightField Combine Masks は好きな枚数だけHeightFieldのレイヤーをあたかもPhotoshopのように合成できます。従来同様の事をしようと思った場合は、重ねたいレイヤーの枚数分Height Layerを設置しなければならず、そうするとノードの数が増えてしまい可読性も落ちてしまいます。
大きさマスクは。ノイズから侵食でできたdebrisや水を引き算し、occlusionを乗算することで山の溝ほど木が徐々に小さくなるようにしました。

次に密度マスクです。密度はどのように決めたかというと、木の大きさが小さいところほど密度が高くなるようにしました。もし木の大きさに関わらず密度が一定だった場合、木が小さいところほど地肌が見えてしまうことに気が付いたからです。こんもりした山は地面がほぼ見えないはずなので木が小さいところは沢山配置して地肌が見えないようにしました。

4. 木の種類を決める

木に何種類かバリエーションを設けないと無機質な見た目になってしまうのでランダムに選んで配置するようにします。ただし、均等なランダムではなく地形の情報に基づいてある程度傾向を持たせます。
今回は4種類の木を置くことにしました。この4種類を白黒のマスクで指定します。白黒のマスクだけでどうやって4種類のうち各要素を指定するかというと、まず1.0を4(木の種類の数)で割った数である0.25に各樹木のインデックを掛けた値を塗ります。
すると、マスクの値x木の種類の数が木のインデックスになります。具体的には、0.0の所=0番目の木, 0.25の所=1番目の木, 0.5の所=2番目の木, 0.75の所=3番目の木といった具合に白黒の値から、どの木を配置するべきかが一意に定まります。
以下は各木のマスクです。










木0のマスク(0番目なので0が乗算される) 木1のマスク
木2のマスク 木3のマスク

この状態を満たすマスクは、まず、各木の配置マスクを作り、各マスクに1.0/木のインデックスを掛けたものをHeightField Layerでmaxブレンドして用意しました。

5. 木の配置

まずHeightField Scatterで木のインスタンスを配置しました。赤く囲ったところが、HeightField Scatterのパラメータのうち調節した箇所です。

  • 先ほど作った密度マスクを使うため、Scatter MethodをBy Density using Mask Layerにします。
  • Densityの値が密度マスクに乗算されます。沢山置きすぎると重くなるので適度な密度にします。
  • 大きさマップに加えて均等なランダムさを加えたいので Uniform Distributionを使います。木が地肌を隠すように気持ち大きめ気味にします。
  • Relax Pointsは必ずoffにします。Relax Pointsがonになっているとマスクに関わらず密度が均一になります。
  • 点だけほしいので Keep Incoming Terrainをoff
  • 木は常にy軸(UE4だとz)にまっすぐ配置されてほしいので、 Match Normals with Terrainをoff
  • ランダムにy軸方向に回転してほしいので Randomize Yawを最大に

点を生成したら、大きさマスクと種類マスクの値を点に代入します。

set_mask_value_to_points
vector pos = set(@P.x,0,@P.z);

vector index = volumepostoindex(1,"mask",pos);

@varience = volumeindex(1,"mask",index);

この時マスクの値をpointSampleしたいのでindexを介して値をサンプルします。heightFieldは2dのボリュームなので座標からサンプルするときはy軸を0にしてください。種類は@varienceに、大きさは@pscaleに代入します。

6. 点に木のアセットを代入

木の種類を@varienceというアトリビュートで点に持たせましたが、次はこの値に基づいて実際にどのアセットを各点に持たせるかを決定します。
まずは、配置したい木をprimitiveとして用意します。

HDAのパラメータにmulti blockのフォルダを作成し、そこにリファレンスを記入していく方式にしました。今回は4種類なのでmulti blockのサイズを4つ分前提でHDAを組みましたが、multi block自体は任意個分にすることが可能です。

import_ue4_assets
int num_assets = chi("../assets");

for(int i=0; i<num_assets; ++i)
{

    int prim = addprim(0,"polyline");
    string name = chs("../asset_path_"+itoa(i+1));
    setprimattrib(0,"name",prim,name);
}

次にHDAの中でdetail attributeを用意し、その中でmulti blockの各要素を読み込みます。パラメータに指定したリファレンスの値を順番に取得し、primitiveを生成、そのprimitiveにnameアトリビュートを作ってリファレンスの値を代入します。この時、multi blcokのインデックが0からではなく1から始まる事に気を付けてください。これで、一つのジオメトリの中に4つのprimitiveができ、各primitiveのnameアトリビュートにリファレンスが代入された状態ができました。

次に、各点にどのアセットをコピーさせるかをセットします。この処理はHoudini 18.5で追加された Attribute from Piecesで簡単に実現できます。 Attribute From Piecesの一つ目のインプットに点群を、二つ目にnameアトリビュートにリファレンスが代入されたジオメトリをつなぎます。

そして、Map Attributeに種類マスクの値が代入されている@varienceを指定し、4つのアセットを指定します。

これでHDAは完成です。後はHDAをUE4のシーンに配置し、4つのアセットのリファレンスをパラメータに打ち込むとこの画像の状態になります。


そして最後に、天球、SkyAtomosphere、Exponential HeightFog、Skylight、PostProcessVolumeを配置して完了。