Post-Processing FX Stack
Screen-space post-processing applied to the full gameplay layer AFTER world/particle/effect drawing but BEFORE HUD. Gives the game a unified cinematic look while keeping UI crisp and readable.
Two cooperating modules
The post-FX stack is split across two files at engine/rendering/:
post-processing.ts— Always-on cinematic pipeline (bloom, dust, fog, vignette, scanlines, god rays, contrast/saturation mastering). Hard-coded effects driven by per-planet presets.post-fx-store.ts— Tweakable art-style and layer-FX subscription store driven by sliders (Nebula viewer / FxTab dev surfaces). Three-layer pipeline: Art Styles → Layer FX → Polish.
Always-on pipeline (post-processing.ts)
Order of operations inside drawScreenEffects(ctx, quality):
- Bloom — full-scene additive glow via downsampled blur. Desktop: 1/4 resolution, 24px blur, alpha 0.55. Mobile: 1/8 resolution, 10px blur, alpha 0.30 (under 2ms on phone). At strong bloom multipliers (≥3.0) multiple passes accumulate.
- Dust above — screen-space soft blobs, atmospheric depth layer. HIGH quality only.
- Contrast / Saturation — CSS filter composite, preset-driven. Only fires when preset has non-zero
contrastBoostorsaturationShift. - Vignette — pre-baked elliptical radial gradient with portrait-mode horizontal squeeze (0.85). Oversized canvas with 200px margin so edges remain pure black past screen bounds. Gated by preset (
vignetteEnabled). - God rays — 4 diagonal volumetric beams from top-right, slowly drifting (~0.002 rad/s). Screen-composite at very low alpha (0.04 base, fading on outer beams). Preset-gated; skipped in
lowquality. - CRT scanlines — pre-baked 1px horizontal lines every 3px at 2.5% alpha. HIGH quality only.
Independent draw calls (called outside drawScreenEffects):
drawFog— multi-texture fog system, 5 texture types (noise,wispy,clouds,dense,patchy) at tileable 512px tiles. Screen-blend composite, parallax 0.15, drift 8 px/s. Called after shadows, before sticker blit.drawDustBehind— world-space soft blobs at parallax 0.3, drawn behind ships insidedrawWorld.drawBokeh— screen-space soft hexagonal lens-bloom particles with chromatic edge ring, screen-blend.
Per-planet mastering presets
Active preset is switched at run start via PostProcessing.setMode(mode) based on PlanetDef.postProcessing. Two presets defined:
| Preset | bloomAlphaMult | vignetteEnabled | contrastBoost | saturationShift | godRaysEnabled |
|---|---|---|---|---|---|
dark (default) | 1.0 | true | 0.0 | 0.0 | false |
sunlit | 1.0 | false | 0.0 | 0.0 | true (gated by preset) |
patchPreset(partial) allows runtime overrides from the VFX dashboard.
Quality tiers
drawScreenEffects accepts a quality parameter:
high— full pipeline (bloom + dust + vignette + scanlines + god rays).mobile— lite bloom (1/8 res) + vignette only. No dust, no scanlines.low— vignette only; thermal-emergency fallback that drops everything non-essential.
Tweakable art-style stack (post-fx-store.ts)
A tiny bespoke subscription store (no Zustand) with three layers, all 0..1 sliders:
Layer 1 — Art Styles (11 wetness sliders, default 0)
Each slider fully transforms the screen to that style at wetness 1:
borderlands— bold ink outlines + 3-tier posterize + halftone shadowssynthwave— purple→magenta→cyan duotone + neon highlight bloom + scanlinesvhs— RGB tape splitting + scrolling tracking band + tape grain + desatpixelArt— uv quantize + 5-step palette + 4×4 Bayer ditheroilPaint— warped color smoothing + canvas grain + warm cast + sat boostwatercolor— paper bleed + lifted midtones + soft edges + desaturatecomic— thick ink lines + 2-step flat color + dense halftoneblueprint— invert luma + cyan tint + grid overlay + edge-only detailthermal— luma→heat LUT (blue→cyan→yellow→red→white) + edge glowstainedGlass— voronoi cells + jewel-tone sat + thick black borderscyberglitch— heavy chromatic aberration + slice RGB shifts + neon bloom + invert pulses
Layer 2 — Layer FX (3 toggleable extras)
Each effect has an On 0/1 flag (treated as boolean by shader: ≥0.5 = on) plus per-effect intensity:
layerBgStars{On,Intensity}— background star dotslayerColorSparkles{On,Intensity}— faint colored sparkleslayerShootingStars{On,Intensity,Angle,Variance}— deterministic streaks. Angle 0..1 maps to 0..2π radians; variance 0 = parallel rain, 1 = chaotic spray.
Layer 3 — Polish (5 always-applied final tweaks)
polishBlur— soft fake blur via gradient damping. 0 = none, 1 = full.polishHue— global hue shift. 0.5 = identity, 0 = -180°, 1 = +180°.polishSat— global saturation. 0.5 = identity, 0 = grayscale, 1 = 2×.polishLight— global lightness. 0.5 = identity, 0 = black, 1 = 2× lift.polishAnimSpeed— animation speed scale. 0.5 = 1× identity, 0 = paused, 1 = 2×.
For Hue/Sat/Light/AnimSpeed, identity is 0.5; sliders map 0..1 to a -shift..+shift range. Blur and intensity defaults are 0 and 0.5 respectively.
Store API
getPostFxState()— full state object (reference replaced on every set for React identity comparisons).getPostFxValue(key)— single knob read.setPostFxValue(key, value)— set single knob; clamps numbers to [0, 1], emits to subscribers.getPostFxIdentity(key)— returns the “does nothing” slider position. Used by reset buttons.subscribePostFx(fn)— returns unsubscribe function.usePostFxState()/usePostFxValue(key)— React hooks viauseSyncExternalStore. Non-React consumers (e.g.NebulaBackgroundRAF) read directly viasubscribePostFx+getPostFxState.
Performance gating
- Bloom mode selected per-frame via
qualityarg (‘high’ / ‘mobile’ / ‘low’). - Static overlays (vignette, scanlines) pre-baked to offscreen canvases and rebuilt only on viewport resize.
- Dust uses a fixed pool (40 behind / 20 above motes) with no per-frame allocation; pre-baked unit gradient stamps rather than
createRadialGradientper mote per frame. - Fog tiles cached in
_fogTilesrecord keyed by texture-type + tint. - Mobile bloom lite mode (
_drawBloomLite) capped at ~47×100px scratch canvas with CSS-filter blur under 2ms total per frame.
Source files
starship-survivors/src/starship-survivors/engine/rendering/post-processing.ts— 871 lines, always-on pipeline.starship-survivors/src/starship-survivors/engine/rendering/post-fx-store.ts— 172 lines, slider state store.