Is Frustum Culling Worth It For 2D Games?
What is Frustum Culling and Why Use It in 2D Games?
Frustum culling is an optimization technique whereby objects located outside of the camera’s field of view are not rendered. This saves on unnecessary draw calls for off-screen objects and can significantly improve frame rates and overall performance in games with large, complex environments.
While most often utilized in 3D games, frustum culling can also provide major performance benefits for 2D games. By culling sprites, game objects and environments that are outside the camera frustum (the region enclosed by the camera’s viewing angles), fewer draw calls are made each frame which allows the game to render faster.
Frustum culling works by mathematically calculating the viewing volume based on the camera position, orientation and field of view angles. This creates a truncated pyramid-like shape representing the frustum area. Game objects that fall outside this volume are culled from rendering.
Implementing Basic Frustum Culling in Unity
Implementing basic frustum culling in Unity for 2D games involves three key steps:
- Calculate the camera frustum planes based on the camera transform and field of view
- Check if each 2D object’s world space bounding box intersects with the frustum planes
- Disable rendering for objects fully outside the frustum
Here is example C# code for calculating the frustum planes in a MonoBehaviour script:
void CalculateFrustumPlanes() { float near = camera.nearClipPlane; float far = camera.farClipPlane; float fov = camera.fieldOfView; float aspect = camera.aspect; float halfHeight = near * Mathf.Tan(fov * 0.5f * Mathf.Deg2Rad); float halfWidth = aspect * halfHeight; Vector3 center = transform.position; Vector3 forward = transform.forward; Vector3 up = transform.up; Vector3 right = Vector3.Cross(up, forward); leftPlane = new Plane(center - right * halfWidth, left); rightPlane = new Plane(center + right * halfWidth, right); topPlane = new Plane(center + up * halfHeight, up); bottomPlane = new Plane(center - up * halfHeight, -up); nearPlane = new Plane(center + forward * near, -forward); farPlane = new Plane(center - forward * far, forward); }
Then we can check if a 2D object bounds intersects the frustum:
bool IsInFrustum(Bounds bounds) { if (leftPlane.GetSide(bounds.center) == PlaneSide.Back) return false; if (rightPlane.GetSide(bounds.center) == PlaneSide.Back) return false; if (topPlane.GetSide(bounds.center) == PlaneSide.Back) return false; if (bottomPlane.GetSide(bounds.center) == PlaneSide.Back) return false; if (nearPlane.GetSide(bounds.center) == PlaneSide.Back) return false; if (farPlane.GetSide(bounds.center) == PlaneSide.Back) return false; return true; }
We can then disable rendering for objects returning false from this check. This avoids unnecessary draw calls and provides a good performance boost.
Optimizing Frustum Culling for Large 2D Scenes
For large 2D scenes with many game objects like backgrounds, platforms and enemies, checking every object against the frustum each frame can get expensive. We can optimize frustum culling using spatial partitioning data structures.
Quadtrees and grids provide faster searching by subdividing the 2D space into cells and only checking objects within certain cells. Here is some sample code for a quadtree implementation:
public class Quadtree { Node rootNode; public class Node { Bounds bounds; List<GameObject> objects; Node topRight, topLeft, bottomLeft, bottomRight; } public void Insert(GameObject obj) { // Insert object by traversing quadtree } public void GetFrustumVisible(Bounds frustum, List<GameObject> visible) { if (!rootNode.bounds.Overlaps(frustum)) return; if (rootNode.bounds.Contains(frustum)) { visible.AddRange(rootNode.objects); return; } // Traverse tree and find objects in frustum } }
We can also implement layer culling for large 2D backgrounds. This renders backgrounds in chunks and disables offscreen chunks.
When Not to Use Frustum Culling in 2D Games
While frustum culling can significantly improve performance, it does have some overhead for calculating planes and object bounds. It may not be worth implementing for:
- Very small 2D games with few active objects on screen
- Frequently moving cameras covering most of the scene
- Games mainly bottlenecked by pixel fill-rate or draw call batching
For these cases, other optimizations like sprite atlasing, static and dynamic batching, object pooling and GPU instancing may provide better performance gains. Profile first before deciding whether to add frustum culling.
Improving Culling Precision for Isometric 2D Games
The standard frustum culling approach can miss some optimization opportunities in isometric 2D games. Objects can still poke into the frustum even when centered outside it.
To improve precision, we need to account for the isometric projection when checking object bounds against planes. One option is to transform the planes into the isometric space before the bounds check:
Matrix4x4 toIsoMatrix = Matrix4x4.Rotate(Quaternion.Euler(0, 45, 0)); IsInFrustum(Bounds bounds) { isoleftPlane = toIsoMatrix.MultiplyPoint(leftPlane); // Transform rest of planes if (isoleftPlane.GetSide(bounds.center) == PlaneSide.Back) return false; // Check other planes }
Additionally, we can expand the near and far clipping planes slightly to avoid false positives. Handling corner cases where objects intersect only the edges of the frustum is also important for robust culling.