Bloom 2D

glow on a pure-2D scene via WorldEnvironment.

▶ Run in browser

Tags: 2d bloom glow post-processing

A 2D-only scene (no 3D) opts into bloom by adding a WorldEnvironment with bloom_enabled = True. Its 2D content is composited through the HDR lane + bloom with a linear tonemap, so bright shapes glow while flat art keeps its authored colour (no tonemap crush). With no WorldEnvironment a 2D scene renders flat as before, at zero cost.

What to look for:

  • The bright stars/bars BLOOM (soft halo) when bloom is on.

  • The dark background bars stay their authored colour (below the threshold).

  • Lowering the threshold makes more of the scene glow; raising it restricts the glow to the brightest pixels.

Controls: B - Toggle bloom Up / Down - Raise / lower bloom threshold Left / Right - Decrease / increase bloom intensity Escape - Quit

Run: uv run python examples/features/2d/bloom.py

Source

  1"""Bloom 2D: glow on a pure-2D scene via WorldEnvironment.
  2
  3# /// simvx
  4# tags = ["2d", "bloom", "glow", "post-processing"]
  5# web = { width = 1280, height = 720, reason = "Pure-2D bloom routes through the full HDR pipeline on web." }
  6# ///
  7
  8A 2D-only scene (no 3D) opts into bloom by adding a WorldEnvironment with
  9``bloom_enabled = True``. Its 2D content is composited through the HDR lane + bloom
 10with a linear tonemap, so bright shapes glow while flat art keeps its authored
 11colour (no tonemap crush). With no WorldEnvironment a 2D scene renders flat as
 12before, at zero cost.
 13
 14What to look for:
 15  - The bright stars/bars BLOOM (soft halo) when bloom is on.
 16  - The dark background bars stay their authored colour (below the threshold).
 17  - Lowering the threshold makes more of the scene glow; raising it restricts the
 18    glow to the brightest pixels.
 19
 20Controls:
 21    B            - Toggle bloom
 22    Up / Down    - Raise / lower bloom threshold
 23    Left / Right - Decrease / increase bloom intensity
 24    Escape       - Quit
 25
 26Run: uv run python examples/features/2d/bloom.py
 27"""
 28
 29import math
 30
 31from simvx.core import Input, InputMap, Key, Node2D, Polygon2D, Text2D, WorldEnvironment
 32from simvx.graphics import App
 33
 34W, H = 1280, 720
 35
 36
 37def _star(cx, cy, r, colour, points=5):
 38    """A bright concave star (now triangulated correctly via ear-clipping)."""
 39    q = Polygon2D()
 40    q.polygon = [
 41        (
 42            math.cos(-math.pi / 2 + i * math.pi / points) * (r if i % 2 == 0 else r * 0.45),
 43            math.sin(-math.pi / 2 + i * math.pi / points) * (r if i % 2 == 0 else r * 0.45),
 44        )
 45        for i in range(points * 2)
 46    ]
 47    q.colour = colour
 48    q.position = (cx, cy)
 49    return q
 50
 51
 52def _bar(x, y, w, h, colour):
 53    q = Polygon2D()
 54    q.polygon = [(0, 0), (w, 0), (w, h), (0, h)]
 55    q.colour = colour
 56    q.position = (x, y)
 57    return q
 58
 59
 60class Bloom2D(Node2D):
 61    def on_ready(self):
 62        InputMap.add_action("toggle_bloom", [Key.B])
 63        InputMap.add_action("thr_up", [Key.UP])
 64        InputMap.add_action("thr_down", [Key.DOWN])
 65        InputMap.add_action("int_up", [Key.RIGHT])
 66        InputMap.add_action("int_down", [Key.LEFT])
 67        InputMap.add_action("quit", [Key.ESCAPE])
 68
 69        self._env = self.add_child(WorldEnvironment())
 70        self._env.bloom_enabled = True
 71        self._env.bloom_threshold = 0.6
 72        self._env.bloom_intensity = 1.3
 73
 74        # Flat dark bars (below the bloom threshold -> keep their authored colour,
 75        # no glow). The label makes their role clear vs the bright shapes above.
 76        self.add_child(Text2D(text="Flat (below threshold) - no glow:", font_scale=0.9, position=(80, 440)))
 77        for i in range(6):
 78            self.add_child(_bar(80 + i * 200, 480, 150, 160, (0.18, 0.20, 0.28, 1.0)))
 79
 80        # Bright (>1) neon stars that glow when bloom is on (concave polygons, now
 81        # triangulated correctly via ear-clipping).
 82        self.add_child(Text2D(text="Bright (above threshold) - glows:", font_scale=0.9, position=(80, 165)))
 83        neon = [(2.6, 0.4, 0.4), (0.4, 2.6, 0.8), (0.5, 0.7, 2.8), (2.6, 2.4, 0.5)]
 84        for i, c in enumerate(neon):
 85            self.add_child(_star(220 + i * 280, 250, 82, (*c, 1.0), points=5 if i % 2 == 0 else 6))
 86
 87        self._hud = self.add_child(Text2D(text="", font_scale=1.2, position=(12, 12)))
 88        self._update_hud()
 89
 90    def on_update(self, dt):
 91        env = self._env
 92        if Input.is_action_just_pressed("quit"):
 93            self.app.quit()
 94            return
 95        if Input.is_action_just_pressed("toggle_bloom"):
 96            env.bloom_enabled = not env.bloom_enabled
 97        if Input.is_action_pressed("thr_up"):
 98            env.bloom_threshold = min(2.0, env.bloom_threshold + 0.5 * dt)
 99        if Input.is_action_pressed("thr_down"):
100            env.bloom_threshold = max(0.0, env.bloom_threshold - 0.5 * dt)
101        if Input.is_action_pressed("int_up"):
102            env.bloom_intensity = min(4.0, env.bloom_intensity + 1.0 * dt)
103        if Input.is_action_pressed("int_down"):
104            env.bloom_intensity = max(0.0, env.bloom_intensity - 1.0 * dt)
105        self._update_hud()
106
107    def _update_hud(self):
108        env = self._env
109        self._hud.text = "\n".join(
110            [
111                "Bloom 2D (pure-2D, no 3D)",
112                f"[B] Bloom: {'ON' if env.bloom_enabled else 'OFF'}",
113                f"Threshold: {env.bloom_threshold:.2f} (Up/Down)",
114                f"Intensity: {env.bloom_intensity:.2f} (Left/Right)",
115                "Bright stars glow; flat bars keep their colour. Esc quit.",
116            ]
117        )
118
119
120if __name__ == "__main__":
121    app = App(title="Bloom 2D", width=W, height=H)
122    app.run(Bloom2D())