97

Poseidon Ocean

A high-performance WebGPU ocean simulation using raymarched fractional brownian motion. Features deep-water scattering, specular sun glitter, and dynamic choppy waves.

60 FPS WebGPU

WGSL Shader Source

struct Uniforms { resolution: vec2f, time: f32, padding: f32, mouse: vec2f, waveHeight: f32, choppiness: f32, windSpeed: f32, sunElevation: f32, pad1: f32, pad2: f32, }
@group(0) @binding(0) var<uniform> u: Uniforms;

@vertex 
fn vs_main(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
  let pos = array(vec2f(-1.0,-1.0), vec2f(3.0,-1.0), vec2f(-1.0,3.0)); 
  return vec4f(pos[vi], 0.0, 1.0);
}

fn hash21(p: vec2f) -> f32 {
  var p3 = fract(vec3f(p.xyx) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}

fn noise(p: vec2f) -> f32 {
  let i = floor(p);
  let f = fract(p);
  let u_smooth = f * f * (3.0 - 2.0 * f);
  return mix(
    mix(hash21(i + vec2f(0.0, 0.0)), hash21(i + vec2f(1.0, 0.0)), u_smooth.x),
    mix(hash21(i + vec2f(0.0, 1.0)), hash21(i + vec2f(1.0, 1.0)), u_smooth.x),
    u_smooth.y
  );
}

fn sea_octave(uv_in: vec2f, choppy: f32) -> f32 {
  let n = vec2f(noise(uv_in), noise(uv_in + vec2f(1.2, 3.4)));
  var uv = uv_in + n;
  var wv = 1.0 - abs(sin(uv));
  var swv = abs(cos(uv));
  wv = mix(wv, swv, wv);
  return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
}

fn map(p: vec3f) -> f32 {
  var freq = 0.16;
  var amp = u.waveHeight * 0.8;
  var choppy = 4.0 * u.choppiness;
  var uv = p.xz;
  var h = 0.0;
  
  let t = u.time * u.windSpeed * 0.8;
  let octMat = mat2x2f(1.6, 1.2, -1.2, 1.6);
  
  for (var i = 0; i < 4; i++) {
    var d = sea_octave((uv + vec2f(t)) * freq, choppy);
    d += sea_octave((uv - vec2f(t)) * freq, choppy);
    h += d * amp;
    uv = octMat * uv;
    freq *= 1.9;
    amp *= 0.22;
    choppy = mix(choppy, 1.0, 0.2);
  }
  return p.y - h;
}

fn map_detailed(p: vec3f) -> f32 {
  var freq = 0.16;
  var amp = u.waveHeight * 0.8;
  var choppy = 4.0 * u.choppiness;
  var uv = p.xz;
  var h = 0.0;
  
  let t = u.time * u.windSpeed * 0.8;
  let octMat = mat2x2f(1.6, 1.2, -1.2, 1.6);
  
  for (var i = 0; i < 7; i++) {
    var d = sea_octave((uv + vec2f(t)) * freq, choppy);
    d += sea_octave((uv - vec2f(t)) * freq, choppy);
    h += d * amp;
    uv = octMat * uv;
    freq *= 1.9;
    amp *= 0.22;
    choppy = mix(choppy, 1.0, 0.2);
  }
  return p.y - h;
}

fn getNormal(p: vec3f, eps: f32) -> vec3f {
  let e = vec2f(eps, 0.0);
  return normalize(vec3f(
    map_detailed(p + e.xyy) - map_detailed(p - e.xyy),
    map_detailed(p + e.yxy) - map_detailed(p - e.yxy),
    map_detailed(p + e.yyx) - map_detailed(p - e.yyx)
  ));
}

fn getSkyColor(rd: vec3f, sunDir: vec3f) -> vec3f {
  let sunDot = max(dot(rd, sunDir), 0.0);
  var sky = vec3f(0.35, 0.6, 0.85) - rd.y * 0.3;
  let horizonHaze = pow(1.0 - max(rd.y, 0.0), 3.0);
  sky = mix(sky, vec3f(0.75, 0.82, 0.92), horizonHaze * 0.8);
  // Sun bloom
  sky += vec3f(1.0, 0.85, 0.6) * pow(sunDot, 500.0) * 3.0;
  sky += vec3f(0.8, 0.6, 0.4) * pow(sunDot, 20.0) * 0.5;
  return max(sky, vec3f(0.0));
}

@fragment 
fn fs_main(@builtin(position) fc: vec4f) -> @location(0) vec4f {
  var uv = (fc.xy - 0.5 * u.resolution) / u.resolution.y;
  uv.y = -uv.y;
  
  let mouseOffset = (u.mouse - 0.5) * 2.0;
  var ro = vec3f(0.0, 4.5, u.time * 2.0 * u.windSpeed);
  var rd = normalize(vec3f(uv.x, uv.y - 0.3 + mouseOffset.y * 0.4, 1.2));
  
  let mx = mouseOffset.x * 0.5;
  let cx = cos(mx); let sx = sin(mx);
  rd = vec3f(rd.x * cx - rd.z * sx, rd.y, rd.x * sx + rd.z * cx);
  
  let sunElevation = clamp(u.sunElevation, 0.05, 0.95);
  let sunDir = normalize(vec3f(0.0, sunElevation, 1.0));
  let skyCol = getSkyColor(rd, sunDir);
  var col = skyCol;
  
  // Raymarch
  var t = 0.0;
  var p = vec3f(0.0);
  for (var i = 0; i < 90; i++) {
    p = ro + rd * t;
    let d = map(p);
    if (d < 0.005 || t > 140.0) { break; }
    t += d * 0.4;
  }
  
  if (t < 140.0) {
    let n = getNormal(p, 0.003 + t * 0.0008);
    let viewDir = -rd;
    let refl = reflect(rd, n);
    
    // Fresnel
    let fresnel = 0.02 + 0.98 * pow(1.0 - max(dot(n, viewDir), 0.0), 5.0);
    
    // Water scattering
    let deepSea = vec3f(0.03, 0.09, 0.18);
    let waveCrestColor = vec3f(0.08, 0.45, 0.55);
    
    // SSS (Translucent wave ridges)
    let sss = pow(max(dot(viewDir, -sunDir + n * 0.5), 0.0), 2.5) * pow(clamp(1.0 - n.y, 0.0, 1.0), 2.0);
    let waterColor = mix(deepSea, waveCrestColor, sss * 0.9 + 0.15 * n.y);
    
    // Sky reflection
    let refSky = getSkyColor(refl, sunDir);
    
    // Sun specular glints
    let halfVec = normalize(viewDir + sunDir);
    let NdotH = max(dot(n, halfVec), 0.0);
    let specular = pow(NdotH, 400.0) * 3.5 + pow(NdotH, 40.0) * 0.4;
    let sunSpecular = vec3f(1.0, 0.95, 0.85) * specular;
    
    // White foam on sharp peaks
    let foam = smoothstep(0.6 * u.waveHeight, 1.1 * u.waveHeight, p.y) * smoothstep(0.55, 0.85, 1.0 - n.y);
    let foamColor = vec3f(0.9, 0.95, 1.0) * foam * 0.8;
    
    col = mix(waterColor, refSky, fresnel) + sunSpecular + foamColor;
    
    // Distance fog fade
    let fog = exp(-pow(t * 0.012, 1.4));
    col = mix(skyCol, col, fog);
  }
  
  // Filmic curve
  col = (col * (2.51 * col + vec3f(0.03))) / (col * (2.43 * col + vec3f(0.59)) + vec3f(0.14));
  col = clamp(col, vec3f(0.0), vec3f(1.0));
  
  let vignette = 1.0 - smoothstep(0.6, 1.5, length(uv));
  col = col * vignette;
  
  return vec4f(col, 1.0);
}

Properties

#webgpu#ocean#raymarching#fluid#poseidon
Author: Math Art Core Target: 60 FPS
ESC
↑↓ Navigate Select
101 Mathematical Artworks

Support the Project

Keep mathematical creative coding alive & open source

Or via Direct Payoneer ($0 Fee)
Payoneer Customer ID: $0 Fee Direct
24076084

💡 Payoneer app: Go to Pay → Pay to recipient → Enter ID 24076084.

Thank you for supporting generative mathematical animations! ✨