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