私が書いているプログラムにQuadtreeを追加する作業を行っていますが、私が探している実装のためのよく説明された/実行するチュートリアルがほとんどないことに気づかずにはいられません。
具体的には、Quadtree(検索、挿入、削除など)で一般的に使用されるメソッド(またはそれらのプロセスの説明)を実装する方法のメソッドと擬似コードのリストが、私が探しているものです。パフォーマンスを改善するためのヒントがあります。これは衝突を検出するためのものであるため、格納されるオブジェクトであるため、2Dの長方形を念頭に置いて説明するのが最善です。
よし、これを撮るよ。最初に、2万人のエージェントが関与することを提案する結果を示すためのティーザー(この特定の質問のために、私は本当に簡単にホイップしました):
GIFでは、このサイトの最大2 MBに適合するように、フレームレートが極端に低下し、解像度が大幅に低下しています。これをフルスピードに近い速度で見たい場合のビデオは次のとおりです。 https://streamable.com/3pgmn 。
そして、100kのGIFをいじりまわす必要がありましたが、クアッドツリーの行をオフにしなければなりませんでした(それらをオンにしたまま圧縮する必要はなかったようです)。 2メガバイトに収まります(GIFの作成は、4分木をコーディングするのと同じくらい簡単でしたかったです)。
20k個のエージェントを使用したシミュレーションには、最大3メガバイトのRAMが必要です。また、フレームレートを犠牲にすることなく100kのより小さなエージェントを簡単に処理できますが、上記のGIFのように何が起こっているかほとんど見えないほど画面が少し混乱します。これはすべてi7のたった1つのスレッドで実行されており、VTuneによると、画面上にこのようなものを描画するだけで(CPUで一度に1ピクセルずつプロットする基本的なスカラー命令を使用するだけで)ほぼ半分の時間を費やしています。
そして、ここに100,000人のエージェントがいるビデオ しかし、何が起こっているのかを見るのは難しいです。それは一種の大きなビデオであり、ビデオ全体がドロドロになることなく圧縮する適切な方法を見つけることができませんでした(合理的なFPSでストリーミングするには、まずダウンロードまたはキャッシュする必要があります)。 100kのエージェントでは、シミュレーションに約4.5メガバイトのRAMが必要です。また、シミュレーションを約5秒間実行した後、メモリ使用量は非常に安定します(ヒープの割り当てが停止するため、上下が停止します)。- スローモーションで同じもの 。
衝突検出のための効率的な四分木
よし、実際、四分木はこの目的のための私のお気に入りのデータ構造ではありません。私は、世界の粗いグリッド、地域の細かいグリッド、サブ地域のさらに細かいグリッド(3つの固定レベルの密なグリッド、および木が含まれていない)のようなグリッド階層を好む傾向があります。エンティティを持たない行の割り当てが解除され、nullポインターになり、同様に完全に空の領域またはサブ領域がnullになります。 1つのスレッドで実行されるクアッドツリーのこの単純な実装は、60以上のFPSでi7の100kエージェントを処理できますが、古いハードウェア(i3)でフレームごとに跳ね返る数百万のエージェントを処理できるグリッドを実装しました。また、グリッドはセルを分割しないため、必要なメモリ量を非常に簡単に予測できることを常に気に入っていました。しかし、合理的に効率的なクワッドツリーを実装する方法をカバーしようとします。
データ構造の完全な理論には進まないことに注意してください。私はあなたが既にそれを知っていて、パフォーマンスの改善に興味があると仮定しています。私はこの問題に取り組む私の個人的な方法にも取り組んでいますが、これは私のケースでオンラインで見つけたほとんどのソリューションよりも優れているようですが、多くのきちんとした方法があり、これらのソリューションはユースケースに合わせて調整されています(非常に大きな入力映画やテレビの視覚効果のためにすべてがフレームごとに動いています)。他の人は、おそらくさまざまなユースケースに合わせて最適化します。特に空間インデックス構造に関しては、ソリューションの効率性はデータ構造よりも実装者について多くを物語っていると思います。また、物事を高速化するために提案するのと同じ戦略が、octreeを使用した3次元にも適用されます。
ノード表現
それではまず、ノード表現について説明しましょう。
// Represents a node in the quadtree.
struct QuadNode
{
// Points to the first child if this node is a branch or the first
// element if this node is a leaf.
int32_t first_child;
// Stores the number of elements in the leaf or -1 if it this node is
// not a leaf.
int32_t count;
};
これは合計8バイトであり、速度の重要な部分であるため、これは非常に重要です。私は実際には小さいもの(ノードあたり6バイト)を使用しますが、読者に演習として残しておきます。
おそらくcount
なしでも実行できます。葉ノードが分割されるたびに要素を直線的にトラバースしてカウントすることを避けるために、病理学的なケースのためにそれを含める。最も一般的なケースでは、ノードはそれほど多くの要素を保存するべきではありません。しかし、私は視覚的FXで働いており、病理学的症例は必ずしも珍しいものではありません。一致するポイントのボートロード、シーン全体にまたがる巨大なポリゴンなどでコンテンツを作成するアーティストに出会うことができるので、最終的にcount
を保存することになります。
AABBはどこにありますか?
したがって、最初に人々が疑問に思うかもしれないことの1つは、ノードの境界ボックス(長方形)がどこにあるかです。私はそれらを保管しません。その場で計算します。私が見たコードではほとんどの人がそうしていないことにちょっと驚いています。私にとっては、それらはツリー構造(基本的にはルートに対して1つだけのAABB)で保存されています。
これらをオンザフライで計算する方が高価に思えるかもしれませんが、ノードのメモリ使用量を減らすと、ツリーを横断するときのキャッシュミスを比例的に減らすことができ、キャッシュミスの削減は必要以上に重要になる傾向がありますトラバース中にいくつかのビットシフトといくつかの加算/減算を行います。トラバーサルは次のようになります。
static QuadNodeList find_leaves(const Quadtree& tree, const QuadNodeData& root, const int rect[4])
{
QuadNodeList leaves, to_process;
to_process.Push_back(root);
while (to_process.size() > 0)
{
const QuadNodeData nd = to_process.pop_back();
// If this node is a leaf, insert it to the list.
if (tree.nodes[nd.index].count != -1)
leaves.Push_back(nd);
else
{
// Otherwise Push the children that intersect the rectangle.
const int mx = nd.crect[0], my = nd.crect[1];
const int hx = nd.crect[2] >> 1, hy = nd.crect[3] >> 1;
const int fc = tree.nodes[nd.index].first_child;
const int l = mx-hx, t = my-hx, r = mx+hx, b = my+hy;
if (rect[1] <= my)
{
if (rect[0] <= mx)
to_process.Push_back(child_data(l,t, hx, hy, fc+0, nd.depth+1));
if (rect[2] > mx)
to_process.Push_back(child_data(r,t, hx, hy, fc+1, nd.depth+1));
}
if (rect[3] > my)
{
if (rect[0] <= mx)
to_process.Push_back(child_data(l,b, hx, hy, fc+2, nd.depth+1));
if (rect[2] > mx)
to_process.Push_back(child_data(r,b, hx, hy, fc+3, nd.depth+1));
}
}
}
return leaves;
}
AABBを省略することは、私が行う最も珍しいことの1つです(ピアを見つけて失敗するために他の人を探し続けています)が、前後を測定し、少なくとも非常に時間を大幅に短縮しました大量の入力。4分木ノードを大幅に圧縮し、走査中にAABBをオンザフライで計算します。空間と時間は必ずしも正反対ではありません。時々、スペースを削減することは、最近のメモリ階層によってパフォーマンスがどれほど支配されているかを考えると、時間を削減することも意味します。データをメモリ使用量の4分の1に圧縮し、その場で解凍することにより、大規模な入力に適用される実際の操作をさらに高速化しました。
多くの人がAABBをキャッシュすることを選択する理由はわかりません。それがプログラミングの利便性なのか、それとも彼らの場合は本当に速いのかどうかです。それでも、通常のクアッドツリーやオクトツリーのように中央で均等に分割されるデータ構造については、AABBを省略してその場で計算することの影響を測定することをお勧めします。びっくりするかもしれません。もちろん、KdツリーやBVHのように均等に分割されない構造や、ゆるい四分木の場合は、AABBを保存するのが理にかなっています。
浮動小数点
空間インデックスには浮動小数点を使用していません。そのため、2で除算するための右シフトでAABBをオンザフライで計算するだけでパフォーマンスが向上するのかもしれません。とはいえ、少なくともSPFPは最近では本当に速いようです。差を測定していないのでわかりません。浮動小数点入力(メッシュの頂点、パーティクルなど)を一般的に使用している場合でも、優先的に整数を使用します。空間クエリを分割して実行するために、それらを整数座標に変換するだけです。これを行うことで速度が大きく向上するかどうかはわかりません。正規化されていないFPなどすべてについて考える必要なく、整数について推論する方が簡単だと思うので、これは単なる習慣と好みです。
中央に配置されたAABB
ルートの境界ボックスのみを保存しますが、ノードの中心と半分のサイズを保存する表現を使用し、クエリに左/上/右/下の表現を使用して、関係する計算量を最小限に抑えると役立ちます。
連続した子供
これも同様に重要であり、ノード担当者に戻って参照すると:
struct QuadNode
{
int32_t first_child;
...
};
4つの子すべてが連続であるため、子の配列を格納する必要はありません。
first_child+0 = index to 1st child (TL)
first_child+1 = index to 2nd child (TR)
first_child+2 = index to 3nd child (BL)
first_child+3 = index to 4th child (BR)
これにより、走査時のキャッシュミスが大幅に減少するだけでなく、ノードを大幅に縮小できるため、キャッシュミスがさらに減少し、4(16バイト)の配列ではなく1つの32ビットインデックス(4バイト)のみが格納されます。
これは、分割時に親の数象限に要素を転送する必要がある場合、4つの子葉すべてを割り当てて、2つの象限を子として要素を格納する必要があることを意味します。ただし、少なくとも私のユースケースでは、パフォーマンスの点でトレードオフの価値があります。また、ノードを圧縮した量を考えると、ノードは8バイトしか消費しないことに注意してください。
子の割り当てを解除するときは、一度に4つすべての割り当てを解除します。次のように、インデックス付き空きリストを使用してこれを一定時間で実行します。
一度に1つではなく、4つの連続した要素を含むメモリチャンクをプールすることを除きます。これにより、通常、シミュレーション中にヒープの割り当てや割り当て解除を行う必要がなくなります。 4つのノードのグループは、別のリーフノードの後続の分割で個別に解放されるようにのみ、個別に解放済みとしてマークされます。
遅延クリーンアップ
要素を削除してもすぐにクアッドツリーの構造を更新しません。要素を削除するときは、ツリーを下ってそれが占有している子ノードに移動してから要素を削除しますが、葉が空になったとしても、それ以上何もする必要はありません。
代わりに、次のような遅延クリーンアップを実行します。
void Quadtree::cleanup()
{
// Only process the root if it's not a leaf.
SmallList<int> to_process;
if (nodes[0].count == -1)
to_process.Push_back(0);
while (to_process.size() > 0)
{
const int node_index = to_process.pop_back();
QuadNode& node = nodes[node_index];
// Loop through the children.
int num_empty_leaves = 0;
for (int j=0; j < 4; ++j)
{
const int child_index = node.first_child + j;
const QuadNode& child = nodes[child_index];
// Increment empty leaf count if the child is an empty
// leaf. Otherwise if the child is a branch, add it to
// the stack to be processed in the next iteration.
if (child.count == 0)
++num_empty_leaves;
else if (child.count == -1)
to_process.Push_back(child_index);
}
// If all the children were empty leaves, remove them and
// make this node the new empty leaf.
if (num_empty_leaves == 4)
{
// Push all 4 children to the free list.
nodes[node.first_child].first_child = free_node;
free_node = node.first_child;
// Make this node the new empty leaf.
node.first_child = -1;
node.count = 0;
}
}
}
これは、すべてのエージェントを移動した後、すべての単一フレームの終わりに呼び出されます。単一の要素を削除するプロセスで一度にすべてではなく、複数の反復でこの種の空の葉ノードの遅延削除を行う理由は、要素A
がノードN2
に移動し、N1
空。ただし、要素B
は、同じフレーム内でN1
に移動し、再び占有される可能性があります。
遅延クリーンアップを使用すると、別の要素がその象限に移動したときに子を追加し直すだけで、子を不必要に削除せずにそのようなケースを処理できます。
私の場合、要素の移動は簡単です。1)要素を削除し、2)移動し、3)四分木に再挿入します。すべての要素をフレームの最後に移動した後(タイムステップではなく、フレームごとに複数のタイムステップが存在する可能性があります)、上記のcleanup
関数が呼び出され、4つの空の葉を持つ親から子を削除します子として、その親を新しい空の葉に効果的に変え、次のフレームで後続のcleanup
呼び出しでクリーンアップすることができます(または、物が挿入された場合、または空の葉の兄弟が非空の)。
遅延クリーンアップを視覚的に見てみましょう。
これから始めて、ツリーからいくつかの要素を削除して、4つの空の葉を残すとしましょう。
この時点で、cleanup
を呼び出すと、4つの空の子の葉が見つかった場合、4つの葉を削除し、次のように親を空の葉にします。
さらにいくつかの要素を削除するとしましょう:
...そしてcleanup
を再度呼び出します:
もう一度呼び出した場合、次のようになります。
...この時点で、ルート自体が空の葉になります。ただし、クリーンアップメソッドはルートを削除しません(子のみを削除します)。繰り返しますが、この方法で複数のステップで延期する主なポイントは、要素が削除されるたびにすべてこれをすぐに行うと、タイムステップごとに発生する可能性のある冗長な作業の量を減らすことです(多くの場合)ツリー。また、フレーム間で機能するように分散させると、スタッターが発生しなくなります。
TBH、私はもともと、この「遅延クリーンアップ」手法を、Cで書いたDOSゲームで、まったくの怠lazから適用しました!もともとトップダウントラバーサル(トップダウンと再度バックアップではない)を優先するようにツリーを記述したため、ツリーを下って要素を削除し、その後ボトムアップでノードを削除することに煩わされたくありませんでした。そして、この怠zyなソリューションは生産性の妥協(最適なパフォーマンスを犠牲にしてより速く実装される)であると本当に考えました。しかし、何年も後に、ノードの削除をすぐに開始する方法でクアッドツリーの削除を実装することに実際に取り掛かり、驚いたことに、私は実際、予測不可能でどもりのフレームレートで大幅に遅くしました。遅延クリーンアップは、私のプログラマーの怠inessに元々触発されていたにもかかわらず、実際には(そして偶然に)動的シーンの非常に効果的な最適化でした。
要素の単リンク索引リスト
要素については、この表現を使用します。
// Represents an element in the quadtree.
struct QuadElt
{
// Stores the ID for the element (can be used to
// refer to external data).
int id;
// Stores the rectangle for the element.
int x1, y1, x2, y2;
};
// Represents an element node in the quadtree.
struct QuadEltNode
{
// Points to the next element in the leaf node. A value of -1
// indicates the end of the list.
int next;
// Stores the element index.
int element;
};
「要素」とは別の「要素ノード」を使用します。要素は、占有するセルの数に関係なく、クワッドツリーに1回だけ挿入されます。ただし、それが占めるセルごとに、その要素にインデックスを付ける「要素ノード」が挿入されます。
要素ノードは、配列への単一リンクインデックスリストノードであり、上記のフリーリストメソッドも使用します。これにより、リーフのすべての要素を連続して保存する場合に、さらにキャッシュミスが発生します。ただし、このクワッドツリーは、タイムステップごとに移動および衝突する非常に動的なデータ用であるため、通常、リーフ要素の完全に連続した表現を探すために保存するよりも処理時間がかかります(変数を効果的に実装する必要があります)サイズの大きなメモリアロケータは非常に高速であり、簡単なことではありません)。したがって、私は、一方向にリンクされたインデックスリストを使用します。これにより、割り当て/割り当て解除に対するフリーリストの一定時間アプローチが可能になります。その表現を使用すると、整数をいくつか変更するだけで、分割された親から新しいリーフに要素を転送できます。
SmallList<T>
ああ、私はこれに言及する必要があります。当然、非再帰的なトラバーサルのために一時的なノードスタックを格納するためだけにヒープを割り当てない場合に役立ちます。 SmallList<T>
はvector<T>
に似ていますが、128個を超える要素を挿入するまでヒープの割り当てを行わない点が異なります。 C++標準ライブラリのSBO文字列最適化に似ています。それは私が実装し、長い間使用してきたものであり、可能な限りスタックを使用することを確実にするのに役立ちます。
ツリー表現
クワッドツリー自体の表現は次のとおりです。
struct Quadtree
{
// Stores all the elements in the quadtree.
FreeList<QuadElt> elts;
// Stores all the element nodes in the quadtree.
FreeList<QuadEltNode> elt_nodes;
// Stores all the nodes in the quadtree. The first node in this
// sequence is always the root.
std::vector<QuadNode> nodes;
// Stores the quadtree extents.
QuadCRect root_rect;
// Stores the first free node in the quadtree to be reclaimed as 4
// contiguous nodes at once. A value of -1 indicates that the free
// list is empty, at which point we simply insert 4 nodes to the
// back of the nodes array.
int free_node;
// Stores the maximum depth allowed for the quadtree.
int max_depth;
};
上記で指摘したように、ルート用の単一の長方形(root_rect
)を保存します。すべてのサブ四角形はオンザフライで計算されます。すべてのノードは、要素と要素ノード(std::vector<QuadNode>
)と共に配列(FreeList<T>
)に連続して格納されます。
FreeList<T>
基本的には配列(およびランダムアクセスシーケンス)であるFreeList
データ構造を使用して、一定時間内のどこからでも要素を削除できるようにします(一定の時間内に後続の挿入時に回収される穴を残します)。以下は、重要なデータ型を処理することに煩わされない(新しい配置呼び出しまたは手動破棄呼び出しを使用しない)単純化されたバージョンです。
/// Provides an indexed free list with constant-time removals from anywhere
/// in the list without invalidating indices. T must be trivially constructible
/// and destructible.
template <class T>
class FreeList
{
public:
/// Creates a new free list.
FreeList();
/// Inserts an element to the free list and returns an index to it.
int insert(const T& element);
// Removes the nth element from the free list.
void erase(int n);
// Removes all elements from the free list.
void clear();
// Returns the range of valid indices.
int range() const;
// Returns the nth element.
T& operator[](int n);
// Returns the nth element.
const T& operator[](int n) const;
private:
union FreeElement
{
T element;
int next;
};
std::vector<FreeElement> data;
int first_free;
};
template <class T>
FreeList<T>::FreeList(): first_free(-1)
{
}
template <class T>
int FreeList<T>::insert(const T& element)
{
if (first_free != -1)
{
const int index = first_free;
first_free = data[first_free].next;
data[index].element = element;
return index;
}
else
{
FreeElement fe;
fe.element = element;
data.Push_back(fe);
return static_cast<int>(data.size() - 1);
}
}
template <class T>
void FreeList<T>::erase(int n)
{
data[n].next = first_free;
first_free = n;
}
template <class T>
void FreeList<T>::clear()
{
data.clear();
first_free = -1;
}
template <class T>
int FreeList<T>::range() const
{
return static_cast<int>(data.size());
}
template <class T>
T& FreeList<T>::operator[](int n)
{
return data[n].element;
}
template <class T>
const T& FreeList<T>::operator[](int n) const
{
return data[n].element;
}
非自明な型で動作し、イテレータなどを提供するものがありますが、より複雑です。最近は、とにかく簡単に構築/破壊可能なCスタイルの構造体を使用する傾向があります(高レベルのものには、非自明なユーザー定義型のみを使用します)。
最大ツリー深度
許容される最大の深さを指定することで、ツリーが細かくなりすぎないようにします。簡単なシミュレーションのために8を使用しました。私にとってこれは非常に重要です。VFXでは、最大の木深さ制限なしに、多くの一致または重複する要素を持つアーティストによって作成されたコンテンツなど、無期限に細分化したい。
許容される最大の深さと、リーフが4つの子に分割される前にリーフに格納できる要素の数に関して最適なパフォーマンスが必要な場合は、微調整が少しあります。最適な結果は、分割する前にノードごとに最大約8要素で得られ、最小のセルサイズが平均エージェントのサイズをわずかに超えるように最大深度を設定すると得られる傾向があります(そうでなければ、より多くの単一エージェントを挿入できます)複数の葉に)。
衝突とクエリ
衝突の検出とクエリを実行するには、いくつかの方法があります。私はよく人々がこれをするのを見ます:
for each element in scene:
use quad tree to check for collision against other elements
これは非常に簡単ですが、このアプローチの問題は、シーンの最初の要素が世界の2番目とはまったく異なる場所にある可能性があることです。その結果、クアッドツリーをたどるパスは完全に散発的になる可能性があります。リーフへの1つのパスをトラバースし、最初のエレメント(たとえば50,000番目のエレメント)に対して同じパスを再度下ることができます。この時点で、そのパスに関係するノードはすでにCPUキャッシュから追い出されている可能性があります。だから私はこのようにすることをお勧めします:
traversed = {}
gather quadtree leaves
for each leaf in leaves:
{
for each element in leaf:
{
if not traversed[element]:
{
use quad tree to check for collision against other elements
traversed[element] = true
}
}
}
それはかなり多くのコードであり、traversed
ビットセットまたはある種の並列配列を保持して要素を2回処理しないようにする必要があります(複数のリーフに挿入される可能性があるため)ループ全体でクアッドツリーを下る同じパス。これは、物事をはるかにキャッシュフレンドリーに保つのに役立ちます。また、タイムステップで要素を移動しようとしても、そのリーフノードに完全に含まれている場合、ルートから再び戻る必要はありません(1つのリーフのみをチェックできます)。
一般的な非効率:避けるべきこと
猫の皮を剥ぎ、効率的な解決策を達成する方法はたくさんありますが、非常に非効率の解決策を達成する一般的な方法があります。そして、VFXで働いている私のキャリアで非常に非効率 quadtree、kd tree、octreeのシェアに出会いました。適切な実装が毎秒同じ何百回も実行でき、わずか数メガしか必要としない場合、構築に30秒かかる間、100,000の三角形でメッシュを分割するためのギガバイトのメモリ使用について話し合っています。理論上の魔法使いであるが、メモリの効率にあまり注意を払わなかった問題を解決するために、これらの問題を解決する人がたくさんいます。
したがって、私が見る絶対的な最も一般的なno-noは、各ツリーノードに1つ以上の本格的なコンテナを格納することです。本格的なコンテナとは、次のように、独自のメモリを所有、割り当て、解放するものを意味します。
struct Node
{
...
// Stores the elements in the node.
List<Element> elements;
};
List<Element>
はPythonのリスト、ArrayList
in JavaまたはC#、std::vector
C++など)である可能性があります。メモリ/リソース。
ここでの問題は、そのようなコンテナは多数の要素を格納するために非常に効率的に実装されますが、すべての言語のallは、いくつかの要素をそれぞれ。理由の1つは、コンテナツリーのメタデータが、単一のツリーノードのこのようなきめ細かいレベルでのメモリ使用量が非常に爆発的になる傾向があることです。コンテナは、サイズ、容量、割り当てるデータへのポインタ/参照などをすべて汎用目的で保存する必要があるため、サイズと容量に64ビット整数を使用する場合があります。その結果、空のコンテナーのメタデータは24バイトになる可能性があり、これは既に提案したノード表現の全体の3倍であり、それは要素をリーフに格納するように設計された空のコンテナーのみです。
さらに、各コンテナは、挿入時にヒープ/ GC割り当てを行うか、事前にさらに事前に割り当てられたメモリを必要とすることがよくあります(この時点で、コンテナ自体だけで64バイトかかる場合があります)。そのため、すべての割り当てが原因で遅くなります(GCの割り当ては、JVMなどの一部の実装では最初は非常に高速ですが、それは最初のバーストエデンサイクルの場合のみです)、またはノードがトラバース時にCPUキャッシュの下位レベルにほとんど適合しない、またはその両方。
しかし、これは非常に自然な傾向であり、これらの構造について次のような言語を使用して理論的に説明しているため、直感的に理解できます。"リーフにノードが格納されている"残念ながら、メモリの使用と処理の面で爆発的なコストがかかります。そのため、合理的に効率的な何かを作成したい場合は、これを避けてください。 Node
共有を作成し、個々のノードごとではなく、ツリー全体に割り当てられて保存されているメモリを指します(参照します)。実際には、要素は葉に保存されるべきではありません。
要素はtreeに保存し、リーフノードはindexまたはpoint toに保存する必要があります。
結論
まあ、それがまともなパフォーマンスのソリューションと考えられるものを達成するために私がする主なことです。それがお役に立てば幸いです。少なくとも一度か二度、すでにクワッドツリーを実装している人々のために、私はこれをやや上級レベルに向けていることに注意してください。ご質問がある場合は、お気軽に撮影してください。
この質問は少し広いので、閉じていない場合は時間をかけて編集し、拡張し続けます(これらのタイプの質問は、フィールドが、サイトは常にそれらを好きではありません)。また、一部の専門家が、私が学び、おそらくは私の鉱山をさらに改善するために使用できる代替ソリューションに飛び込むことを期待しています。
繰り返しますが、クアッドツリーは、このような非常に動的な衝突シナリオで実際に私のお気に入りのデータ構造ではありません。そのため、この目的のためにクアッドツリーを好む人々から学び、それらを何年も微調整し、調整していることをおそらく1つまたは2つ持っています。ほとんどの場合、すべてのフレームを移動しない静的データにクアッドツリーを使用し、上記に提案したものとは非常に異なる表現を使用します。
2。基礎
この答え(申し訳ありませんが、文字数の制限をもう一度使い果たしました)については、これらの構造を初めて使用する人を対象とした基礎に焦点を当てます。
それでは、空間に次のような要素がたくさんあるとしましょう。
そして、どの要素がマウスカーソルの下にあるのか、どの要素が互いに交差/衝突するのか、最も近い要素が別の要素にあるのか、またはこの種のものを見つけたいのです。
その場合、持っているデータが空間内の要素の位置とサイズ/半径の束だけだった場合、特定の検索領域内にある要素を見つけるためにすべてをループする必要があります。衝突を検出するには、すべての要素をループ処理し、次に各要素について他のすべての要素をループ処理する必要があります。これにより、爆発的な2次複雑度アルゴリズムになります。それは、些細でない入力サイズに耐えることはできません。
細分化
それでは、この問題について何ができるでしょうか?簡単なアプローチの1つは、次のように、検索スペース(画面など)を固定数のセルに分割することです。
ここで、マウスカーソルの下の(cx, cy)
の位置にある要素を検索するとします。その場合、私たちがしなければならないのは、マウスカーソルの下のセルの要素をチェックすることだけです。
grid_x = floor(cx / cell_size);
grid_y = floor(cy / cell_size);
for each element in cell(grid_x, grid_y):
{
if element is under cx,cy:
do something with element (hover highlight it, e.g)
}
衝突検出についても同様です。特定の要素とどの要素が交差(衝突)するかを確認する場合:
grid_x1 = floor(element.x1 / cell_size);
grid_y1 = floor(element.y1 / cell_size);
grid_x2 = floor(element.x2 / cell_size);
grid_y2 = floor(element.y2 / cell_size);
for grid_y = grid_y1, grid_y2:
{
for grid_x = grid_x1, grid_x2:
{
for each other_element in cell(grid_x, grid_y):
{
if element != other_element and collide(element, other_element):
{
// The two elements intersect. Do something in response
// to the collision.
}
}
}
}
そして、スペース/画面を10x10、100x100、さらには1000x1000のような固定数のグリッドセルに分割することから始めることをお勧めします。 1000x1000はメモリ使用量が爆発的に増加すると考える人もいるかもしれませんが、次のように各セルに32ビット整数で4バイトのみを必要とすることができます。
...その時点で、100万個のセルでも4メガバイト未満で済みます。
固定解像度グリッドの欠点
固定解像度グリッドは、私に尋ねるとこの問題の素晴らしいデータ構造です(衝突検出で私の個人的なお気に入り)が、いくつかの弱点があります。
ロードオブザリングのビデオゲームがあるとします。あなたのユニットの多くが、人間、オーク、エルフのようなマップ上の小さなユニットであるとしましょう。ただし、ドラゴンやエントなどのgiganticユニットもあります。
ここでのグリッドの固定解像度の問題は、ほとんどの場合1セルしか占有しない人間やエルフ、オークなどの小さなユニットを保存するのにセルサイズが最適であるのに対し、ドラゴンやテントのような巨大な男は多くのセル、たとえば400セル(20x20)を占有します。その結果、これらの大物を多くのセルに挿入し、多くの冗長データを保存する必要があります。
また、マップの大きな長方形の領域で関心のあるユニットを検索するとします。その場合、理論的に最適なセルよりも多くのセルをチェックする必要があるかもしれません。
これは、固定解像度グリッド*の主な欠点です。最終的には、大きなものを挿入して、理想的に保存する必要があるよりもはるかに多くのセルに保存する必要が生じる可能性があります。また、大きな検索エリアでは、理想的に検索する必要があるよりもはるかに多くのセルをチェックする必要があります。
- つまり、理論を別にすれば、多くの場合、画像処理と同様の方法で非常にキャッシュフレンドリーな方法でグリッドを操作できます。その結果、これらの理論上の欠点はありますが、実際には、キャッシュに優しいトラバーサルパターンを実装する単純さと容易さにより、グリッドは見た目よりもはるかに良くなります。
四分木
したがって、四分木はこの問題の解決策の1つです。いわば、固定解像度グリッドを使用する代わりに、いくつかの基準に基づいて解像度を調整し、4つの子セルに分割/分割して解像度を高めます。たとえば、特定のセルに3つ以上の子がある場合、セルを分割する必要があると言う場合があります。その場合、これ:
最終的にこれになります:
これで、セルに2つ以上の要素が格納されない、すてきな表現ができました。一方、巨大なドラゴンを挿入するとどうなるか考えてみましょう。
ここでは、固定解像度グリッドとは異なり、ドラゴンが占有するセルには要素が1つしかないため、ドラゴンを1つのセルに挿入できます。同様に、マップの大きな領域を検索する場合、セルを占有する要素が多くない限り、それほど多くのセルをチェックする必要はありません。
実装
では、これらの1つをどのように実装するのでしょうか?まあ、それは一日の終わりのツリーであり、その時の4進ツリーです。したがって、4つの子を持つルートノードの概念から始めますが、それらは現在null/nilであり、ルートも現時点ではリーフです。
挿入
いくつかの要素の挿入を始めましょう。簡単にするために、ノードが3つ以上の要素を持つときにノードを分割するとします。そのため、要素を挿入します。要素を挿入するときは、その要素が属するリーフ(セル)に挿入する必要があります。この場合、ルートノード/セルは1つだけなので、そこに挿入します。
...そして別のものを挿入しましょう:
...そしてさらに別の:
そして今、葉ノードに2つ以上の要素があります。これで分割されるはずです。この時点で、リーフノード(この場合はルート)に4つの子を作成し、各要素が空間で占める面積/セルに基づいて、分割されるリーフ(ルート)から適切な象限に要素を転送します。
別の要素を挿入し、それが属する適切なリーフに再度挿入しましょう。
...および別の:
そして今、葉に再び2つ以上の要素があるので、それを4つの象限(子供)に分割する必要があります。
それが基本的な考え方です。あなたが気づくかもしれないことの1つは、無限に小さくないポイントではない要素を挿入するとき、それらが複数のセル/ノードを簡単にオーバーラップできることです。
結果として、セル間の多くの境界に重なる多くの要素がある場合、それらは、場合によっては無限に細分化することになる可能性があります。この問題を軽減するために、一部の人々は要素を分割することを選択します。要素に関連付けるすべてが長方形である場合、長方形をさいの目に切るのはかなり簡単です。他の人は、ツリーが分割できる量に深さ/再帰の制限を置くだけかもしれません。少なくともより効率的に実装する方が簡単なので、これら2つの間の衝突検出シナリオには後者のソリューションを好む傾向があります。ただし、別の選択肢があります。表現が緩やかであり、別のセクションで説明します。
また、要素が互いの上にある場合、無限に小さなポイントを格納している場合でも、ツリーを無期限に分割することができます。たとえば、空間内に25個のポイントが互いに真上にある場合(VFXでかなり頻繁に遭遇するシナリオ)、ツリーは再帰/深さの制限なしに無期限に分割されます。その結果、病理学的なケースを処理するには、要素を細かく分けても深度制限が必要になる場合があります。
要素の削除
要素を削除することは、ツリーをクリーンアップして空の葉を削除するためのノードの削除とともに、最初の回答で説明されています。しかし、基本的に、提案されたアプローチを使用して要素を削除するために行うことは、要素が格納されている葉/葉までツリーを降りて(長方形などを使用して決定できます)、それらの葉からそれを削除するだけです。
次に、空のリーフノードの削除を開始するために、元の回答で説明した遅延クリーンアップアプローチを使用します。
結論
時間は足りませんが、これに戻って答えを改善しようとします。練習が必要な場合は、単純な古い固定解像度グリッドを実装することをお勧めします。各セルがちょうど32ビット整数である場所に到達できるかどうかを確認します。クアッドツリーを検討する前に、まずグリッドとその固有の問題を理解してください。グリッドに問題はないかもしれません。グリッドとクアッドツリーをどの程度効率的に実装できるかに応じて、最適なソリューションを提供することもあります。
上記の小さなGIFは、緩い四分木についての答えを書いた後に作成した新しい「緩い/密なグリッド」データ構造を使用して、タイムステップごとに500,000人のエージェントが互いに跳ね返る様子を示しています。それがどのように機能するかを調べてみます。
これは、私が実装したことを示したすべてのデータ構造の中でこれまでで最高のパフォーマンスを発揮するデータ構造であり(最初のクワッドツリーが100kを処理するよりも50万人、ルーズよりも優れたエージェントを処理します) quadtreeは250kを処理しました。また、必要なメモリ量が最小であり、これら3つのメモリの中で最も安定したメモリ使用が可能です。これは、たった1つのスレッドで動作し、SIMDコードも、生産コードに通常適用されるような凝ったマイクロ最適化も行われません-数時間の作業からの単純な実装です。
また、ラスター化コードをまったく改善せずに、描画のボトルネックを改善しました。これは、グリッドを使用すると、画像処理にキャッシュフレンドリーな方法で簡単にトラバースできるためです(グリッドのセル内の要素を1つずつ描画すると、ラスタライズ時に非常にキャッシュフレンドリーな画像処理パターンが発生します)。
面白いことに、実装するのに最短時間を要し(ゆるいクワッドツリーに5時間か6時間を費やしたのに2時間しかかからなかった)、最小限のコードしか必要としません(そしておそらく最も簡単なコードを持っています)。それは、グリッドを実装した経験が豊富だからかもしれません。
そのため、基本のセクション(第2部を参照)でグリッドの仕組みを説明しましたが、これは「ルーズグリッド」です。各グリッドセルには、要素が削除されると縮小し、要素が追加されると拡大できる独自の境界ボックスが格納されます。その結果、各要素は、次のように、中心位置が内部にあるセルに基づいて、グリッドに一度挿入するだけで済みます。
// Ideally use multiplication here with inv_cell_w or inv_cell_h.
int cell_x = clamp(floor(elt_x / cell_w), 0, num_cols-1);
int cell_y = clamp(floor(ely_y / cell_h), 0, num_rows-1);
int cell_idx = cell_y*num_rows + cell_x;
// Insert element to cell at 'cell_idx' and expand the loose cell's AABB.
セルには、次のような要素とAABBが格納されます。
struct LGridLooseCell
{
// Stores the index to the first element using an indexed SLL.
int head;
// Stores the extents of the grid cell relative to the upper-left corner
// of the grid which expands and shrinks with the elements inserted and
// removed.
float l, t, r, b;
};
ただし、セルが緩んでいると概念上の問題が生じます。巨大な要素を挿入すると巨大になる可能性のあるこれらの境界ボックスがあるので、検索長方形と交差するルーズなセルと要素を見つけたいときに、グリッドのすべての異常なセルをチェックしないようにするにはどうすればよいですか?
実際、これは「二重ルーズグリッド」です。ルーズグリッドセル自体は、タイトグリッドに分割されます。上記の類推検索を行うとき、最初に次のようにタイトなグリッドを調べます。
tx1 = clamp(floor(search_x1 / cell_w), 0, num_cols-1);
tx2 = clamp(floor(search_x2 / cell_w), 0, num_cols-1);
ty1 = clamp(floor(search_y1 / cell_h), 0, num_rows-1);
ty2 = clamp(floor(search_y2 / cell_h), 0, num_rows-1);
for ty = ty1, ty2:
{
trow = ty * num_cols
for tx = tx1, tx2:
{
tight_cell = tight_cells[trow + tx];
for each loose_cell in tight_cell:
{
if loose_cell intersects search area:
{
for each element in loose_cell:
{
if element intersects search area:
add element to query results
}
}
}
}
}
タイトセルには、次のように、ルーズセルの単一リンクインデックスリストが格納されます。
struct LGridLooseCellNode
{
// Points to the next loose cell node in the tight cell.
int next;
// Stores an index to the loose cell.
int cell_idx;
};
struct LGridTightCell
{
// Stores the index to the first loose cell node in the tight cell using
// an indexed SLL.
int head;
};
そして出来上がり、それが「ルーズダブルグリッド」の基本的な考え方です。要素を挿入するとき、ルーズクアッドツリーの場合と同じようにルーズセルのAABBを展開します。ただし、四角形に基づいてタイトグリッドにルーズセルを挿入します(複数のセルに挿入することもできます)。
これら2つのコンボ(ゆるいセルをすばやく見つけるための密なグリッド、および要素をすばやく見つけるためのゆるいセル)は、一定の時間の検索、挿入、および削除で各要素が単一のセルに挿入される非常に美しいデータ構造を提供します。
私が見る唯一の大きな欠点は、これらのセルをすべて保存する必要があり、必要以上に多くのセルを検索する必要がある可能性があることですが、それでもかなり安く(私の場合はセルあたり20バイト)、簡単にトラバースすることができます非常にキャッシュフレンドリーなアクセスパターンでの検索のセル。
この「ルーズグリッド」のアイデアを試してみることをお勧めします。すぐにキャッシュフレンドリーなメモリレイアウトに適しているため、クワッドツリーやルーズクアッドツリーよりも実装がはるかに簡単であり、さらに重要なことに最適化が可能です。超クールなボーナスとして、世界のエージェントの数を事前に予測できる場合、要素は常に正確に1つのセルを占有し、合計セル数は修正されました(分割/分割されないため)。その結果、メモリの使用は非常に安定しています。これは、事前にすべてのメモリを事前に割り当て、メモリ使用量がそのポイントを超えないことを確認したい特定のハードウェアおよびソフトウェアにとって大きなボーナスになる可能性があります。
また、SIMDと連携して、マルチスレッドに加えて、ベクトル化されたコードで複数のコヒーレントクエリを同時に実行できるようにすることも非常に簡単です。これは、トラバースがフラットコールである場合でも、フラットであるためです(単なる一定時間のルックアップです)算術演算を含むセルインデックス)。その結果、Intelがレイトレーシングカーネル/ BVH(Embree)に適用するレイパケットと同様の最適化戦略を適用して、複数のコヒーレントレイを同時にテストすることは非常に簡単です(この場合、衝突の「エージェントパケット」になります)グリッドの「トラバーサル」は非常に単純なので、そのようなファンシー/複雑なコード。
メモリの使用と効率について
パート1でこれを効率的なクワッドツリーで少し説明しましたが、最近L1やレジスタにデータを入れるとプロセッサーが非常に高速になりますが、DRAMアクセスは比較的速いので、メモリ使用量を削減することが最近のスピードの鍵になります。 、 とても遅いです。非常に貴重な小さな高速メモリがありますが、非常に多くの低速メモリがあります。
メガバイトのDRAMでさえ素晴らしいと考えられていたメモリ使用については非常にfru約しなければならなかった(幸いなことに、私よりも前の人ほどではありませんでした)ので、幸運なことから始めたと思います。当時学んだことや習った習慣のいくつかは(私は専門家とは程遠いですが)偶然にもパフォーマンスと一致しています。
したがって、衝突の検出に使用される空間インデックスだけでなく、一般的な効率性に関する一般的なアドバイスの1つは、そのメモリ使用量を監視することです。爆発的な場合、ソリューションがあまり効率的ではない可能性があります。もちろん、データ構造にもう少しメモリを使用すると、処理速度が大幅に低下しますが、データ構造に必要なメモリ量、特に「ホットメモリ」を大幅に削減できるグレーゾーンがあります。繰り返しアクセスされるため、速度の向上にかなり比例して変換できます。私のキャリアで出会った最も効率の悪い空間インデックスはすべて、メモリ使用量が最も爆発的でした。
少なくとも大まかに、理想的に必要なメモリ量を保存および計算する必要があるデータ量を確認すると役立ちます。次に、実際に必要な量と比較します。 2つがワールド離れている場合、メモリ使用量を削減するための適切なブーストが得られる可能性があります。メモリ階層のメモリの。
大丈夫、私はゆるいクワッドツリーを実装して説明するのに少し時間をかけたかったのです。なぜなら、それらは非常に興味深く、おそらく非常にダイナミックなシーンを含む最も多様なユースケースにとって最もバランスが取れているからです。
そのため、私は昨夜の1つを実装することになり、それを微調整し、チューニングとプロファイリングに時間を費やしました。以下は、すべてのタイムステップで互いに動いたり跳ね返ったりする動的エージェントが25万人いるティーザーです。
ゆるい四分木のすべての境界長方形とともにすべての四百万人のエージェントを見るためにズームアウトすると、フレームレートが低下し始めますが、これは主に描画機能のボトルネックによるものです。ズームアウトして画面にすべてを一度に描画すると、それらはホットスポットになり始めますが、それらを最適化することはまったくありませんでした。以下は、エージェントがほとんどいない基本レベルでの仕組みです。
ゆるい四分木
それでは、ゆるい四分木とは何ですか?これらは基本的に、ノードが中央で4つの偶数象限に完全に分割されていない4分割ツリーです。代わりに、AABB(境界の長方形)が重なって、ノードを完全に中央の4つの象限に分割した場合に得られるものよりも大きく、またはしばしば小さくなります。
したがって、この場合、各ノードにバウンディングボックスを保存する必要があるため、次のように表現しました。
struct LooseQuadNode
{
// Stores the AABB of the node.
float rect[4];
// Stores the negative index to the first child for branches or the
// positive index to the element list for leaves.
int children;
};
今回は、単精度浮動小数点を使用してパフォーマンスを確認しようとしましたが、非常にまともな仕事をしました。
ポイントは何ですか?
大丈夫、だから何がポイントですか?緩いクアッドツリーで活用できる主なことは、挿入および削除のために、クアッドツリーに挿入する各要素を単一のポイントのように扱うことができるということです。したがって、要素は無限に小さなポイントのように扱われるため、ツリー全体の複数のリーフノードに挿入されることはありません。
ただし、これらの「要素ポイント」をツリーに挿入するとき、要素の境界(要素の長方形など)を囲むために、挿入する各ノードの境界ボックスを拡張します。これにより、検索クエリを実行するときにこれらの要素を確実に見つけることができます(例:長方形または円形の領域と交差するすべての要素を検索する)。
長所:
短所:
高価なクエリ
この最初の欠点は、静的要素にとっては恐ろしいことです。なぜなら、ツリーを構築し、そのような場合に検索するだけだからです。そして、このゆるいクワッドツリーでは、数時間かけて微調整とチューニングを行ったにもかかわらず、クエリに巨大なホットスポットが含まれていることがわかりました。
とはいえ、これは実際には動的シーン用のこれまでの「個人的な」クワッドツリーの実装です(ただし、この目的には階層グリッドを優先し、動的シーン用のクアッドツリーの使用経験はあまりないことに注意してください)この明白な詐欺。それは、少なくとも動的なシーンでは、タイムステップごとに要素を絶えず移動する必要があるためです。そのため、ツリーを照会するだけでなく、ツリーにはもっと多くのことが関係します。それは常に更新する必要があり、これは実際にはかなりまともな仕事をします。
ゆるいクワッドツリーについて私が気に入っているのは、最も小さな要素のボートロードに加えて、大きな要素のボートロードがある場合でも、安全に感じることができるということです。大規模な要素は、小さな要素よりも多くのメモリを必要としません。その結果、私が巨大な世界でビデオゲームを書いていて、通常のようにデータ構造の階層に煩わされることなくすべてを加速するためにすべてを1つの中央空間インデックスに投げたい場合、緩い四分木と緩い八分木は完璧かもしれません「ダイナミックな世界全体で1つだけを使用する場合、1つの中心的なユニバーサルデータ構造」としてバランスが取れています。
メモリ使用量
メモリーの使用に関しては、要素はより少ないメモリー(特に大規模なもの)を使用しますが、ノードはAABBを保存する必要さえない私の実装と比較して、かなり多くを消費します。多数の巨大な要素を含むさまざまなテストケースで、ゆるい四分木はその太いノードで少しだけ多くのメモリを消費する傾向があることが全体的にわかりました(概算で〜33%増加)。そうは言っても、元の答えのクワッドツリー実装よりもパフォーマンスが優れています。
プラス面では、メモリの使用量はstable(より安定したスムーズなフレームレートに変換される傾向があります)です。私の元の答えのクワッドツリーは、メモリ使用量が完全に安定するまで約5秒以上かかりました。これは、起動してから1〜2秒で安定する傾向があります。おそらく、要素を2回以上挿入する必要がないためです(2つ以上のノードが重複している場合、元の4分木に2回挿入することもできます)境界で)。その結果、データ構造は、いわば、すべての場合に割り当てるために必要なメモリ量をすばやく発見します。
理論
それでは、基本的な理論について説明しましょう。最初に通常のクワッドツリーを実装し、それを理解してから実装するのが少し難しいため、ルーズバージョンに移行することをお勧めします。空のツリーから始めると、空の長方形もあると想像できます。
1つの要素を挿入しましょう:
現時点ではルートノードのみがリーフでもあるため、そこに挿入するだけです。これを行うと、ルートノードの以前は空だった四角形が、挿入した要素を囲むようになります(赤い点線で表示)。別のものを挿入しましょう:
挿入する要素のAABBによって、挿入するときにノードのAABB(今回はルートのみ)を展開します。別のものを挿入してみましょう。ノードに3つ以上の要素が含まれている場合、ノードを分割する必要があるとします。
この場合、リーフノード(ルート)に3つ以上の要素があるため、4つの象限に分割する必要があります。これは、子を転送するときに再び境界ボックスを展開することを除いて、通常のポイントベースの四分木を分割するのとほとんど同じです。分割するノードの中心位置を検討することから始めます。
これで、ルートノードに4つの子があり、各子には、緑色のタイトなバウンディングボックスも格納されます。別の要素を挿入しましょう:
ここでは、この要素を挿入すると、左下の子の長方形だけでなく、ルートも展開されることがわかります(挿入するパスに沿ってすべてのAABBを展開します)。別のものを挿入しましょう:
この場合、リーフノードに再び3つの要素があるため、分割する必要があります。
そのように。では、左下の円はどうでしょうか? 2つの象限が交差しているように見えます。ただし、要素の1つのポイント(例:その中心)のみを考慮して、要素が属する象限を決定します。そのため、その円は実際には左下の象限にのみ挿入されます。
ただし、左下の象限の境界ボックスはその範囲を含むように拡張されます(シアンで表示され、気にしないでくださいが、色が見えにくくなったのでBGの色を変更しました)レベル2のノード(シアンで表示)は、実際には互いの象限にあふれています。
各象限は、その要素を包含することが常に保証される独自の長方形を格納するという事実により、その領域が複数のノードと交差する場合でも、1つのリーフノードに要素を挿入できます。代わりに、要素を複数のノードに挿入する代わりに、リーフノードの境界ボックスを展開します。
AABBの更新
そのため、AABBがいつ更新されるのかという質問につながる可能性があります。要素の挿入時にAABBのみを拡張すると、要素はますます大きくなる傾向があります。要素が削除されたときにそれらをどのように縮小しますか?これに取り組むには多くの方法がありますが、元の答えで説明した「クリーンアップ」メソッドで、階層全体の境界ボックスを更新することでそれを行います。これは十分に速いようです(ホットスポットとして表示されることさえありません)。
グリッドとの比較
私はまだ、階層グリッドの実装ほど衝突検出のためにこれをほぼ効率的に実装することはできないようですが、これもデータ構造というよりも私に関することかもしれません。ツリー構造で私が見つけた主な困難は、すべてがメモリ内のどこにあるのか、どのようにアクセスされるのかを簡単に制御することです。グリッドを使用すると、たとえば、行のすべての列が連続しており、連続して配置されていることを確認し、その行に連続して格納されている要素と一緒に順次アクセスすることができます。ツリーでは、メモリアクセスは本質的に少し散発的になる傾向があり、ノードが複数の子に分割されるとツリーが要素を頻繁に転送するため、急速に低下する傾向があります。とはいえ、ツリーである空間インデックスを使用したい場合、これまでこれらのゆるいバリエーションを掘り下げていて、「ゆるいグリッド」を実装するためのアイデアが頭の中に浮かんできました。
結論
つまり、これは一言で言えばルーズなクアッドツリーであり、基本的に、途中でAABBを展開/更新することを除いて、ポイントのみを保存する通常のクアッドツリーの挿入/削除ロジックを持っています。検索のために、長方形が検索領域と交差するすべての子ノードを走査します。
私はそんなに多くの長い答えを投稿することを人々が気にしないことを願っています。私は本当にそれらを書くことからキックを得ています、そして、これらすべての答えを書くことを試みるためにquadtreesを再訪することにおいて、それは私にとって役に立つ練習でした。また、これらのトピックに関する本(日本語でも)を検討し、ここでいくつかの答えを書いています。急いで英語で書いているので、頭の中ですべてをまとめるのに役立ちます。ここで、衝突検出のために効率的な八分木またはグリッドを作成する方法の説明を求めて、それらの主題について同じことをする言い訳をする必要があります。
この回答については、データが適切な場合(多くの場合、たとえば多くのビデオゲームで使用されることが多い)、シミュレーションを1桁高速に実行できるようにする手違いのトリックについて説明します。数万から数十万のエージェント、または数十万から数百万のエージェントを取得できます。今までの回答で示されたデモンストレーションのどれにも少しのごまかしがあるので、これを適用していませんが、実稼働で使用しており、世界を変えることができます。そしておかしなことに、私はそれが頻繁に議論されるのを見ません。実際、私はそれが議論されているのを見たことがありません。
それでは、ロードオブザリングの例に戻りましょう。人間、エルフ、ドワーフ、オーク、ホビットなどの「人間に近いサイズ」のユニットが多数あります。また、ドラゴンやテントなどの巨大なユニットもあります。
「人間に近いサイズ」のユニットのサイズはそれほど変わりません。ホビットは身長4フィート、少しずんぐりしていて、オークは6'4かもしれません。多少の違いはありますが、壮大な違いではありません。桁違いではありません。
したがって、粗い交差点クエリのために、オークの境界球/ボックスのサイズであるホビットの周りに境界球/ボックスを配置するとどうなりますか(詳細レベルでのより正確な衝突のチェックにドリルダウンする前に) )?無駄なネガティブスペースが少しありますが、本当に面白いことが起こります。
一般的なケースの単位でこのような上限を予測できる場合、すべてのものがの上限サイズを持っていると想定するデータ構造に格納できます。この場合、いくつかの非常に興味深いことが起こります。
1ポイントだけを保存する
この2番目の部分は注意が必要ですが、次のようなケースがあると想像してください。
さて、緑色の円を見てその半径を検索すると、空間インデックスに単一の点としてのみ保存されている場合、青い円の中心点が失われます。しかし、円の半径の2倍のエリアを検索するとどうなりますか?
その場合、青い円が空間インデックスの1つのポイント(オレンジの中心点)としてのみ格納されている場合でも、交差点を見つけます。これが機能することを視覚的に示すためだけに:
この場合、円は交差しておらず、中心点は拡張された2倍の検索半径の外側にあることがわかります。したがって、すべての要素が均一な上限サイズを持っていると仮定する空間インデックスで半径の2倍を検索する限り、上限半径の2倍の領域を検索すると、粗いクエリでそれらを見つけることが保証されます(またはAABBの2倍の長方形のハーフサイズ)。
これは、検索クエリで必要以上に多くのセル/ノードをチェックするような無駄に思えるかもしれませんが、それは単に説明のために図を描いたからです。この戦略を使用する場合、サイズが一般的に単一の葉ノード/セルのサイズの一部である要素に使用します。
巨大な最適化
したがって、適用できる大きな最適化は、コンテンツを3つの異なるタイプに分けることです。
したがって、共通ケース要素を均一な上限範囲(境界球またはボックス)で分離するというこのアイデアは、適用できる場合、非常に有用な最適化戦略になります。また、私は議論されていないものです。開発者が動的コンテンツと静的コンテンツの分離について話しているのをよく見かけますが、よくあるケースの同様のサイズの動的要素をさらにグループ化し、それらが均一な上限サイズを持っているかのように処理することで、さらに改善することができますタイトなデータ構造の1つのリーフノードにのみ挿入される無限小点のようにそれらを格納できる効果がある粗衝突テスト。
「不正行為」の利点について
したがって、このソリューションは特に賢くも面白いものでもありませんが、その背後にある考え方は、少なくとも私のような人にとっては言及する価値があると感じています。 「ユーバー」ソリューションを探している私のキャリアのかなりの部分を無駄にしました:ワンサイズに適合するすべてのデータ構造とアルゴリズムは、それを得るために少し余分な時間を前もって取ることができることを期待してあらゆるユースケースを美しく処理できます正しいものを使用して、未来に至るまで、さまざまなユースケースでそれを狂ったように再利用します。もちろん、同じものを求めた多くの同僚と協力することは言うまでもありません。
また、生産性を優先してパフォーマンスをあまり妥協できないシナリオでは、そのようなソリューションを熱心に探しても、パフォーマンスも生産性も向上しません。そのため、この例のように、ソフトウェアの特定のデータ要件の性質をただ止めて見て、それらの固有の要件に対して「チート」し、いくつかの「調整された」より狭い適用可能なソリューションを作成できるかどうかを確認することが良い場合があります。時には、一方が他方に有利になりすぎて妥協できない場合に、パフォーマンスと生産性の両方の良い組み合わせを得るための最も便利な方法です。
人々が別の答えを気にしないことを願っていますが、30kの制限を使い果たしました。私は今日、私の最初の答えがどのように言語に依存しないかについて考えていました。私はメモリ割り当て戦略、クラステンプレートなどについて話していましたが、すべての言語がそのようなことを許可しているわけではありません。
だから私は、ほぼ普遍的に適用可能な効率的な実装について考えることに時間を費やしました(例外は関数型言語でしょう)。そのため、必要なのはすべてを格納するint
の配列であるような方法で、クワッドツリーをCに移植することになりました。
結果はきれいではありませんが、int
の連続した配列を格納できる任意の言語で非常に効率的に動作するはずです。 Pythonにはndarray
にnumpy
のようなライブラリがあります。JSには 型付き配列 があります。ForJavaおよびC#、int
配列を使用できます(Integer
ではなく、連続して格納されることは保証されず、従来のint
よりも多くのメモリを使用します)。
C IntList
そこで、他の言語への移植を可能な限り簡単にするために、クアッドツリー全体に対してint
配列に構築されたone補助構造を使用します。スタック/空きリストを組み合わせます。これが、他の回答で述べたすべてを効率的な方法で実装するために必要なすべてです。
#ifndef INT_LIST_H
#define INT_LIST_H
#ifdef __cplusplus
#define IL_FUNC extern "C"
#else
#define IL_FUNC
#endif
typedef struct IntList IntList;
enum {il_fixed_cap = 128};
struct IntList
{
// Stores a fixed-size buffer in advance to avoid requiring
// a heap allocation until we run out of space.
int fixed[il_fixed_cap];
// Points to the buffer used by the list. Initially this will
// point to 'fixed'.
int* data;
// Stores how many integer fields each element has.
int num_fields;
// Stores the number of elements in the list.
int num;
// Stores the capacity of the array.
int cap;
// Stores an index to the free element or -1 if the free list
// is empty.
int free_element;
};
// ---------------------------------------------------------------------------------
// List Interface
// ---------------------------------------------------------------------------------
// Creates a new list of elements which each consist of integer fields.
// 'num_fields' specifies the number of integer fields each element has.
IL_FUNC void il_create(IntList* il, int num_fields);
// Destroys the specified list.
IL_FUNC void il_destroy(IntList* il);
// Returns the number of elements in the list.
IL_FUNC int il_size(const IntList* il);
// Returns the value of the specified field for the nth element.
IL_FUNC int il_get(const IntList* il, int n, int field);
// Sets the value of the specified field for the nth element.
IL_FUNC void il_set(IntList* il, int n, int field, int val);
// Clears the specified list, making it empty.
IL_FUNC void il_clear(IntList* il);
// ---------------------------------------------------------------------------------
// Stack Interface (do not mix with free list usage; use one or the other)
// ---------------------------------------------------------------------------------
// Inserts an element to the back of the list and returns an index to it.
IL_FUNC int il_Push_back(IntList* il);
// Removes the element at the back of the list.
IL_FUNC void il_pop_back(IntList* il);
// ---------------------------------------------------------------------------------
// Free List Interface (do not mix with stack usage; use one or the other)
// ---------------------------------------------------------------------------------
// Inserts an element to a vacant position in the list and returns an index to it.
IL_FUNC int il_insert(IntList* il);
// Removes the nth element in the list.
IL_FUNC void il_erase(IntList* il, int n);
#endif
#include "IntList.h"
#include <stdlib.h>
#include <string.h>
#include <assert.h>
void il_create(IntList* il, int num_fields)
{
il->data = il->fixed;
il->num = 0;
il->cap = il_fixed_cap;
il->num_fields = num_fields;
il->free_element = -1;
}
void il_destroy(IntList* il)
{
// Free the buffer only if it was heap allocated.
if (il->data != il->fixed)
free(il->data);
}
void il_clear(IntList* il)
{
il->num = 0;
il->free_element = -1;
}
int il_size(const IntList* il)
{
return il->num;
}
int il_get(const IntList* il, int n, int field)
{
assert(n >= 0 && n < il->num);
return il->data[n*il->num_fields + field];
}
void il_set(IntList* il, int n, int field, int val)
{
assert(n >= 0 && n < il->num);
il->data[n*il->num_fields + field] = val;
}
int il_Push_back(IntList* il)
{
const int new_pos = (il->num+1) * il->num_fields;
// If the list is full, we need to reallocate the buffer to make room
// for the new element.
if (new_pos > il->cap)
{
// Use double the size for the new capacity.
const int new_cap = new_pos * 2;
// If we're pointing to the fixed buffer, allocate a new array on the
// heap and copy the fixed buffer contents to it.
if (il->cap == il_fixed_cap)
{
il->data = malloc(new_cap * sizeof(*il->data));
memcpy(il->data, il->fixed, sizeof(il->fixed));
}
else
{
// Otherwise reallocate the heap buffer to the new size.
il->data = realloc(il->data, new_cap * sizeof(*il->data));
}
// Set the old capacity to the new capacity.
il->cap = new_cap;
}
return il->num++;
}
void il_pop_back(IntList* il)
{
// Just decrement the list size.
assert(il->num > 0);
--il->num;
}
int il_insert(IntList* il)
{
// If there's a free index in the free list, pop that and use it.
if (il->free_element != -1)
{
const int index = il->free_element;
const int pos = index * il->num_fields;
// Set the free index to the next free index.
il->free_element = il->data[pos];
// Return the free index.
return index;
}
// Otherwise insert to the back of the array.
return il_Push_back(il);
}
void il_erase(IntList* il, int n)
{
// Push the element to the free list.
const int pos = n * il->num_fields;
il->data[pos] = il->free_element;
il->free_element = n;
}
IntListの使用
このデータ構造を使用してすべてを実装しても、最もきれいなコードは得られません。次のような要素とフィールドにアクセスする代わりに:
elements[n].field = elements[n].field + 1;
...最終的には次のようになります。
il_set(&elements, n, idx_field, il_get(&elements, n, idx_field) + 1);
...これは嫌なことですが、このコードのポイントは、できる限り保守が容易ではなく、できる限り効率的でポータブルであることです。希望は、人々がこのクワッドツリーを変更したり維持したりせずに、プロジェクトにそのまま使用できることです。
ああ、私が投稿したこのコードは、商業プロジェクトでも自由に使用できます。人々がそれが役に立つと思うかどうかを私に知らせてくれるなら、私は本当にそれを愛しますが、あなたが望むようにします。
C Quadtree
さて、上記のデータ構造を使用すると、Cのクアッドツリーは次のようになります。
#ifndef QUADTREE_H
#define QUADTREE_H
#include "IntList.h"
#ifdef __cplusplus
#define QTREE_FUNC extern "C"
#else
#define QTREE_FUNC
#endif
typedef struct Quadtree Quadtree;
struct Quadtree
{
// Stores all the nodes in the quadtree. The first node in this
// sequence is always the root.
IntList nodes;
// Stores all the elements in the quadtree.
IntList elts;
// Stores all the element nodes in the quadtree.
IntList enodes;
// Stores the quadtree extents.
int root_mx, root_my, root_sx, root_sy;
// Maximum allowed elements in a leaf before the leaf is subdivided/split unless
// the leaf is at the maximum allowed tree depth.
int max_elements;
// Stores the maximum depth allowed for the quadtree.
int max_depth;
// Temporary buffer used for queries.
char* temp;
// Stores the size of the temporary buffer.
int temp_size;
};
// Function signature used for traversing a tree node.
typedef void QtNodeFunc(Quadtree* qt, void* user_data, int node, int depth, int mx, int my, int sx, int sy);
// Creates a quadtree with the requested extents, maximum elements per leaf, and maximum tree depth.
QTREE_FUNC void qt_create(Quadtree* qt, int width, int height, int max_elements, int max_depth);
// Destroys the quadtree.
QTREE_FUNC void qt_destroy(Quadtree* qt);
// Inserts a new element to the tree.
// Returns an index to the new element.
QTREE_FUNC int qt_insert(Quadtree* qt, int id, float x1, float y1, float x2, float y2);
// Removes the specified element from the tree.
QTREE_FUNC void qt_remove(Quadtree* qt, int element);
// Cleans up the tree, removing empty leaves.
QTREE_FUNC void qt_cleanup(Quadtree* qt);
// Outputs a list of elements found in the specified rectangle.
QTREE_FUNC void qt_query(Quadtree* qt, IntList* out, float x1, float y1, float x2, float y2, int omit_element);
// Traverses all the nodes in the tree, calling 'branch' for branch nodes and 'leaf'
// for leaf nodes.
QTREE_FUNC void qt_traverse(Quadtree* qt, void* user_data, QtNodeFunc* branch, QtNodeFunc* leaf);
#endif
#include "Quadtree.h"
#include <stdlib.h>
enum
{
// ----------------------------------------------------------------------------------------
// Element node fields:
// ----------------------------------------------------------------------------------------
enode_num = 2,
// Points to the next element in the leaf node. A value of -1
// indicates the end of the list.
enode_idx_next = 0,
// Stores the element index.
enode_idx_elt = 1,
// ----------------------------------------------------------------------------------------
// Element fields:
// ----------------------------------------------------------------------------------------
elt_num = 5,
// Stores the rectangle encompassing the element.
elt_idx_lft = 0, elt_idx_top = 1, elt_idx_rgt = 2, elt_idx_btm = 3,
// Stores the ID of the element.
elt_idx_id = 4,
// ----------------------------------------------------------------------------------------
// Node fields:
// ----------------------------------------------------------------------------------------
node_num = 2,
// Points to the first child if this node is a branch or the first element
// if this node is a leaf.
node_idx_fc = 0,
// Stores the number of elements in the node or -1 if it is not a leaf.
node_idx_num = 1,
// ----------------------------------------------------------------------------------------
// Node data fields:
// ----------------------------------------------------------------------------------------
nd_num = 6,
// Stores the extents of the node using a centered rectangle and half-size.
nd_idx_mx = 0, nd_idx_my = 1, nd_idx_sx = 2, nd_idx_sy = 3,
// Stores the index of the node.
nd_idx_index = 4,
// Stores the depth of the node.
nd_idx_depth = 5,
};
static void node_insert(Quadtree* qt, int index, int depth, int mx, int my, int sx, int sy, int element);
static int floor_int(float val)
{
return (int)val;
}
static int intersect(int l1, int t1, int r1, int b1,
int l2, int t2, int r2, int b2)
{
return l2 <= r1 && r2 >= l1 && t2 <= b1 && b2 >= t1;
}
void leaf_insert(Quadtree* qt, int node, int depth, int mx, int my, int sx, int sy, int element)
{
// Insert the element node to the leaf.
const int nd_fc = il_get(&qt->nodes, node, node_idx_fc);
il_set(&qt->nodes, node, node_idx_fc, il_insert(&qt->enodes));
il_set(&qt->enodes, il_get(&qt->nodes, node, node_idx_fc), enode_idx_next, nd_fc);
il_set(&qt->enodes, il_get(&qt->nodes, node, node_idx_fc), enode_idx_elt, element);
// If the leaf is full, split it.
if (il_get(&qt->nodes, node, node_idx_num) == qt->max_elements && depth < qt->max_depth)
{
int fc = 0, j = 0;
IntList elts = {0};
il_create(&elts, 1);
// Transfer elements from the leaf node to a list of elements.
while (il_get(&qt->nodes, node, node_idx_fc) != -1)
{
const int index = il_get(&qt->nodes, node, node_idx_fc);
const int next_index = il_get(&qt->enodes, index, enode_idx_next);
const int elt = il_get(&qt->enodes, index, enode_idx_elt);
// Pop off the element node from the leaf and remove it from the qt.
il_set(&qt->nodes, node, node_idx_fc, next_index);
il_erase(&qt->enodes, index);
// Insert element to the list.
il_set(&elts, il_Push_back(&elts), 0, elt);
}
// Start by allocating 4 child nodes.
fc = il_insert(&qt->nodes);
il_insert(&qt->nodes);
il_insert(&qt->nodes);
il_insert(&qt->nodes);
il_set(&qt->nodes, node, node_idx_fc, fc);
// Initialize the new child nodes.
for (j=0; j < 4; ++j)
{
il_set(&qt->nodes, fc+j, node_idx_fc, -1);
il_set(&qt->nodes, fc+j, node_idx_num, 0);
}
// Transfer the elements in the former leaf node to its new children.
il_set(&qt->nodes, node, node_idx_num, -1);
for (j=0; j < il_size(&elts); ++j)
node_insert(qt, node, depth, mx, my, sx, sy, il_get(&elts, j, 0));
il_destroy(&elts);
}
else
{
// Increment the leaf element count.
il_set(&qt->nodes, node, node_idx_num, il_get(&qt->nodes, node, node_idx_num) + 1);
}
}
static void Push_node(IntList* nodes, int nd_index, int nd_depth, int nd_mx, int nd_my, int nd_sx, int nd_sy)
{
const int back_idx = il_Push_back(nodes);
il_set(nodes, back_idx, nd_idx_mx, nd_mx);
il_set(nodes, back_idx, nd_idx_my, nd_my);
il_set(nodes, back_idx, nd_idx_sx, nd_sx);
il_set(nodes, back_idx, nd_idx_sy, nd_sy);
il_set(nodes, back_idx, nd_idx_index, nd_index);
il_set(nodes, back_idx, nd_idx_depth, nd_depth);
}
static void find_leaves(IntList* out, const Quadtree* qt, int node, int depth,
int mx, int my, int sx, int sy,
int lft, int top, int rgt, int btm)
{
IntList to_process = {0};
il_create(&to_process, nd_num);
Push_node(&to_process, node, depth, mx, my, sx, sy);
while (il_size(&to_process) > 0)
{
const int back_idx = il_size(&to_process) - 1;
const int nd_mx = il_get(&to_process, back_idx, nd_idx_mx);
const int nd_my = il_get(&to_process, back_idx, nd_idx_my);
const int nd_sx = il_get(&to_process, back_idx, nd_idx_sx);
const int nd_sy = il_get(&to_process, back_idx, nd_idx_sy);
const int nd_index = il_get(&to_process, back_idx, nd_idx_index);
const int nd_depth = il_get(&to_process, back_idx, nd_idx_depth);
il_pop_back(&to_process);
// If this node is a leaf, insert it to the list.
if (il_get(&qt->nodes, nd_index, node_idx_num) != -1)
Push_node(out, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
else
{
// Otherwise Push the children that intersect the rectangle.
const int fc = il_get(&qt->nodes, nd_index, node_idx_fc);
const int hx = nd_sx >> 1, hy = nd_sy >> 1;
const int l = nd_mx-hx, t = nd_my-hx, r = nd_mx+hx, b = nd_my+hy;
if (top <= nd_my)
{
if (lft <= nd_mx)
Push_node(&to_process, fc+0, nd_depth+1, l,t,hx,hy);
if (rgt > nd_mx)
Push_node(&to_process, fc+1, nd_depth+1, r,t,hx,hy);
}
if (btm > nd_my)
{
if (lft <= nd_mx)
Push_node(&to_process, fc+2, nd_depth+1, l,b,hx,hy);
if (rgt > nd_mx)
Push_node(&to_process, fc+3, nd_depth+1, r,b,hx,hy);
}
}
}
il_destroy(&to_process);
}
static void node_insert(Quadtree* qt, int index, int depth, int mx, int my, int sx, int sy, int element)
{
// Find the leaves and insert the element to all the leaves found.
int j = 0;
IntList leaves = {0};
const int lft = il_get(&qt->elts, element, elt_idx_lft);
const int top = il_get(&qt->elts, element, elt_idx_top);
const int rgt = il_get(&qt->elts, element, elt_idx_rgt);
const int btm = il_get(&qt->elts, element, elt_idx_btm);
il_create(&leaves, nd_num);
find_leaves(&leaves, qt, index, depth, mx, my, sx, sy, lft, top, rgt, btm);
for (j=0; j < il_size(&leaves); ++j)
{
const int nd_mx = il_get(&leaves, j, nd_idx_mx);
const int nd_my = il_get(&leaves, j, nd_idx_my);
const int nd_sx = il_get(&leaves, j, nd_idx_sx);
const int nd_sy = il_get(&leaves, j, nd_idx_sy);
const int nd_index = il_get(&leaves, j, nd_idx_index);
const int nd_depth = il_get(&leaves, j, nd_idx_depth);
leaf_insert(qt, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy, element);
}
il_destroy(&leaves);
}
void qt_create(Quadtree* qt, int width, int height, int max_elements, int max_depth)
{
qt->max_elements = max_elements;
qt->max_depth = max_depth;
qt->temp = 0;
qt->temp_size = 0;
il_create(&qt->nodes, node_num);
il_create(&qt->elts, elt_num);
il_create(&qt->enodes, enode_num);
// Insert the root node to the qt.
il_insert(&qt->nodes);
il_set(&qt->nodes, 0, node_idx_fc, -1);
il_set(&qt->nodes, 0, node_idx_num, 0);
// Set the extents of the root node.
qt->root_mx = width >> 1;
qt->root_my = height >> 1;
qt->root_sx = qt->root_mx;
qt->root_sy = qt->root_my;
}
void qt_destroy(Quadtree* qt)
{
il_destroy(&qt->nodes);
il_destroy(&qt->elts);
il_destroy(&qt->enodes);
free(qt->temp);
}
int qt_insert(Quadtree* qt, int id, float x1, float y1, float x2, float y2)
{
// Insert a new element.
const int new_element = il_insert(&qt->elts);
// Set the fields of the new element.
il_set(&qt->elts, new_element, elt_idx_lft, floor_int(x1));
il_set(&qt->elts, new_element, elt_idx_top, floor_int(y1));
il_set(&qt->elts, new_element, elt_idx_rgt, floor_int(x2));
il_set(&qt->elts, new_element, elt_idx_btm, floor_int(y2));
il_set(&qt->elts, new_element, elt_idx_id, id);
// Insert the element to the appropriate leaf node(s).
node_insert(qt, 0, 0, qt->root_mx, qt->root_my, qt->root_sx, qt->root_sy, new_element);
return new_element;
}
void qt_remove(Quadtree* qt, int element)
{
// Find the leaves.
int j = 0;
IntList leaves = {0};
const int lft = il_get(&qt->elts, element, elt_idx_lft);
const int top = il_get(&qt->elts, element, elt_idx_top);
const int rgt = il_get(&qt->elts, element, elt_idx_rgt);
const int btm = il_get(&qt->elts, element, elt_idx_btm);
il_create(&leaves, nd_num);
find_leaves(&leaves, qt, 0, 0, qt->root_mx, qt->root_my, qt->root_sx, qt->root_sy, lft, top, rgt, btm);
// For each leaf node, remove the element node.
for (j=0; j < il_size(&leaves); ++j)
{
const int nd_index = il_get(&leaves, j, nd_idx_index);
// Walk the list until we find the element node.
int node_index = il_get(&qt->nodes, nd_index, node_idx_fc);
int prev_index = -1;
while (node_index != -1 && il_get(&qt->enodes, node_index, enode_idx_elt) != element)
{
prev_index = node_index;
node_index = il_get(&qt->enodes, node_index, enode_idx_next);
}
if (node_index != -1)
{
// Remove the element node.
const int next_index = il_get(&qt->enodes, node_index, enode_idx_next);
if (prev_index == -1)
il_set(&qt->nodes, nd_index, node_idx_fc, next_index);
else
il_set(&qt->enodes, prev_index, enode_idx_next, next_index);
il_erase(&qt->enodes, node_index);
// Decrement the leaf element count.
il_set(&qt->nodes, nd_index, node_idx_num, il_get(&qt->nodes, nd_index, node_idx_num)-1);
}
}
il_destroy(&leaves);
// Remove the element.
il_erase(&qt->elts, element);
}
void qt_query(Quadtree* qt, IntList* out, float x1, float y1, float x2, float y2, int omit_element)
{
// Find the leaves that intersect the specified query rectangle.
int j = 0;
IntList leaves = {0};
const int elt_cap = il_size(&qt->elts);
const int qlft = floor_int(x1);
const int qtop = floor_int(y1);
const int qrgt = floor_int(x2);
const int qbtm = floor_int(y2);
if (qt->temp_size < elt_cap)
{
qt->temp_size = elt_cap;
qt->temp = realloc(qt->temp, qt->temp_size * sizeof(*qt->temp));
memset(qt->temp, 0, qt->temp_size * sizeof(*qt->temp));
}
// For each leaf node, look for elements that intersect.
il_create(&leaves, nd_num);
find_leaves(&leaves, qt, 0, 0, qt->root_mx, qt->root_my, qt->root_sx, qt->root_sy, qlft, qtop, qrgt, qbtm);
il_clear(out);
for (j=0; j < il_size(&leaves); ++j)
{
const int nd_index = il_get(&leaves, j, nd_idx_index);
// Walk the list and add elements that intersect.
int elt_node_index = il_get(&qt->nodes, nd_index, node_idx_fc);
while (elt_node_index != -1)
{
const int element = il_get(&qt->enodes, elt_node_index, enode_idx_elt);
const int lft = il_get(&qt->elts, element, elt_idx_lft);
const int top = il_get(&qt->elts, element, elt_idx_top);
const int rgt = il_get(&qt->elts, element, elt_idx_rgt);
const int btm = il_get(&qt->elts, element, elt_idx_btm);
if (!qt->temp[element] && element != omit_element && intersect(qlft,qtop,qrgt,qbtm, lft,top,rgt,btm))
{
il_set(out, il_Push_back(out), 0, element);
qt->temp[element] = 1;
}
elt_node_index = il_get(&qt->enodes, elt_node_index, enode_idx_next);
}
}
il_destroy(&leaves);
// Unmark the elements that were inserted.
for (j=0; j < il_size(out); ++j)
qt->temp[il_get(out, j, 0)] = 0;
}
void qt_cleanup(Quadtree* qt)
{
IntList to_process = {0};
il_create(&to_process, 1);
// Only process the root if it's not a leaf.
if (il_get(&qt->nodes, 0, node_idx_num) == -1)
{
// Push the root index to the stack.
il_set(&to_process, il_Push_back(&to_process), 0, 0);
}
while (il_size(&to_process) > 0)
{
// Pop a node from the stack.
const int node = il_get(&to_process, il_size(&to_process)-1, 0);
const int fc = il_get(&qt->nodes, node, node_idx_fc);
int num_empty_leaves = 0;
int j = 0;
il_pop_back(&to_process);
// Loop through the children.
for (j=0; j < 4; ++j)
{
const int child = fc + j;
// Increment empty leaf count if the child is an empty
// leaf. Otherwise if the child is a branch, add it to
// the stack to be processed in the next iteration.
if (il_get(&qt->nodes, child, node_idx_num) == 0)
++num_empty_leaves;
else if (il_get(&qt->nodes, child, node_idx_num) == -1)
{
// Push the child index to the stack.
il_set(&to_process, il_Push_back(&to_process), 0, child);
}
}
// If all the children were empty leaves, remove them and
// make this node the new empty leaf.
if (num_empty_leaves == 4)
{
// Remove all 4 children in reverse order so that they
// can be reclaimed on subsequent insertions in proper
// order.
il_erase(&qt->nodes, fc + 3);
il_erase(&qt->nodes, fc + 2);
il_erase(&qt->nodes, fc + 1);
il_erase(&qt->nodes, fc + 0);
// Make this node the new empty leaf.
il_set(&qt->nodes, node, node_idx_fc, -1);
il_set(&qt->nodes, node, node_idx_num, 0);
}
}
il_destroy(&to_process);
}
void qt_traverse(Quadtree* qt, void* user_data, QtNodeFunc* branch, QtNodeFunc* leaf)
{
IntList to_process = {0};
il_create(&to_process, nd_num);
Push_node(&to_process, 0, 0, qt->root_mx, qt->root_my, qt->root_sx, qt->root_sy);
while (il_size(&to_process) > 0)
{
const int back_idx = il_size(&to_process) - 1;
const int nd_mx = il_get(&to_process, back_idx, nd_idx_mx);
const int nd_my = il_get(&to_process, back_idx, nd_idx_my);
const int nd_sx = il_get(&to_process, back_idx, nd_idx_sx);
const int nd_sy = il_get(&to_process, back_idx, nd_idx_sy);
const int nd_index = il_get(&to_process, back_idx, nd_idx_index);
const int nd_depth = il_get(&to_process, back_idx, nd_idx_depth);
const int fc = il_get(&qt->nodes, nd_index, node_idx_fc);
il_pop_back(&to_process);
if (il_get(&qt->nodes, nd_index, node_idx_num) == -1)
{
// Push the children of the branch to the stack.
const int hx = nd_sx >> 1, hy = nd_sy >> 1;
const int l = nd_mx-hx, t = nd_my-hx, r = nd_mx+hx, b = nd_my+hy;
Push_node(&to_process, fc+0, nd_depth+1, l,t, hx,hy);
Push_node(&to_process, fc+1, nd_depth+1, r,t, hx,hy);
Push_node(&to_process, fc+2, nd_depth+1, l,b, hx,hy);
Push_node(&to_process, fc+3, nd_depth+1, r,b, hx,hy);
if (branch)
branch(qt, user_data, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
}
else if (leaf)
leaf(qt, user_data, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
}
il_destroy(&to_process);
}
一時的な結論
これはそれほど良い答えではありませんが、戻って編集を続けようと思います。ただし、上記のコードは、プレーンオールド整数の連続した配列を許可するほとんどすべての言語で非常に効率的である必要があります。その連続性の保証がある限り、非常に小さいmemフットプリントを使用する非常にキャッシュに優しいクアッドツリーを思いつくことができます。
全体的なアプローチの詳細については、元の回答を参照してください。
3回目の回答を投稿してもかまわないことを願っていますが、再び文字数制限を使い果たしました。 Javaへの2番目の回答でCコードの移植を終了しました。 Javaポートは、オブジェクト指向言語に移植する人にとって参照しやすいかもしれません。
class IntList
{
private int data[] = new int[128];
private int num_fields = 0;
private int num = 0;
private int cap = 128;
private int free_element = -1;
// Creates a new list of elements which each consist of integer fields.
// 'start_num_fields' specifies the number of integer fields each element has.
public IntList(int start_num_fields)
{
num_fields = start_num_fields;
}
// Returns the number of elements in the list.
int size()
{
return num;
}
// Returns the value of the specified field for the nth element.
int get(int n, int field)
{
assert n >= 0 && n < num && field >= 0 && field < num_fields;
return data[n*num_fields + field];
}
// Sets the value of the specified field for the nth element.
void set(int n, int field, int val)
{
assert n >= 0 && n < num && field >= 0 && field < num_fields;
data[n*num_fields + field] = val;
}
// Clears the list, making it empty.
void clear()
{
num = 0;
free_element = -1;
}
// Inserts an element to the back of the list and returns an index to it.
int pushBack()
{
final int new_pos = (num+1) * num_fields;
// If the list is full, we need to reallocate the buffer to make room
// for the new element.
if (new_pos > cap)
{
// Use double the size for the new capacity.
final int new_cap = new_pos * 2;
// Allocate new array and copy former contents.
int new_array[] = new int[new_cap];
System.arraycopy(data, 0, new_array, 0, cap);
data = new_array;
// Set the old capacity to the new capacity.
cap = new_cap;
}
return num++;
}
// Removes the element at the back of the list.
void popBack()
{
// Just decrement the list size.
assert num > 0;
--num;
}
// Inserts an element to a vacant position in the list and returns an index to it.
int insert()
{
// If there's a free index in the free list, pop that and use it.
if (free_element != -1)
{
final int index = free_element;
final int pos = index * num_fields;
// Set the free index to the next free index.
free_element = data[pos];
// Return the free index.
return index;
}
// Otherwise insert to the back of the array.
return pushBack();
}
// Removes the nth element in the list.
void erase(int n)
{
// Push the element to the free list.
final int pos = n * num_fields;
data[pos] = free_element;
free_element = n;
}
}
Java Quadtree
そして、これはJavaのクワッドツリーです(あまり慣用的でない場合はごめんなさい。約10年ほどでJavaを書いていないので、多くのことを忘れています):
interface IQtVisitor
{
// Called when traversing a branch node.
// (mx, my) indicate the center of the node's AABB.
// (sx, sy) indicate the half-size of the node's AABB.
void branch(Quadtree qt, int node, int depth, int mx, int my, int sx, int sy);
// Called when traversing a leaf node.
// (mx, my) indicate the center of the node's AABB.
// (sx, sy) indicate the half-size of the node's AABB.
void leaf(Quadtree qt, int node, int depth, int mx, int my, int sx, int sy);
}
class Quadtree
{
// Creates a quadtree with the requested extents, maximum elements per leaf, and maximum tree depth.
Quadtree(int width, int height, int start_max_elements, int start_max_depth)
{
max_elements = start_max_elements;
max_depth = start_max_depth;
// Insert the root node to the qt.
nodes.insert();
nodes.set(0, node_idx_fc, -1);
nodes.set(0, node_idx_num, 0);
// Set the extents of the root node.
root_mx = width / 2;
root_my = height / 2;
root_sx = root_mx;
root_sy = root_my;
}
// Outputs a list of elements found in the specified rectangle.
public int insert(int id, float x1, float y1, float x2, float y2)
{
// Insert a new element.
final int new_element = elts.insert();
// Set the fields of the new element.
elts.set(new_element, elt_idx_lft, floor_int(x1));
elts.set(new_element, elt_idx_top, floor_int(y1));
elts.set(new_element, elt_idx_rgt, floor_int(x2));
elts.set(new_element, elt_idx_btm, floor_int(y2));
elts.set(new_element, elt_idx_id, id);
// Insert the element to the appropriate leaf node(s).
node_insert(0, 0, root_mx, root_my, root_sx, root_sy, new_element);
return new_element;
}
// Removes the specified element from the tree.
public void remove(int element)
{
// Find the leaves.
final int lft = elts.get(element, elt_idx_lft);
final int top = elts.get(element, elt_idx_top);
final int rgt = elts.get(element, elt_idx_rgt);
final int btm = elts.get(element, elt_idx_btm);
IntList leaves = find_leaves(0, 0, root_mx, root_my, root_sx, root_sy, lft, top, rgt, btm);
// For each leaf node, remove the element node.
for (int j=0; j < leaves.size(); ++j)
{
final int nd_index = leaves.get(j, nd_idx_index);
// Walk the list until we find the element node.
int node_index = nodes.get(nd_index, node_idx_fc);
int prev_index = -1;
while (node_index != -1 && enodes.get(node_index, enode_idx_elt) != element)
{
prev_index = node_index;
node_index = enodes.get(node_index, enode_idx_next);
}
if (node_index != -1)
{
// Remove the element node.
final int next_index = enodes.get(node_index, enode_idx_next);
if (prev_index == -1)
nodes.set(nd_index, node_idx_fc, next_index);
else
enodes.set(prev_index, enode_idx_next, next_index);
enodes.erase(node_index);
// Decrement the leaf element count.
nodes.set(nd_index, node_idx_num, nodes.get(nd_index, node_idx_num)-1);
}
}
// Remove the element.
elts.erase(element);
}
// Cleans up the tree, removing empty leaves.
public void cleanup()
{
IntList to_process = new IntList(1);
// Only process the root if it's not a leaf.
if (nodes.get(0, node_idx_num) == -1)
{
// Push the root index to the stack.
to_process.set(to_process.pushBack(), 0, 0);
}
while (to_process.size() > 0)
{
// Pop a node from the stack.
final int node = to_process.get(to_process.size()-1, 0);
final int fc = nodes.get(node, node_idx_fc);
int num_empty_leaves = 0;
to_process.popBack();
// Loop through the children.
for (int j=0; j < 4; ++j)
{
final int child = fc + j;
// Increment empty leaf count if the child is an empty
// leaf. Otherwise if the child is a branch, add it to
// the stack to be processed in the next iteration.
if (nodes.get(child, node_idx_num) == 0)
++num_empty_leaves;
else if (nodes.get(child, node_idx_num) == -1)
{
// Push the child index to the stack.
to_process.set(to_process.pushBack(), 0, child);
}
}
// If all the children were empty leaves, remove them and
// make this node the new empty leaf.
if (num_empty_leaves == 4)
{
// Remove all 4 children in reverse order so that they
// can be reclaimed on subsequent insertions in proper
// order.
nodes.erase(fc + 3);
nodes.erase(fc + 2);
nodes.erase(fc + 1);
nodes.erase(fc + 0);
// Make this node the new empty leaf.
nodes.set(node, node_idx_fc, -1);
nodes.set(node, node_idx_num, 0);
}
}
}
// Returns a list of elements found in the specified rectangle.
public IntList query(float x1, float y1, float x2, float y2)
{
return query(x1, y1, x2, y2, -1);
}
// Returns a list of elements found in the specified rectangle excluding the
// specified element to omit.
public IntList query(float x1, float y1, float x2, float y2, int omit_element)
{
IntList out = new IntList(1);
// Find the leaves that intersect the specified query rectangle.
final int qlft = floor_int(x1);
final int qtop = floor_int(y1);
final int qrgt = floor_int(x2);
final int qbtm = floor_int(y2);
IntList leaves = find_leaves(0, 0, root_mx, root_my, root_sx, root_sy, qlft, qtop, qrgt, qbtm);
if (temp_size < elts.size())
{
temp_size = elts.size();
temp = new boolean[temp_size];;
}
// For each leaf node, look for elements that intersect.
for (int j=0; j < leaves.size(); ++j)
{
final int nd_index = leaves.get(j, nd_idx_index);
// Walk the list and add elements that intersect.
int elt_node_index = nodes.get(nd_index, node_idx_fc);
while (elt_node_index != -1)
{
final int element = enodes.get(elt_node_index, enode_idx_elt);
final int lft = elts.get(element, elt_idx_lft);
final int top = elts.get(element, elt_idx_top);
final int rgt = elts.get(element, elt_idx_rgt);
final int btm = elts.get(element, elt_idx_btm);
if (!temp[element] && element != omit_element && intersect(qlft,qtop,qrgt,qbtm, lft,top,rgt,btm))
{
out.set(out.pushBack(), 0, element);
temp[element] = true;
}
elt_node_index = enodes.get(elt_node_index, enode_idx_next);
}
}
// Unmark the elements that were inserted.
for (int j=0; j < out.size(); ++j)
temp[out.get(j, 0)] = false;
return out;
}
// Traverses all the nodes in the tree, calling 'branch' for branch nodes and 'leaf'
// for leaf nodes.
public void traverse(IQtVisitor visitor)
{
IntList to_process = new IntList(nd_num);
pushNode(to_process, 0, 0, root_mx, root_my, root_sx, root_sy);
while (to_process.size() > 0)
{
final int back_idx = to_process.size() - 1;
final int nd_mx = to_process.get(back_idx, nd_idx_mx);
final int nd_my = to_process.get(back_idx, nd_idx_my);
final int nd_sx = to_process.get(back_idx, nd_idx_sx);
final int nd_sy = to_process.get(back_idx, nd_idx_sy);
final int nd_index = to_process.get(back_idx, nd_idx_index);
final int nd_depth = to_process.get(back_idx, nd_idx_depth);
final int fc = nodes.get(nd_index, node_idx_fc);
to_process.popBack();
if (nodes.get(nd_index, node_idx_num) == -1)
{
// Push the children of the branch to the stack.
final int hx = nd_sx >> 1, hy = nd_sy >> 1;
final int l = nd_mx-hx, t = nd_my-hx, r = nd_mx+hx, b = nd_my+hy;
pushNode(to_process, fc+0, nd_depth+1, l,t, hx,hy);
pushNode(to_process, fc+1, nd_depth+1, r,t, hx,hy);
pushNode(to_process, fc+2, nd_depth+1, l,b, hx,hy);
pushNode(to_process, fc+3, nd_depth+1, r,b, hx,hy);
visitor.branch(this, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
}
else
visitor.leaf(this, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
}
}
private static int floor_int(float val)
{
return (int)val;
}
private static boolean intersect(int l1, int t1, int r1, int b1,
int l2, int t2, int r2, int b2)
{
return l2 <= r1 && r2 >= l1 && t2 <= b1 && b2 >= t1;
}
private static void pushNode(IntList nodes, int nd_index, int nd_depth, int nd_mx, int nd_my, int nd_sx, int nd_sy)
{
final int back_idx = nodes.pushBack();
nodes.set(back_idx, nd_idx_mx, nd_mx);
nodes.set(back_idx, nd_idx_my, nd_my);
nodes.set(back_idx, nd_idx_sx, nd_sx);
nodes.set(back_idx, nd_idx_sy, nd_sy);
nodes.set(back_idx, nd_idx_index, nd_index);
nodes.set(back_idx, nd_idx_depth, nd_depth);
}
private IntList find_leaves(int node, int depth,
int mx, int my, int sx, int sy,
int lft, int top, int rgt, int btm)
{
IntList leaves = new IntList(nd_num);
IntList to_process = new IntList(nd_num);
pushNode(to_process, node, depth, mx, my, sx, sy);
while (to_process.size() > 0)
{
final int back_idx = to_process.size() - 1;
final int nd_mx = to_process.get(back_idx, nd_idx_mx);
final int nd_my = to_process.get(back_idx, nd_idx_my);
final int nd_sx = to_process.get(back_idx, nd_idx_sx);
final int nd_sy = to_process.get(back_idx, nd_idx_sy);
final int nd_index = to_process.get(back_idx, nd_idx_index);
final int nd_depth = to_process.get(back_idx, nd_idx_depth);
to_process.popBack();
// If this node is a leaf, insert it to the list.
if (nodes.get(nd_index, node_idx_num) != -1)
pushNode(leaves, nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy);
else
{
// Otherwise Push the children that intersect the rectangle.
final int fc = nodes.get(nd_index, node_idx_fc);
final int hx = nd_sx / 2, hy = nd_sy / 2;
final int l = nd_mx-hx, t = nd_my-hx, r = nd_mx+hx, b = nd_my+hy;
if (top <= nd_my)
{
if (lft <= nd_mx)
pushNode(to_process, fc+0, nd_depth+1, l,t,hx,hy);
if (rgt > nd_mx)
pushNode(to_process, fc+1, nd_depth+1, r,t,hx,hy);
}
if (btm > nd_my)
{
if (lft <= nd_mx)
pushNode(to_process, fc+2, nd_depth+1, l,b,hx,hy);
if (rgt > nd_mx)
pushNode(to_process, fc+3, nd_depth+1, r,b,hx,hy);
}
}
}
return leaves;
}
private void node_insert(int index, int depth, int mx, int my, int sx, int sy, int element)
{
// Find the leaves and insert the element to all the leaves found.
final int lft = elts.get(element, elt_idx_lft);
final int top = elts.get(element, elt_idx_top);
final int rgt = elts.get(element, elt_idx_rgt);
final int btm = elts.get(element, elt_idx_btm);
IntList leaves = find_leaves(index, depth, mx, my, sx, sy, lft, top, rgt, btm);
for (int j=0; j < leaves.size(); ++j)
{
final int nd_mx = leaves.get(j, nd_idx_mx);
final int nd_my = leaves.get(j, nd_idx_my);
final int nd_sx = leaves.get(j, nd_idx_sx);
final int nd_sy = leaves.get(j, nd_idx_sy);
final int nd_index = leaves.get(j, nd_idx_index);
final int nd_depth = leaves.get(j, nd_idx_depth);
leaf_insert(nd_index, nd_depth, nd_mx, nd_my, nd_sx, nd_sy, element);
}
}
private void leaf_insert(int node, int depth, int mx, int my, int sx, int sy, int element)
{
// Insert the element node to the leaf.
final int nd_fc = nodes.get(node, node_idx_fc);
nodes.set(node, node_idx_fc, enodes.insert());
enodes.set(nodes.get(node, node_idx_fc), enode_idx_next, nd_fc);
enodes.set(nodes.get(node, node_idx_fc), enode_idx_elt, element);
// If the leaf is full, split it.
if (nodes.get(node, node_idx_num) == max_elements && depth < max_depth)
{
// Transfer elements from the leaf node to a list of elements.
IntList elts = new IntList(1);
while (nodes.get(node, node_idx_fc) != -1)
{
final int index = nodes.get(node, node_idx_fc);
final int next_index = enodes.get(index, enode_idx_next);
final int elt = enodes.get(index, enode_idx_elt);
// Pop off the element node from the leaf and remove it from the qt.
nodes.set(node, node_idx_fc, next_index);
enodes.erase(index);
// Insert element to the list.
elts.set(elts.pushBack(), 0, elt);
}
// Start by allocating 4 child nodes.
final int fc = nodes.insert();
nodes.insert();
nodes.insert();
nodes.insert();
nodes.set(node, node_idx_fc, fc);
// Initialize the new child nodes.
for (int j=0; j < 4; ++j)
{
nodes.set(fc+j, node_idx_fc, -1);
nodes.set(fc+j, node_idx_num, 0);
}
// Transfer the elements in the former leaf node to its new children.
nodes.set(node, node_idx_num, -1);
for (int j=0; j < elts.size(); ++j)
node_insert(node, depth, mx, my, sx, sy, elts.get(j, 0));
}
else
{
// Increment the leaf element count.
nodes.set(node, node_idx_num, nodes.get(node, node_idx_num) + 1);
}
}
// ----------------------------------------------------------------------------------------
// Element node fields:
// ----------------------------------------------------------------------------------------
// Points to the next element in the leaf node. A value of -1
// indicates the end of the list.
static final int enode_idx_next = 0;
// Stores the element index.
static final int enode_idx_elt = 1;
// Stores all the element nodes in the quadtree.
private IntList enodes = new IntList(2);
// ----------------------------------------------------------------------------------------
// Element fields:
// ----------------------------------------------------------------------------------------
// Stores the rectangle encompassing the element.
static final int elt_idx_lft = 0, elt_idx_top = 1, elt_idx_rgt = 2, elt_idx_btm = 3;
// Stores the ID of the element.
static final int elt_idx_id = 4;
// Stores all the elements in the quadtree.
private IntList elts = new IntList(5);
// ----------------------------------------------------------------------------------------
// Node fields:
// ----------------------------------------------------------------------------------------
// Points to the first child if this node is a branch or the first element
// if this node is a leaf.
static final int node_idx_fc = 0;
// Stores the number of elements in the node or -1 if it is not a leaf.
static final int node_idx_num = 1;
// Stores all the nodes in the quadtree. The first node in this
// sequence is always the root.
private IntList nodes = new IntList(2);
// ----------------------------------------------------------------------------------------
// Node data fields:
// ----------------------------------------------------------------------------------------
static final int nd_num = 6;
// Stores the extents of the node using a centered rectangle and half-size.
static final int nd_idx_mx = 0, nd_idx_my = 1, nd_idx_sx = 2, nd_idx_sy = 3;
// Stores the index of the node.
static final int nd_idx_index = 4;
// Stores the depth of the node.
static final int nd_idx_depth = 5;
// ----------------------------------------------------------------------------------------
// Data Members
// ----------------------------------------------------------------------------------------
// Temporary buffer used for queries.
private boolean temp[];
// Stores the size of the temporary buffer.
private int temp_size = 0;
// Stores the quadtree extents.
private int root_mx, root_my, root_sx, root_sy;
// Maximum allowed elements in a leaf before the leaf is subdivided/split unless
// the leaf is at the maximum allowed tree depth.
private int max_elements;
// Stores the maximum depth allowed for the quadtree.
private int max_depth;
}
一時的な結論
申し訳ありませんが、それは少しコードダンプの答えです。私は戻って編集し、ますます多くのことを説明しようとします。
全体的なアプローチの詳細については、元の回答を参照してください。