Object Culling System

Overview

The Culling Volume system is a custom render optimization technique that hides objects (props, static objects) inside a defined 3D region when the player does not need to see them — typically when they are behind walls or in a separate room. It operates entirely in game code, independent of the engine's built-in frustum culling.

Class Hierarchy

CommonVolumeClass (base: position, size, 8 corners, Is_Inside check)
├── CullingVolumeClass (main controller)
├── CullingGateClass (state transition trigger)
├── CullingVolumeExtendClass (secondary collection region)
└── CullingPlaneClass (per-frame angle-based occlusion)

CommonVolumeClass - Base

All volume types share the same oriented bounding box structure:

  • size — half-extents on X/Y/Z axes

  • matrix — world position and orientation

  • corners[8] — pre-computed world-space corner vertices

  • splitPoints[8] — points offset 2% from center, used for split-visualization

Spatial query methods:

Method

Description

Is_Inside(pos)

Point is fully within the box

In_Forward(pos)

Point is in the front half (local z ≥ 0)

In_Backward(pos)

Point is in the back half (local z ≤ 0)

CullingVolumeClass - Main Controller

Each Culling Volume is a 3D box placed on the map. It collects references to all objects inside it at load time, then hides or shows them as a batch based on the player's position relative to its gates.

Collected object lists:

  • collectedProps — static prop objects

  • collectedCommonObjects — common static objects

State:

  • activated = true → all collected objects are currently hidden

Key methods:

Method

When called

Purpose

Collect_Objects()

Once at map load

Populate collected lists; also calls Collect_Objects_Behind() on every plane

Culling_Control(hide)

On gate crossing

Bulk set hiddenByCullingTechnique on all collected objects

Culling_Plane_Control()

Every frame

Run angle-based plane culling when volume is open

Culling_Control(hide) uses a guard if(activated != hide) to avoid redundant writes every frame. On call it also clears planeHiddenCommonObjects / planeHiddenProps and invalidates the camera-position cache, forcing plane culling to re-evaluate immediately on the next frame.

CullingGateClass - State Transition Trigger

A Gate is a smaller 3D box placed at a transition point (e.g. a doorway or corridor entrance). It uses the In_Forward / In_Backward split of CommonVolumeClass to detect which direction the character's head crossed it.

Logic (inside CharacterClass::Process_Culling_Volume_Profiles()):

Volume is HIDDEN (currentStatus = true):
Head enters gate's In_Forward half → Open volume (currentStatus = false)
 
Volume is OPEN (currentStatus = false):
Head enters gate's In_Backward half → Hide volume (currentStatus = true)

The gate's direction vector defines which side is "forward" (entering the room) and which is "backward" (leaving it). A single Culling Volume can have multiple gates (gateList), allowing several entry/exit points.

CullingVolumeExtendClass - Secondary Collection Region

An Extend is an auxiliary 3D box that enlarges the object-collection area without changing the main volume's bounds. Useful for objects that sit near the room's edge but whose BBCenter falls slightly outside the main box.

During Collect_Props() and Collect_Common_Objects(), the main volume is checked first, then each extend is checked in sequence:

if(Is_Inside(obj.BBCenter)) collected.add(obj);
REPAD(j, volumeExtendList)
if(volumeExtendList[j].Is_Inside(obj.BBCenter)) collected.add(obj);

An extend shares the same hide/show batch as the parent volume — there is no separate state per extend.

CullingPlaneClass - Angle-Based Per-Frame Occlusion

A Plane is a 2D occluder inside a volume that hides objects sitting behind it when they are also within the camera's viewing angle toward the plane — a manual approximation of occlusion culling for large flat surfaces (walls, partitions).

Pre-computed edge vectors:

  • x1 / x2 — right and left horizontal edges (projected onto the XZ plane)

  • y1 / y2 — top and bottom vertical edge centers

Per-frame flow (Culling_Plane_Control()):

  1. Read current camera position (supports GoPro / spectator cameras)

  2. Cache check — if the camera has not moved far enough since last evaluation, skip entirely

  3. Unhide only the objects recorded in planeHiddenCommonObjects / planeHiddenProps (O(k) where k = objects hidden last frame, not O(n) for all collected objects)

  4. Filter planes that face the camera (local z > 0)

  5. Sort facing planes so the one covering the widest angle is processed first

  6. Call Culling_Process() on each plane, which hides qualifying objects and records them in the tracking lists

Check_Inside_Angle(viewerPos, obj) — determines whether an object is fully hidden behind the plane from the camera's point of view:

  • Horizontal: Projects the plane's x1/x2 edges and the object's leftmost/rightmost bounding-box points into a flat (XZ) view-space. The object must fall entirely within x2 → x1.

  • Vertical: Uses the sine of the elevation angle (Normalized(worldPos - viewerPos).y) from the camera to y1/y2 and to the object's top/bottom points. The object must fall entirely within elevY2 → elevY1.

Only objects that are already classified as "behind" the plane (Check_Object_Behind — all BB vertices have local z < −0.1) participate in the angle check each frame.

CullingVolumeProfileClass - Per-Character State Record

Each character holds a list of profiles (cullingVolumeProfiles). Culling_Control() is called only when prevStatus != currentStatus, preventing redundant bulk visibility writes on frames where no gate was crossed.

class CullingVolumeProfileClass {
CullingVolumeClass *relatedCullingVolume;
bool currentStatus; // true = volume should be hidden
bool prevStatus; // diff-check to avoid redundant Culling_Control calls
}

Full Data Flow

Map Load
└─ Load_Data_Culling_Volume()
├─ Create CullingVolumeClass (size, position, direction)
├─ Create CullingGateClass (gateList)
├─ Create CullingVolumeExtendClass (volumeExtendList)
└─ Create CullingPlaneClass (planeList, with edge vectors pre-computed)
 
Setup_Culling_Combo()
├─ Collect_Objects() on every volume → fills collected lists + plane behind-lists
└─ Assign cullingVolumeProfiles to each spawned character
 
Every Frame — CharacterClass update
├─ Process_Culling_Volume_Profiles()
│ └─ Test character head position against each Gate
│ → update currentStatus on gate crossing
└─ Culling_Control_Process() [player-controlled character only]
└─ If status changed → call CullingVolume.Culling_Control(hide)
→ bulk hide/show all collected objects
→ invalidate plane cache
 
Every Frame — CullingVolumeClass (when volume is open)
└─ Culling_Plane_Control()
├─ Skip if camera has not moved enough (cache)
├─ Unhide objects hidden by planes last frame (targeted, O(k))
├─ Filter + sort facing planes
└─ Culling_Process() per plane
→ Check_Inside_Angle() per behind-object
→ Hide qualifying objects + record in tracking list


Was this article helpful?
© 2026 CSCD: Vietnam Mobile Police Wiki