Binary Space Partitioning Trees (BSP)

BSP Trees


Introduction

BSP trees are becoming one of the most popular spatial subdivision algorithms, due to their flexibility, and their ability to draw a scene with perfect order. However, though they are becoming popular, they are complex and difficult to code at first, and the theory can be hard to understand for some people. In this page, rather than going over and over the same things that have been said a thousand times before, I'll instead give a brief overview of the theory, and concentrate on more details regarding the implementation. More information concerning one specific part of BSP trees can be found in the BSP FAQ, though it does have to be said that there are some unfinished parts in it. This is no criticism on the author, just a neutral observation. (And perhaps a hint that people should help finish the FAQ!).


Theory

Note: this section is only intended as a refresher. If you don't know BSP trees, then there may be better definitions for you to learn from. This is to set the scene, and introduce the contents of this file.

A BSP tree carves up space into sets of successive half-spaces. A half space is the portion of space that is either in front of a hyperplane, or behind it. A hyperplane is a plane, with the same dimensions as the space it is in. For example a 2D hyperplane is a line. A 3D hyperplane is plane. However, a 2D hyperplane (line) cannot be used to partition 3D space into two half-spaces. Likewise a 3D plane cannot partition 4D space into two halfspaces. Halfspaces are infinite, and every polygon must lie entirely in only one halfspace.

A BSP tree is built by making a binary tree, where each node has a plane, which may or may not be from the source model, and the branches point to the sub-nodes that either lie entirely behind that plane, or, entirely in front of that node. Similary, the sub-nodes of that node have their own planes, which contain sub-nodes and so on. The planes stored in the nodes gives us a space partitioning scheme, as they can be used to identify polygons or objects that lie in regions of space enclosed by a set of planes. These planes are later used for sorting the scene. A node may or may not have polygons stored with it; this is the key distinction between node-based and leaf-based BSP (more on that later).

At each node, there is some form of cheap, simple bounding volume, such as a bounding sphere or a bounding box. This box/sphere encloses both the current node, and all its children. This volume is used to aid rendering and culling, as this gives a tree a hierarchial nature, so that subtrees can be discarded if their bounding volume fails to fulfill some criteria.


Code and data structures

The data structure for a BSP tree is fairly simple. Below is a pseudo-code description, derived from the BSP tree structure I use in my own BSP engine.

Structure BSPNode
    BSPNode pointer     frontnode, backnode;
    Vector              centre, transformed_centre;
    List                polygon_list;
    Plane               plane;
    Vector              boxmin, boxmax;
    Vector              transformed_boxmin, transformed_boxmax;
End Structure

Structure BSPTree
    BSPNode Pointer     root
    List                vertex_list
End Structure

These two structures enable us to represent the BSP tree in memory. Included in the structure is enough information to make the tree practical. Note that you should aim to try and make this structure as small as possible, as there may be a great many of them loaded into memory.

Code structures for the BSP tree largely reflect their recursive nature. Recursive functions are very common when dealing with BSP trees, whether they explicity recur on the CPU stack, or, instead use a stack of BSP trees to handle the recursion, so avoiding possible stack overflow. Each method has its advantages and distadvantages, and you should experiment with both. However, for the purposes of this file, I'll stick to explicit recursion, as it makes the code easier to follow.

Loading and saving a BSP tree can be done in the natural recursive manner for this data structure. All it takes is to simply dump the node structure, along with a flag, to the file, and traverse the tree in the same order.

In your flags field, you would have something like:

00000000000000xx - Has front sub-tree
              |--- Has back sub-tree
This flag field contains all the information you need. Then, you choose which order to save nodes in, either [front node, back node], or [back node, front node]. Choose an order and stick to it. Then when you load save, its simple. If a node has a front sub-node, set the bit to 1. Same for the back node. Then, when writing/reading, simply traverse the front or back node if it exists, then traverse the other if it exists. Eg:
    If (node has front sub-node)
        Set bit in flags variable

    If (node has back sub-node)
        Set bit in flags variable

    Read/Write flags field
    Read/Write node data

    If node has front sub-node
        Recur with front-node

    If node has back sub-node
        Recur with back-node
Its that simple. Makes stuff a lot more elegant. Although this might seem hard at first, it soon comes to you. This is partly the reason I don't like or use the Jaluit compiler, because its output format is so weird... this way is so much simpler.


Building the tree

Building the tree is the most expensive process, and it can quickly become very time- and memory-consuming, as the size of the polygon database increases. Building the most optimal tree is a desirable goal, but is very expensive for large databases. Also, for different purposes, there may be different optimal trees. However, there are two general "best" cases; Split-optimized, and Balance-optimized.

To construct a Split-optimized tree, you simply have to select the plane that will cause the least amount of polygons to be split. This is done by classifying what side of the plane a polygon lies. If all of its vertices are in front, its in front, no split. If all are behind, its behind, no split. If all are on the plane, then its just added to the list, no split. However, if some are in front, and some are behind, then theres a split. However, if some are on the plane, and some are [in front / behind], then there is no split. That last point once caused me a lot of problems, and not many people spot it when writing a compiler. Consider:


      /-----\
      |     |
    ============== <--- plane
      |     |
      \-----/

That case needs a split. However:

   ===------======
      |    |
      \----/

Does not need to be split. Its behind the plane, though it may not seem
so at first.
The reason this distinction is so important is that often we have such surfaces butted against each other, and a naive compiler will attempt to split, possibly causing both a degeneration in the polygon, and un-needed increase in polygon count.

Balanced trees must have roughly the same number of polygons either side of the plane. This is quite simple to calculate. Count the number of polygons in front of the plane. Count the number behind. Don't count the number on the plane, or those split, as they don't really need to be considered. To find the difference, just do abs(numback - numfront). If this is less than say some value, eg 5, then take the tree. Else carry on searching. You may wish to look for a balance of zero, which would give you a perfectly balanced tree.

You can also combine the two criteria together. I do this by calculating a "score". The score is derived from the number of splits, the balance, and the number of the plane. I use the formula:

Score = numsplit*ksplit + balance*kbalance + onplane*konplane

Where
Numsplit = number of polygons split
Balance = abs(front - back), the balancing value
Onplane = Number of coplanar polygons
ksplit, kbalance, konplane = Weighting of those threee criteria.
A smaller score will yield a better tree.

Classification Routine

You'll need a routine to classify the polygon against the plane. Though this seems easy enough at first, the case I pointed out above can often complicate things. The best routine I have found is something like:

Function ClassifyPolygon(Polygon, Plane)
    Integer front, back, on

    front = back = on = 0

    For i=0 to Polygon.Numvert
        Switch(ClassifyVertex(Polygon.vertex[i], Plane))
            Case InFront:
                front++
            Case Behind
                back++
            Case OnPlane
                on++
                front++
                back++
        End Switch
    End For

    If on == Polygon.Numvert Then
        Return OnPlane
    Else If front == Polygon.Numvert Then
        Return InFront
    Else If back == Polygon.Numvert Then
        Return Behind
    Else
        Return Split
    End IF
End Function
I've found that this routine works well for me. Now, this routine will have to be coded very well, because its going to be called over and over again.

Bounding Boxes

For BSP trees, I find personally that bounding boxes work as the best bounding volume, because the further you go down the tree, generally, the flatter the box tends to become, because you come closer and closer to a single plane (for node based BSP. So, a box will have the least wasted space. However, a sphere will be easier to code, and slightly cheaper to test. However, you have to trade off the cheaper test against the fact that the wasted space may cause a sphere to be inside the volume, with no polygons inside after all.

Constructing the bounding box for a node and all its children is very simple. Firstly, construct the bounding box for the nodes polygons. Thats done by examining each vertex, and updating the minimum/maximum co-ordinate values accordingly. The two values, minimum and maximum, give you a bounding box for that node alone. Now, traverse either/both/neither of the subtrees, depending on whether they exist or not. Now, find the maximum bounding box out of all (1-3) boxes. Hey presto, thats your box.

Splits

I don't intend to go into the precise details of splitting polygons, as there is pseudo-code in the FAQ, and real code in my compiler. However, there are some things you should be aware of: