Phaser AE

Underwater

Pro packv3.0.0-23DPost

Being under the water, rather than behind a blue filter. A full-screen pass that does nothing until the eye goes under, then shifts colour with distance channel by channel, draws a clean waterline, lays caustics on the seabed, hangs shafts of sunlight under the surface and hands the scene's fog to the water.

This is a Pro pack. Upgrade to Phaser Pro to use it in your games.See Phaser Pro →

Released October 14, 2026 · updated October 19, 2026

Underwater: a screenshot of its demo
A note about these graphics

Expansion packs don't come with graphics. This screenshot is one way of drawing what the pack does, made for its demo: none of the sprites, tiles or backgrounds in it are in the pack.

That's by design. A pack is a game's logic, mechanics and effects, and it works with whatever art you give it. Some packs draw effects, backdrops or 3D scenes of their own, but none carries a game's sprites.

So the look is yours. Make the art on phaser.io, bring your own, or let your agent draw it in code: the same pack can look like anything.

The pack

underwater3d

function underwater3d(opts: Underwater3dOptions = {}): Pack<{ underwater: Underwater3dApi }>

The underwater-3d pack: a full-screen pass that runs only while the eye is below the surface. Requires a 3D world, render-3d (its scene depth and lighting record) and a WebGPU or WebGL2 backend; pair it with water-3d so waves overhead decide where the waterline actually is. Throws on an unknown preset or a waterColor that is not a css colour.

Helpers

absorb

function absorb(sigma: readonly [number, number, number], dist: number): Sigma

Per-channel Beer–Lambert transmittance, exp(−σ · distance) — the CPU twin of the shader's law.

bakeCausticField

function bakeCausticField(size = 256, t = 1.0): Float32Array

One Hoskins caustic field, size×size texels over one tile, row-major. Values are the raw pattern — mostly 0..1 with sparkle peaks above 1. Pure and deterministic; t picks the phase slice to bake.

submersionState

function submersionState(prev: boolean, camY: number, surfY: number, hysteresis: number): { submerged: boolean; depth: number }

The submersion decision, with HYSTERESIS: the camera has to cross hysteresis BEYOND the surface line to flip state, so a hull bobbing exactly at the waterline never strobes the whole effect on and off. depth is how far the camera sits below the surface, and 0 above it.

UNDERWATER_PRESETS

const UNDERWATER_PRESETS: Record<string, UnderwaterPreset>

The named waters. These are BRIGHT BY DEFAULT on purpose: triplets taken from physical tables kill red in about five metres, which is correct and reads as instant murk in a game — the stylised reef shots everyone pictures have almost no view extinction at all. murk is the one that keeps the heavy physical feel.

underwaterFog

function underwaterFog( opts: { waterColor: string; sunSigma: Sigma; sigma: Sigma; maxView: number }, depth: number, storm = 0, ): { color: string; near: number; far: number }

The gentle base tint every OTHER 3D shader gets for free: the water colour, darkened with depth by the downwelling law and desaturated toward grey-green by storm, over a fog band that closes in with σ.

The composite owns the per-channel extinction; this only has to make the murk, the translucents, the beams and the particles agree with the underwater look — otherwise they float in front of it as if dry.

Functions

createCausticTexture

function createCausticTexture(device: GPUDevice, size = 256): GPUTexture

Bake the field into an rg8unorm texture with a full 2×2-box mip chain. TWO INDEPENDENT PHASES, one per channel: the surface shader min-blends a tap of R with a tap of G, so no repeating motif can line up with itself — two taps of ONE field at related scales tile visibly. The texel stores value/2 so the above-1 sparkle peaks survive quantisation, and the shader multiplies the tap by 2 to undo it.

Types

Sigma

type Sigma = [number, number, number]

A per-channel extinction triplet, in inverse world units.

Underwater3dApi

interface Underwater3dApi

game.underwater.

  • diagnostics(): { active: boolean; submerged: boolean; depth: number; fullUnder: number; shallowK: number; surfaceY: number; sigma: Sigma; viewSigma: Sigma; sunSigma: Sigma; waterColor: string; maxView: number; fog: { color: string; near: number; far: number }; rayStrength: number } — Optical values used by the composite at the current camera position, suitable for CPU ray diagnostics.
  • rayStrength: number — Additional shaft brightness multiplier, independent of the on/off ray control. Default 1.
  • sigma: Sigma — Per-channel view extinction. Replace the tuple to retune absorption live.
  • grade: number — Underwater colour grading strength, 0..1.
  • causticTile: number — World units per caustic tile.
  • causticSpeed: number — Caustic animation speed.
  • rayBand: number — Clearance below the surface at which light shafts begin.
  • rayGap: number — Arc spacing between light shafts, world units.
  • level: number — Sea level.
  • clarity: number — Master clarity, 0..1.
  • caustics: number — Caustic brightness.
  • wobble: number — Screen wobble.
  • vignette: number — Soft screen vignette.
  • storm: number — Storm dial, 0..1.
  • enabled: boolean — Turn it off entirely.
  • rays: number — The bars of sunlight hanging under the surface, 0 for none. Default 1.
  • rayFlicker: number — How deeply each beam dips as it flickers, 0 for steady. Default 1.
  • rayNear: number — How far the wall of beams stands from the camera, world units. Default 14.
  • groundAt(fn: ((x: number, z: number) => number) | null): void — Give the beams a seabed so each is capped above it and shortens smoothly in the shallows.
  • setPreset(name: string): void — Swap the water live: 'reef', 'ocean', 'pool', 'murk'. Throws on any other name.
  • shaftMod(): { atten: number; tint: [number, number, number] } | null — How the SURFACE god rays should be modulated while the eye is under: the downwelling transmittance to this depth, and the water's own hue. Null when dry, or when rays are off. god-rays-3d reads it, so shafts from the sun die with depth and go green rather than hanging in the water at full brightness.
  • readonly submerged: boolean — Is the camera under the surface right now? Hysteresis keeps this from flickering at the line.
  • readonly depth: number — How deep the camera is, or 0 above.
  • surfaceAt(fn: ((x: number, z: number) => number) | null): void — Give it a moving surface — water-3d's heightAt — so waves overhead decide where the line is.

Underwater3dOptions

interface Underwater3dOptions

How the water looks from inside it. Anything left unset comes from the preset.

  • preset?: string — 'reef' (the default), 'ocean', 'pool' or 'murk'; any other name throws.
  • level?: number — Sea level; the effect is on below it. Default 0.
  • sigma?: Sigma — Per-channel view extinction — how fast the murk closes in.
  • sunSigma?: Sigma — Per-channel downwelling extinction — how fast light from above dies with depth.
  • waterColor?: string — The in-scatter murk colour: any css colour the kernel reads (#2f9fc4, rgb(47, 159, 196), cyan); anything else throws.
  • maxView?: number — Roughly how far you can see, world units.
  • clarity?: number — Master CLARITY, 0..1: 1 is the water as authored, 0 is crystal clear. Default 1.
  • caustics?: number — Caustic brightness, 0..1.
  • causticTile?: number — World units per caustic tile. Default 6.
  • causticFade?: number — Receiver depth at which caustics fade to nothing. Default 20.
  • causticSpeed?: number — How fast caustics crawl. Default 1.
  • wobble?: number — Screen wobble in uv; kept small on purpose. Default 0.002.
  • vignette?: number — Soft screen vignette, 0..1.
  • meniscus?: number — How bright the meniscus band at the crossing is. Default 0.35.
  • hysteresis?: number — How far the camera must cross the line to flip state — this is what stops it strobing. Default 0.06.
  • storm?: number — Storm dial 0..1: desaturates the murk toward grey-green and closes it in. Default 0.
  • rays?: number — The bars of sunlight hanging under the surface, 0 for none. Default 1.
  • rayFlicker?: number — How deeply each beam dips as it flickers, 0 for steady. Default 1.
  • rayNear?: number — How far the wall of beams stands from the camera. Default 14.
  • rayBand?: number — How far below the flat surface a beam starts, clearing the ripples. Default 1.2.
  • rayGap?: number — Arc gap between beams; smaller is a denser wall. Default 12.
  • driveFog?: boolean — Drive the scene's fog from the water while submerged. Default true.
  • enabled?: boolean — Whether the pass runs at all. Default true.

UnderwaterPreset

interface UnderwaterPreset

One named water: how fast each channel dies, what colour the murk is, and how the screen is graded.

  • sigma: Sigma — View extinction — how fast the murk closes in on what you are looking at.
  • sunSigma: Sigma — Downwelling extinction — how fast the light from above dies with depth.
  • waterColor: string — The in-scatter murk colour, css.
  • maxView: number — Roughly how far you can see, world units.
  • caustics: number — Caustic brightness for this water.
  • vignette: number — The soft screen vignette, 0..1.
  • grade: number — The depth grade, 0..1.