afterglow/view/environment.pyΒΆ
Part of Afterglow.
1"""Per-world WorldEnvironment presets for the Afterglow diorama.
2
3One tuned post-processing/environment preset per world id. Art direction is
4VIBRANT & PUNCHY: high exposure, high contrast, saturated, with the Wisp always
5popping. All three share a base mood (ACES tonemap, threshold-gated bloom that
6hits only the bright emissive gameplay elements, light SSAO contact shadows, a
7*touch* of depth haze, a tasteful vignette + DoF, FXAA) and differ in their
8colour grade, expressed through ambient/sky/fog tints since the LUT path needs a
9baked LUT texture we don't ship:
10
11 glade : bright warm greens + golden rim light, clear shadows.
12 caverns : deep blue/violet dark with intense glowing cyan crystals.
13 spire : near-black + hot gold, dramatic bloom (god-ray feel).
14
15Exposure runs high and fog stays a thin depth haze so rooms read bright rather
16than murky, the grade sits near neutral instead of green-washed, and the bloom
17threshold is tight enough that platforms stay crisp while only crystals, orbs,
18gates and the Wisp bloom.
19
20``per_world_environment(world_id)`` returns a configured ``WorldEnvironment``
21ready to add as a child of the scene root; the renderer syncs it each frame.
22GPU-free to construct (it is plain Property data).
23"""
24
25from __future__ import annotations
26
27from dataclasses import dataclass
28
29from simvx.core import WorldEnvironment
30
31
32@dataclass(frozen=True)
33class _Grade:
34 """Per-world look: exposure, ambient/sky/fog tints + energies, bloom + fog."""
35
36 exposure: float
37 ambient: tuple[float, float, float]
38 ambient_energy: float
39 sky_top: tuple[float, float, float]
40 sky_bottom: tuple[float, float, float]
41 fog: tuple[float, float, float]
42 fog_density: float
43 bloom_threshold: float
44 bloom_intensity: float
45 vignette: float
46
47
48# Tuned per-world grades. Ambient stays low-ish and *neutral-cool* (not green) so
49# the warm key/rim lights from lighting.py do the colouring and the grade does
50# not wash everything one hue. Exposure is pushed up for brightness/contrast; fog
51# is a thin haze only. Bloom thresholds sit around lit-stone luminance (~0.9-1.0
52# HDR here) so platforms stay crisp while the emissive gameplay elements and the
53# bright Wisp light-pool bloom; caverns/spire run a touch higher intensity so
54# their accents punch harder out of the dark.
55_GRADES: dict[str, _Grade] = {
56 # GLADE: bright + sunny. Strong warm-green ambient is the readability floor so
57 # the WHOLE room reads, not just the lit faces; a clean mid forest-green sky
58 # frames it as an intentional backdrop. Exposure dropped to match the brighter
59 # scene so the lit greens stay saturated without clipping white.
60 "glade": _Grade(
61 exposure=1.25,
62 ambient=(0.40, 0.54, 0.36),
63 ambient_energy=1.25,
64 sky_top=(0.18, 0.34, 0.22),
65 sky_bottom=(0.30, 0.50, 0.34),
66 fog=(0.22, 0.38, 0.27),
67 fog_density=0.004,
68 bloom_threshold=1.15,
69 bloom_intensity=0.9,
70 vignette=0.18,
71 ),
72 # CAVERNS: deep blue/violet but the stone plainly READS now. Bright cool-blue
73 # ambient lifts the whole room out of the black while keeping the moody hue; a
74 # clean indigo sky frames it. Bloom stays high so the cyan crystals + Wisp
75 # blaze, but thresholded above the lit stone so platforms do not bloom.
76 "caverns": _Grade(
77 exposure=1.40,
78 ambient=(0.34, 0.44, 0.66),
79 ambient_energy=1.15,
80 sky_top=(0.10, 0.12, 0.30),
81 sky_bottom=(0.16, 0.22, 0.46),
82 fog=(0.16, 0.22, 0.44),
83 fog_density=0.004,
84 bloom_threshold=1.20,
85 bloom_intensity=1.0,
86 vignette=0.20,
87 ),
88 # SPIRE: warm gold + bronze, dramatic but READABLE. Warm ambient lifts the
89 # bronze across the whole room against a deep-amber sky; minimal fog. Bloom
90 # gives the gold elements their god-ray pop without blooming lit stone.
91 "spire": _Grade(
92 exposure=1.45,
93 ambient=(0.56, 0.42, 0.24),
94 ambient_energy=1.10,
95 sky_top=(0.16, 0.11, 0.05),
96 sky_bottom=(0.30, 0.21, 0.10),
97 fog=(0.26, 0.19, 0.10),
98 fog_density=0.003,
99 bloom_threshold=1.20,
100 bloom_intensity=1.1,
101 vignette=0.22,
102 ),
103}
104
105
106def per_world_environment(world_id: str) -> WorldEnvironment:
107 """Return a configured ``WorldEnvironment`` for ``world_id``.
108
109 Unknown ids fall back to the glade grade so the view never crashes on a
110 not-yet-authored world.
111 """
112 g = _GRADES.get(world_id, _GRADES["glade"])
113 env = WorldEnvironment(name=f"env_{world_id}")
114
115 # -- shared mood -------------------------------------------------------
116 env.tonemap_mode = "aces"
117 env.tonemap_exposure = g.exposure
118 env.tonemap_white = 6.0 # let bright emissive elements run hot before clipping
119 env.fxaa_enabled = True
120
121 # Bloom: threshold-gated so it hits only the bright emissive gameplay
122 # elements (crystals / orbs / gates / glowing Wisp), keeping platforms crisp.
123 env.bloom_enabled = True
124 env.bloom_threshold = g.bloom_threshold
125 env.bloom_intensity = g.bloom_intensity
126 env.bloom_soft_knee = 0.5
127
128 # SSAO is the diorama's CONTACT-SHADOW mechanism. The relief is a wall of
129 # coplanar-fronted cubes, so directional/point cast shadows have no receiving
130 # surface in view (see lighting.py); SSAO darkens the crevices and silhouette
131 # contacts the camera DOES see, which is the right tool for this shallow look.
132 # A wider radius + stronger intensity (was 0.45 / 0.7, nearly invisible) makes
133 # the relief read as carved depth rather than a flat tile sheet, while a small
134 # bias keeps the lit faces clean (no muddy self-occlusion patches).
135 env.ssao_enabled = True
136 env.ssao_radius = 1.0
137 env.ssao_intensity = 1.6
138 env.ssao_bias = 0.02
139
140 # Just a touch of depth haze toward the back wall; never a fog soup.
141 env.fog_enabled = True
142 env.fog_mode = "exponential"
143 env.fog_density = g.fog_density
144 env.fog_start = 6.0
145 env.fog_end = 24.0
146
147 # DoF is OFF: the camera tilt put the near (bottom) of the framed room
148 # outside the focus band, blurring the lower screen on menus and gameplay
149 # alike. A fast precision platformer needs crisp tiles everywhere, so we keep
150 # the play space fully sharp and rely on fog + vignette for depth instead.
151 env.dof_enabled = False
152
153 env.vignette_enabled = True
154 env.vignette_intensity = g.vignette
155 env.vignette_smoothness = 0.5
156
157 # -- per-world colour grade -------------------------------------------
158 env.ambient_light_colour = (*g.ambient, 1.0)
159 env.ambient_light_energy = g.ambient_energy
160 env.sky_mode = "colour"
161 env.sky_colour_top = (*g.sky_top, 1.0)
162 env.sky_colour_bottom = (*g.sky_bottom, 1.0)
163 env.fog_colour = (*g.fog, 1.0)
164
165 return env