Custom Post-Process¶
your own GLSL fullscreen effect.
▶ Run in browserTags: 3d
A PostProcessEffect is a fullscreen fragment shader you add to a
WorldEnvironment. It runs after the built-in post-processing (including the
temporal anti-aliasing resolve) and before tonemapping. You write only main();
the engine declares frag_colour and these uniforms for you:
u_colour_tex: the current frame colour, already anti-aliased when TAA is onu_depth_tex: the scene depthu_resolution: the target size in pixelsu_time: elapsed seconds
Any uniform of your own is set with set_uniform, and order sorts several
effects. Adding one looks like this:
effect = PostProcessEffect(ABERRATION_SHADER, order=10)
effect.set_uniform("u_aberration", 0.28)
env.add_post_process(effect)
The effect here splits red and blue outward toward the screen edges (chromatic
aberration) and darkens the same falloff (a vignette). Setting effect.enabled
switches it off without removing it, which returns cleanly to the plain resolved
image. u_colour_tex clamps at the frame edge, which suits taps around a
pixel; pass wrap="repeat" or wrap="mirror" for an effect that tiles the
frame on purpose.
The checker floor and thin pillars alias badly without TAA, and the orbiting sphere gives TAA motion to reproject, so toggling TAA shows that the effect samples the resolved image rather than replacing it.
Controls: E / click / tap : Toggle the custom effect T : Toggle TAA
Source¶
1#!/usr/bin/env python3
2# /// simvx
3# screenshot_frame = 40
4# ///
5"""Custom Post-Process: your own GLSL fullscreen effect.
6
7A ``PostProcessEffect`` is a fullscreen fragment shader you add to a
8``WorldEnvironment``. It runs after the built-in post-processing (including the
9temporal anti-aliasing resolve) and before tonemapping. You write only ``main()``;
10the engine declares ``frag_colour`` and these uniforms for you:
11
12- ``u_colour_tex``: the current frame colour, already anti-aliased when TAA is on
13- ``u_depth_tex``: the scene depth
14- ``u_resolution``: the target size in pixels
15- ``u_time``: elapsed seconds
16
17Any uniform of your own is set with ``set_uniform``, and ``order`` sorts several
18effects. Adding one looks like this:
19
20 effect = PostProcessEffect(ABERRATION_SHADER, order=10)
21 effect.set_uniform("u_aberration", 0.28)
22 env.add_post_process(effect)
23
24The effect here splits red and blue outward toward the screen edges (chromatic
25aberration) and darkens the same falloff (a vignette). Setting ``effect.enabled``
26switches it off without removing it, which returns cleanly to the plain resolved
27image. ``u_colour_tex`` clamps at the frame edge, which suits taps around a
28pixel; pass ``wrap="repeat"`` or ``wrap="mirror"`` for an effect that tiles the
29frame on purpose.
30
31The checker floor and thin pillars alias badly without TAA, and the orbiting
32sphere gives TAA motion to reproject, so toggling TAA shows that the effect
33samples the resolved image rather than replacing it.
34
35Controls:
36 E / click / tap : Toggle the custom effect
37 T : Toggle TAA
38"""
39
40import math
41
42from simvx.core import (
43 Anchor,
44 Camera3D,
45 CanvasLayer,
46 DirectionalLight3D,
47 Key,
48 Label,
49 Material,
50 Mesh,
51 MeshInstance3D,
52 Node,
53 Pointer,
54 PostProcessEffect,
55 WorldEnvironment,
56 on_input,
57)
58from simvx.graphics import App
59
60ABERRATION_SHADER = """
61void main() {
62 vec2 uv = gl_FragCoord.xy / u_resolution;
63 vec2 centre = uv - 0.5;
64 float r2 = dot(centre, centre);
65 vec2 offset = centre * (u_aberration * r2);
66 float r = texture(u_colour_tex, uv + offset).r;
67 float g = texture(u_colour_tex, uv).g;
68 float b = texture(u_colour_tex, uv - offset).b;
69 frag_colour = vec4(vec3(r, g, b) * (1.0 - u_vignette * r2), 1.0);
70}
71"""
72
73
74class CustomPostProcessDemo(Node):
75 input_context = {"toggle_effect": [Key.E, Pointer.PRIMARY], "toggle_taa": [Key.T]}
76
77 def on_ready(self):
78 self.add_child(Camera3D(position=(0, 3.5, 8.5), look_at=(0, 0.6, 0)))
79 self.add_child(DirectionalLight3D(direction=(-0.5, -1, -0.3), colour=(1, 0.97, 0.92), intensity=1.6))
80
81 tile = Mesh.box((1, 0.1, 1))
82 light_tile = Material(colour=(0.85, 0.85, 0.88), roughness=0.85)
83 dark_tile = Material(colour=(0.12, 0.12, 0.15), roughness=0.85)
84 for x in range(-6, 6):
85 for z in range(-6, 6):
86 material = light_tile if (x + z) % 2 == 0 else dark_tile
87 self.add_child(MeshInstance3D(mesh=tile, material=material, position=(x + 0.5, -0.55, z + 0.5)))
88
89 pillar = Mesh.box((0.12, 2.4, 0.12))
90 for i in range(-4, 5):
91 material = Material(colour=(0.9, 0.5 + 0.05 * i, 0.2), roughness=0.4, metallic=0.1)
92 self.add_child(MeshInstance3D(mesh=pillar, material=material, position=(i * 1.1, 1.2, -2)))
93
94 self.sphere = self.add_child(
95 MeshInstance3D(mesh=Mesh.sphere(), material=Material(colour=(0.1, 0.5, 0.9), roughness=0.2, metallic=0.8))
96 )
97
98 self.env = self.add_child(WorldEnvironment(taa_enabled=True))
99 self.effect = PostProcessEffect(ABERRATION_SHADER, order=10)
100 self.effect.set_uniform("u_aberration", 0.28)
101 self.effect.set_uniform("u_vignette", 0.55)
102 self.env.add_post_process(self.effect)
103
104 hud = self.add_child(CanvasLayer())
105 self.status = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=12, font_size=24))
106 hud.add_child(Label("E / click / tap: toggle effect T: toggle TAA", anchor=Anchor.BOTTOM_LEFT, inset=12))
107
108 @on_input("toggle_effect")
109 def _toggle_effect(self, _event):
110 self.effect.enabled = not self.effect.enabled
111
112 @on_input("toggle_taa")
113 def _toggle_taa(self, _event):
114 self.env.taa_enabled = not self.env.taa_enabled
115
116 def on_update(self, dt):
117 t = self.tree.now * 1.5
118 self.sphere.position = (math.sin(t) * 2.5, 1, 1.5 + math.cos(t))
119 on_off = {True: "ON", False: "OFF"}
120 self.status.text = f"Effect: {on_off[self.effect.enabled]} TAA: {on_off[self.env.taa_enabled]}"
121
122
123if __name__ == "__main__":
124 App(title="Custom Post-Process", width=1280, height=720).run(CustomPostProcessDemo())