UE4の勉強記録

UE4の勉強の記録です。個人用です。

12章8節Landscape and Foliage API ランドスケープ(landscape)と枝葉(Foliage)APIを使用したマップの生成

<前文>

f:id:kazuhironagai77:20181202234933p:plain

前回は、エディター上から枝葉(Foliage)を自動作成する方法を勉強しました。今回は、C++からする方法を学ぶようです。

f:id:kazuhironagai77:20181202235004p:plain

ここで、前々回のレシピでランドスケープ(landscape)を自動生成した時の紹介があり同じ方法を使用して今回はランドスケープ(landscape)と枝葉(foliage)を自動作成するとあり、もう一度UMGをgame modeから作成してゲーム実行中にランドスケープ(landscape)を自動生成する方法が紹介されています。これについては、ランドスケープ(landscape)を自動生成した時は残念ながら出来ないという結論に達しました(注1)。しかしながら、アクタークラスなどの子クラスの変数をエディター上から操作可能にして、ランドスケープ(landscape)の自動生成を行う事は可能ですので、今回はその方法で行います。

(注1)ここで出来ないと言っているのは、客観的に絶対出来ないと言っているのではなくあくまで私が試した限りではと言う意味です。

繰り返しになりますが、以下に簡単に理由をまとめます。

LandScapeモジュールのクラスを使用するためには、以下に示すULandscapeInfo::RecreateLandscapeInfo()関数を最初に使用する必要があります。

f:id:kazuhironagai77:20181202235028p:plain

しかしこの関数をゲーム実行中に使用すると必ず例外を投げて来ます。理由は、

f:id:kazuhironagai77:20181202235112p:plain

IsGameWorld()関数をcheckしているからです。

なので、ゲーム中以外でこのモジュールのクラスは使用しなければなりません。しかしLandScapeを実行するボタンを備えたUMGを実行するためには、ゲームを実行しなければなりません。

UMGを使用するためには、ゲームを実行しなければならないが、ゲームを実行するとRecreateLandscapeInfo()関数が例外を投げます。このため教科書の方法では少なくとも私は出来ません。

<本文>

<目的>

ランドスケープ(landscape)と枝葉(Foliage)APIを使用したマップの生成を勉強します。

<方法>

サラッと読んだのですが、非常に少ない説明でこの情報量ではレシピの再現は無理だと思いました。念のためにサンプルコードを見てみると、そちらの方は実際のコードが紹介されていました。そのコードとHow to do it…の説明を基にやって見たいと思います。

Step.0

前回、作成したプロジェクトをそのまま使用するつもりですが、教科書やサンプルコードに書かれているようにGame modeに直接書いても出来ないので大胆な改良が必要な事は明らかです。ので、まずは空のアクターのサブクラスの作成とそのクラスにエディター内から呼べる関数、Gen()の作成を行います。

f:id:kazuhironagai77:20181202235359p:plain

アクターのサブクラスであるForGenFunctionクラスを作成しました。

f:id:kazuhironagai77:20181202235425p:plain

関数Gen()をヘッダーファイル内で宣言します。エディター上から操作しますので、CallInEditorをUFUNCTION()に追加します。

f:id:kazuhironagai77:20181202235454p:plain

正常に作動しているか確認するために、実行された時LOGにメッセージを表示するようにします。

f:id:kazuhironagai77:20181202235512p:plain

押しました。

f:id:kazuhironagai77:20181202235533p:plain

表示されたのでこのクラス内で作成していきます。

それでは、教科書のHow to do it…の説明文とサンプルコードを見ていこうと思います。

Step.1

f:id:kazuhironagai77:20181202235600p:plain

教科書の説明はこれだけです。UPROPERTY()内で何を指定すべきなのか、その変数のタイプは単に整数でいいのかなど全く分かりません。サンプルコードを見てみましょう。

f:id:kazuhironagai77:20181202235619p:plain

はい。思いっきり書いてありました。

f:id:kazuhironagai77:20181202235641p:plain

そのまま追加しました。

コンパイルするとエラーになりました。

EditAnywhereはprivateでは使えないそうです。Publicに直してもう一度コンパイルしました。

f:id:kazuhironagai77:20181202235708p:plain

今度は出来ました。

ループについても、サンプルコードには

f:id:kazuhironagai77:20181202235728p:plain

完全な形で紹介されていました。

Step.2

f:id:kazuhironagai77:20181202235756p:plain

はい。まずサンプルコード内から何が2D のボックスなのかを調べて見ます。

f:id:kazuhironagai77:20181202235814p:plain

ボックスは、landscapeから得ているようです。ではこれをGen()関数内に再現します。

以下に示すように再現しました。

f:id:kazuhironagai77:20181202235834p:plain

Info()の部分は要らないので省きましたが、取りあえずボックスは得られました。

次にそのボックス内のランダムなXYの座標を獲得します。以下にサンプルコードの例を示します。

f:id:kazuhironagai77:20181202235852p:plain

ここからはちょっと複雑になるので、まずコードを全部読んで理解する事にします。

一行一行見ていきましょう。

f:id:kazuhironagai77:20181202235910p:plain

FVectorはUE4のAPIによると

f:id:kazuhironagai77:20181202235928p:plain

3D空間のベクトルはフロート(float)の精度における(X,Y,Z)の要素で構成されています。

FVetor3D空間における点もしくは方向です。

となっているので、ボックス内のランダムな点がposに保持されてます。

少し脱線しますが3D graphicsの研究室で私が最初に作ったクラスがPoint3dで3D空間内のポイントを表すクラスでした。そのクラスを作成した時Vector3dクラスも作成すべきなのかPoint3dクラスで代用すべきなのか真剣に悩みました。幾ら考えてもこの二つは同じものとして扱えるのですが概念的には全く違うもので同じクラスで代用した場合、教授にこの生徒はポイントとベクトルの違いも分かっていないのかと思われてしまうかもとかいろいろ考えました。懐かしい思い出です。

f:id:kazuhironagai77:20181203000020p:plain

次にランダムで得た点の高さの値をボックスの最高点に置き換えています。

FHitResultクラスを使用しています。

f:id:kazuhironagai77:20181203000042p:plain

FHitResultのUE4のAPIによると

f:id:kazuhironagai77:20181203000100p:plain

衝突の関数内にパスするパラメーターを決定するストラクチャー

とあります。さらにコンストラクターについて見てみると、

f:id:kazuhironagai77:20181203000133p:plain

2番目のコンストラクターを使用しているみたいです。そのremarkをみると

f:id:kazuhironagai77:20181203000152p:plain

非推奨!

他のコンストラクターを使用するFCollisionQueryParamsのオブジェクトを構築(construct)する時、FNameのパラメーターのみの提供をしないでください。

例えばTEXT(“foo”)のような明快なFNAMEの代わりに唯一つのストリング定数のアギュメント(argument)を提供すると、このコンストラクターが他のプログラマーが呼ぼうとしているコンストラクターの代わりに呼ばれてしまいます。

このコンストラクターは最後には、この潜在的な曖昧なケースを避けるために非推奨になります。

非推奨かよ! 直さないと使えないの?と焦りましたが、良く読んでみると、他のコンストラクターを呼びたいときに間違ってこのコンストラクターが呼ばれるから非推奨にしますと説明しているので、このコンストラクターを使用したいときに他のコンストラクターが呼ばれるわけではないようです。

取りあえずそのまま使用してみます。

f:id:kazuhironagai77:20181203000233p:plain

線(ray)の終の位置を計算しています。

BoxHeightは

f:id:kazuhironagai77:20181203000251p:plain

RayEndは、

rayEnd.X=pos.X

rayEnd.Y=pos.Y

rayEnd.Z= box.Min.Z

となります。

f:id:kazuhironagai77:20181203000314p:plain

ActorLineTraceSingleのUE4のAPIによると

f:id:kazuhironagai77:20181203000332p:plain

このアクターのコンポーネント(component)から線(ray)で追跡します。もし何かブロックする物が発見されたら最初のブロックの衝突(first blocking hit)trueで返します。

とありました。パラメーターを見てみると、

f:id:kazuhironagai77:20181203000412p:plain

OutHit: 最初の衝突によって発見されたブロック

Start: その線分(ray)の開始の位置

End: その線分(ray)の終わりの位置

TraceChannel: この線分(ray)が入っているチャンネル。どのコンポーネント(component)が衝突するが決定するために使用

Params: 追跡に使用する追加のパラメーター

となっており、更に、シンタックスには

f:id:kazuhironagai77:20181203000504p:plain

となっているので、この関数からTrueが返されれば、hitオブジェクトが初期化され、衝突した点のzの値が判明するはずです。

f:id:kazuhironagai77:20181203000524p:plain

衝突したアクターがある場合は、その衝突点の値をposに代入しています。

これで、ランドスケープ上におけるx、yのランダムな値を得る事が出来ました。

f:id:kazuhironagai77:20181203000542p:plain

FFoliageInstanceAPIによると

f:id:kazuhironagai77:20181203000558p:plain

個々のインスタンスのエディターの情報

とだけあります。これだけでは何の事だかわかりませんね。次を見てみましょう。

f:id:kazuhironagai77:20181203000630p:plain

InstanceのLocationをposと指定しました。

f:id:kazuhironagai77:20181203000646p:plain

FFoliageInstanceの変数をみてもLocationはありませんが、

その親クラスのFFoliageInstancePlacementInfoの変数をみると

f:id:kazuhironagai77:20181203000712p:plain

Locationがありました。解説はありませんが、FFoliageInstanceを作成する位置を示していると考えられます。

f:id:kazuhironagai77:20181203000732p:plain

0度から360度の間からランダムな値を一個選んでるようです。何に使用するのでしょうか?

f:id:kazuhironagai77:20181203000749p:plain

0.05radは2.864789度なので、大体6度としても大変小さい変化ですが、何に使用するのでしょうか?木の生える方向でしょうか?

f:id:kazuhironagai77:20181203000806p:plain

正規化しましたね。

f:id:kazuhironagai77:20181203000823p:plain

Rotationの変数は、親クラスのFFoliageInstancePlacementInfoにありました。

f:id:kazuhironagai77:20181203000841p:plain

FQuatについて調べます。

ありました。

f:id:kazuhironagai77:20181203000900p:plain

一般的なクォータニオンの説明みたいなので、訳はスキップします。

それよりも、このコンストラクターの解説に注目したいです。

f:id:kazuhironagai77:20181203000916p:plain

新しいクォータニオンを与えられた軸からの回転から作成しかつ初期化

とありました。これでdirとangleが何をしているのかが分かりました。

因みに、.Rotator()関数はそのオブジェクトのFRotatorを返すだけです。

f:id:kazuhironagai77:20181203000949p:plain

パラメーターのScalingMin, ScalingMaxは

f:id:kazuhironagai77:20181203001005p:plain

ヘッダーファイルで宣言されていました。一般的なC++の場合、プリミティブの宣言は初期化を含むんでしたっけ。コンストラクターが呼ばれる呼ばれない論争があったのは覚えているのですが肝心な所は覚えていません。UE4C++なのでエディターで値は追加されるから関係ないのでしょうか?

f:id:kazuhironagai77:20181203001023p:plain

この変数も、親クラスのFFoliageInstancePlacementInfoにありました。

f:id:kazuhironagai77:20181203001039p:plain

これも解説がありませんが枝葉(foliage)のサイズを決めていると思われます。

f:id:kazuhironagai77:20181203001057p:plain

ZOffset変数も親クラスのFFoliageInstancePlacementInfoにありました。

f:id:kazuhironagai77:20181203001113p:plain

なんと、ZoffsetのAPIがありました。

f:id:kazuhironagai77:20181203001131p:plain

オフセットを枝葉(foliage)インスタンスZ locationに提供するために最小値と最大値からオフセットの範囲を特定します

このZは位置のzのようです。

f:id:kazuhironagai77:20181203001204p:plain

パーリン(perlin)サンプリングのために位置を正規化します。

とありますので、続きはStep.3で行います。

Step.3

f:id:kazuhironagai77:20181203001255p:plain

f:id:kazuhironagai77:20181203001305p:plain

何故か、Zをゼロにしてから正規化しています。それ以外は特に不思議な事はないです。

f:id:kazuhironagai77:20181203001327p:plain

前回のレシピで散々、パーリンノイズ(Perlin noise)を勉強したので、今回はサラッと行きます。位置(x, y)における濃さpnがパーリンノイズによって計算されました。

f:id:kazuhironagai77:20181203001349p:plain

まず、fabsfから調べますか。(ここから引用)

f:id:kazuhironagai77:20181203001406p:plain

単にfloatの絶対値を計算する関数でした。

次は、PerlinTreeValueとPerlinTreeRangeです。

f:id:kazuhironagai77:20181203001426p:plain

f:id:kazuhironagai77:20181203001436p:plain

説明文に書いてある通り、pnの値がPerlinTreeValueの値からPerlinTreeRangeの範囲に存在する場合はtrueになります。その条件に当てはまった場合のみ木を作成するのでしょう。

ああ…分かりました。高度によって、自生する木の種類は変わりますね。それをここでコントロールしているのですね。この木はこの高度+-10mにしか生えない。みたいなのをこれで実現しているのですね。

次の行はSpawnFoliageInstance()関数を使用しているので、Step.4に移ります。

Step.4

f:id:kazuhironagai77:20181203001515p:plain

f:id:kazuhironagai77:20181203001525p:plain

まず、SpawnFoliageInstance()関数を調べて見ましょう。

GoogleUE4API見たら、この関数がなかったです。えーと思ってゲームモード内のサンプルコードを見たらありました。うーん。自作の関数だったのか。

f:id:kazuhironagai77:20181203001545p:plain

これで枝葉(foliage)の作成方法がやっと判明するのか。と思うと随分ここまで来るのにかかったなと思います。

最初のパラメーターのUWORLDは単なるUWORLDなのでスキップして次のUFoliageTypeクラスから調べます。

ここAPIはありましたがremarkがありません。前回作成したFoliageTypeのC++バージョンに間違いないですはずです。

f:id:kazuhironagai77:20181203001602p:plain

宣言はTArrayを使用して行われています。

f:id:kazuhironagai77:20181203001620p:plain

三番目のパラメーターはFFoliageInstanceタイプです。これは散々調べたので今はスキップします。

最期のパラメーターはUActorComponentです。これは何を表しているのでしょうか?この関数の実際のアギュメント(argument)を見ないと分かりません。

f:id:kazuhironagai77:20181203001637p:plain

RootComponentをパスしているようです。

では実装部をみていきましょう。

f:id:kazuhironagai77:20181203001656p:plain

本来ならコードから見ていくのですが、最初のコメントが気になります。

f:id:kazuhironagai77:20181203001712p:plain

インスタンス(instance)は常にベースコンポーネント(base component )のレベル内で生成(spawn)します。

ベースコンポーネントのレベル(base component level)とは何でしょうか?

f:id:kazuhironagai77:20181203001746p:plain

これの事でしょうね。

ULevelについてUE4APIによると、

f:id:kazuhironagai77:20181203001801p:plain

レベルオブジェクトはレベルのアクターのリスト、BSPの情報、そしてブラシ(brush)のリストを保持している。

全てのレベルは世界(world)をその外枠として持つが、レベルがOwningWorld**内からその世界(world)の一部としてストリームされる場合PersistentLevel*として使用出来る。

レベルはアクター(ライト、ボリューム、メッシュインスタンスなど)の集合体である。

複数のレベルはストリームする経験を作成するために世界(world)にロードされるしアンロードもされる。

何を言っているのか良く分かないです。特に2番目の文章は良く分からないです。取りあえず、PersistentLeventとOwningWorldについて以下に示します。

* PersistentLevelについてUE4APIから

f:id:kazuhironagai77:20181203001844p:plain

Persistent レベルは世界の情報(world info)、デファルトのブラシ(default brush)そしてゲームプレイ(gameplay)中に生成したアクターなどを保持する。

OwningWorldについてUE4APIから

f:id:kazuhironagai77:20181203001918p:plain

レベルの配列内のこのレベルを持つ世界。これはGetOuter()と同じではない。

それはストリームするレベルのGetOuter() は使用されていない痕跡の世界(vestigial world)である。

他のUObjectの参照と同じようにそれはBeginDestroy()中にアクセスすべきではない。GCがすぐにでも発生するかもしれないから。

ますます分からないです。

まず、レベル (ULevel)と世界(UWorld)の関係から調べて見ます。

ULevelをみると

f:id:kazuhironagai77:20181203001952p:plain

変数の一つにUWorldがあります。レベル(ULevel)があってその中に世界(UWorld)があるようです。これは、UE4のエディター上で、それぞれのレベルに応じて、

f:id:kazuhironagai77:20181203002025p:plain

World Settingがある事でも納得できます。

次にUWorldを見てみます。

f:id:kazuhironagai77:20181203002042p:plain

なんと、UWorldもULevelを変数として持っていました。

分かりました。

全てのレベルは世界(world)をその外枠として持つが、レベルがOwningWorld**内からその世界(world)の一部としてストリームされる場合PersistentLevel*として使用出来る。

まずULevelとUWorldの関係ですが、ULevelの外側にUWorld があります。しかしそのULevelの変数であるOwnWorldはそのULevelが属するUWorldを指します。そのULevelが指すUWorldが持つ変数であるPersistentLevelはそのレベルでストリームしている限りそのULevelを返します。

UWorldとULevelの関係については分かりましたので、次は、BaseComponentについて調べます。

f:id:kazuhironagai77:20181203002102p:plain

BaseComponentはUActorComponentクラスのオブジェクトですので、UActorComponentAPIを見てみます。

f:id:kazuhironagai77:20181203002130p:plain

このコンポーネントが一部であるULevelを返します。

ULevelをUActorComponentから得ているようです。実際のコードではランドスケープを使用していますが、このレベル内に存在するアクターなら何でもいいみたいです。

次の行に行きます。

f:id:kazuhironagai77:20181203002207p:plain

まず、AInstancedFoliageActorについて調べます。

Remarkがありませんでした。

AInstancedFoliageActor::GetInstancedFoliageActorForLevelはありました。

f:id:kazuhironagai77:20181203002227p:plain

特定のストリームしているレベルのFoliageのアクターのインスタンスを獲得します。

うーん。これってない場合はどうなんでしょうか?作って置かないといけないのでしょうか?

f:id:kazuhironagai77:20181203002314p:plain

今度はFFoliageMeshInfoクラスから変数を宣言しています。

f:id:kazuhironagai77:20181203002335p:plain

全ての一致する枝葉(foliage)メッシュのためのエディター情報

まあ、枝葉(foliage)メッシュの情報が保持される変数と考えておきましょう。

f:id:kazuhironagai77:20181203002414p:plain

AddFoliageType関数は

f:id:kazuhironagai77:20181203002431p:plain

と説明されており、OutInfoと書かれているので、ここでMeshInfoはFoliageTypeから枝葉(foliage)メッシュの情報を受け取ると考えられます。またIFAもここで、FoliageTypeからFoliageのアクターのインスタンスの情報を得るみたいです。

f:id:kazuhironagai77:20181203002452p:plain

AddInstance関数は、

f:id:kazuhironagai77:20181203002512p:plain

パスしているパラメーターからこの関数が使用されているようですが、それ以上の説明はなく、インスタンスをMeshInfoに追加しているみたいとしか言えません。

これでこの関数の実装部は終わりです。

うーん。一応、C++から枝葉(Foliage)を自動作成する方法の流れは解明しましたが、完全に理解したとはいえません。

しかし今週はここまでにして、実際の実装は来週行います。

<結果>

来週行います。

<考察>

来週行います。

<まとめ>

来週行います。

<おまけ>

来週行います。