PostProcessParity

Desktop parity for WorldEnvironment post-process knobs.

▶ Run in browser

Tags: 3d post-process ssao tonemap fxaa dof

Demonstrates the WorldEnvironment post-process Properties that now drive the desktop Vulkan renderer with the same results as the web backend:

  • SSAO with tunable radius / bias / intensity (dedicated compute pass).

  • Tonemap operator selection (ACES / Neutral / Reinhard / Uchimura) plus white-point, matching the web WGSL enumeration.

  • FXAA toggle.

  • Depth-of-field driven by the canonical dof_max_coc (resolution-aware).

Controls: A / D - Orbit camera left / right W / S - Pitch camera up / down Q / E - Zoom in / out 1 - Toggle SSAO 2 - Cycle tonemap operator 3 - Toggle FXAA 4 - Toggle depth of field Up / Down - SSAO radius +/- Left / Right- SSAO intensity -/+ Escape - Quit

On-screen buttons (mouse / touch) mirror the 1-4 toggles.

Source

  1"""PostProcessParity: Desktop parity for WorldEnvironment post-process knobs.
  2
  3# /// simvx
  4# tags = ["post-process", "ssao", "tonemap", "fxaa", "dof"]
  5# web = { width = 1280, height = 720 }
  6# ///
  7
  8Demonstrates the WorldEnvironment post-process Properties that now drive the
  9desktop Vulkan renderer with the same results as the web backend:
 10
 11  - SSAO with tunable radius / bias / intensity (dedicated compute pass).
 12  - Tonemap operator selection (ACES / Neutral / Reinhard / Uchimura) plus
 13    white-point, matching the web WGSL enumeration.
 14  - FXAA toggle.
 15  - Depth-of-field driven by the canonical ``dof_max_coc`` (resolution-aware).
 16
 17Controls:
 18    A / D       - Orbit camera left / right
 19    W / S       - Pitch camera up / down
 20    Q / E       - Zoom in / out
 21    1           - Toggle SSAO
 22    2           - Cycle tonemap operator
 23    3           - Toggle FXAA
 24    4           - Toggle depth of field
 25    Up / Down   - SSAO radius +/-
 26    Left / Right- SSAO intensity -/+
 27    Escape      - Quit
 28
 29On-screen buttons (mouse / touch) mirror the 1-4 toggles.
 30"""
 31
 32
 33import math
 34
 35import numpy as np
 36
 37from simvx.core import (
 38    AnchorPreset,
 39    Button,
 40    Camera3D,
 41    Colour,
 42    DirectionalLight3D,
 43    Input,
 44    InputMap,
 45    Key,
 46    Material,
 47    Mesh,
 48    MeshInstance3D,
 49    Node,
 50    Panel,
 51    Text2D,
 52    Vec2,
 53    WorldEnvironment,
 54)
 55from simvx.graphics import App
 56
 57TONEMAP_MODES = ["aces", "neutral", "reinhard", "uchimura"]
 58
 59
 60class PostProcessParity(Node):
 61    def on_ready(self):
 62        InputMap.add_action("orbit_left", [Key.A])
 63        InputMap.add_action("orbit_right", [Key.D])
 64        InputMap.add_action("pitch_up", [Key.W])
 65        InputMap.add_action("pitch_down", [Key.S])
 66        InputMap.add_action("zoom_in", [Key.Q])
 67        InputMap.add_action("zoom_out", [Key.E])
 68        InputMap.add_action("toggle_ssao", [Key.KEY_1])
 69        InputMap.add_action("cycle_tonemap", [Key.KEY_2])
 70        InputMap.add_action("toggle_fxaa", [Key.KEY_3])
 71        InputMap.add_action("toggle_dof", [Key.KEY_4])
 72        InputMap.add_action("radius_up", [Key.UP])
 73        InputMap.add_action("radius_down", [Key.DOWN])
 74        InputMap.add_action("intensity_up", [Key.RIGHT])
 75        InputMap.add_action("intensity_down", [Key.LEFT])
 76        InputMap.add_action("quit", [Key.ESCAPE])
 77
 78        self._yaw = 35.0
 79        self._pitch = 28.0
 80        self._distance = 16.0
 81        self._target = (0.0, 1.0, 0.0)
 82        self._tonemap_idx = 0
 83
 84        self._cam = Camera3D(name="Camera", fov=55, near=0.1, far=200.0)
 85        self.add_child(self._cam)
 86
 87        # WorldEnvironment: every knob below is now honoured on desktop.
 88        env = self.add_child(WorldEnvironment())
 89        env.ssao_enabled = True
 90        env.ssao_radius = 0.6
 91        env.ssao_bias = 0.025
 92        env.ssao_intensity = 1.5
 93        env.tonemap_mode = "aces"
 94        env.tonemap_white = 1.0
 95        env.tonemap_exposure = 1.0
 96        env.fxaa_enabled = True
 97        env.dof_enabled = False
 98        env.dof_focus_distance = 8.0
 99        env.dof_focus_range = 2.0
100        env.dof_max_coc = 0.03
101        env.bloom_enabled = False
102        env.sky_mode = "colour"
103        self._env = env
104
105        # Lighting: a single strong key light so SSAO contact shadows read.
106        key = DirectionalLight3D(name="KeyLight", intensity=2.2)
107        key.look_at((-0.6, -1.0, -0.4))
108        self.add_child(key)
109        fill = DirectionalLight3D(name="FillLight", intensity=0.25, colour=(0.6, 0.7, 1.0))
110        fill.look_at((0.8, -0.6, 1.0))
111        self.add_child(fill)
112
113        # Ground plane.
114        ground = MeshInstance3D(name="Ground", mesh=Mesh.cube())
115        ground.material = Material(colour=(0.55, 0.55, 0.58), roughness=0.85, metallic=0.0)
116        ground.scale = (40.0, 0.1, 40.0)
117        ground.position = (0.0, -0.05, 0.0)
118        self.add_child(ground)
119
120        palette = [
121            (0.85, 0.3, 0.3), (0.3, 0.75, 0.4), (0.35, 0.45, 0.85),
122            (0.85, 0.75, 0.3), (0.8, 0.4, 0.8),
123        ]
124
125        # A packed central cluster: cubes seated on the ground with shared
126        # faces, spheres nestled against them. The crevices give SSAO obvious
127        # contact occlusion to darken when toggled.
128        cluster = [
129            # (mesh, position): unit cubes rest at y=0.5, r=0.6 spheres at 0.6.
130            ("cube", (-1.0, 0.5, 0.0)),
131            ("cube", (0.0, 0.5, 0.0)),
132            ("cube", (1.0, 0.5, 0.0)),
133            ("cube", (-0.5, 1.5, 0.0)),
134            ("cube", (0.5, 1.5, 0.0)),
135            ("cube", (0.0, 0.5, -1.0)),
136            ("sphere", (-1.1, 0.6, 1.1)),
137            ("sphere", (0.0, 0.6, 1.1)),
138            ("sphere", (1.1, 0.6, 1.1)),
139        ]
140        for i, (kind, position) in enumerate(cluster):
141            mat = Material(colour=palette[i % len(palette)], roughness=0.6, metallic=0.05)
142            mesh = Mesh.cube() if kind == "cube" else Mesh.sphere(radius=0.6)
143            obj = MeshInstance3D(name=f"Cluster{i}", mesh=mesh, material=mat)
144            obj.position = position
145            self.add_child(obj)
146
147        # Outer rings, also seated on the ground: their depth spread shows DoF.
148        rng = np.random.default_rng(7)
149        for i in range(16):
150            colour = palette[i % len(palette)]
151            mat = Material(colour=colour, roughness=0.6, metallic=0.05)
152            is_cube = i % 2 == 0
153            mesh = Mesh.cube() if is_cube else Mesh.sphere(radius=0.6)
154            obj = MeshInstance3D(name=f"Ring{i}", mesh=mesh, material=mat)
155            ring = 4.0 + (i % 2) * 2.5
156            angle = i * math.pi * 2 / 8
157            obj.position = (
158                math.cos(angle) * ring + rng.uniform(-0.4, 0.4),
159                0.5 if is_cube else 0.6,
160                math.sin(angle) * ring + rng.uniform(-0.4, 0.4),
161            )
162            self.add_child(obj)
163
164        self._hud = self.add_child(Text2D(name="HUD", text="", font_scale=1.4, position=(12.0, 12.0)))
165        self._build_buttons()
166        self._update_camera()
167        self._update_hud()
168
169    def _build_buttons(self):
170        """Bottom-left bar of touch-friendly buttons mirroring the 1-4 keys.
171
172        The bar is anchored (never positioned from module constants) so it
173        tracks the window edge on resize; touch arrives as a left click.
174        """
175        btn_w, btn_h, gap, gutter = 108.0, 34.0, 6.0, 12.0
176        actions = [
177            ("ssao", self._toggle_ssao),
178            ("tonemap", self._cycle_tonemap),
179            ("fxaa", self._toggle_fxaa),
180            ("dof", self._toggle_dof),
181        ]
182        bar_w = gap + (btn_w + gap) * len(actions)
183        bar = Panel(name="ToggleBar")
184        bar.set_anchor_preset(AnchorPreset.BOTTOM_LEFT)
185        bar.margin_left = gutter
186        bar.margin_right = bar_w + gutter
187        bar.margin_top = -(btn_h + 12.0 + gutter)
188        bar.margin_bottom = -gutter
189        bar.bg_colour = Colour((0.0, 0.0, 0.0, 0.45))
190        self.add_child(bar)
191
192        self._buttons = {}
193        for i, (key, handler) in enumerate(actions):
194            btn = Button("", name=f"Btn_{key}", on_press=handler)
195            btn.position = Vec2(gap + (btn_w + gap) * i, 6.0)
196            btn.size = Vec2(btn_w, btn_h)
197            bar.add_child(btn)
198            self._buttons[key] = btn
199
200    def _toggle_ssao(self):
201        self._env.ssao_enabled = not self._env.ssao_enabled
202
203    def _cycle_tonemap(self):
204        self._tonemap_idx = (self._tonemap_idx + 1) % len(TONEMAP_MODES)
205        self._env.tonemap_mode = TONEMAP_MODES[self._tonemap_idx]
206
207    def _toggle_fxaa(self):
208        self._env.fxaa_enabled = not self._env.fxaa_enabled
209
210    def _toggle_dof(self):
211        self._env.dof_enabled = not self._env.dof_enabled
212
213    def on_update(self, dt):
214        if Input.is_action_just_pressed("quit"):
215            self.app.quit()
216            return
217
218        if Input.is_action_pressed("orbit_left"):
219            self._yaw += 60.0 * dt
220        if Input.is_action_pressed("orbit_right"):
221            self._yaw -= 60.0 * dt
222        if Input.is_action_pressed("pitch_up"):
223            self._pitch = min(80.0, self._pitch + 30.0 * dt)
224        if Input.is_action_pressed("pitch_down"):
225            self._pitch = max(-10.0, self._pitch - 30.0 * dt)
226        if Input.is_action_pressed("zoom_in"):
227            self._distance = max(6.0, self._distance - 8.0 * dt)
228        if Input.is_action_pressed("zoom_out"):
229            self._distance = min(40.0, self._distance + 8.0 * dt)
230
231        env = self._env
232        if Input.is_action_just_pressed("toggle_ssao"):
233            self._toggle_ssao()
234        if Input.is_action_just_pressed("cycle_tonemap"):
235            self._cycle_tonemap()
236        if Input.is_action_just_pressed("toggle_fxaa"):
237            self._toggle_fxaa()
238        if Input.is_action_just_pressed("toggle_dof"):
239            self._toggle_dof()
240
241        if Input.is_action_pressed("radius_up"):
242            env.ssao_radius = min(5.0, env.ssao_radius + 1.0 * dt)
243        if Input.is_action_pressed("radius_down"):
244            env.ssao_radius = max(0.0, env.ssao_radius - 1.0 * dt)
245        if Input.is_action_pressed("intensity_up"):
246            env.ssao_intensity = min(5.0, env.ssao_intensity + 1.5 * dt)
247        if Input.is_action_pressed("intensity_down"):
248            env.ssao_intensity = max(0.0, env.ssao_intensity - 1.5 * dt)
249
250        self._update_camera()
251        self._update_hud()
252
253    def _update_camera(self):
254        yaw_rad = math.radians(self._yaw)
255        pitch_rad = math.radians(self._pitch)
256        cp = math.cos(pitch_rad)
257        x = self._target[0] + self._distance * cp * math.sin(yaw_rad)
258        y = self._target[1] + self._distance * math.sin(pitch_rad)
259        z = self._target[2] + self._distance * cp * math.cos(yaw_rad)
260        self._cam.position = (x, y, z)
261        self._cam.look_at(self._target)
262
263    def _update_hud(self):
264        env = self._env
265        lines = [
266            "Post-Process Parity (WorldEnvironment)",
267            f"[1] SSAO: {'ON' if env.ssao_enabled else 'OFF'}",
268            f"    radius={env.ssao_radius:.2f} (Up/Down)",
269            f"    intensity={env.ssao_intensity:.2f} (Left/Right)",
270            f"[2] Tonemap: {env.tonemap_mode}  white={env.tonemap_white:.2f}",
271            f"[3] FXAA: {'ON' if env.fxaa_enabled else 'OFF'}",
272            f"[4] DoF: {'ON' if env.dof_enabled else 'OFF'}  max_coc={env.dof_max_coc:.3f}",
273            "A/D orbit  W/S pitch  Q/E zoom  Esc quit",
274        ]
275        self._hud.text = "\n".join(lines)
276        self._buttons["ssao"].text = f"SSAO {'ON' if env.ssao_enabled else 'OFF'}"
277        self._buttons["tonemap"].text = env.tonemap_mode
278        self._buttons["fxaa"].text = f"FXAA {'ON' if env.fxaa_enabled else 'OFF'}"
279        self._buttons["dof"].text = f"DoF {'ON' if env.dof_enabled else 'OFF'}"
280
281
282def main() -> None:
283    app = App(title="PostProcessParity", width=1280, height=720)
284    app.run(PostProcessParity())
285
286
287if __name__ == "__main__":
288    main()