Custom Post-Process

your own GLSL fullscreen effect.

▶ Run in browser

Tags: 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 on

  • u_depth_tex: the scene depth

  • u_resolution: the target size in pixels

  • u_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())