World 2D in HDR¶
2D that tonemaps and blooms with the 3D scene.
▶ Run in browserTags: 3d 2d hdr post-processing bloom
Demonstrates the N1 “2D in HDR” model: when post-processing is on, world-space 2D
(sprites / shapes / particles) renders INTO the HDR buffer before tonemap, so it
gets exposure, tonemap and bloom consistently with the 3D scene. Screen-space 2D
(HUD/UI) stays crisp post-tonemap at authored LDR. A per-node hdr override
forces a node either way.
What to look for:
The bright 2D star (a world-space Polygon2D with colour > 1) BLOOMS, just like an emissive 3D surface – impossible before N1 (2D used to paste on flat).
The grey “auto” quad and the 3D cube darken/brighten TOGETHER as you change exposure (both go through tonemap).
The “hdr=False” quad keeps its exact authored colour at any exposure (the escape hatch for stylised flat 2D).
The HUD text stays crisp and fixed regardless of exposure (screen-space LDR).
Controls: Left / Right - Adjust tonemap exposure Space - Cycle the demo quad’s hdr override (auto / on / off) [desktop only] B - Toggle bloom A / D - Orbit camera Escape - Quit
Run: uv run python examples/features/3d/world_2d_in_hdr.py
Source¶
1"""World 2D in HDR: 2D that tonemaps and blooms with the 3D scene.
2
3# /// simvx
4# tags = ["2d", "hdr", "post-processing", "bloom"]
5# web = { width = 1280, height = 720, reason = "Works on both backends; the hdr override (Space) is desktop-only." }
6# ///
7
8Demonstrates the N1 "2D in HDR" model: when post-processing is on, world-space 2D
9(sprites / shapes / particles) renders INTO the HDR buffer before tonemap, so it
10gets exposure, tonemap and bloom consistently with the 3D scene. Screen-space 2D
11(HUD/UI) stays crisp post-tonemap at authored LDR. A per-node ``hdr`` override
12forces a node either way.
13
14What to look for:
15 - The bright 2D star (a world-space Polygon2D with colour > 1) BLOOMS, just like
16 an emissive 3D surface -- impossible before N1 (2D used to paste on flat).
17 - The grey "auto" quad and the 3D cube darken/brighten TOGETHER as you change
18 exposure (both go through tonemap).
19 - The "hdr=False" quad keeps its exact authored colour at any exposure (the
20 escape hatch for stylised flat 2D).
21 - The HUD text stays crisp and fixed regardless of exposure (screen-space LDR).
22
23Controls:
24 Left / Right - Adjust tonemap exposure
25 Space - Cycle the demo quad's hdr override (auto / on / off) [desktop only]
26 B - Toggle bloom
27 A / D - Orbit camera
28 Escape - Quit
29
30Run: uv run python examples/features/3d/world_2d_in_hdr.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 Node,
45 Polygon2D,
46 Text2D,
47 WorldEnvironment,
48)
49from simvx.graphics import App
50
51HDR_CYCLE = [None, True, False]
52HDR_LABEL = {None: "auto (by role)", True: "on (force HDR)", False: "off (flat LDR)"}
53
54
55def _quad(x, y, w, h, colour):
56 q = Polygon2D()
57 q.polygon = [(0, 0), (w, 0), (w, h), (0, h)]
58 q.colour = colour
59 q.position = (x, y)
60 return q
61
62
63class World2DInHDR(Node):
64 def on_ready(self):
65 InputMap.add_action("exposure_up", [Key.RIGHT])
66 InputMap.add_action("exposure_down", [Key.LEFT])
67 InputMap.add_action("cycle_hdr", [Key.SPACE])
68 InputMap.add_action("toggle_bloom", [Key.B])
69 InputMap.add_action("orbit_left", [Key.A])
70 InputMap.add_action("orbit_right", [Key.D])
71 InputMap.add_action("quit", [Key.ESCAPE])
72
73 self._yaw = 25.0
74 self._hdr_idx = 0
75
76 self._env = self.add_child(WorldEnvironment())
77 self._env.bloom_enabled = True
78 self._env.bloom_threshold = 0.9
79 self._env.bloom_intensity = 1.1
80 self._env.tonemap_mode = "aces"
81 self._env.tonemap_exposure = 1.0
82
83 self._cam = self.add_child(Camera3D(fov=60, near=0.1, far=100.0))
84
85 key = DirectionalLight3D(intensity=2.0)
86 key.look_at((-1.0, -1.5, -1.0))
87 self.add_child(key)
88
89 # 3D content: a lit cube the world 2D is composited over.
90 self._cube = self.add_child(
91 MeshInstance3D(mesh=Mesh.cube(2.0), material=Material(colour=(0.75, 0.3, 0.3), roughness=0.5))
92 )
93 self._cube.position = (2.2, 0.0, 0.0)
94
95 # World-space 2D, by role -> HDR lane. A bright (>1) star that BLOOMS.
96 star = _quad(80, 120, 0, 0, (2.4, 2.1, 0.6, 1.0))
97 star.polygon = self._star_points(70)
98 self.add_child(star)
99
100 # The "auto" grey quad: tonemaps with the scene (compare to the cube).
101 self._auto_quad = self.add_child(_quad(60, 300, 200, 180, (0.6, 0.6, 0.6, 1.0)))
102
103 # The toggleable demo quad: Space cycles its hdr override.
104 self._demo_quad = self.add_child(_quad(300, 300, 200, 180, (0.6, 0.6, 0.6, 1.0)))
105
106 # Screen-space HUD (Text2D is screen_space by default): stays crisp LDR.
107 self._hud = self.add_child(Text2D(text="", font_scale=1.2, position=(10.0, 10.0)))
108 self._update_camera()
109 self._update_hud()
110
111 @staticmethod
112 def _star_points(r):
113 pts = []
114 for i in range(10):
115 ang = -math.pi / 2 + i * math.pi / 5
116 rad = r if i % 2 == 0 else r * 0.45
117 pts.append((math.cos(ang) * rad + r, math.sin(ang) * rad + r))
118 return pts
119
120 def on_update(self, dt):
121 if Input.is_action_just_pressed("quit"):
122 self.app.quit()
123 return
124 env = self._env
125 if Input.is_action_pressed("exposure_up"):
126 env.tonemap_exposure = min(5.0, env.tonemap_exposure + 1.0 * dt)
127 if Input.is_action_pressed("exposure_down"):
128 env.tonemap_exposure = max(0.1, env.tonemap_exposure - 1.0 * dt)
129 if Input.is_action_pressed("orbit_left"):
130 self._yaw += 60.0 * dt
131 if Input.is_action_pressed("orbit_right"):
132 self._yaw -= 60.0 * dt
133 if Input.is_action_just_pressed("toggle_bloom"):
134 env.bloom_enabled = not env.bloom_enabled
135 if Input.is_action_just_pressed("cycle_hdr"):
136 self._hdr_idx = (self._hdr_idx + 1) % len(HDR_CYCLE)
137 self._demo_quad.hdr = HDR_CYCLE[self._hdr_idx]
138 self._update_camera()
139 self._update_hud()
140
141 def _update_camera(self):
142 yaw = math.radians(self._yaw)
143 self._cam.position = (math.sin(yaw) * 8.0, 1.5, math.cos(yaw) * 8.0)
144 self._cam.look_at((0.0, 0.0, 0.0))
145
146 def _update_hud(self):
147 env = self._env
148 self._hud.text = "\n".join(
149 [
150 "World 2D in HDR (N1)",
151 f"Exposure: {env.tonemap_exposure:.2f} (Left/Right)",
152 f"Bloom: {'ON' if env.bloom_enabled else 'OFF'} (B)",
153 f"Demo quad hdr: {HDR_LABEL[HDR_CYCLE[self._hdr_idx]]} (Space)",
154 "Star blooms; auto quad tracks exposure; off quad stays flat; HUD fixed.",
155 "A/D orbit Esc quit",
156 ]
157 )
158
159
160if __name__ == "__main__":
161 app = App(title="World 2D in HDR", width=1280, height=720)
162 app.run(World2DInHDR())