# How Pyrite64's Culling Component Optimizes Rendering Performance: Algorithmic Approaches Explained

> Discover how Pyrite64's culling component uses AABB and bounding spheres for view-frustum culling, optimizing rendering performance by excluding off-screen objects.

- Repository: [Max Bebök/pyrite64](https://github.com/hailtododongo/pyrite64)
- Tags: performance
- Published: 2026-02-19

---

**The Culling component in Pyrite64 reduces GPU rasterization workload by implementing view-frustum culling using Axis-Aligned Bounding Boxes (AABB) and bounding spheres to exclude off-screen objects from the per-frame draw loop.**

The HailToDodongo/pyrite64 engine targets the Nintendo 64 architecture where fill-rate and vertex processing are severely constrained. To maintain performance, the **culling component** implements a deterministic geometric culling pipeline that evaluates object visibility against the camera frustum before expensive render commands are issued. This analysis examines the specific algorithmic approaches implemented in [`scene/components/culling.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene/components/culling.cpp) and [`scene.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene.cpp) that enable these optimizations.

## View-Frustum Culling Algorithms

Pyrite64 employs two complementary geometric tests to determine object visibility. Both algorithms test whether an object's bounding volume intersects the view frustum retrieved from `t3d_viewport_get()->viewFrustum`.

### Axis-Aligned Bounding Box (AABB) Tests

The AABB approach provides cheap rejection for rectangular objects. In [`scene/components/culling.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene/components/culling.cpp), the `Comp::Culling::draw` function computes a world-space AABB by scaling the component's stored `halfExtend` vector by the object's transform matrix.

The engine then calls `t3d_frustum_vs_aabb` to test intersection. If the box lies completely outside the frustum, the system sets the `IS_CULLED` flag, immediately disqualifying the object from rasterization. This test is particularly effective for architecture and terrain chunks where bounding boxes tightly fit the geometry.

### Bounding Sphere Tests

For roughly spherical objects, the component supports a **bounding sphere** mode that reduces the culling test to a single radius comparison. The algorithm derives the sphere radius from the largest component of the scale vector (`maxSize`), then invokes `t3d_frustum_vs_sphere`.

Sphere tests require fewer floating-point operations than AABB tests, making them ideal for particles, characters, and organic shapes where precise box fitting would add unnecessary overhead.

## Per-Object Culling Pipeline

The culling system operates as a pre-process stage within the main render loop, integrating tightly with the scene graph traversal.

### Bounding Volume Definition and Dynamic Scaling

Each object declares its culling properties via JSON configuration or programmatic setup in [`compCulling.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/compCulling.cpp). The component stores a `halfExtend` vector, an `offset` translation, and a `type` discriminator (0 for box, 1 for sphere).

During evaluation, `Comp::Culling::draw` dynamically scales these extents by the object's current world transform. This ensures that animated or moving objects maintain accurate culling bounds without requiring manual updates per frame.

### Frustum Testing and Flag Assignment

The core visibility test occurs in [`scene/components/culling.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene/components/culling.cpp) within the `draw` method. The function computes the final world-space position and scaled size, selects the appropriate frustum test based on `type`, and mutates the object's flag state:

```cpp
if (/* outside frustum */) {
    obj->setFlag(ObjectFlags::IS_CULLED, true);
}

```

This flag-based architecture decouples the geometric test from the render submission logic, allowing the culling system to run as an isolated evaluation pass.

### Early-Out in the Draw Loop

The primary optimization occurs in [`scene.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene.cpp), where the main draw loop checks the culling flag before processing any render components:

```cpp
if(obj->flags & ObjectFlags::IS_CULLED) break;

```

When `IS_CULLED` is set, the loop terminates component processing for that object immediately, preventing vertex buffer uploads, texture binds, and draw call submissions. This early-out mechanism ensures that off-screen objects consume only the minimal CPU overhead of the frustum test itself.

### Multi-Camera Flag Management

Because Pyrite64 supports multiple viewports and camera passes, the culling system clears the `IS_CULLED` flag after each camera render. The reset logic in [`scene.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene.cpp) executes:

```cpp
obj->setFlag(ObjectFlags::IS_CULLED, false);

```

This per-camera evaluation ensures that an object culled from the main view remains visible in a secondary mirror or shadow camera if it falls within that frustum.

## Implementation Details and Code Examples

### Configuring Culling in Project Files

Define bounding volumes declaratively in JSON scene definitions processed by [`compCulling.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/compCulling.cpp):

```json
{
  "name": "Tree",
  "components": {
    "Culling": {
      "halfExtend": [0.5, 1.2, 0.5],
      "offset": [0.0, 0.0, 0.0],
      "type": 0
    }
  },
  "transform": {
    "pos": [10, 0, -5],
    "scale": [1, 1, 1]
  }
}

```

The `type` field selects the algorithmic test: 0 triggers `t3d_frustum_vs_aabb`, while 1 triggers `t3d_frustum_vs_sphere`.

### Manual Culling Verification

Access culling state programmatically to implement custom LOD or gameplay logic:

```cpp
#include "scene/components/culling.h"
#include "scene/object.h"

void example(Object& obj)
{
    auto *cull = obj.getComp<P64::Comp::Culling>();
    if (cull) {
        P64::Comp::Culling::draw(obj, cull, 0.0f);
        if (obj.isFlagSet(ObjectFlags::IS_CULLED))
            std::cout << "Object is outside the view frustum.\n";
    }
}

```

### Visualizing Bounds in the Editor

The editor leverages `Utils::Mesh::addLineBox` and `addLineSphere` (defined in [`compCulling.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/compCulling.cpp)) to render wireframe overlays:

```cpp
if (type == TYPE_BOX)
    Utils::Mesh::addLineBox(*vp.getLines(), center, halfExt, aabbCol);
else
    Utils::Mesh::addLineSphere(*vp.getLines(), center, halfExt, aabbCol);

```

Developers observe red wireframe volumes in the viewport to tune `halfExtend` and `offset` values for optimal culling accuracy.

## Summary

- **Pyrite64** implements view-frustum culling via `t3d_frustum_vs_aabb` and `t3d_frustum_vs_sphere` in [`scene/components/culling.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene/components/culling.cpp).
- The system supports **AABB** and **bounding sphere** algorithms selected per-object via the `type` parameter.
- **Early-out logic** in [`scene.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene.cpp) checks `ObjectFlags::IS_CULLED` to skip draw calls for off-screen objects.
- **Per-camera flag resets** ensure correct visibility across multiple viewports.
- Bounding volumes dynamically scale with object transforms, maintaining accurate culling for animated geometry.

## Frequently Asked Questions

### What bounding volume types does the Pyrite64 culling component support?

The component supports **Axis-Aligned Bounding Boxes (AABB)** and **bounding spheres**. The `type` field in the Culling component (0 for box, 1 for sphere) determines which algorithm `Comp::Culling::draw` executes. Boxes suit architectural geometry, while spheres optimize roughly circular objects with cheaper intersection math.

### How does Pyrite64 handle culling when multiple cameras render the same scene?

The engine clears the `IS_CULLED` flag on every object after each camera pass using `obj->setFlag(ObjectFlags::IS_CULLED, false)` in [`scene.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene.cpp). This ensures that an object culled from the primary camera's frustum can still render if visible to a secondary camera, such as those used for mirrors, portals, or split-screen multiplayer.

### Where does the culling optimization actually prevent rendering?

The early-out occurs in the main draw loop within [`scene.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/scene.cpp). The code checks `if(obj->flags & ObjectFlags::IS_CULLED) break;` immediately after culling evaluation, stopping all subsequent component processing for that object. This prevents vertex data uploads, uniform buffer updates, and final draw call submission to the GPU.

### Can developers visualize culling boundaries to debug coverage?

Yes. The editor implementation in [`compCulling.cpp`](https://github.com/HailToDodongo/pyrite64/blob/main/compCulling.cpp) renders wireframe overlays using `Utils::Mesh::addLineBox` for AABB volumes and `addLineSphere` for spherical bounds. These red wireframes display directly in the editor viewport, allowing precise tuning of the `halfExtend` and `offset` parameters to eliminate false negatives where geometry clips the view edge.