JSFiddle - React, Tailwind, and code Playground

HTML

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ocean Surface Simulation — Gerstner Waves 3D</title>
    <meta name="description"
        content="Real-time 3D ocean surface simulation using Gerstner waves, Fresnel reflection, subsurface scattering, and procedural foam rendering.">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            overflow: hidden;
            background: #000;
            font-family: 'Helvetica Neue', Arial, sans-serif;
        }

        canvas {
            display: block;
        }

        #overlay {
            position: absolute;
            bottom: 24px;
            left: 50%;
            transform: translateX(-50%);
            color: rgba(255, 255, 255, 0.35);
            font-size: 11px;
            letter-spacing: 3px;
            text-transform: uppercase;
            pointer-events: none;
            user-select: none;
        }

        #fps {
            position: absolute;
            top: 16px;
            right: 20px;
            color: rgba(255, 255, 255, 0.3);
            font-size: 11px;
            font-family: monospace;
            pointer-events: none;
        }
    </style>
</head>

<body>
    <div id="overlay">Ocean Surface Simulation</div>
    <div id="fps"></div>

    <script type="importmap">
    {
        "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
        }
    }
    </script>
</body>

</html>

JavaScript

// ═══════════════════════════════════════════════════════════════
// Ocean Surface Simulation — GLSL Shaders
// Gerstner Waves + PBR Ocean Rendering
// ═══════════════════════════════════════════════════════════════

const oceanVertexShader = /* glsl */ `
precision highp float;

uniform float uTime;
uniform vec4 uWaves[12];

varying vec3 vWorldPosition;
varying vec3 vWorldNormal;
varying float vHeight;
varying float vFoamFactor;
varying vec2 vUv;

#define PI 3.14159265359
#define GRAVITY 9.81

vec3 gerstnerWave(vec4 wave, vec3 p, inout vec3 tangent, inout vec3 binormal) {
    float steepness = wave.z;
    float wavelength = wave.w;
    float k = 2.0 * PI / wavelength;
    float c = sqrt(GRAVITY / k);
    vec2 d = normalize(wave.xy);
    float f = k * (dot(d, p.xz) - c * uTime);
    float a = steepness / k;

    tangent += vec3(
        -d.x * d.x * steepness * sin(f),
         d.x * steepness * cos(f),
        -d.x * d.y * steepness * sin(f)
    );
    binormal += vec3(
        -d.x * d.y * steepness * sin(f),
         d.y * steepness * cos(f),
        -d.y * d.y * steepness * sin(f)
    );

    return vec3(
        d.x * a * cos(f),
        a * sin(f),
        d.y * a * cos(f)
    );
}

// ── Hash-based value noise for vertex displacement chaos ──
float hash31(vec3 p) {
    p = fract(p * vec3(0.1031, 0.1030, 0.0973));
    p += dot(p, p.yxz + 33.33);
    return fract((p.x + p.y) * p.z);
}

float vnoise(vec3 p) {
    vec3 i = floor(p);
    vec3 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    return mix(
        mix(mix(hash31(i), hash31(i+vec3(1,0,0)), f.x),
            mix(hash31(i+vec3(0,1,0)), hash31(i+vec3(1,1,0)), f.x), f.y),
        mix(mix(hash31(i+vec3(0,0,1)), hash31(i+vec3(1,0,1)), f.x),
            mix(hash31(i+vec3(0,1,1)), hash31(i+vec3(1,1,1)), f.x), f.y), f.z);
}

float vfbm(vec3 p) {
    float f = 0.0;
    f += 0.500 * vnoise(p); p *= 2.03;
    f += 0.250 * vnoise(p); p *=...