Jordan Savant # Software Engineer

Binary Space Partitioning

Binary Space Partitioning is an algorithim that takes a set of 2D lines or 3D polygons and recursively groups them into a BSP Tree. Each node has three lists: two child nodes representing lines in front of and lines behind and a thirt list representing lines on the same plane. Once the BSP Tree is built it can be used to search which lines are facing any position in linear time.

It originally was created in 1969 during early attempts at 3D rendering but was most popularly introduced by John Carmack in the original DOOM game. In the game, a level editor was used by the DOOM team to create levels which were comprised by sectors and line data called linedefs. When publishing their level data, Binary Space Partitioning was run against the level and produced a BSP tree of LineDefs and subsplit linedefs called Segs. This BSP tree was then included in their level data for the game engine called a WAD file.

When the DOOM engine loaded the data from the level, it would use the BSP tree nodes to traverse and render the game in a reverse painters algorithm. The BSP tree allowed the game engine to only search and draw walls that were facing the player, culling large numbers of walls from the process. This was where the BSP shined allowing DOOM to run in ways other DOS games had not at the time.

A BSP tree is computationally expensive to run, which is why it was done during level creation and as such required levels be static in nature without horizontal changes in the map's layout.

The Algorithm

  • Two connected vertices define a LineDef
  • A LineDef has an outward face to define which side of it is considered open and which side is solid
  • A list of LineDefs form a Polygon that can define the boundaries of a room, or "sector"
  • A Binary Space Partition class accepts the worlds list of LineDefs
    • The BSP chooses a best candidate "splitter" LineDef by which to judge all other LineDefs
    • The BSP creates two child BSPs to place other LineDefs behind or in front of the splitter
    • LineDefs that cross the plane of the splitter and split into smaller LineDefs to be sorted
  • The BSP sorts all LineDefs recursively in the child trees
  • Using the BSP we can test whether a position is in open space with a depth search
  • Using the BSP we can also render all walls from either back to front or front to back
    • Doom rendered them front to back and used culling to prevent overdraw
    • Other engines have rendered back to front called the Painters Algorithm
  • The BSP Tree is built before the game begins as a means of significant performance gain

Creating 2D Rooms of Connected Lines

Using Python I will demonstrate how a BSP is built, but first we need to create some meaningful polygons to split into a BSP tree. This set of polygons will be comprised of lines with: x coord, y coord, which way it is "facing", and the height of the wakll (used only in rendering).

NOTE: all math is included at the end.

# Lines, each vertex connects to the next one in CW fashion
# third element is direction its facing, when CW facing 1 = left
polygons = [
    # open room
    [
        # x, y, facing, height
        [30,  30, 0, 10],
        [300, 20, 0, 10],
        [400, 300, 0, 10],
        [30, 200, 0, 10]
    ],
    # inner col
    [
        # x, y, facing
        [50,  50, 1, 5],
        [100, 50, 1, 5],
        [75,  75, 1, 5],
        [100, 100, 1, 5],
        [50,  100, 1, 5]
    ],
    # inner room
    [
        [55, 55, 0, 5],
        [70, 55, 0, 5],
        [70, 95, 0, 5],
        [55, 95, 0, 5],
    ]
]

# Line Defs built Clockwise
allLineDefs = []
for i, v in enumerate(polygons):
    polygon = polygons[i]
    lineDefs = []
    for idx, val in enumerate(polygon):
        lineDef = LineDef()

        # first point, connect to second point
        if idx == 0:
            lineDef.asRoot(polygon[idx][0], polygon[idx][1], polygon[idx + 1][0], polygon[idx + 1][1], polygon[idx + 1][2], polygon[idx + 1][3])
            lineDefs.append(lineDef)
            allLineDefs.append(lineDef)

        # some point in the middle
        elif idx < len(polygon) - 1:
            lineDef.asChild(lineDefs[-1], polygon[idx + 1][0], polygon[idx + 1][1], polygon[idx + 1][2], polygon[idx + 1][3])
            lineDefs.append(lineDef)
            allLineDefs.append(lineDef)

        # final point, final line, connects back to first point
        elif idx == len(polygon) - 1:
            lineDef.asLeaf(lineDefs[-1], lineDefs[0], polygon[idx][2], polygon[idx][3])
            lineDefs.append(lineDef)
            allLineDefs.append(lineDef)

These polygons contain line data which are similar to LineDefs in DOOM, and of note the have a "face" direction which indicates which side is the exposed side to the game world and should be rendered. This facilitates the concept of a wall and is important for subdividing the other lines into the BSP tree.

These polygons when glued together and rendered look like this, with a larger room in which our world is contained and within a smaller room completely enclosed within another set of walls.

The blue lines are the overall walls, the smaller lines projecting as their normal vector represent their face's direction. The Dot and Line are the camera position and angle in the 2D world.

This algorithm converts the raw polygon data into a LineDef object that is defined as:

import random
from engine.mathdef import crossProductLine
from engine.mathdef import normalize
from engine.mathdef import intersection2d
from engine.mathdef import pointBehindSegment

# A polygon is a collection of lines, each line has a direction
# All lines should be connected, meaning one should start where the last one ends

class LineDef(object):
    def __init__(self):
        self.start = []
        self.end = []
        self.facing = None
        self.mid = []
        self.normals = []
        self.drawColor = (random.randint(10, 255), random.randint(10, 255), random.randint(50, 255))
        self.height = 10
        # TODO: get normals and stuff calculated here

    def asRoot(self, startX, startY, endX, endY, facing, height):
        self.start = [startX, startY]
        self.end = [endX, endY]
        self.facing = facing
        self.height = height
        self.setup()

    def asChild(self, preLineDef, endX, endY, facing, height):
        self.start = [preLineDef.end[0], preLineDef.end[1]]
        self.end = [endX, endY]
        self.facing = facing
        self.height = height
        self.setup()

    def asLeaf(self, preLineDef, rootLineDef, facing, height):
        self.start = [preLineDef.end[0], preLineDef.end[1]]
        self.end = [rootLineDef.start[0], rootLineDef.start[1]]
        self.facing = facing
        self.height = height
        self.setup()

    def setup(self):
        self.setMidpoint()
        self.setNormals()

    def setMidpoint(self):
        # (a.x+b.x)/2,(a.y+b.y)/2
        self.mid.append((self.start[0] + self.end[0]) / 2)
        self.mid.append((self.start[1] + self.end[1]) / 2)

    def setNormals(self):
        dx = self.end[0] - self.start[0]
        dy = self.end[1] - self.start[1]
        self.normals.append(normalize(-dy, dx)) # First normal is the one facing in (if we are Clockwise)
        self.normals.append(normalize(dy, -dx)) # Second normal is the one facing out (if we are Clockwise)

    def isPointBehind(self, a, b):
        # If it is behind and we are facing left CW
        beh = pointBehindSegment([a, b], self.start, self.end) # true, false or none (for on the same plane)
        if beh != None:
            if self.facing == 1:
                return beh
            return not beh
        return None

    def classifyLine(self, testLine):
        # 1 = back
        # 2 = front
        # 3 = spanning
        # 4 = co planar
        points = [testLine.start, testLine.end]
        backCounter = 0
        frontCounter = 0
        for point in points:
            result = self.isPointBehind(point[0], point[1])
            if result == True:
                backCounter += 1
            if result == False:
                frontCounter +=1
            # if result == None:
                # co planar, no counters

        # spanning
        if backCounter != 0 and frontCounter != 0:
            return 3
        # back
        if backCounter:
            return 1
        # front
        if frontCounter:
            return 2
        return 4

    def split(self, other):
        # get the intersecting point
        intersection = self.findIntersection(other)
        if intersection:
            # create a line from other start to intersection and return that as front??
            splitA = [other.start, intersection]
            splitB = [intersection, other.end]
            aBehind = self.isPointBehind(other.start[0], other.start[1])
            # return them with position 0 behind and position 1 in front
            if aBehind:
                return [splitA, splitB]
            else:
                return [splitB, splitA]
        return None

    def findIntersection(self, other):
        return intersection2d(self.start, self.end, other.start, other.end)

The BSP Tree Data Structure

Here is a BSP Tree class that does all the brutal work. Being a tree the actual class is just a sindle Node of the tree with recursive functions for traversing and building child Node lists.

class SolidBSPNode(object):
    def __init__(self, lineDefs):
        self.splitter = None # LineDef
        self.front = None # Self
        self.back =  None # Self
        self.isLeaf = False
        self.isSolid = False

        if len(lineDefs) == 0: # leaf
            return

        # get splitter line
        splitterIndex = self.selectBestSplitter(lineDefs)
        self.splitter = lineDefs[splitterIndex]

        # iterate the lineDefs and figure out which side of the splitter they are on
        frontList = []
        backList = []
        for lineDef in lineDefs:
            # skip splitter self check
            if lineDef != self.splitter:
                d = self.classifyLine(self.splitter, lineDef)

                if d == 1:
                    # Behind
                    backList.append(lineDef)
                elif d == 2 or d == 4:
                    # Front or coplanar
                    frontList.append(lineDef)
                elif d == 3:
                    # Spanning
                    # first element is behind, second is in front
                    splits = self.splitLine(self.splitter, lineDef)
                    backList.append(splits[0])
                    frontList.append(splits[1])

        # all lines have been split and put into front or back list
        if len(frontList) == 0:
            # create an empty leaf node
            self.front = SolidBSPNode([])
            self.front.isLeaf = True
            self.front.isSolid = False
        else:
            # create our recursive front
            self.front = SolidBSPNode(frontList)

        if len(backList) == 0:
            # create a solid back node
            self.back = SolidBSPNode([])
            self.back.isLeaf = True
            self.back.isSolid = True
        else:
            # create our recursive back
            self.back = SolidBSPNode(backList)

    def splitLine(self, splitterLineDef, lineDef):
        # first element is behind, second is in front, use facing from parent
        splits = splitterLineDef.split(lineDef)

        splitBehind = splits[0]
        splitBehindDef = linedef.LineDef()
        splitBehindDef.asRoot(splitBehind[0][0], splitBehind[0][1], splitBehind[1][0], splitBehind[1][1], lineDef.facing,  lineDef.height)

        splitFront = splits[1]
        splitFrontDef = linedef.LineDef()
        splitFrontDef.asRoot(splitFront[0][0], splitFront[0][1], splitFront[1][0], splitFront[1][1], lineDef.facing,  lineDef.height)
        return [splitBehindDef, splitFrontDef]

    # if all points behind, we would put whole poly in back list
    # if all points ahead, we would put whole poly in front list
    # if overlap, split and put into both
    def classifyLine(self, splitterLineDef, lineDef):
        return splitterLineDef.classifyLine(lineDef)

    def selectBestSplitter(self, lineDefs):
        # compare each line to each other line
        # classify where the line falls and increment number
        best = 99999
        splitterIndex = 0
        for idxA, lineDefA, in enumerate(lineDefs):
            fronts = 0
            backs = 0
            splits = 0
            for idxB, lineDefB, in enumerate(lineDefs):
                if lineDefA != lineDefB:
                    d = self.classifyLine(lineDefA, lineDefB)
                    if d == 1: #back
                        backs += 1
                    elif d == 2 or d == 4: # front or coplanar
                        fronts += 1
                    elif d == 3: # spanning
                        splits += 1
                    # make splits more costly
                    score = abs(fronts - backs) + (splits * 8)
                    if score < best:
                        best = score
                        splitterIndex = idxA
        return splitterIndex

    def inEmpty(self, testPoint):
        # recurse the tree until we find a leaf node
        if self.isLeaf:
            return self.isSolid == False
        beh = self.splitter.isPointBehind(testPoint[0], testPoint[1])
        if beh:
            return self.back.inEmpty(testPoint)
        else:
            return self.front.inEmpty(testPoint)

    def getWallsSorted(self, posA, posB, walls, depth = 0):
        if not self.isLeaf:
            behind = self.splitter.isPointBehind(posA, posB)
            # painter's algorithm paints back to front, (front to back recursively)
            # to switch to doom algorithm (requires clipping) swap front and back orders
            if behind:
                self.front.getWallsSorted(posA, posB, walls, depth + 1)
                # walls.append(self.splitter)
                self.back.getWallsSorted(posA, posB, walls, depth + 1)
            else:
                self.back.getWallsSorted(posA, posB, walls, depth + 1)
                walls.append(self.splitter)
                self.front.getWallsSorted(posA, posB, walls, depth + 1)

Hopefully the code is self explanatory in regards to the algorithm itself.

Some of the more complicated elements include determining if a LineDef plane crosses another line def plane and splitting that location.

Of note as well is the concept of if a node isSolid. This is a flag that is set for all node children that are created. All Leaf nodes do not actually contain line lists but two primay flags: isLeaf and isSolid. The parent of a leaf node is actually the parent of two leaf nodes, one in front and one behind. We can designate all "behind" leaf nodes as isSolid=true.

If we test a position against the tree and we wind up at a leaf node that isSolid we know the position is invalid.

Segs

The splitting and sorting operation creates the segs, or sub segments of lines and can be seen here as different colored lines that comprise either all or portions of our previous line defs.

Can you determine which line was the "Splitter" line of this BSP tree from the segs created?

This drawing routine is appended to our BSP class such as

    def drawSegs(self, display, depth = 0):
        # draw self
        if self.isLeaf == False:

            ln = 4
            mx = self.splitter.mid[0]
            my = self.splitter.mid[1]
            nx = self.splitter.normals[self.splitter.facing][0] * ln
            ny = self.splitter.normals[self.splitter.facing][1] * ln
            if self.splitter.facing == 1:
                display.drawLine([ [mx, my] , [mx + nx, my + ny] ], (0, 255, 255), 1)
            else:
                display.drawLine([ [mx, my] , [mx + nx, my + ny] ], (255, 0, 255), 1)

            display.drawLine([self.splitter.start, self.splitter.end], self.splitter.drawColor, 1)

        if self.back:
            self.back.drawSegs(display, depth + 1)
        if self.front:
            self.front.drawSegs(display, depth + 1)

Faces

We can also visualize the faces of the lines and which ones face our chosen position by recursing and testing the position with the seg's face direction. The faded lines represent those facing away from the position.

posA being the x coord and posB being the y coord of the camera or player

    def drawFaces(self, display, posA, posB, depth = 0):
        # draw self
        if self.isLeaf == False:
            behind = self.splitter.isPointBehind(posA, posB)
            if not behind:
                display.drawLine([self.splitter.start, self.splitter.end], (255, depth * 20, 0), 1)
            else:
                display.drawLine([self.splitter.start, self.splitter.end], (60, depth * 5, 0), 1)

        if self.back:
            self.back.drawFaces(display, posA, posB, depth + 1)
        if self.front:
            self.front.drawFaces(display, posA, posB, depth + 1)

Walls

We simply need to adjust how we are rendering our faces to not render lines facing away from the player. Traversing before or after rendering defines in which order we render our walls facing our player.

The traditional Painters Algorithm renders based on furthest to closest, each wall covering the one behind it. This is simple but non-performant as large portions of walls are rendered and then overwritten wastefully.

In DOOM they revesed the approach rendering from closest to furthest. Because the game was actually 2D projected as 3D the geometry was simple enough to keep a running list of screen coords that were rendered to and to cull any walls behind the walls already rendered.

    def drawWalls(self, camera, display, depth = 0):
        if self.isLeaf == False:
            behind = self.splitter.isPointBehind(camera.worldX, camera.worldY)
            if not behind:
                topLeft, topRight, bottomRight, bottomLeft = camera.projectWall(self.splitter, display.width, display.height)
                if topLeft:
                    wallLines = [ topLeft, topRight, bottomRight, bottomLeft ]
                    display.drawPolygon(wallLines, self.splitter.drawColor, 0)
        if self.front:
            self.front.drawWalls(camera, display, depth + 1)
        if self.back:
            self.back.drawWalls(camera, display, depth + 1)

Basic Collision/Bounds Checking

It was simple enough to test if the position is "within" a wall" through a form of BSP collision detection. Enabling this check in code prevented the player from crossing a wall.

We can facilitate this with a check to see if the leaf node of the tree we wind up in is considered solid or not. If the position we are testing is in a lead node that is solid then it is not allowed.

    def inEmpty(self, testPoint):
        # recurse the tree until we find a leaf node
        if self.isLeaf:
            return self.isSolid == False
        beh = self.splitter.isPointBehind(testPoint[0], testPoint[1])
        if beh:
            return self.back.inEmpty(testPoint)
        else:
            return self.front.inEmpty(testPoint)

Conclusions

The BSP tree is really an awesome algorithm and data structure in one. The selection of a splitter line can affect the performance. The wider and shallower our tree the faster our checks become, so more than one BSP could be tested for performance improvements.

You can see this BSP Tree in action in my Doom Engine recreation in Python.

Some of the code here was of course referenced from other documentation about a BSP tree.

Maths

As I stated before here is the math functions referenced in the code above

import math

def crossProductLine(a, b):
    return a[0] * b[1] - a[1] * b[0]
    #{   return (p.x*q.y - p.y*q.x); }

def pointBehindSegment(point, a, b):
    cross = (b[0] - a[0]) * (point[1] - a[1]) - (b[1] - a[1]) * (point[0] - a[0])
    if cross > 0:
        return True
    if cross < 0:
        return False
    if cross == 0:
        return None

def normalize(a, b):
    if a != 0 or b != 0:
        length = math.sqrt(a * a + b * b)
        return [a / length, b / length]
    return [a, b]

def perp2d(a, b):
    return [-b, a]

def rotate2d(x, y, rads):
    cos = math.cos(rads)
    sin = math.sin(rads)
    return [(x * cos) - (y * sin), (x * sin) + (y * cos)]

def distance2d(x1, y1, x2, y2):
    x = x1 - x2
    y = y1 - y2
    return math.sqrt(x*x  + y*y)

def toRadians(x, y):
    v = normalize(x, y)
    return math.atan2(v[1], v[0])

def toVector(rads):
    return [math.cos(rads), math.sin(rads)]

def intersection2d(splitterStart, splitterEnd, lineStart, lineEnd):
    s1 = splitterStart
    e1 = splitterEnd
    s2 = lineStart
    e2 = lineEnd

    a1 = e1[1] - s1[1]
    b1 = s1[0] - e1[0]
    c1 = a1 * s1[0] + b1 * s1[1]

    a2 = e2[1] - s2[1]
    b2 = s2[0] - e2[0]
    c2 = a2 * s2[0] + b2 * s2[1]

    delta = a1 * b2 - a2 * b1
    # if lines are parallel, the result will be delta = 0
    if delta != 0:
        return [(b2 * c1 - b1 * c2) / delta, (a1 * c2 - a2 * c1) / delta]
    return None