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