Occlusion culling¶
a Hi-Z GPU cull drops objects hidden behind a near wall.
📄 Docs onlyTags: 3d culling performance
A large wall stands close to the camera. Directly behind it sits a dense field of
cubes that are almost entirely hidden, plus a few cubes off to the sides that the
wall does NOT cover. With occlusion culling ON (WorldEnvironment .occlusion_culling_enabled = True) the renderer rejects the instances the wall
occludes against the previous frame’s depth pyramid, so the hidden field is never
drawn. Toggle it OFF and every frustum-visible cube is submitted again.
The on-screen overlay reads App.last_telemetry and reports, live:
drawn – instances that survived the cull and were drawn this frame
total – frustum-visible instances submitted to the cull (pre-cull)
culled – total - drawn (the objects the wall hid)
The camera slowly orbits a small arc in front of the wall (and can be nudged with A/D) so the occlusion is observable as the hidden field pops in/out at the wall’s edges.
Controls: C : Toggle occlusion culling on/off A/D : Orbit the camera left / right ESC : Quit
Usage: uv run python examples/features/3d/occlusion_culling.py
Source¶
1#!/usr/bin/env python3
2"""Occlusion culling: a Hi-Z GPU cull drops objects hidden behind a near wall.
3
4# /// simvx
5# tags = ["3d", "culling", "performance"]
6# screenshot_frame = 60
7# web = { disabled = true, reason = "occlusion culling is desktop-only until the WebGPU Hi-Z port" }
8# ///
9
10A large wall stands close to the camera. Directly behind it sits a dense field of
11cubes that are almost entirely hidden, plus a few cubes off to the sides that the
12wall does NOT cover. With occlusion culling ON (``WorldEnvironment
13.occlusion_culling_enabled = True``) the renderer rejects the instances the wall
14occludes against the previous frame's depth pyramid, so the hidden field is never
15drawn. Toggle it OFF and every frustum-visible cube is submitted again.
16
17The on-screen overlay reads ``App.last_telemetry`` and reports, live:
18 drawn -- instances that survived the cull and were drawn this frame
19 total -- frustum-visible instances submitted to the cull (pre-cull)
20 culled -- total - drawn (the objects the wall hid)
21
22The camera slowly orbits a small arc in front of the wall (and can be nudged with
23A/D) so the occlusion is observable as the hidden field pops in/out at the wall's
24edges.
25
26Controls:
27 C : Toggle occlusion culling on/off
28 A/D : Orbit the camera left / right
29 ESC : Quit
30
31Usage:
32 uv run python examples/features/3d/occlusion_culling.py
33"""
34
35import math
36
37from simvx.core import (
38 AnchorPreset,
39 Camera3D,
40 DirectionalLight3D,
41 Input,
42 Key,
43 Label,
44 Material,
45 Mesh,
46 MeshInstance3D,
47 Node,
48 Panel,
49 Property,
50 Vec3,
51 WorldEnvironment,
52)
53from simvx.graphics import App
54
55WIDTH, HEIGHT = 1280, 720
56
57# Dense hidden field: a grid of cubes packed BEHIND the wall. Big enough that the
58# culled count is unmistakable in the overlay.
59FIELD_COLS, FIELD_ROWS, FIELD_LAYERS = 14, 8, 6
60
61
62class OcclusionDemo(Node):
63 """A near wall occluding a dense cube field, with a live cull-telemetry HUD."""
64
65 input_actions = {
66 "toggle_occlusion": [Key.C],
67 "orbit_left": [Key.A],
68 "orbit_right": [Key.D],
69 "quit": [Key.ESCAPE],
70 }
71
72 orbit_speed = Property(0.25, range=(0.0, 2.0))
73 orbit_extent = Property(0.6, range=(0.0, 2.0))
74
75 def __init__(self, **kwargs):
76 super().__init__(**kwargs)
77 self._time = 0.0
78 self._occlusion_on = True
79 self._orbit_offset = 0.0
80
81 def on_ready(self):
82 super().on_ready()
83
84 self.camera = self.add_child(Camera3D(position=Vec3(0.0, 1.5, 9.0), look_at=Vec3(0.0, 1.5, -8.0)))
85
86 light = DirectionalLight3D()
87 light.direction = Vec3(-0.4, -1.0, -0.5)
88 light.colour = (1.0, 0.97, 0.92)
89 light.intensity = 1.7
90 self.add_child(light)
91
92 cube = Mesh.cube()
93
94 # Ground plane (a flat scaled cube) so the scene reads as a space.
95 floor = MeshInstance3D(mesh=cube, material=Material(colour=(0.18, 0.19, 0.22), roughness=0.9))
96 floor.position = Vec3(0.0, -0.6, -6.0)
97 floor.scale = Vec3(40.0, 0.1, 40.0)
98 self.add_child(floor)
99
100 # The occluder: a large wall close to the camera, centred so it hides the
101 # field behind it but leaves the flanks open.
102 wall = MeshInstance3D(mesh=cube, material=Material(colour=(0.55, 0.42, 0.30), roughness=0.8))
103 wall.position = Vec3(0.0, 2.0, 2.0)
104 wall.scale = Vec3(7.0, 5.0, 0.4)
105 self.add_child(wall)
106
107 # Dense hidden field directly behind the wall: mostly occluded.
108 spacing = 0.85
109 x0 = -(FIELD_COLS - 1) * spacing * 0.5
110 y0 = 0.2
111 z0 = -2.0
112 for cx in range(FIELD_COLS):
113 for cy in range(FIELD_ROWS):
114 for cz in range(FIELD_LAYERS):
115 c = MeshInstance3D(
116 mesh=cube,
117 material=Material(colour=(0.30, 0.55, 0.85), roughness=0.5, metallic=0.1),
118 )
119 c.position = Vec3(x0 + cx * spacing, y0 + cy * spacing, z0 - cz * spacing)
120 c.scale = Vec3(0.32, 0.32, 0.32)
121 self.add_child(c)
122
123 # A few clearly visible cubes off to the sides (NOT behind the wall): these
124 # must keep drawing whether culling is on or off.
125 for sign in (-1, 1):
126 for k in range(3):
127 v = MeshInstance3D(
128 mesh=cube,
129 material=Material(colour=(0.95, 0.55, 0.2), roughness=0.4, emissive_colour=(0.4, 0.2, 0.05)),
130 )
131 v.position = Vec3(sign * (5.0 + k * 0.8), 1.0, 0.0 - k * 1.2)
132 v.scale = Vec3(0.5, 0.5, 0.5)
133 self.add_child(v)
134
135 self._env = self.add_child(WorldEnvironment(name="Env"))
136 self._env.occlusion_culling_enabled = self._occlusion_on
137
138 self._build_hud()
139
140 def _build_hud(self) -> None:
141 # Top-left status panel: anchored (NOT absolute) so it tracks the viewport.
142 panel = Panel(name="HUD")
143 panel.set_anchor_preset(AnchorPreset.TOP_LEFT)
144 panel.margin_left = 12.0
145 panel.margin_top = 12.0
146 panel.size = (340, 132)
147 self.add_child(panel)
148
149 self._hud = Label("", name="HUDLabel")
150 self._hud.set_anchor_preset(AnchorPreset.FULL_RECT)
151 self._hud.margin_left = 12.0
152 self._hud.margin_top = 10.0
153 self._hud.font_size = 18.0
154 self._hud.vertical_alignment = "top"
155 panel.add_child(self._hud)
156
157 # Bottom controls strip.
158 hint = Label("C: Toggle culling A/D: Orbit ESC: Quit", name="Hint")
159 hint.set_anchor_preset(AnchorPreset.BOTTOM_WIDE)
160 hint.margin_left = 12.0
161 hint.margin_bottom = 34.0
162 hint.font_size = 15.0
163 self.add_child(hint)
164
165 def on_update(self, dt: float):
166 if Input.is_action_just_pressed("quit"):
167 self.app.quit()
168 return
169
170 self._time += dt
171
172 if Input.is_action_pressed("orbit_left"):
173 self._orbit_offset -= dt
174 if Input.is_action_pressed("orbit_right"):
175 self._orbit_offset += dt
176
177 angle = math.sin(self._time * self.orbit_speed) * self.orbit_extent + self._orbit_offset
178 x = math.sin(angle) * 9.0
179 z = math.cos(angle) * 9.0
180 self.camera.position = Vec3(x, 1.6, z)
181 self.camera.look_at(Vec3(0.0, 1.5, -6.0))
182
183 if Input.is_action_just_pressed("toggle_occlusion"):
184 self._occlusion_on = not self._occlusion_on
185 self._env.occlusion_culling_enabled = self._occlusion_on
186
187 self._update_hud()
188
189 def _update_hud(self) -> None:
190 t = self.app.last_telemetry if self.app else {}
191 state = "ON" if self._occlusion_on else "OFF"
192 if "occlusion_total" in t:
193 total = int(t["occlusion_total"])
194 drawn = int(t["occlusion_drawn"])
195 culled = max(0, total - drawn)
196 self._hud.text = f"Occlusion: {state}\n" f"drawn: {drawn}\n" f"total: {total}\n" f"culled: {culled}"
197 else:
198 self._hud.text = f"Occlusion: {state}\n(no cull telemetry yet)"
199
200
201if __name__ == "__main__":
202 scene = OcclusionDemo(name="OcclusionDemo")
203 app = App(title="Occlusion Culling Demo", width=WIDTH, height=HEIGHT)
204 app.run(scene)