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 axesmatrix— world position and orientationcorners[8]— pre-computed world-space corner verticessplitPoints[8]— points offset 2% from center, used for split-visualization
Spatial query methods:
Method | Description |
|---|---|
| Point is fully within the box |
| Point is in the front half (local z ≥ 0) |
| 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 objectscollectedCommonObjects— common static objects
State:
activated = true→ all collected objects are currently hidden
Key methods:
Method | When called | Purpose |
|---|---|---|
| Once at map load | Populate collected lists; also calls |
| On gate crossing | Bulk set |
| 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()):
Read current camera position (supports GoPro / spectator cameras)
Cache check — if the camera has not moved far enough since last evaluation, skip entirely
Unhide only the objects recorded in
planeHiddenCommonObjects/planeHiddenProps(O(k) where k = objects hidden last frame, not O(n) for all collected objects)Filter planes that face the camera (local z > 0)
Sort facing planes so the one covering the widest angle is processed first
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