3次元の経路探索 - データ作成編
この記事は qiita.comUnreal Engine 4(UE4) #1 Advent Calendar 2020
の23日目の投稿記事です。
ごりです。久々の投稿になります。
3次元の経路探索を何回かに分けて書かせていただきます。
今回はデータの作成編です。
はじめに
この記事は、下記を参考に作成したものになります。
誤っている点などございましたら、連絡いただけると助かります。
サンプルプロジェクト
下記で配布しています。
drive.google.com
バージョン
ソースコードも含めていますので、ビルド環境が必要となります。
以下の環境で作成しました。
UE4 Ver.4.25.4
Microsoft Visual Studio Community 2019 Version 16.8.3
使い方
プロジェクトを開くとSVOBoundsVolumeが置かれたレベルが開きます。
詳細タブのカテゴリ【SVO】で階層数を指定し、Generateボタンを押すと、SVOが構築されます。
カテゴリ【デバッグ】ではそれぞれチェックを入れることで可視化することができます。
全てにチェックをいれると、このようになります。
SVO( Sparse Voxel Octrees )
SVOはライティングやレイトレーシングで使用される一般的なデータ構造となっています。
基本的な作りは八分木で、各ツリーのボリュームを8つに階層的に分割することで、
高速な位置検索を可能としています。
SVOはリーフノード、ノード、リンクの3つの要素で構築できます。
リーフノード
SVOでのリーフノードは通常の八分木とは扱いが異なり、
最下層(レイヤー0)のノードのことを指します。
リーフノードは衝突or空き領域だけを考慮しているので、
必要なデータは状態を表す1ビットのみとなります。
リーフノードは通常のノードとは異なり、
64個の場所を表すノードで、サブノードと呼ばれます。
上層のノードはこのリーフノードを元に構築されます。
USTRUCT(BlueprintType) struct FSVOLeafNode { //! サブノードインデックスリスト int64 mSubNodeIndices; };
ノード
ツリーのノードには下記が含まれます。
・空間上の位置を知るための位置情報
・親ノードへのリンク
・最初の子ノードへのリンク
・ノード間を移動できるように隣接ノードへのリンク
子ノードへのリンクが最初の子だけで済むのは、
ノードがモートンコード順に格納されているため、
0~7をオフセットして子ノードへ移動することができます。
子ノードへのリンクが無効の場合、このノードにはボクセルが含まれていないと判断できます。
USTRUCT(BlueprintType) struct FSVONode { //! 位置情報 FVector mLocation; //! 親ノードへのリンク FSVOLink mParent; //! 最初の子へのリンク FSVOLink mFirstChild; //! 隣接リンク TArray< FSVOLink > mNeighbours; };
隣接リンク
隣接ノードへのリンクはポインタを使用すると、32ビットと64ビットOSで
データサイズが大きく変化してしまうので、メモリ使用量制御のため、
ポインタの代わりに配列へのオフセットが使用されます。
リンクは経路探索でも使用されるので、同じ階層のノードだけでなく、
上下の層を行き来できるように階層番号とノード番号が必要になります。
また、リーフノードは64個の場所を表すノード(サブノードと呼ばれています)で、
サブノード番号もリンクに必要になります。
この3つの情報は32ビットの整数に変換して格納されます。
USTRUCT(BlueprintType) struct FSVOLink { //! レイヤーインデックス( 0 ~ 15 ) int32 mLayerIndex : 4; //! ノードインデックス( 0 ~ 4194303 ) int32 mNodeIndex : 22; //! サブノートインデックス( 0 ~ 63 ) //! リーフノード内のボクセルインデックスにのみ使用 int32 mSubNodeIndex : 6; };
構築手順
1層分のSVOの構築手順は下図のようになっており、
リーフノードを生成し、それをもとに上層のノードを生成、
最後に均一なノードを削除してSVOが構築されます。
リーフノードの生成
リーフノードは最下層から更に2階層細かくボリュームを分割し、ボリューム内に
コリジョンボクセルが含まれているかどうかを64ビットの値に格納していきます。
ここで、使用しているUSVOSystemLibrary::BoxOverlapActors は
UKismetSystemLibrary::BoxOverlapActorsを、
回転値をオーバーラップ判定に用いれるように変更しています。
void ASVOBoundsVolume::GenerateLeafNodes(){ /** リーフノード数を計算 **/ int32 NodeNum = GetNodeNum(SVO::LAYER_LEAF); /** リーフノードのボクセルサイズを取得 **/ FVector VoxelSize = GetVoxelSizeInLayer(SVO::LAYER_LEAF); /** 障害物判定 **/ TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes = { EObjectTypeQuery::ObjectTypeQuery1, //! WorldStatic }; TArray<AActor*> IgnoreActors = { this }; TArray<AActor*> OverlapActors; for (int32 i = 0; i < NodeNum; i++) { int32 LeafNodeIndex = i >> SVO::BIT_LEAFLAYER; if (!mLeafNodes.Contains(LeafNodeIndex)) { mLeafNodes.Add(LeafNodeIndex, FSVOLeafNode()); } /** 指定階層のノード座標を取得 **/ FVector Location = GetNodeLocationInLayer(SVO::LAYER_LEAF, i); /** コリジョンがノード内に存在するか判定 **/ if (USVOSystemLibrary::BoxOverlapActors( this, Location, VoxelSize * 0.5f, GetActorRotation(), ObjectTypes, nullptr, IgnoreActors, OverlapActors )) { mLeafNodes[LeafNodeIndex].SetBlock(i & SVO::BIT_LEAFNODEINDEX); } } }
各層のノードの生成
各層のノードは先程生成したリーフノードをもとに
下層から上層に向けて、生成していきます。
リーフノードの値が0以外の場合、リーフノードには
コリジョンが含まれているということがわかります。
このときに、上層のノードへリンクづけを行います。
void ASVOBoundsVolume::GenerateLayerNodes() { /** 指定層分の領域確保 **/ mNodeList.SetNum(mNumOfLayers+1); for (auto LeafNode : mLeafNodes) { int32 CurrentLayer = 0; while (CurrentLayer <= mNumOfLayers) { /** 子の階層とインデックスを計算 **/ int32 ChildLayer = FMath::Max(0, CurrentLayer - 1); int32 ChildIndex = LeafNode.Key >> (SVO::BIT_LAYER * ChildLayer); if (CurrentLayer != 0) { /** 最下層でない場合は自身へのリンクを子ノードに設定する **/ int32 ParentIndex = LeafNode.Key >> (SVO::BIT_LAYER * CurrentLayer); mNodeList[CurrentLayer - 1][ChildIndex].SetParent(FSVOLink(CurrentLayer, ParentIndex)); } int32 NodeIndex = LeafNode.Key >> (SVO::BIT_LAYER * CurrentLayer); if (!mNodeList[CurrentLayer].Contains(NodeIndex)) { FSVONode Node; Node.SetLocation(GetNodeLocationInLayer(CurrentLayer, NodeIndex)); mNodeList[CurrentLayer].Add(NodeIndex, Node); } if (!mNodeList[CurrentLayer][NodeIndex].HasAnyChildren()) { if (!LeafNode.Value.IsOpen()) { FSVOLink Child(ChildLayer, ChildIndex); if (CurrentLayer == 0) { /** 最下層のノードが均一でない場合はリーフノードの最初のサブノートインデックスを最初の子に設定 **/ int32 SubNodeIndex = LeafNode.Key << SVO::BIT_LEAFNODEINDEX; Child.SetSubNodeIndex(SubNodeIndex); } mNodeList[CurrentLayer][NodeIndex].SetFirstChild(Child); } } CurrentLayer++; } } }
隣接ノードのリンクづけ
上層までノードが生成されたので、次は上層から下層の順に
隣接ノードのリンクづけをしていきます。
この時点では均一なノードは削除していないので、ノードリストに含まれていない
モートンコードはボリューム外として、処理をスキップします。
均一なノード(子を持たないノード)だった場合は上層も均一かどうか判定し、
均一だった場合は上層のノードを隣接ノードとしてリンクを生成していきます。
void ASVOBoundsVolume::GenerateNeighbourLink() { int32 CurrentLayer = mNumOfLayers; while ( CurrentLayer >= 0 ) { for (auto& Node : mNodeList[CurrentLayer]) { /** モートンコードを1度座標に戻す **/ FVector Location = Morton::Decode( Node.Key ); for (int32 i = 0; i < SVO::Directions.Num(); i++) { /** 上下左右前後の6方向の座標でモートンコードに変換 **/ int32 NeighborIndex = Morton::Code( Location + SVO::Directions[i] ); FSVOLink Neighbour; if (FindNeighbour(CurrentLayer, NeighborIndex, Neighbour )) { Node.Value.AddNeighbour( Neighbour ); } } } CurrentLayer--; } } bool ASVOBoundsVolume::FindNeighbour(int32 CurrentLayer, int32 NeighbourIndex, FSVOLink& Neighbour) { /** 隣接ノード情報をクリア **/ Neighbour.Clear(); if (!mNodeList[CurrentLayer].Contains(NeighbourIndex)) { /** 領域外の場合スキップ **/ return false; } Neighbour.SetLayerIndex(CurrentLayer); Neighbour.SetNodeIndex(NeighbourIndex); if (mNodeList[CurrentLayer][NeighbourIndex].HasAnyChildren()) { if (CurrentLayer == 0 && mLeafNodes[NeighbourIndex].IsClosed()) { return false; } /** ノードが均一でない場合は、ここでストップ **/ return true; } /** 均一な場合、均一な上層ノードを探索 **/ int32 Layer = CurrentLayer+1; int32 Index = NeighbourIndex >> SVO::BIT_LAYER; while ( Layer <= mNumOfLayers ) { if (!mNodeList[CurrentLayer][Index].HasAnyChildren()) { Neighbour.SetLayerIndex(Layer); Neighbour.SetNodeIndex(Index); } Layer++; Index = Index >> SVO::BIT_LAYER; } return true; }
均一ノードの削除
隣接ノードへのリンクを生成したら、最後に下層から上層に向けて
均一なノードの子ノードを削除していきます。
void ASVOBoundsVolume::Rasterize() { int32 CurrentLayer = 1; while ( CurrentLayer <= mNumOfLayers) { for ( auto Node : mNodeList[CurrentLayer] ) { if ( Node.Value.HasAnyChildren()) { continue; } /** 均一なノードの場合、子ノードは不要なので削除 **/ for (int32 i = 0; i < SVO::NUM_CHILDREN; i++) { int32 RemoveNodeIndex = Node.Key << SVO::BIT_LAYER | i; mNodeList[CurrentLayer-1].Remove(RemoveNodeIndex); if (CurrentLayer-1 == 0) { /** 最下層の場合はリーフノードも削除 **/ mLeafNodes.Remove(RemoveNodeIndex); } } } CurrentLayer++; } }
まとめ
ここまで見ていただきありがとうございました。
昔に作ったプロジェクトを引っ張り出してきて確認してみたら、
リーフノードの部分完全に抜けており、
「あれ?これ全然違うな。」となり、大慌てで作り直してました。
続編については年内に投稿予定です。
明日は @nokonoko_08 さんの記事になります。
果たして上司に脅されずに済んだのでしょうか。