2D Frustum Culling – Tutorial (Part 1)

Hello and welcome to my tutorial on 2D Frustum Culling!

Have you wanted to only draw and update only the objects (eg. player, enemies, items) visible in your game? Particularly in a scrolling-level based game, such as a space shooter or side-on platformer, levels can be much longer than the width of the screen.

For example, perhaps you have a level that scrolls along while the player is close to either side of the screen. Without using some form of culling method, in this case the player could still be at the start of the level and those enemies at the opposite end of the level are still being drawn, updated and checking collision with the level every frame – even if they’re not on the screen yet!!

By using frustum culling, you can make sure an object (or all objects if you like) is only updated, drawn and checked for collision if it’s within the frustum (in this tutorial, it’s simply a box). Based on how busy your game is (eg. number of objects, processes, etc), culling can dramatically (or marginally) improve framerate and free up the CPU/GPU to move onto other tasks.

What exactly is ‘Frustum Culling’?

Lighthouse3D has an excellent description and well-explained tutorial about frustum culling with 3D graphics in mind. In this tutorial we’ll be working in 2D, however it’s still worth a read if you’re interested in the theory behind frustum culling.

Let’s get started!

Taking the theory and principle of frustum culling from a 3D point of view, we’ll develop our own frustum culling system for a 2D engine or game. I found creating this system to be easier and more simple than I first thought, which is why I wrote this tutorial!

Before starting this tutorial, I recommend the following:

  • Use of your own or a 3rd party engine for graphics, I won’t be covering 2D rendering in this tutorial.
  • Use of your own Vector2 class (or struct) OR a 3rd party one will simplify parts of this tutorial

Before we can start writing our 2D camera (and frustum) code, we need some utility classes/structs in place – namely a ‘Vector2‘ class and a ‘BoxCollider‘ struct. These are both going to be used later on in the tutorial, which is why this first part is only about getting these set up and ready for use.

Please note: If you’re using your own or a 3rd party library for maths, physics or collision (or all), you could use that instead of the ‘Vector2‘ and ‘BoxCollider‘ systems I cover in this tutorial. Just keep in mind some form of ‘Collider’ class or struct is used once we get to the frustum code, if you have access to one, that’s great! If not, then you may want to use the one from this tutorial. All code for this and all parts of this tutorial is pure C++, no 3rd party APIs are used.

A bare-bones ‘Vector2’ class:

#ifndef VECTOR2_H
#define VECTOR2_H

// A simple 2D vector class
class Vector2
{
public:
      // Function prototypes
      //---------------------//

      // Constructors
      Vector2();
      Vector2(float a_fValue);
      Vector2(float a_fX, float a_fY);

      // Overloaded operators
      Vector2 operator +(const Vector2 &a_v2);   // '+' operator
      Vector2 &operator +=(const Vector2 &a_v2); // '+=' operator
      Vector2 operator -(const Vector2 &a_v2);   // '-' operator
      Vector2 &operator -=(const Vector2 &a_v2); // '-=' operator

      bool operator ==(Vector2 &a_v2); // '==' (Equality) operator
      bool operator !=(Vector2 &a_v2); // '!=' (Not equal to) operator

      // Member variables
      //---------------------//

      float X; // X (horizontal) axis
      float Y; // Y (vertical) axis
};

#endif // VECTOR2_H

That’s the class structure done. The constructors can set the X and Y axes to either a single value, separate values, or zero. With the overloaded operators, we can perform simple math operations including addition, subtraction, equality and inequality (or ‘not equal to’).

#include "Vector2.h"

// Default constructor
Vector2::Vector2()
{
      // Set X/Y axes to zero
      X = 0;
      Y = 0;
}

// Overloaded constructor (single value)
Vector2::Vector2(float a_fValue)
{
      // Set X/Y axes to specified value
      X = a_fValue;
      Y = a_fValue;
}

// Overloaded constructor (separate values)
Vector2::Vector2(float a_fX, float a_fY)
{
      // Set X/Y axes to specified values
      X = a_fX;
      Y = a_fY;
}

// Overloaded '+' operator
Vector2 Vector2::operator +(const Vector2 &a_v2)
{
      return Vector2(a_v2.X + X, a_v2.Y + Y);
}

// Overloaded '+=' operator
Vector2 &Vector2::operator +=(const Vector2 &a_v2)
{
      return (*this = *this + a_v2);
}

// Overloaded '-' operator
Vector2 Vector2::operator -(const Vector2 &a_v2)
{
      return Vector2(X - a_v2.X, Y - a_v2.Y);
}

// Overloaded '-=' operator
Vector2 &Vector2::operator -=(const Vector2 &a_v2)
{
      return (*this = *this - a_v2);
}

// Overloaded '==' (equality) operator
bool Vector2::operator ==(Vector2 &a_v2)
{
      return (X == a_v2.X && Y == a_v2.Y);
}

// Overloaded '!=' (inequality) operator
bool Vector2::operator !=(Vector2 &a_v2)
{
      return (X != a_v2.X && Y != a_v2.Y);
}

That completes our basic Vector2 class. It makes a great starting template and could use more work not covered in this tutorial (eg. magnitude and distance functions), feel free to build onto it if you like!

A basic ‘BoxCollider’ struct:

When working in 2D, collision objects are often basic shapes such as square and circle. Simply put, a BoxCollider is essentially just a box (square) with four sides and there are many different implementations out there across games, game engines and physics/collision libraries. In the case of this tutorial, yes, the BoxCollider is indeed just a box with four sides. However it needs a position, size (width and height) and velocity (if it’s going to move) to be a collider.

#ifndef COLLISION_H
#define COLLISION_H

#include "Vector2.h"

// A simple 'BoxCollider' struct
struct BoxCollider
{
public:
      // Function prototypes
      //---------------------//

      // Constructor(s)
      BoxCollider();
      BoxCollider(Vector2 a_v2Pos, Vector2 a_v2Size);

      // Return edges of the collider
      float GetTop();
      float GetBottom();
      float GetLeft();
      float GetRight();

      // Member variables
      //---------------------//

      Vector2 Position; // Position in 2D space
      Vector2 Size;     // Width & height of collider
      Vector2 Velocity; // Velocity (for movement)
};
#endif // COLLISION_H

You can now see why it was a good idea to already have a Vector2 class on hand before writing our BoxCollider struct.

Feel free to skip this next section if you are using your own or a 3rd party collider. I’m going through writing these systems from scratch with readers who don’t have their own (or 3rd party ones) in mind.

Much like the overloaded constructor of the Vector2 class, the one for our BoxCollider struct takes in two Vector2 parameters – a position and size, which it will use to set up the collider for you. Alternatively, you could directly assign values to the ‘Position‘ and ‘Size‘ member variables.

#include "Collision.h"

// 'BoxCollider' struct
//-------------------------------------//

// Default constructor
BoxCollider::BoxCollider() {}

// Overloaded constructor
BoxCollider::BoxCollider(Vector2 a_v2Pos, Vector2 a_v2Size)
{
      Position = a_v2Pos;  // Set position
      Size     = a_v2Size; // Set box size
}

// Return the top edge of the collider
float BoxCollider::GetTop()
{
      return Position.Y;
}

// Return the bottom edge of the collider
float BoxCollider::GetBottom()
{
      return Position.Y + Size.Y;
}

// Return the left edge of the collider
float BoxCollider::GetLeft()
{
      return Position.X;
}

// Return the right edge of the collider
float BoxCollider::GetRight()
{
      return Position.X + Size.X;
}

The ‘Get‘ functions allow us to obtain all sides of the collider and will come into play once we get to the frustum checking code. By all means, you can still write out this code

#include "Collision.h"

BoxCollider myCollider; // BoxCollider for this example

float RightSide = myCollider.Position.X + myCollider.Size.X;

to obtain a side of a collider. However, calling one of the ‘Get’ functions like so

#include "Collision.h"

BoxCollider myCollider;     // BoxCollider for this example
float Boundary_Right = 100; // Example screen boundary

if (myCollider.GetRight() < Boundary_Right)
{
// Do something...
}

I find to be much neater and faster. It’s all a matter of what you type out or personal preference, either way will do the job.

Part 1 Conclusion:

That brings us to the end of Part 1. We haven’t even touched on the 2D camera and frustum checking code yet, not to worry though because we’ll cover that in part 2!

For now, we’ve got the Vector2 class and BoxCollider struct all set and ready to go, which is great! It means we can jump straight into the next part!

Click here to continue to Part 2.

Leave a comment