自前でLandscape Blueprint Brushを実装してみました

4.24で追加されたLandscape Blueprint Brush(以降Brushと略す)機能は非常に便利です。
アートの方はそれを使ってランドスケープの調整を行いましたが、公式で提供されたブラッシュには足りない感じがしましたので自前で新しいブラッシュを実装してみました。

今回の実装は4.26.2で行っております。まだLandmassやLandscape Blueprint Brushが実験的機能のため、今後のバージョンアップで仕様の変更等が入る可能性がありますのでご了承ください。

#実装するBrushの仕様
今回実装するBrushの仕様は以下の通りです：

- HeightmapとWeightmapのテクスチャー1セットを扱って一定範囲内の地形を再現します。
- XY平面上の回転や拡大縮小が出来ます。 
- Heightmapが元のランドスケープとブレンドできます。以下の4つのブレンドモードを実装します：
  - **加算**: 元の高さにHeightmapの高さを足したものを反映
  - **最大値**: 元の高さとHeightmapの高さの高い方を反映
  - **最小値**: 元の高さとHeightmapの高さの低い方を反映
  - **絶対値**: 元の高さを無視してHeightmapの高さを反映 
- *最大値*と*最小値*モードにおいては元のランドスケープと交差した部分がブレンドできる
- HeightmapとWeightmapの境界線付近で元のランドスケープとブレンドできる

#事前準備
実装を行う前には**Landmassプラグイン**を有効化にして、ランドスケープの「編集レイヤーを有効にする」のチェックボックスを入れておく。

まだWeightmapの影響を確認するためには簡単のマテリアルを作っておきます。
こんな感じです：
![LandscapeMaterial.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/7f4035dd-8ff7-7e31-b40e-ba4afd5c94b6.png)


#Brushの仕様解析

自前でブラッシュを実装するにあたって、仕組みを知っておく必要があります。
*/Landmass/Landscape/BlueprintBrushes/CustomBrush_Landmass*とエンジンのソースコードを解析した結果、Brushは以下のような動作をします：

- Brushをレベルに配置したときはInitialize関数が呼ばれて、初期化を行います。
- Brushに変更があった場合はRender関数が呼ばれて、渡されるRenderTarget2Dに対して変更処理を行ってランドスケープへの改変を実現する。実際の操作はマテリアルで行われます。

これらに踏まえて自前でBrushを実装するためにはInitializeとRender関数をオーバーライドして、HeightmapとWeightmapを操作するマテリアルを作成する必要があります。

#Brushの実装

##必要な初期設定
LandscapeBlueprintBrushクラスを継承したBPを作ってもランドスケープ編集モードで自作するBPの配置が出来ません。原因は新しいBrushを生成する部分のコードに以下のように実装されています：

```c++
// LandscapeEDModeBPCustomTools.cppより抜粋
// FLandscapeToolBlueprintBrush::BeginTool()の実装例

....
// Only allow placing brushes that would affect our target type
if ((DefaultObject->IsAffectingHeightmap() && Target.TargetType == ELandscapeToolTargetType::Heightmap) || (DefaultObject->IsAffectingWeightmap() && Target.TargetType == ELandscapeToolTargetType::Weightmap))
{
    ....(生成コードがここ)
}
```
DefaultObjectは生成するクラスのCDOを指しています、まだIsAffectingHeightmap()とIsAffectingWeightmap()はそれぞれALandscapeBluerpintBrushBaseの下記変数を参照しています：

```c++
	UPROPERTY(Category = "Settings", EditAnywhere, BlueprintReadWrite)
	bool AffectHeightmap; // IsAffectingHeightmap()が参照

	UPROPERTY(Category = "Settings", EditAnywhere, BlueprintReadWrite)
	bool AffectWeightmap; // IsAffectingWeightmap()が参照
```
つまりこの変数を使い道に応じてBPクラス内であらかじめ設定しておかないと生成ができません。今回実装するものはHeightmapを操作しますのでAffectHeightmapの方をtrueに設定します。

##Initializeの実装

今回実装するBrushは初期化処理で特殊な処理を行う必要がないので、Initialize関数はエンジン側で用意したBrushと同じ処理にします。
![initialize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/b3ef9288-ab48-98e3-c0a9-39ee05002e17.png)
Spawn Or Update Managerマクロの中身は/Landmass/Landscape/BlueprintBrushes/CustomBrush_Landmassと同じです。
##Heightmap処理の実装
Heightmapを一定範囲内に適用するためには以下の2つの作業が必要です：
1. Brushのワールド座標からHeightmapのレンダーターゲットのUV座標への変換
2. Heightmapの影響範囲の大きさの設定

座標系の変換については公式のブラッシュの実装を参考してBPで実装しました：
![CoordinatorChange.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/5dc06ae3-4e78-0df8-ae87-24f1bfb7c930.png)
変換した後のUV座標とBrushのXY方向のスケールをVectorParameterにパックしてマテリアル側に渡します。

まだ調整用のHeightmapの回転を対応するため、CustomRotatorノード用の回転値をCPU側で計算しておきます：
![RotationChange.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/b036a948-0fc4-6579-e00a-cfd356f5ec83.png)

上記パラメータを受け取ってマテリアル関数で調整用HeightmapをサンプリングするためのUVを計算する関数を実装しました：
![CalcUV.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/2457201e-14c3-b62b-24ae-0b5aa6e6ec79.png)

UVScaleはランドスケープのHeightmapの解像度とBrushの調整用Heightmapの解像度の比率です。

これを元に実装したHeightmapを調整するマテリアルの全体像はこちらになります：
![HeightmapMaterial.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/1e5abdee-94c3-c5ea-dcff-9b37b6b1c19b.png)

CalcBrushUVは先ほど実装したUV計算関数です。
ちゃんと影響範囲内のみHeightmapを適用するためにサンプリングUVが[0, 1]の間だけ適用するためにInBoundのカスタムノードに下記判定コードを入れました：

```
float2 inRange = step(0, UV.xy)*step(UV.xy, 1);
return inRange.x*inRange.y;
```

調整用Heightmapの影響範囲の設定について今回は使用するテクスチャの解像度に依存するように設定しました。スケール1.0の時にテクスチャの1ピクセルが1メートルを影響するようにします。

最終的に実装したBrushのRender関数はこちらになります：
![Render Heightmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/25f40618-b256-78b3-3b6c-d3b8050afe68.png)


これをランドスケープへ適用した結果はこのようになります：
![HeightmapResult.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/f14976a2-63d8-e7ae-44eb-70702f2bdb5a.png)

##Weightmap処理の実装
Weightmapの扱いは基本的にHeightmapと同じですが、影響するレイヤーの数はランドスケープのマテリアル次第で変わりますのでレイヤー名と扱う情報の構造体のMapで保存します。
Weightmap用の構造体はこんな感じで作成します：
![WeightmapStructure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/f86eb0c3-1e93-a130-036c-ce85921db8d7.png)

Weightmapを調整するマテリアル：
![WeightmapMaterial.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/1cd22764-9f26-a3b7-9562-00be32e9ded6.png)

Weightmapの調整処理を加えた後のRender関数：
![RenderFinished.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/5ce4d14c-aa61-8296-5d43-7e0bc7026229.png)

最終的にはこのような感じ出力されます：
![FinalResult.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/72f7316d-094b-b411-e17e-ac0eed6c4542.png)

#HeightMapの画像ファイル形式について
ここからはエンジンの挙動に関連するトピックになります。
外部ツールからHeightmapの画像を出力する際にはいろんな画像形式を選べられるのですが、PNGファイル形式で出力するとそれをテクスチャアセットとしてインポートしてBrushに使うとHeight情報が一部壊れて地形が階段状になる問題が発生します：
![stepping.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1786401/52ba7d54-8962-d075-bf02-8cf71c9831bb.png)
ちなみに同じPNGファイルをランドスケープのHeightmapとしてインポートすると階段状になりませんのでテクスチャのPNGファイルインポート処理には何か問題があると考えられます。
今回はWorldMachineを使ったので、Heightmapの出力をEXR形式にすることで回避できました。テクスチャーでHeight情報を扱うときは画像ファイルの形式を注意した方がいいと思います。

#最後に
今回はLandscapeBlueprintBrushの自前実装を試してみましたが、公式のブラッシュの機能がかなり充実したおかげで予想より簡単にできました。今後のバージョン更新での機能強化が楽しみです。

参考資料：
https://docs.unrealengine.com/4.26/ja/BuildingWorlds/Landscape/Editing/SculptMode/Blueprint/