| Home Products Screenshots Company
Quad Tree Design Part I: Quad Tree Introduction A quad tree is a data structure used often in programs that must draw graphics on the screen. The one we use here is designed to optimize graphics and computation in a two-dimensional (2D) space. The quad tree, when subdivided one time, divides the screen into 4 rectangles (quadrants) of equal size: Quad Tree subdivided once You can probably already imagine the benefit ot using such a structure: We now can do a simple bounding box test and determine in which quadrant(s) one of our objects exists. In Abound, we have balls moving around the screen. Using this quad tree, we can reduce by 75% the number of objects to check for collisions against by determining two things:
Moving object (blue ball) doesn't have to check for collisions Since
the other objects (depicted as green balls) are in other quadrants than
the moving ball, we do a simple check of our quad tree and discover
there are no objects in the moving ball's quadrant! This reduces
our collision detection computation time greatly because we don't have
to do any collision detection at all. Without using the quad
tree, we would have had 21 circle-to-circle or ball-to-ball collisions
to test for, since there are 21 other objects.
We have simplified some issues so far in order to give you an idea of what this is all about, but don't worry, we will visit each one of the nitty gritty details later. For example, since the blue ball is moving around, don't we need to check both the quadrant its currently in AND the quadrant it will be moving to, if it is crossing quadrant boundaries? The answer is yes, and we will explore this topic a bit later in our article. But first, let's explore the power of the quad tree more. This question may have arisen in your mind: Why stop at just one subdivision (which resulted in 4 quadrants)? Why not subdivide these 4 quadrants again?! This is exactly what we can do to increase the speed of our program even more by decreasing the computation time. The figure below demonstrates that the 2nd division of the quad tree looks like: 2 subdivisions results in 16
quadrants
What did we just do? We took
each one of the 4 quadrants we already had and subdivided each one of
them into 4 more quadrants, which results in a total of 16
quadrants! You can see how this helps us even more, because now
the moving ball has to be very near the stationary balls before we need
to check for any collisions, and even once it gets near them it only
has to check for collisions against the subset of balls in the
particular quadrant it is in.
You can also see from the above figure that some of the green balls are on quadrant boundaries and are partially in two quadrants. What do we do with these? Well, we have to include them in both quadrants they overlap with, otherwise we will have errors if the moving ball should hit one of them but we don't think it needs to check against them, so it would pass through them. This is all great and dandy, you are probably saying to yourself, and it looks nice to draw little lines everywhere, but what about the code to do this? Great question! Let's get after it. We are going to create a class that will represent the quad tree. Two things will help us in the initial design of this class:
class CQuadTreeNode { public: CQuadTreeNode(long lowerLeftX, long lowerLeftY, long width, long height); virtual ~CQuadTreeNode(); HRESULT SubDivide(); private: long m_LowerLeftX; long m_LowerLeftY; long m_Width; long m_Height; CQuadTreeNode** m_SubNodes; }; Alright, let's examine what we have created here in depth. First off, we have the constructor, which takes the values for the lower left corner of the quadrant this node describes. We also create four member variables to store these values. Then we have our destructor, and our first and only function, SubDivide(). Don't worry about the HRESULT return value if you don't know what it is. On Windows systems an HRESULT is just a typedef for a long, which is just an integer. Finally, we have the strange looking line CQuadTreeNode** m_SubNodes; . What can this mean? It is a pointer to a pointer to a CQuadTreeNode, which is what our class is, so it will be used to store the children nodes, if any exist, of this node. We will explain this more in a bit. Here is the body of our constructor, in case you are wondering how to write it: CQuadTreeNode::CQuadTreeNode(long lowerLeftX, long lowerLeftY, long width, long height) : m_LowerLeftX(lowerLeftX), m_LowerLeftY(lowerLeftY), m_Width(width), m_Height(height), m_SubNodes(NULL) { } All we do is initialize our member variables to what they pass us in for the quadrant coordinates, and then the m_SubNodes to NULL, since we don't know what those are yet, or even if we will have any. You can download the code for the entire class at the end of this article. Let's use this new class immediately in our program. When we start out, we have the entire screen to draw to. Using whatever means are available for your platform, get the size of the screen. On Windows, this is often done with the GetClientRect function. For an example, you can download our screensaver example project. Think of the screen as one big quadrant, since that is all it is, which means we can represent it with one CQuadTreeNode object! // get width and height of screen somehow CQuadTreeNode* entireScreen = new CQuadTreeNode(0, 0, screenWidth, screenHeight); Alright! We have created one quad tree node object that has its lower left corner at the origin and stretches the entire screen width and height. One Quad tree node represents the whole screen Windows users caveat: Beware that Windows thinks of the origin as the upper left corner of the screen, and positive y values indicate movement downwards from the top of the screen. In my programs, I reverse this logic in my graphics class by making the origin the lower left of the screen and the graphics class hides from the rest of the program the detail of converting the more logical Cartesian coordinates into Windows drawing coordinates. This quad tree node doesn't do us much good as it is because it represents the entire screen. We need to subdivide it into 4 quadrants to start taking advantage of the quad tree's power. Before we do that, let's understand what type of node this first quad tree node we created is. Is it a leaf node or a branch node? Well, since a leaf node has no children, and a branch node has children, this one is a leaf node. When we call subdivide on it, since it is a leaf node without children, it will create four CQuadTreeNode's which will be its children, and it will automatically become a branch node. Let's look at the first pass at the subdivide function: HRESULT CQuadTreeNode::SubDivide() { HRESULT result = S_OK; if (!m_SubNodes) { // We have no subnodes, so this must be // a leaf node. Create 4 children nodes // which will be its children. // w is new width for subnodes // (half of current width), h similarly long w = m_Width / 2; long h = m_Height / 2; // shorter variable names for current lower x,y position long llx = m_LowerLeftX; long lly = m_LowerLeftY; // node 0: upper left quadrant // node 1: upper right quadrant // node 2: lower right quadrant // node 3: lower left quadrant // Since we have a pointer to a pointer for // our member variable, we need to allocate // memory for the 4 pointers that we are // creating. Note CoTaskMemAlloc is a Windows // function, you can just use malloc in other // platforms. m_SubNodes = (CQuadTreeNode**) ::CoTaskMemAlloc(4 * sizeof(CQuadTreeNode*)); // Now that we have room for the four pointers, // actually create the 4 CQuadTreeNode objects and // store them in our array. We pass in the correct // values for the lower left points of each one, // and the width and height of them are all the // same (1/2 the width and height of this parent node). m_SubNodes[0] = new CQuadTreeNode(llx, lly + h, w, h); m_SubNodes[1] = new CQuadTreeNode(llx + w, lly + h, w, h); m_SubNodes[2] = new CQuadTreeNode(llx + w, lly, w, h); m_SubNodes[3] = new CQuadTreeNode(llx, lly, w, h); } else { // This node already has children, so it is a branch // node. All we have to do is forward this call to // our sub nodes and tell them to subdivide. If they // are leaf nodes, they will subdivide and create 4 // children of their own, otherwise they will just do // what we do and tell their existing children to // subdivide. for (int i = 0; i < 4 && SUCCEEDED(result); i++) { result = m_SubNodes[i]->SubDivide(); } } return result; } This function is at the heart of the quad tree class. Let's call it in our code: // get width and height of screen somehow CQuadTreeNode* entireScreen = new CQuadTreeNode(0, 0, screenWidth, screenHeight); // tell the quad tree node to subdivide itself. entireScreen->SubDivide(); What we have after subdividing our only quad tree node (the "root" node, since it is at the root of the tree), is the root node plus 4 more quad tree nodes, which are the root node's children: The four children of the root node over the quadrants they represent Now we're getting somewhere!
Note that the parent node, the root, still exists and represents the
entire screen area, however, it is a branch node and will farm out
almost all the "real" work that we desire of the quad tree to its
children. Notice also that from the program's perspective that is
using the quad tree, we only have to
deal with the one quad tree root node. We don't have to
manage its children as it does that for us. All we have is a
pointer to the root node, and we told it to subdivide. We do not
get nor do we want pointers to its children. Let the quad tree
manage itself! The way we do this is already apparent to some
degree in the latter part of the SubDivide function, but it will become
even more obvious as we implement more functions.
Here is another way of seeing the tree structure: Tree structure of quad tree nodes we have created so far Okay, so hopefully you understand
what we have done with the first subdivide call. What if we now
do this:
// get width and height of screen somehow CQuadTreeNode* entireScreen = new CQuadTreeNode(0, 0, screenWidth, screenHeight); // tell the quad tree node to subdivide itself. entireScreen->SubDivide(); // tell it to divide itself again! entireScreen->SubDivide(); We have told it to subdivide itself twice! The second time we call SubDivide, we will enter into the else clause of the conditional for the root node, since it now has valid m_SubNodes. This means we will then call into the 4 children quad tree nodes of the root node and tell them to subdivide. They will then do what our root node did in the first call we made to it, since they have no children, they will each create 4 children of their own to make a total of 16 nodes. Also, the four children nodes of the root node become branch nodes instead of leaf nodes. We can go back to an earlier diagram to see the situation we have now: 16 leaf nodes, 4 branch nodes, 1 root node We have labeled each quadrant with its array pseudo-index from the root node. For example, since the upper left 1st level quadrant has an index of [0] in the m_SubNodes array member of the root node, the upper left 2nd level quadrant has an index of [0, 0]. These are just logical indices; in the code you never have to actually index into the nodes like this because each quad tree node manages its own children. We can subdivide like this as many times as we want. Each time we divide, we exponentially increase the number of leaf nodes. The formula for the number of leaf nodes is 4 ^ numSubdivisions. For example, if we subdivide 3 times, we have 4 ^ 3 = 4*4*4 = 64 leaf nodes. If we subdivide 4 times we get 256 leaf nodes. You must experiment with your own program to find the optimal number of subdivisions. For Abound, we use 256 leaf nodes since that works out well for the size of objects we usually have. Part II: Quad Tree Usage Okay, so now we have this quad tree with all these nodes. Great, what good is it? Well, we need to create a function to add objects to the quad tree, and also one that will give us all the objects in the same quadrant or quadrants as a specific object (like the moving one). These functions will allow us to make use of the quad tree by getting the small subset of objects that are around an object, and only doing collision detection with those objects. Let's add some functions and members to our class: class CQuadTreeNode { public: CQuadTreeNode(long lowerLeftX, long lowerLeftY, long width, long height); virtual ~CQuadTreeNode(); HRESULT SubDivide(); HRESULT AddObject(ICollidableObject* object); HRESULT GetSize(long* llx, long* lly, long* width, long* height); private: long m_LowerLeftX; long m_LowerLeftY; long m_Width; long m_Height; CQuadTreeNode** m_SubNodes; // This stl set will store the objects added to our node typedef std::set<ICollidableObject*> CollidablesSet; CollidablesSet m_Collidables; }; "Wait a second! What's with this weird-looking ICollidableobject type I am seeing everywhere?" Don't worry! This type is nothing to worry about: we will explain it and if you want you can either implement your own ICollidableObject class or you can completely remove this type and use your own object class. ICollidableObject is an interface that we use in Abound that contains all the functions that we need to model a collidable object. Examples of collidable objects we use in Abound are the balls, the walls around the screen (they're invisible in Abound but still objects in our program), and the black holes. We actually further distinguish balls into normal moving balls and stationary balls (or pins). All of these different classes implement the ICollidableObject interface. Here are the functions of the ICollidableObject interface: interface ICollidableObject : IUnknown { HRESULT GetPosition([out] double* xPos, [out] double* yPos); HRESULT GetPrevPosition([out] double* xPos, [out] double* yPos); }; The CBall class, which represents moving balls, implements this interface like this: class CBall : public ICollidableObject { public: CBall(POINT* center, long radius); virtual ~CBall(); public: // IUnknown HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, LPVOID* obp); ULONG STDMETHODCALLTYPE AddRef(void); ULONG STDMETHODCALLTYPE Release(void); // ICollidableObject HRESULT STDMETHODCALLTYPE GetPosition(double* xPos, double* yPos); HRESULT STDMETHODCALLTYPE GetPrevPosition(double* xPos, double* yPos); }; If you are unfamiliar with interfaces and Windows programming, this may look like gibberish. Don't worry too much about it. You can make your own simple class CBall or CBox or whatever that just implements the GetPosition and GetPrevPosition functions. Okay, so back to the CQuadTreeNode class, we have added an AddObject function which should add the object to every quad tree leaf node that it overlaps with. Branch nodes never have objects in them, since they relegate all their calls to their children nodes. Also, we have created the GetSize function that just returns the dimensions of a quad tree node. Finally, we have created an stl set to store the objects added to this particular node. If you are unfamiliar with the Standard Template Library (stl), it is a set of containers and functions that are basically very useful and dynamic data structures. The stl set is a data structure that stores its items in a red-black tree, which is a form of binary tree, allowing logarithmic speed access to objects. If you are not comfortable with stl, you can always substitute this data structure out with your own array class or regular C-style array (though you must dynamically grow it and manage it, one of the main benefits of stl containers). Alright, let's check out the implementation for the AddObject function: HRESULT CQuadTreeNode::AddObject(ICollidableObject* object) { HRESULT result = S_OK; // If this node has subnodes, figure out // which subnode this object belongs // in and add it to him, otherwise, just add it to our list if (!m_SubNodes) { // no subnodes, this is a leaf node, so add it to the set std::pair<CollidablesSet::iterator, bool> newInsertion; newInsertion = m_Collidables.insert(object); if (newInsertion.second) { // only add ref if this is object is not already in our set, // otherwise we will over ref count it. object->AddRef(); } else { // they should never add the same object to us twice assert(false); } } else { // This is a branch node, so we need to see which of our // children nodes need to have this object added to them. // the object may need to go in more than one sub node if it // spans multiple quadrants int collisionArray[4] = { -1, -1, -1, -1 }; int collisionIndex = 0; // Simplify and assume this object is a circular object // that has a GetRadius function. For other object // types you must detect the type of object and then // do an appropriate test for overlap with this quadrant. long radius = 0; result = object->GetRadius(&radius); long lowerLeftX = 0; long lowerLeftY = 0; long width = 0; long height = 0; double cx, cy; // Get this object's position and then make a bounding // box that encompasses the ball. This function returns // the center point of the ball. result = object->GetPosition(&cx, &cy); long lcx, lcy; lcx = ::Roundoff(cx); lcy = ::Roundoff(cy); assert(SUCCEEDED(result)); lowerLeftX = lcx - radius; lowerLeftY = lcy - radius; width = 2 * radius; height = width; long llx, lly, w, h; // check each subnode to see if it is inside it. for (int i = 0; i < 4; i++) { result = m_SubNodes[i]->GetSize(&llx, &lly, &w, &h); assert(SUCCEEDED(result)); int left1, left2; int right1, right2; int top1, top2; int bottom1, bottom2; // This code tests whether two rectangles overlap or not. // It tests to see if one box's bottom edge is above the // other box's top edge, and if so then they don't overlap, // then it makes the other 3 similar tests to definitively // determine overlap. left1 = lowerLeftX; left2 = llx; right1 = lowerLeftX + width; right2 = llx + w; bottom1 = lowerLeftY; bottom2 = lly; top1 = lowerLeftY + height; top2 = lly + h; if (bottom1 > top2 || top1 < bottom2 || right1 < left2 || left1 > right2) { // they do not overlap, move onto the next subnode. continue; } else { // they overlap, so add to our array of subnodes that // we need to tell to add this object. collisionArray[collisionIndex++] = i; } } // now we have found all the subnodes that this object overlaps // with, so tell them to add this object to themselves. If // the node is a branch node, it will examine its children // nodes and tell the appropriate ones to add this object, or // else if it is a leaf node it will simply add this object to // its set. for (int j = 0; j < collisionIndex; j++) { m_SubNodes[collisionArray[j]]->AddObject(object); } } return result; } That's a pretty big function. If the node is a leaf node, it just adds the object to its set because it knows that its parent nodes have determined that this object is overlapping with itself. If the node is a branch node with children it only wants to tell its subnodes that overlap with the object to add the object. Okay, so now we need a function that will take in an object and give us the other objects that are close to it. We can pass in an object that we have already moved forward to its next location, a test move if you will, such that its GetPosition function returns its current (new) position, and the GetPrevPosition function returns the position it was just at. Or we can pass in an object that is not moving, and it will simply return to us the objects around it. The last parameter is a reference to an stl set of ICollidableObject pointers. This is how we pass back the objects to the caller. If you want to be old school, you can replace this parameter with an array of pointers to your object type. class CQuadTreeNode { public: CQuadTreeNode(long lowerLeftX, long lowerLeftY, long width, long height); virtual ~CQuadTreeNode(); HRESULT SubDivide(); HRESULT AddObject(ICollidableObject* object); HRESULT GetSize(long* llx, long* lly, long* width, long* height); HRESULT GetCollidableObjects(ICollidableObject* object, bool moving, set<ICollidableObject*>& collidables); private: long m_LowerLeftX; long m_LowerLeftY; long m_Width; long m_Height; CQuadTreeNode** m_SubNodes; // This stl set will store the objects added to our node typedef std::set<ICollidableObject*> CollidablesSet; CollidablesSet m_Collidables; }; Let's explore the implementation of this function: // if moving is true, caller should have // moved the object forward a time step before // calling this function HRESULT CQuadTreeNode::GetCollidableObjects( ICollidableObject* object, bool moving, std::set<ICollidableObject*>& collidables) { HRESULT result = S_OK; if (!m_SubNodes) { // no subnodes, this is a leaf node, so return entire set CollidablesSet::iterator it = m_Collidables.begin(); while (it != m_Collidables.end()) { // exclude the actual object they are passing in, // since they want objects // other than this object in the area. if (*it != object) { collidables.insert(*it); } it++; } } else { // the object may be spanning more than one sub node long radius = 0; result = object->GetRadius(&radius); assert(SUCCEEDED(result)); // store the indices for subnodes that this object // overlaps with. If we are dealing with a moving // object, we have to make two passes through our // nodes, once for its current position and once for // its previous position, so use this set to store // the indices of the subnodes we find, and avoid // adding the same subnode twice. std::set<int> collisionSave; double cx, cy; // if the ball is moving, then look at both // the ball's previous position // and the ball's current position, // otherwise just look at the current // position int times2loop = moving ? 2 : 1; for (int k = 0; k < times2loop; k++) { if (k == 0) { result = object->GetPosition(&cx, &cy); } else if (k == 1) { result = object->GetPrevPosition(&cx, &cy); } else { assert(false); } // create bounding box around circular object long lcx, lcy; lcx = ::Roundoff(cx); lcy = ::Roundoff(cy); long lowerLeftX = lcx - radius; long lowerLeftY = lcy - radius; long width = 2 * radius; long height = width; long llx, lly, w, h; int collisionArray[4] = { -1, -1, -1, -1 }; int collisionIndex = 0; // check for overlap just like in AddObject for (int i = 0; i < 4; i++) { result = m_SubNodes[i]->GetSize(&llx, &lly, &w, &h); assert(SUCCEEDED(result)); int left1, left2; int right1, right2; int top1, top2; int bottom1, bottom2; left1 = lowerLeftX; left2 = llx; right1 = lowerLeftX + width; right2 = llx + w; bottom1 = lowerLeftY; bottom2 = lly; top1 = lowerLeftY + height; top2 = lly + h; if (bottom1 > top2 || top1 < bottom2 || right1 < left2 || left1 > right2) { // no overlap continue; } else { // overlap collisionArray[collisionIndex++] = i; } } // save off all the subnodes we found that overlap with // this object before we begin our second pass (for moving // objects only). if (k == 0) { for (int j = 0; j < collisionIndex; j++) { collisionSave.insert(collisionArray[j]); } } // go through each subnode we have found that overlaps with // this object and tell it to give us its objects by // calling ITS GetCollidableObjects function. for (int j = 0; j < collisionIndex; j++) { if (k == 1) { // kind of confusing, but this just avoids calling // into the same subnodes more than once. if (collisionSave.find(collisionArray[j]) != collisionSave.end()) { continue; } } // get all the collidable objects from this subnode. CollidablesSet subNodeCollidables; m_SubNodes[collisionArray[j]]->GetCollidableObjects( object, moving, subNodeCollidables); // now add them to the set of objects we pass back to the // caller. CollidablesSet::iterator it = subNodeCollidables.begin(); while (it != subNodeCollidables.end()) { collidables.insert(*it); it++; } } } } return result; } Another monster function! This function is a bit complex because it handles both cases of object: moving and stationary. For moving objects it takes into account both the previous position and the current position to find objects in all quadrants these positions overlap with. This is not fool-proof as the ball could actually move across more than one quadrant and "skip" over quadrants entirely, but provided that ouir movement timestep is small enough, we can be fairly certain we won't get into trouble with this case. Like the AddObject function, if it is a leaf node it just returns all the objects in its set, otherwise if it is a branch node it will find which subnodes overlap with the object and tell them to give it their objects. This recursive type of behavior is very powerful, as you can see, as no matter how many times you subdivide there are only two types of nodes: leaf nodes without children nodes but with objects in them and branch nodes with children but without objects. Congratulations! You have now learned the basic design of a quad tree. This is only the tip of the iceberg as far as this data structure is concerned, but I am confident you can take this starter code and modify it to fit your own program. Download the C++ source code for the quad tree class if you want to work with it more. It includes more code that removes objects from the quad tree and even dynamically updates obects in the quad tree which is great because you can have moving objects in the quad tree, too, and call the update function after you move them so that their location in the quad tree is correct. |
| 2004 Heroic Virtue Creations. All rights reserved. Contact us, Privacy Policy |