Post-processing¶
SSAO, tonemapping, FXAA and depth of field on a WorldEnvironment.
▶ Run in browserTags: 3d post-process ssao tonemap fxaa dof
Screen-space effects are properties of the scene’s WorldEnvironment, and
each one renders the same on the desktop and in the browser. Set them when the
node is built and change them at any time; a change shows on the next frame:
env = WorldEnvironment(ssao_enabled=True, ssao_radius=0.6, fxaa_enabled=True)
env.dof_enabled = True
SSAO (
ssao_enabled) darkens creases and contact points where surrounding geometry blocks ambient light.ssao_radiusis how far it looks for occluders,ssao_intensityhow dark it gets andssao_biaskeeps flat surfaces from shading themselves.Tonemapping maps the HDR image to the display.
tonemap_modepicks the curve fromTonemapMode, withtonemap_exposureandtonemap_whiteto tune it. This example cycles the four filmic curves (ACES,NEUTRAL,REINHARD,UCHIMURA);LINEARis a plain clamp.FXAA (
fxaa_enabled) smooths jagged edges in one cheap pass.Depth of field (
dof_enabled) keeps a band of depth sharp and blurs the rest:dof_focus_distanceis the centre of that band,dof_focus_rangeits depth, anddof_max_coccaps the blur as a fraction of the screen, so it looks the same at any resolution.
The scene is built for the effects: a packed cluster of cubes and spheres gives
SSAO deep crevices to darken, and two rings of shapes spread over depth make the
depth-of-field blur obvious. The camera is an OrbitCamera3D, which takes its
own drag, stick and scroll input; on_update adds keyboard orbit, turning
the camera the way its right stick does, and keyboard zoom.
Each button is Button(action=name): it presses the action’s key, so one
@on_input handler serves both the key and the button, and its label shows
the key’s prompt.
Controls: 1-4 / buttons - Toggle SSAO, cycle the tonemap curve, toggle FXAA, toggle depth of field Up / Down - SSAO radius Right / Left - SSAO intensity W A S D / drag - Orbit the camera Q / E / scroll - Zoom in / out
Source¶
1# /// simvx
2# tags = ["post-process", "ssao", "tonemap", "fxaa", "dof"]
3# web = { width = 1280, height = 720 }
4# ///
5"""Post-processing: SSAO, tonemapping, FXAA and depth of field on a WorldEnvironment.
6
7Screen-space effects are properties of the scene's ``WorldEnvironment``, and
8each one renders the same on the desktop and in the browser. Set them when the
9node is built and change them at any time; a change shows on the next frame:
10
11 env = WorldEnvironment(ssao_enabled=True, ssao_radius=0.6, fxaa_enabled=True)
12 env.dof_enabled = True
13
14- **SSAO** (``ssao_enabled``) darkens creases and contact points where
15 surrounding geometry blocks ambient light. ``ssao_radius`` is how far it
16 looks for occluders, ``ssao_intensity`` how dark it gets and ``ssao_bias``
17 keeps flat surfaces from shading themselves.
18- **Tonemapping** maps the HDR image to the display. ``tonemap_mode`` picks the
19 curve from ``TonemapMode``, with ``tonemap_exposure`` and ``tonemap_white``
20 to tune it. This example cycles the four filmic curves (``ACES``,
21 ``NEUTRAL``, ``REINHARD``, ``UCHIMURA``); ``LINEAR`` is a plain clamp.
22- **FXAA** (``fxaa_enabled``) smooths jagged edges in one cheap pass.
23- **Depth of field** (``dof_enabled``) keeps a band of depth sharp and blurs
24 the rest: ``dof_focus_distance`` is the centre of that band,
25 ``dof_focus_range`` its depth, and ``dof_max_coc`` caps the blur as a
26 fraction of the screen, so it looks the same at any resolution.
27
28The scene is built for the effects: a packed cluster of cubes and spheres gives
29SSAO deep crevices to darken, and two rings of shapes spread over depth make the
30depth-of-field blur obvious. The camera is an ``OrbitCamera3D``, which takes its
31own drag, stick and scroll input; ``on_update`` adds keyboard orbit, turning
32the camera the way its right stick does, and keyboard zoom.
33
34Each button is ``Button(action=name)``: it presses the action's key, so one
35``@on_input`` handler serves both the key and the button, and its label shows
36the key's prompt.
37
38Controls:
39 1-4 / buttons - Toggle SSAO, cycle the tonemap curve, toggle FXAA, toggle depth of field
40 Up / Down - SSAO radius
41 Right / Left - SSAO intensity
42 W A S D / drag - Orbit the camera
43 Q / E / scroll - Zoom in / out
44"""
45
46import math
47
48import numpy as np
49
50from simvx.core import (
51 Anchor,
52 Axis1D,
53 Axis2D,
54 Button,
55 CanvasLayer,
56 DirectionalLight3D,
57 HBoxContainer,
58 Input,
59 Key,
60 Label,
61 Material,
62 Mesh,
63 MeshInstance3D,
64 Node,
65 OrbitCamera3D,
66 TonemapMode,
67 WorldEnvironment,
68 on_input,
69)
70from simvx.graphics import App
71
72TONEMAP_CURVES = [TonemapMode.ACES, TonemapMode.NEUTRAL, TonemapMode.REINHARD, TonemapMode.UCHIMURA]
73PALETTE = [(0.85, 0.3, 0.3), (0.3, 0.75, 0.4), (0.35, 0.45, 0.85), (0.85, 0.75, 0.3), (0.8, 0.4, 0.8)]
74
75
76class PostProcessing(Node):
77 input_context = {
78 "ssao": [Key.KEY_1],
79 "tonemap": [Key.KEY_2],
80 "fxaa": [Key.KEY_3],
81 "dof": [Key.KEY_4],
82 "ssao_radius": [Axis1D(low=Key.DOWN, high=Key.UP)],
83 "ssao_intensity": [Axis1D(low=Key.LEFT, high=Key.RIGHT)],
84 "orbit": [Axis2D.WASD],
85 "zoom": [Axis1D(low=Key.E, high=Key.Q)],
86 }
87
88 def on_ready(self):
89 self.env = self.add_child(
90 WorldEnvironment(
91 ssao_enabled=True,
92 ssao_radius=0.6,
93 ssao_intensity=1.5,
94 fxaa_enabled=True,
95 dof_focus_distance=8.0,
96 dof_focus_range=2.0,
97 dof_max_coc=0.03,
98 )
99 )
100 self.camera = self.add_child(
101 OrbitCamera3D(
102 pivot=(0, 1, 0),
103 distance=16,
104 yaw=math.radians(35),
105 pitch=math.radians(-28),
106 min_pitch=math.radians(-80),
107 max_pitch=math.radians(10),
108 min_distance=6,
109 max_distance=40,
110 fov=55,
111 )
112 )
113 # One strong key light so the SSAO contact shadows read, and a faint blue fill.
114 self.add_child(DirectionalLight3D(intensity=2.2, direction=(-0.6, -1.0, -0.4)))
115 self.add_child(DirectionalLight3D(intensity=0.25, colour=(0.6, 0.7, 1.0), direction=(0.8, -0.6, 1.0)))
116 ground = Material(colour=(0.55, 0.55, 0.58), roughness=0.85)
117 self.add_child(MeshInstance3D(mesh=Mesh.box((40, 0.1, 40)), material=ground, position=(0, -0.05, 0)))
118
119 # Unit cubes rest at y=0.5 and the 0.6-radius spheres at y=0.6, all on the ground.
120 cube, ball = Mesh.box(), Mesh.sphere(radius=0.6)
121 cubes = [(-1, 0.5, 0), (0, 0.5, 0), (1, 0.5, 0), (-0.5, 1.5, 0), (0.5, 1.5, 0), (0, 0.5, -1)]
122 balls = [(-1.1, 0.6, 1.1), (0, 0.6, 1.1), (1.1, 0.6, 1.1)]
123 shapes = [(cube, p) for p in cubes] + [(ball, p) for p in balls]
124
125 rng = np.random.default_rng(7)
126 for i in range(16):
127 radius = 6.5 if i % 2 else 4.0
128 angle = i * math.tau / 8
129 jx, jz = rng.uniform(-0.4, 0.4, size=2)
130 x = math.cos(angle) * radius + jx
131 z = math.sin(angle) * radius + jz
132 shapes.append((ball, (x, 0.6, z)) if i % 2 else (cube, (x, 0.5, z)))
133
134 for i, (mesh, position) in enumerate(shapes):
135 material = Material(colour=PALETTE[i % len(PALETTE)], roughness=0.6, metallic=0.05)
136 self.add_child(MeshInstance3D(mesh=mesh, material=material, position=position))
137
138 hud = self.add_child(CanvasLayer())
139 self.status = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=12, font_size=18))
140 bar = hud.add_child(HBoxContainer(anchor=Anchor.BOTTOM_LEFT, inset=12))
141 labels = {"ssao": "SSAO", "tonemap": "Tonemap", "fxaa": "FXAA", "dof": "DOF"}
142 self.buttons = {name: bar.add_child(Button(label, action=name)) for name, label in labels.items()}
143 self.show_state()
144
145 # Example help overlay: not part of the technique shown.
146 help_text = "Arrows: SSAO WASD / drag: orbit Q/E / scroll: zoom"
147 hud.add_child(Label(help_text, anchor=Anchor.TOP_RIGHT, inset=12, font_size=14))
148
149 @on_input("ssao")
150 def toggle_ssao(self, _event):
151 self.env.ssao_enabled = not self.env.ssao_enabled
152 self.show_state()
153
154 @on_input("tonemap")
155 def cycle_tonemap(self, _event):
156 next_curve = (TONEMAP_CURVES.index(self.env.tonemap_mode) + 1) % len(TONEMAP_CURVES)
157 self.env.tonemap_mode = TONEMAP_CURVES[next_curve]
158 self.show_state()
159
160 @on_input("fxaa")
161 def toggle_fxaa(self, _event):
162 self.env.fxaa_enabled = not self.env.fxaa_enabled
163 self.show_state()
164
165 @on_input("dof")
166 def toggle_dof(self, _event):
167 self.env.dof_enabled = not self.env.dof_enabled
168 self.show_state()
169
170 def show_state(self):
171 self.buttons["ssao"].checked = self.env.ssao_enabled
172 self.buttons["fxaa"].checked = self.env.fxaa_enabled
173 self.buttons["dof"].checked = self.env.dof_enabled
174 self.buttons["tonemap"].text = f"Tonemap: {self.env.tonemap_mode}"
175
176 def on_update(self, dt):
177 env = self.env
178 env.ssao_radius += Input.value("ssao_radius") * dt
179 env.ssao_intensity += Input.value("ssao_intensity") * 1.5 * dt
180 self.status.text = f"SSAO radius {env.ssao_radius:.2f}\nSSAO intensity {env.ssao_intensity:.2f}"
181
182 keys = Input.value("orbit")
183 speed = self.camera.stick_speed * dt
184 self.camera.orbit(-keys.x * speed, -keys.y * speed)
185 self.camera.zoom(Input.value("zoom") * 8.0 * dt)
186
187
188if __name__ == "__main__":
189 App(title="Post-Processing", width=1280, height=720).run(PostProcessing())