Custom post-process¶
a user GLSL effect runs on the TAA-resolved image before tonemap.
▶ Run in browserTags: 3d
A PostProcessEffect injects a fullscreen GLSL pass between the built-in
post-processing and tonemap. It receives the current frame colour (u_colour_tex),
depth (u_depth_tex), resolution and time, plus any uniforms you set. The effect
below applies radial chromatic aberration and a vignette.
The scene runs with temporal anti-aliasing ON, so the custom effect samples the TAA-RESOLVED (anti-aliased) image, and tonemap samples the effect’s output: the resolve and the user effect compose correctly rather than one discarding the other. Toggling the effect off while TAA stays on returns cleanly to the plain resolved image.
u_colour_tex clamps at the frame edge by default (correct for neighbourhood
taps); pass wrap="repeat" / "mirror" to PostProcessEffect for an effect
that deliberately tiles the framebuffer.
Controls: E / click / tap : Toggle the custom effect on/off T : Toggle TAA on/off ESC : Quit
Usage: uv run python examples/features/3d/custom_post_process.py
Source¶
1#!/usr/bin/env python3
2"""Custom post-process: a user GLSL effect runs on the TAA-resolved image before tonemap.
3
4# /// simvx
5# screenshot_frame = 40
6# ///
7
8A ``PostProcessEffect`` injects a fullscreen GLSL pass between the built-in
9post-processing and tonemap. It receives the current frame colour (``u_colour_tex``),
10depth (``u_depth_tex``), resolution and time, plus any uniforms you set. The effect
11below applies radial chromatic aberration and a vignette.
12
13The scene runs with temporal anti-aliasing ON, so the custom effect samples the
14TAA-RESOLVED (anti-aliased) image, and tonemap samples the effect's output: the
15resolve and the user effect compose correctly rather than one discarding the other.
16Toggling the effect off while TAA stays on returns cleanly to the plain resolved
17image.
18
19``u_colour_tex`` clamps at the frame edge by default (correct for neighbourhood
20taps); pass ``wrap="repeat"`` / ``"mirror"`` to ``PostProcessEffect`` for an effect
21that deliberately tiles the framebuffer.
22
23Controls:
24 E / click / tap : Toggle the custom effect on/off
25 T : Toggle TAA on/off
26 ESC : Quit
27
28Usage:
29 uv run python examples/features/3d/custom_post_process.py
30"""
31
32import math
33
34from simvx.core import (
35 Camera3D,
36 DirectionalLight3D,
37 Input,
38 InputMap,
39 Key,
40 Material,
41 Mesh,
42 MeshInstance3D,
43 MouseButton,
44 Node,
45 PostProcessEffect,
46 Text2D,
47 Vec3,
48 WorldEnvironment,
49)
50from simvx.graphics import App
51
52WIDTH, HEIGHT = 1280, 720
53
54# A user fullscreen effect: radial chromatic aberration + vignette. Reads the
55# current frame colour with a radial offset that grows toward the edges, so R/G/B
56# separate at the corners; the vignette darkens the same falloff.
57ABERRATION_SHADER = """
58void main() {
59 vec2 uv = gl_FragCoord.xy / u_resolution;
60 vec2 centre = uv - 0.5;
61 float r2 = dot(centre, centre);
62 vec2 offset = centre * (u_aberration * r2);
63 float cr = texture(u_colour_tex, uv + offset).r;
64 float cg = texture(u_colour_tex, uv).g;
65 float cb = texture(u_colour_tex, uv - offset).b;
66 vec3 col = vec3(cr, cg, cb) * (1.0 - u_vignette * r2);
67 frag_colour = vec4(col, 1.0);
68}
69"""
70
71
72class CustomPostProcessDemo(Node):
73 """A high-frequency 3D scene under TAA with a user chromatic-aberration effect."""
74
75 def __init__(self, **kwargs):
76 super().__init__(**kwargs)
77 self._time = 0.0
78 self._effect_on = True
79 self._taa_on = True
80
81 def on_ready(self):
82 super().on_ready()
83
84 InputMap.add_action("toggle_effect", [Key.E, MouseButton.LEFT])
85 InputMap.add_action("toggle_taa", [Key.T])
86 InputMap.add_action("quit", [Key.ESCAPE])
87
88 # Static camera: the moving sphere supplies the only motion, so the frame
89 # is deterministic under the headless fixed-step clock (golden-stable).
90 self.camera = self.add_child(Camera3D(position=Vec3(0.0, 3.5, 8.5), look_at=Vec3(0.0, 0.6, 0.0)))
91
92 light = DirectionalLight3D()
93 light.direction = Vec3(-0.5, -1.0, -0.3)
94 light.colour = (1.0, 0.97, 0.92)
95 light.intensity = 1.6
96 self.add_child(light)
97
98 # Checker floor + a picket of thin pillars: high-frequency edges that
99 # alias badly, so the TAA resolve the effect samples is clearly at work.
100 tile = Mesh.cube()
101 for gx in range(-6, 6):
102 for gz in range(-6, 6):
103 col = (0.85, 0.85, 0.88) if (gx + gz) % 2 == 0 else (0.12, 0.12, 0.15)
104 t = MeshInstance3D(mesh=tile, material=Material(colour=col, roughness=0.85))
105 t.position = Vec3(gx + 0.5, -0.55, gz + 0.5)
106 t.scale = Vec3(1.0, 0.1, 1.0)
107 self.add_child(t)
108
109 for i in range(-4, 5):
110 pillar = MeshInstance3D(
111 mesh=tile,
112 material=Material(colour=(0.9, 0.5 + 0.05 * i, 0.2), roughness=0.4, metallic=0.1),
113 )
114 pillar.position = Vec3(i * 1.1, 1.2, -2.0)
115 pillar.scale = Vec3(0.12, 2.4, 0.12)
116 self.add_child(pillar)
117
118 self._sphere = MeshInstance3D(
119 mesh=Mesh.sphere(),
120 material=Material(colour=(0.1, 0.5, 0.9), roughness=0.2, metallic=0.8),
121 )
122 self._sphere.position = Vec3(0.0, 1.0, 1.5)
123 self.add_child(self._sphere)
124
125 self._env = self.add_child(WorldEnvironment(name="Env"))
126 self._env.taa_enabled = self._taa_on
127
128 self._effect = PostProcessEffect(ABERRATION_SHADER, order=10)
129 self._effect.set_uniform("u_aberration", 0.28)
130 self._effect.set_uniform("u_vignette", 0.55)
131 self._env.add_post_process(self._effect)
132
133 self._hud = self.add_child(Text2D(text="", position=(12, 12), font_scale=1.8))
134 self._hud_hint = self.add_child(
135 Text2D(text="E / click / tap: Toggle effect T: Toggle TAA", position=(12, HEIGHT - 34), font_scale=1.4)
136 )
137 self._refresh_hud()
138
139 def on_update(self, dt: float):
140 self._hud_hint.position = (12, self.app.height - 34)
141 if Input.is_action_just_pressed("quit"):
142 self.app.quit()
143 return
144
145 self._time += dt
146 # Orbit the sphere so TAA has per-object motion to reproject.
147 self._sphere.position = Vec3(math.sin(self._time * 1.5) * 2.5, 1.0, 1.5 + math.cos(self._time * 1.5) * 1.0)
148
149 if Input.is_action_just_pressed("toggle_effect"):
150 self._effect_on = not self._effect_on
151 if self._effect_on:
152 self._env.add_post_process(self._effect)
153 else:
154 self._env.remove_post_process(self._effect)
155 self._refresh_hud()
156
157 if Input.is_action_just_pressed("toggle_taa"):
158 self._taa_on = not self._taa_on
159 self._env.taa_enabled = self._taa_on
160 self._refresh_hud()
161
162 def _refresh_hud(self):
163 self._hud.text = f"Effect: {'ON' if self._effect_on else 'OFF'} TAA: {'ON' if self._taa_on else 'OFF'}"
164
165
166if __name__ == "__main__":
167 scene = CustomPostProcessDemo(name="CustomPostProcessDemo")
168 app = App(title="Custom Post-Process Demo", width=WIDTH, height=HEIGHT)
169 app.run(scene)