TAA

Temporal anti-aliasing stabilises a high-frequency scene under an orbiting camera.

▶ Run in browser

Tags: 3d

Temporal anti-aliasing jitters the camera sub-pixel each frame and accumulates the result into a history buffer, reprojecting it through PER-PIXEL motion so the accumulation tracks both camera AND moving-object motion. The scene below packs thin edges and a checker floor (high-frequency detail that aliases badly) and orbits the camera so the difference between TAA on/off is visible: edges crawl and shimmer with TAA off, and resolve to stable smooth lines with TAA on.

A fast-moving emissive cube sweeps across the frame: with only camera-motion reprojection it would smear a ghost trail behind itself, but the per-object velocity buffer reprojects its history through its own motion, so the trailing edge stays clean (no smear).

TAA is OFF by default engine-wide (it costs a history buffer + resolve pass); this demo opts in via WorldEnvironment.taa_enabled = True.

Controls: T / click / tap : Toggle TAA on/off A/D : Slow down / speed up the orbit ESC : Quit

Usage: uv run python examples/features/3d/taa.py

Source

  1#!/usr/bin/env python3
  2"""TAA: Temporal anti-aliasing stabilises a high-frequency scene under an orbiting camera.
  3
  4# /// simvx
  5# screenshot_frame = 90
  6# web = { width = 1280, height = 720, root = "TAADemo", responsive = true }
  7# ///
  8
  9Temporal anti-aliasing jitters the camera sub-pixel each frame and accumulates
 10the result into a history buffer, reprojecting it through PER-PIXEL motion so the
 11accumulation tracks both camera AND moving-object motion. The scene below packs
 12thin edges and a checker floor (high-frequency detail that aliases badly) and
 13orbits the camera so the difference between TAA on/off is visible: edges crawl
 14and shimmer with TAA off, and resolve to stable smooth lines with TAA on.
 15
 16A fast-moving emissive cube sweeps across the frame: with only camera-motion
 17reprojection it would smear a ghost trail behind itself, but the per-object
 18velocity buffer reprojects its history through its own motion, so the trailing
 19edge stays clean (no smear).
 20
 21TAA is OFF by default engine-wide (it costs a history buffer + resolve pass); this
 22demo opts in via ``WorldEnvironment.taa_enabled = True``.
 23
 24Controls:
 25    T / click / tap : Toggle TAA on/off
 26    A/D             : Slow down / speed up the orbit
 27    ESC             : Quit
 28
 29Usage:
 30    uv run python examples/features/3d/taa.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    Property,
 47    Text2D,
 48    Vec3,
 49    WorldEnvironment,
 50)
 51from simvx.graphics import App
 52
 53WIDTH, HEIGHT = 1280, 720
 54
 55
 56class TAADemo(Node):
 57    """Orbiting camera over a high-frequency scene to demonstrate TAA stability."""
 58
 59    orbit_speed = Property(0.8, range=(0.1, 5.0))
 60    orbit_radius = Property(9.0, range=(3.0, 20.0))
 61
 62    def __init__(self, **kwargs):
 63        super().__init__(**kwargs)
 64        self._time = 0.0
 65        # Integrated orbit angle. Integrating speed*dt (rather than computing
 66        # angle = time * speed) means an A/D speed change only affects motion
 67        # from now on; scaling the whole accumulated time would snap the camera
 68        # by time * delta_speed radians per frame while a key is held.
 69        self._angle = 0.0
 70        self._taa_on = True
 71
 72    def on_ready(self):
 73        super().on_ready()
 74
 75        # Touch arrives as MouseButton.LEFT on web, so click/tap anywhere
 76        # toggles TAA and keeps the comparison playable without a keyboard.
 77        InputMap.add_action("toggle_taa", [Key.T, MouseButton.LEFT])
 78        InputMap.add_action("orbit_faster", [Key.D])
 79        InputMap.add_action("orbit_slower", [Key.A])
 80        InputMap.add_action("quit", [Key.ESCAPE])
 81
 82        self.camera = self.add_child(Camera3D(position=Vec3(0.0, 4.0, 9.0), look_at=Vec3(0.0, 0.5, 0.0)))
 83
 84        light = DirectionalLight3D()
 85        light.direction = Vec3(-0.5, -1.0, -0.3)
 86        light.colour = (1.0, 0.97, 0.92)
 87        light.intensity = 1.6
 88        self.add_child(light)
 89
 90        # Checker floor: a grid of alternating dark/light tiles. The tile edges
 91        # are exactly the kind of high-frequency horizontal detail that crawls
 92        # under camera motion without temporal accumulation.
 93        tile = Mesh.cube()
 94        for gx in range(-6, 6):
 95            for gz in range(-6, 6):
 96                light_tile = (gx + gz) % 2 == 0
 97                col = (0.85, 0.85, 0.88) if light_tile else (0.12, 0.12, 0.15)
 98                t = MeshInstance3D(mesh=tile, material=Material(colour=col, roughness=0.85))
 99                t.position = Vec3(gx + 0.5, -0.55, gz + 0.5)
100                t.scale = Vec3(1.0, 0.1, 1.0)
101                self.add_child(t)
102
103        # A picket of thin tall pillars: near-vertical edges that shimmer badly
104        # when undersampled. TAA should resolve these to clean stable lines.
105        for i in range(-4, 5):
106            pillar = MeshInstance3D(
107                mesh=tile,
108                material=Material(colour=(0.9, 0.5 + 0.05 * i, 0.2), roughness=0.4, metallic=0.1),
109            )
110            pillar.position = Vec3(i * 1.1, 1.2, -2.0)
111            pillar.scale = Vec3(0.12, 2.4, 0.12)
112            self.add_child(pillar)
113
114        # A metallic sphere for a smooth-shaded reference.
115        sphere = MeshInstance3D(
116            mesh=Mesh.sphere(),
117            material=Material(colour=(0.1, 0.5, 0.9), roughness=0.2, metallic=0.8),
118        )
119        sphere.position = Vec3(2.5, 1.0, 1.5)
120        self.add_child(sphere)
121
122        # A fast-moving bright cube: the per-object velocity buffer reprojects its
123        # TAA history through its OWN motion, so its trailing edge stays clean.
124        # Without per-object velocity (camera-only reproject) it would smear a
125        # ghost trail. It sweeps left<->right low over the dark checker, in a
126        # distinct emissive magenta so it is unambiguous against the orange picket.
127        self._mover = MeshInstance3D(
128            mesh=tile,
129            material=Material(colour=(1.0, 0.1, 0.8), roughness=0.3, emissive_colour=(1.0, 0.05, 0.7)),
130        )
131        self._mover.scale = Vec3(0.45, 0.45, 0.45)
132        self._mover.position = Vec3(0.0, 0.4, 3.0)
133        self.add_child(self._mover)
134
135        self._env = self.add_child(WorldEnvironment(name="Env"))
136        self._env.taa_enabled = self._taa_on
137
138        # Top-left status + bottom-left controls (font sizes match the sibling
139        # 3D feature examples; HiDPI scaling is handled by the engine).
140        self._hud_taa = self.add_child(Text2D(text="TAA: ON", position=(12, 12), font_scale=1.8))
141        self._hud_speed = self.add_child(Text2D(text="Orbit: 0.8 (A/D)", position=(12, 52), font_scale=1.4))
142        self._hud_hint = self.add_child(
143            Text2D(text="T / click / tap: Toggle TAA", position=(12, HEIGHT - 34), font_scale=1.4)
144        )
145
146    def on_update(self, dt: float):
147        # Keep the controls hint pinned to the live window's bottom edge.
148        self._hud_hint.position = (12, self.app.height - 34)
149        if Input.is_action_just_pressed("quit"):
150            self.app.quit()
151            return
152
153        self._time += dt
154
155        # Sweep the bright cube horizontally (fast, large screen-space motion):
156        # its trailing edge ghosts under camera-only TAA, stays clean with
157        # per-object velocity.
158        self._mover.position = Vec3(math.sin(self._time * 2.2) * 3.4, 0.4, 3.0)
159
160        self._angle += self.orbit_speed * dt
161        x = math.cos(self._angle) * self.orbit_radius
162        z = math.sin(self._angle) * self.orbit_radius
163        y = 3.5 + math.sin(self._time * 0.4) * 1.2
164        self.camera.position = Vec3(x, y, z)
165        self.camera.look_at(Vec3(0.0, 0.6, 0.0))
166
167        if Input.is_action_just_pressed("toggle_taa"):
168            self._taa_on = not self._taa_on
169        if Input.is_action_pressed("orbit_faster"):
170            self.orbit_speed = min(5.0, self.orbit_speed + 1.5 * dt)
171        if Input.is_action_pressed("orbit_slower"):
172            self.orbit_speed = max(0.1, self.orbit_speed - 1.5 * dt)
173
174        self._env.taa_enabled = self._taa_on
175        self._hud_taa.text = f"TAA: {'ON' if self._taa_on else 'OFF'}"
176        self._hud_speed.text = f"Orbit: {self.orbit_speed:.1f} (A/D)"
177
178
179if __name__ == "__main__":
180    scene = TAADemo(name="TAADemo")
181    app = App(title="TAA Demo", width=WIDTH, height=HEIGHT)
182    app.run(scene)