2D Frustum Culling – Tutorial (Part 2)

Hello and welcome to part 2 of my tutorial on 2D Frustum Culling.

Before we go any further with the tutorial, if you’re reading this and have not yet read part 1, it is definitely worth your while to go back and read it. It lays the foundations that we need before we can write our 2D Camera class, which will contain the frustum and frustum checking code.

Return to Part 1

If you’ve read part 1 and have either implemented its Vector2 and BoxCollider code or are using your own (or 3rd party equivalent), then you’re all set to continue into part 2!

2D Camera – Background theory:

Yes, theory…hooray! This is important though and good to know before we write code for the camera. First thing’s first, this is NOT an actual camera as you may think! No rendering, no fancy visual effects, nothing. Stripped down, it is simply a Vector2 which represents the “Position” if you like, of the ‘camera’.

This position is then used to offset all sprites (eg. player, enemies, bullets) as they are drawn, giving the illusion of a camera being used. Simply put, you can call this technique ‘side-scrolling’ or even ‘faked-scrolling’, as that’s essentially what this ‘camera’ class does – changes a position, then subtracts that position from the position of everything you want to scroll.

I’ve used this technique in several games and as simple as it is, it does the job quite well. If the explanation of how this camera will work didn’t make sense, the following code should clear things up. Create a new C++ header (‘.h’) file and name it ‘Camera‘.

Simple 2D ‘Camera’ class:

#ifndef CAMERA_H
#define CAMERA_H

#include "Collision.h"

// Simple 2D 'Camera' class
class Camera
{
public:
      // Function prototypes
      //---------------------//

      Camera(); // Default constructor

      // Set up frustum box using specified screen margins
      void InitFrustum(int a_iMargin_X = 20, int a_iMargin_Y = 20);

      // Return true if specified collider is within camera frustum
      bool InFrustum(BoxCollider* a_pCollider, bool a_bOverlap = true);

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

      Vector2 m_Position; // 'Camera' offset position

private:
     // Frustum box
     struct FrustumBox
     {
            // Edges of the frustum box
            float Left, Right, Top, Bottom;
     }Frustum;
};

#endif // CAMERA_H

// Externally define the 'Camera' class as 'camera'
extern Camera camera;

The structure of the Camera class is done. Our frustum is simply a struct called ‘FrustumBox‘ (defined for use as ‘Frustum‘), which contains four float values. Each value represents a respective on-screen boundary, imagine the four boundaries together as a rectangle. Yes, that is our frustum ‘box’.

Create a new C++ source file (‘.cpp’) and name it ‘Camera‘.

‘InitFrustum’ function:

#include "Camera.h"

// Define the 'Camera' class as 'camera'
Camera camera;

// Set up frustum box using specified screen margins
void Camera::InitFrustum(int a_iMargin_X, int a_iMargin_Y)
{
      Frustum.Left   = SCREEN_W - (SCREEN_W - a_iMargin_X);
      Frustum.Right  = SCREEN_W - a_iMargin_X;
      Frustum.Top    = SCREEN_H - (SCREEN_H - a_iMargin_Y);
      Frustum.Bottom = SCREEN_H - a_iMargin_Y;
}

You may have noticed in the function prototype, both the ‘a_iMargin_X‘ and ‘a_iMargin_Y‘ parameters are set to 20 by default. This will give you a margin of 20 on each side of the frustum box, between it and the respective edge of the screen. By passing other values into this function, you can control how big you want the frustum to be with ease. I found 20 was a decent starting value, which is why I used that.

Note that ‘SCREEN_W‘ and ‘SCREEN_H‘ are just example variables I made up, to represent the width and height of the screen. Replace these with your own variables or if you like, just straight numbers (eg. 1280, 720).

‘InFrustum’ function:

// Return true if specified collider is within camera frustum
bool Camera::InFrustum(BoxCollider* a_pCollider, bool a_bOverlap)
{
      // Set frustum boundaries to use, based on 'a_bOverlap' value
      float fFrustum_Left   = a_bOverlap ? Frustum.Left - a_pCollider->Size.X : Frustum.Left;
      float fFrustum_Right  = a_bOverlap ? Frustum.Right + a_pCollider->Size.X : Frustum.Right;
      float fFrustum_Top    = a_bOverlap ? Frustum.Top - a_pCollider->Size.Y : Frustum.Top;
      float fFrustum_Bottom = a_bOverlap ? Frustum.Bottom + a_pCollider->Size.Y : Frustum.Bottom;

      if (a_pCollider->GetLeft() >= fFrustum_Left         // Within left side of frustum
          && a_pCollider->GetRight() <= fFrustum_Right    // Within right side of frustum
          && a_pCollider->GetTop() >= fFrustum_Top        // Below top side of frustum
          && a_pCollider->GetBottom() <= fFrustum_Bottom) // Above bottom of frustum
      {
          // Collider is inside camera frustum
          return true;
      }

      // Collider is outside camera frustum
      return false;
}

The return value of boolean parameter ‘a_bOverlap‘, determines whether we want to allow the collider to be PARTIALLY (overlapping) inside the frustum, in which case we would pass ‘true‘, or whether we want to make sure the collider is ENTIRELY inside the frustum, in which case we would pass ‘false‘.

The four temporary frustum values, are then either directly assigned to the default frustum values (‘a_bOverlap‘ is ‘false‘) or are assigned to the default frustum values with the collider’s width and height added or subtracted (‘a_bOverlap‘ is ‘true‘).

Note that the ‘a_bOverlap‘ parameter is set to ‘true‘ by default.

Example – putting it all together:

Now that we’ve finished the Camera class, it’s time for an example of putting it to use!


#include "Camera.h"

BoxCollider myCollider; // BoxCollider for this example

// Set up our collider (use your own values if you like)
myCollider.Size     = Vector2(60);
myCollider.Position = Vector2(50);

// INSIDE GAME LOOP (update/draw each frame)
//------------------------------------------//

if (camera.InFrustum(&myCollider))
{
     // The collider is inside the camera frustum!

     // Reset collider velocity
     myCollider.Velocity = Vector2(0);

     // Move collider left
     // If the 'A' key was pressed
     // myCollider.Velocity.X -= 40.0f;

     // Move collider right
     // If the 'D' key was pressed
     // myCollider.Velocity.X += 40.0f;

     // Apply velocity to collider position
     myCollider.Position += myCollider.Velocity;

     // Eg. Check for collision...
     // Eg. Draw object using this collider...
}

Several comments in this example are pseudocode, because this tutorial doesn’t cover using a 3rd-party (or making one) engine for rendering and input. They still get the idea across of how you could go about updating the collider.

Part 2 Conclusion:

And there you have it, a complete simple, yet effective 2D Camera system with frustum culling functionality. This system can quite easily be built upon to allow for more advanced functionality, but as is, it’s all one needs for simple 2D culling (and even side-scrolling, as I mentioned earlier). On a side note, this tutorial has laid all the foundations needed for an upcoming tutorial I have planned!

If this tutorial has been helpful, I truly appreciate any thanks or feedback. You could share a link to this tutorial on another site, follow this blog or list this blog for thanks/special thanks in the credits of your game – any of these would be greatly appreciated!

Return to Part 1                                                                                                                      Return to Tutorials page

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.