Bloom 2D

glow on a pure-2D scene.

▶ Run in browser

Tags: 2d bloom glow post-processing

A 2D scene opts into bloom by adding a WorldEnvironment with bloom_enabled=True. Its 2D content then renders through the HDR path with a linear tonemap, so a colour brighter than 1.0 glows while ordinary art keeps its authored colour. A 2D scene with no WorldEnvironment renders flat and pays nothing for the feature.

Two settings shape the glow:

  • bloom_threshold is the luminance a pixel must exceed to glow. The neon stars are over-bright (colour channels above 1.0), so their luminance clears the starting threshold of 0.6. The dark bars sit well below it and keep their colour until the threshold is lowered toward them. Raise the threshold and only the brightest stars still glow.

  • bloom_intensity is how strongly the blurred glow is added back over the scene.

The HUD is a Label on a CanvasLayer, which pins it to the screen and keeps it out of the bloom.

Controls: B or a click / tap toggles bloom, Up / Down change the threshold and Left / Right change the intensity.

Source

 1# /// simvx
 2# tags = ["2d", "bloom", "glow", "post-processing"]
 3# web = { width = 1280, height = 720, reason = "Pure-2D bloom routes through the full HDR pipeline on web." }
 4# ///
 5"""Bloom 2D: glow on a pure-2D scene.
 6
 7A 2D scene opts into bloom by adding a ``WorldEnvironment`` with ``bloom_enabled=True``.
 8Its 2D content then renders through the HDR path with a linear tonemap, so a colour
 9brighter than 1.0 glows while ordinary art keeps its authored colour. A 2D scene with
10no ``WorldEnvironment`` renders flat and pays nothing for the feature.
11
12Two settings shape the glow:
13
14- ``bloom_threshold`` is the luminance a pixel must exceed to glow. The neon stars
15  are over-bright (colour channels above 1.0), so their luminance clears the starting
16  threshold of 0.6. The dark bars sit well below it and keep their colour until the
17  threshold is lowered toward them. Raise the threshold and only the brightest stars
18  still glow.
19- ``bloom_intensity`` is how strongly the blurred glow is added back over the scene.
20
21The HUD is a ``Label`` on a ``CanvasLayer``, which pins it to the screen and keeps it
22out of the bloom.
23
24Controls: B or a click / tap toggles bloom, Up / Down change the threshold and
25Left / Right change the intensity.
26"""
27
28import math
29
30from simvx.core import Anchor, Axis1D, CanvasLayer, Input, Key, Label, Node2D, Pointer, Polygon2D, WorldEnvironment
31from simvx.graphics import App
32
33NEON = [(2.6, 0.4, 0.4, 1.0), (0.4, 2.6, 0.8, 1.0), (0.5, 0.7, 2.8, 1.0), (2.6, 2.4, 0.5, 1.0)]
34DARK = (0.18, 0.20, 0.28, 1.0)
35
36
37def star(radius: float, points: int) -> list[tuple[float, float]]:
38    """A concave star outline; Polygon2D triangulates concave shapes."""
39    outline = []
40    for i in range(points * 2):
41        angle = -math.pi / 2 + i * math.pi / points
42        r = radius if i % 2 == 0 else radius * 0.45
43        outline.append((math.cos(angle) * r, math.sin(angle) * r))
44    return outline
45
46
47class Bloom2D(Node2D):
48    input_context = {
49        "toggle_bloom": [Key.B, Pointer.PRIMARY],
50        "threshold": [Axis1D(low=Key.DOWN, high=Key.UP)],
51        "intensity": [Axis1D(low=Key.LEFT, high=Key.RIGHT)],
52    }
53
54    def on_ready(self):
55        self.env = self.add_child(WorldEnvironment(bloom_enabled=True, bloom_threshold=0.6, bloom_intensity=1.3))
56        self.stars = [
57            self.add_child(Polygon2D(polygon=star(80, 5 + i % 2), colour=colour)) for i, colour in enumerate(NEON)
58        ]
59        bar = [(-75, -80), (75, -80), (75, 80), (-75, 80)]
60        self.bars = [self.add_child(Polygon2D(polygon=bar, colour=DARK)) for _ in range(6)]
61        self.hud = self.add_child(CanvasLayer()).add_child(Label(anchor=Anchor.TOP_LEFT, inset=12))
62
63    def on_update(self, dt):
64        env = self.env
65        if Input.just_pressed("toggle_bloom"):
66            env.bloom_enabled = not env.bloom_enabled
67        # The ranges on these Properties are soft, so the example keeps them in bounds.
68        env.bloom_threshold = min(2.0, max(0.0, env.bloom_threshold + 0.5 * Input.value("threshold") * dt))
69        env.bloom_intensity = min(4.0, max(0.0, env.bloom_intensity + Input.value("intensity") * dt))
70
71        # Lay the rows out across the window: over-bright stars above, dark bars below.
72        w, h = self.viewport_size
73        for i, s in enumerate(self.stars):
74            s.position = (w * (i + 0.5) / len(self.stars), h * 0.35)
75        for i, b in enumerate(self.bars):
76            b.position = (w * (i + 0.5) / len(self.bars), h * 0.75)
77
78        # Example help overlay: not part of the technique shown.
79        self.hud.text = (
80            f"Bloom: {'ON' if env.bloom_enabled else 'OFF'} (B or click)\n"
81            f"Threshold: {env.bloom_threshold:.2f} (Up / Down)\n"
82            f"Intensity: {env.bloom_intensity:.2f} (Left / Right)\n"
83            "Over-bright stars glow; lower the threshold and the dark bars glow too."
84        )
85
86
87if __name__ == "__main__":
88    App(title="Bloom 2D", width=1280, height=720).run(Bloom2D())