TAA¶
Temporal anti-aliasing stabilises a high-frequency scene under an orbiting camera.
▶ Run in browserTags: 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)