SSAO Demo

Screen-Space Ambient Occlusion visualization.

▶ Run in browser

Tags: 3d

Demonstrates:

  • SSAO effect on a scene with many objects (columns, walls, corners)

  • Toggle SSAO on/off with Space key to see the difference

  • PBR materials with ambient occlusion darkening in crevices

  • Post-processing (HDR + bloom + SSAO)

Run: uv run python examples/features/3d/ssao.py

Controls: Space - Toggle SSAO on/off A / D - Orbit camera left / right W / S - Zoom in / out Q / E - Raise / lower camera

Source

  1"""
  2SSAO Demo: Screen-Space Ambient Occlusion visualization.
  3
  4Demonstrates:
  5  - SSAO effect on a scene with many objects (columns, walls, corners)
  6  - Toggle SSAO on/off with Space key to see the difference
  7  - PBR materials with ambient occlusion darkening in crevices
  8  - Post-processing (HDR + bloom + SSAO)
  9
 10Run: uv run python examples/features/3d/ssao.py
 11
 12Controls:
 13    Space       - Toggle SSAO on/off
 14    A / D       - Orbit camera left / right
 15    W / S       - Zoom in / out
 16    Q / E       - Raise / lower camera
 17"""
 18
 19
 20import math
 21
 22from simvx.core import (
 23    Camera3D,
 24    DirectionalLight3D,
 25    Input,
 26    InputMap,
 27    Key,
 28    Material,
 29    Mesh,
 30    MeshInstance3D,
 31    Node3D,
 32    PointLight3D,
 33    Quat,
 34    Text2D,
 35    Vec3,
 36    WorldEnvironment,
 37)
 38from simvx.graphics import App
 39
 40WIDTH, HEIGHT = 1280, 720
 41
 42
 43class SSAOScene(Node3D):
 44    def __init__(self, **kwargs):
 45        super().__init__(name="SSAODemo", **kwargs)
 46
 47        # Camera
 48        self._cam_angle = 30.0
 49        self._cam_height = 8.0
 50        self._cam_dist = 18.0
 51        self.camera = self.add_child(
 52            Camera3D(
 53                name="Camera",
 54                fov=55,
 55                near=0.1,
 56                far=100.0,
 57            )
 58        )
 59        self._update_camera()
 60
 61        # Lighting
 62        sun = self.add_child(DirectionalLight3D(name="Sun"))
 63        sun.colour = (1.0, 0.95, 0.85)
 64        sun.intensity = 1.2
 65        sun.rotation = Quat.from_euler(math.radians(-50), math.radians(-30), 0)
 66
 67        fill = self.add_child(
 68            PointLight3D(
 69                name="Fill",
 70                position=Vec3(6, 6, 6),
 71            )
 72        )
 73        fill.colour = (0.4, 0.5, 0.8)
 74        fill.intensity = 0.6
 75        fill.range = 25.0
 76
 77        # Ground
 78        ground_mat = Material(colour=(0.3, 0.3, 0.32), metallic=0.0, roughness=0.9)
 79        self.add_child(
 80            MeshInstance3D(
 81                name="Ground",
 82                mesh=Mesh.cube(1.0),
 83                material=ground_mat,
 84                position=Vec3(0, -0.05, 0),
 85                scale=Vec3(20, 0.1, 20),
 86            )
 87        )
 88
 89        # Build a scene with many objects close together to show SSAO
 90        self._build_scene()
 91
 92        # SSAO state via WorldEnvironment
 93        self._ssao_on = True
 94        self._toggle_cooldown = 0.0
 95        self._env = self.add_child(WorldEnvironment(name="Env"))
 96        self._env.ssao_enabled = True
 97        self._env.bloom_enabled = True
 98
 99        # HUD
100        self._title = self.add_child(
101            Text2D(
102                text="SSAO DEMO",
103                x=10,
104                y=8,
105                font_scale=1.6,
106            )
107        )
108        self._status = self.add_child(
109            Text2D(
110                text="SSAO: ON",
111                x=10,
112                y=40,
113                font_scale=1.3,
114            )
115        )
116        self._controls = self.add_child(
117            Text2D(
118                text="SPACE:Toggle SSAO  WASD/QE:Camera",
119                x=10,
120                y=690,
121                font_scale=1.1,
122            )
123        )
124
125    def on_ready(self):
126        InputMap.add_action("cam_left", [Key.A, Key.LEFT])
127        InputMap.add_action("cam_right", [Key.D, Key.RIGHT])
128        InputMap.add_action("cam_fwd", [Key.W, Key.UP])
129        InputMap.add_action("cam_back", [Key.S, Key.DOWN])
130        InputMap.add_action("cam_up", [Key.Q])
131        InputMap.add_action("cam_down", [Key.E])
132        InputMap.add_action("toggle_ssao", [Key.SPACE])
133
134    def _build_scene(self):
135        """Create columns, walls, and objects to demonstrate AO in crevices."""
136        wall_mat = Material(colour=(0.7, 0.68, 0.65), metallic=0.0, roughness=0.85)
137        column_mat = Material(colour=(0.6, 0.58, 0.55), metallic=0.1, roughness=0.7)
138        dark_mat = Material(colour=(0.35, 0.33, 0.30), metallic=0.0, roughness=0.95)
139        bright_mat = Material(colour=(0.85, 0.4, 0.15), metallic=0.3, roughness=0.4)
140        metal_mat = Material(colour=(0.8, 0.82, 0.85), metallic=0.9, roughness=0.1)
141
142        # Back wall
143        self.add_child(
144            MeshInstance3D(
145                name="BackWall",
146                mesh=Mesh.cube(1.0),
147                material=wall_mat,
148                position=Vec3(0, 3, -6),
149                scale=Vec3(12, 6, 0.3),
150            )
151        )
152
153        # Side walls
154        for i, x in enumerate([-6, 6]):
155            self.add_child(
156                MeshInstance3D(
157                    name=f"SideWall{i}",
158                    mesh=Mesh.cube(1.0),
159                    material=wall_mat,
160                    position=Vec3(x, 3, -3),
161                    scale=Vec3(0.3, 6, 6),
162                )
163            )
164
165        # Columns along back wall
166        for i, x in enumerate([-4, -2, 0, 2, 4]):
167            self.add_child(
168                MeshInstance3D(
169                    name=f"Column{i}",
170                    mesh=Mesh.cylinder(0.25, 5.5, segments=16),
171                    material=column_mat,
172                    position=Vec3(x, 2.75, -5.5),
173                )
174            )
175            # Column base
176            self.add_child(
177                MeshInstance3D(
178                    name=f"ColumnBase{i}",
179                    mesh=Mesh.cube(1.0),
180                    material=dark_mat,
181                    position=Vec3(x, 0.15, -5.5),
182                    scale=Vec3(0.7, 0.3, 0.7),
183                )
184            )
185            # Column capital
186            self.add_child(
187                MeshInstance3D(
188                    name=f"ColumnCap{i}",
189                    mesh=Mesh.cube(1.0),
190                    material=dark_mat,
191                    position=Vec3(x, 5.35, -5.5),
192                    scale=Vec3(0.7, 0.3, 0.7),
193                )
194            )
195
196        # Stacked boxes in corner (shows AO between adjacent surfaces)
197        box_positions = [
198            Vec3(-4.5, 0.5, -4.5),
199            Vec3(-4.5, 1.5, -4.5),
200            Vec3(-3.5, 0.5, -4.5),
201            Vec3(-4.5, 0.5, -3.5),
202            Vec3(-4.0, 0.5, -4.0),
203            Vec3(-4.0, 1.5, -4.0),
204        ]
205        for i, pos in enumerate(box_positions):
206            mat = bright_mat if i % 3 == 0 else dark_mat
207            self.add_child(
208                MeshInstance3D(
209                    name=f"StackedBox{i}",
210                    mesh=Mesh.cube(1.0),
211                    material=mat,
212                    position=pos,
213                    scale=Vec3(0.9, 0.9, 0.9),
214                )
215            )
216
217        # Spheres on the ground (shows AO at ground contact)
218        for i in range(5):
219            x = -2.0 + i * 1.5
220            r = 0.4 + (i % 3) * 0.15
221            mat = metal_mat if i % 2 == 0 else bright_mat
222            self.add_child(
223                MeshInstance3D(
224                    name=f"GroundSphere{i}",
225                    mesh=Mesh.sphere(r, rings=16, segments=24),
226                    material=mat,
227                    position=Vec3(x, r, -2.0),
228                )
229            )
230
231        # Archway (two columns + lintel)
232        for i, x in enumerate([-1.5, 1.5]):
233            self.add_child(
234                MeshInstance3D(
235                    name=f"ArchColumn{i}",
236                    mesh=Mesh.cylinder(0.3, 4.0, segments=16),
237                    material=column_mat,
238                    position=Vec3(x, 2.0, 0),
239                )
240            )
241        self.add_child(
242            MeshInstance3D(
243                name="ArchLintel",
244                mesh=Mesh.cube(1.0),
245                material=wall_mat,
246                position=Vec3(0, 4.2, 0),
247                scale=Vec3(3.6, 0.4, 0.6),
248            )
249        )
250
251        # Stepped platform (shows AO on step edges)
252        for i in range(4):
253            self.add_child(
254                MeshInstance3D(
255                    name=f"Step{i}",
256                    mesh=Mesh.cube(1.0),
257                    material=dark_mat,
258                    position=Vec3(4.0, 0.15 + i * 0.3, -3.0 + i * 0.8),
259                    scale=Vec3(2.0, 0.3, 0.8),
260                )
261            )
262
263    def _update_camera(self):
264        rad = math.radians(self._cam_angle)
265        x = math.cos(rad) * self._cam_dist
266        z = math.sin(rad) * self._cam_dist
267        self.camera.position = Vec3(x, self._cam_height, z)
268        self.camera.look_at(Vec3(0, 2.5, -2))
269
270    def on_physics_process(self, dt: float):
271        speed = 40.0
272        if Input.is_action_pressed("cam_left"):
273            self._cam_angle += speed * dt
274        if Input.is_action_pressed("cam_right"):
275            self._cam_angle -= speed * dt
276        if Input.is_action_pressed("cam_fwd"):
277            self._cam_dist = max(8, self._cam_dist - 10 * dt)
278        if Input.is_action_pressed("cam_back"):
279            self._cam_dist = min(35, self._cam_dist + 10 * dt)
280        if Input.is_action_pressed("cam_up"):
281            self._cam_height = min(20, self._cam_height + 6 * dt)
282        if Input.is_action_pressed("cam_down"):
283            self._cam_height = max(2, self._cam_height - 6 * dt)
284        self._update_camera()
285
286    def on_process(self, dt: float):
287        # Toggle SSAO with cooldown
288        self._toggle_cooldown = max(0, self._toggle_cooldown - dt)
289        if Input.is_action_just_pressed("toggle_ssao") and self._toggle_cooldown <= 0:
290            self._ssao_on = not self._ssao_on
291            self._toggle_cooldown = 0.3
292            self._env.ssao_enabled = self._ssao_on
293
294        self._status.text = f"SSAO: {'ON' if self._ssao_on else 'OFF'}"
295
296
297def main():
298    scene = SSAOScene()
299    app = App(title="SimVX SSAO Demo", width=WIDTH, height=HEIGHT, physics_fps=60)
300    app.run(scene)
301
302
303if __name__ == "__main__":
304    main()