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