Gallery

Procedural Water with Caustics & Sky

Live parameters

Water
0.50
12.0
Quality
256
50.00
Sun
Sky
GLSL source
const float EPSILON = 0.001;
const float WATER_HEIGHT = 0.5;
const int MAX_STEPS = 256;
const float MAX_DIST = 50.0;
const float MIN_DIST = 0.0001;
const vec3 LIGHT_POS = vec3(0.0, 0.15, 20.0);
const vec3 SUN_COLOR = vec3(1.0, 0.6, 0.3);
const vec3 SKY_COLOR = vec3(0.2, 0.4, 0.8);
const vec3 HORIZON_COLOR = vec3(0.9, 0.5, 0.3);
const float WATER_REFLECTION_DISTANCE = 12.0;

float hash21(vec2 p) {
    p = fract(p * vec2(233.34, 851.73));
    p += dot(p, p + 23.45);
    return fract(p.x * p.y);
}

float noise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    float a = hash21(i);
    float b = hash21(i + vec2(1.0, 0.0));
    float c = hash21(i + vec2(0.0, 1.0));
    float d = hash21(i + vec2(1.0, 1.0));
    vec2 u = f * f * (3.0 - 2.0 * f);
    return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

float fbm(vec2 p) {
    float value = 0.0;
    float amplitude = 0.5;
    float frequency = 1.0;
    float sum = 0.0;
    mat2 rotation = mat2(cos(0.5), sin(0.5), -sin(0.5), cos(0.5));
    for(int i = 0; i < 8; i++) {
        value += amplitude * noise(p * frequency);
        p = rotation * p * 1.1;
        sum += amplitude;
        amplitude *= 0.5;
        frequency *= 2.1;
    }
    return value / sum;
}

float waterHeight(vec2 pos, float time) {
    vec2 movement = vec2(time * 0.2, time * 0.1);
    float dist = length(pos);
    float largeWaveAtten = 1.0 / (1.0 + dist * 0.1);
    float smallWaveAtten = 1.0 / (1.0 + dist * 0.3);
    float largeWaves = fbm(pos * 1.0 + movement) * largeWaveAtten;
    float smallWaves = fbm(pos * 3.0 - movement * 1.3) * smallWaveAtten;
    float waves = largeWaves * 0.15 + smallWaves * 0.05;
    waves += fbm(pos * 8.0 - movement * 0.7) * smallWaveAtten * 0.02;
    float minDist = WATER_REFLECTION_DISTANCE * 0.8;
    float maxDist = WATER_REFLECTION_DISTANCE * 1.2;
    float distFade = 1.0 - smoothstep(minDist, maxDist, dist);
    waves *= 0.8 + 0.2 * smoothstep(-1.0, 1.0, waves);
    return waves * distFade;
}

vec3 waterNormal(vec2 pos, float time) {
    float dist = length(pos);
    float baseEps = 0.05;
    float distFactor = 0.01;
    float adaptiveEps = baseEps + dist * distFactor;
    vec2 epsX = vec2(adaptiveEps, 0.0);
    vec2 epsY = vec2(0.0, adaptiveEps);
    float r1 = waterHeight(pos + epsX, time);
    float r2 = waterHeight(pos + epsX * 0.5, time);
    float l1 = waterHeight(pos - epsX, time);
    float l2 = waterHeight(pos - epsX * 0.5, time);
    float t1 = waterHeight(pos + epsY, time);
    float t2 = waterHeight(pos + epsY * 0.5, time);
    float b1 = waterHeight(pos - epsY, time);
    float b2 = waterHeight(pos - epsY * 0.5, time);
    float dX = ((r1 - l1) * 0.3 + (r2 - l2) * 0.7) / (2.0 * adaptiveEps);
    float dY = ((t1 - b1) * 0.3 + (t2 - b2) * 0.7) / (2.0 * adaptiveEps);
    float normalStrength = 1.0 / (1.0 + dist * 0.1);
    return normalize(vec3(-dX * normalStrength, 1.0, -dY * normalStrength));
}

float calculateCaustics(vec3 pos, float time) {
    vec2 waterPos = pos.xz;
    vec3 normal = waterNormal(waterPos, time);
    float ior = 1.33;
    vec3 lightDir = normalize(LIGHT_POS - pos);
    vec3 refracted = refract(lightDir, normal, 1.0 / ior);
    float depth = (pos.y - (-1.0)) / refracted.y;
    vec2 causticPos = pos.xz + refracted.xz * depth;
    float causticIntensity = fbm(causticPos * 5.0 + time * 0.1);
    return pow(max(causticIntensity, 0.0), 2.0);
}

float sceneSDF(vec3 p) {
    float ground = p.y + 1.0;
    float water = p.y - WATER_HEIGHT - waterHeight(p.xz, iTime);
    return min(ground, water);
}

vec2 rayMarch(vec3 ro, vec3 rd) {
    float depth = 0.0;
    float id = 0.0;
    for(int i = 0; i < MAX_STEPS; i++) {
        vec3 p = ro + rd * depth;
        float dist = sceneSDF(p);
        float minDist = MIN_DIST * (1.0 + depth * 0.1);
        if(dist < minDist) {
            if(p.y > -0.99) id = 1.0;
            break;
        }
        if(depth > MAX_DIST) break;
        float stepSize = max(dist * 0.5, minDist);
        depth += stepSize;
    }
    return vec2(depth, id);
}

float rayleighPhase(float cosTheta) {
    return 3.0 * (1.0 + cosTheta * cosTheta) / (16.0 * 3.14159);
}

float miePhase(float cosTheta) {
    const float g = 0.76;
    float g2 = g * g;
    return (1.0 - g2) / pow(1.0 + g2 - 2.0 * g * cosTheta, 1.5);
}

vec3 getSkyColor(vec3 rd) {
    vec3 sunDir = normalize(LIGHT_POS);
    float cosTheta = dot(rd, sunDir);
    float sunDot = max(cosTheta, 0.0);
    float rayleigh = rayleighPhase(cosTheta) * 0.15;
    float mie = miePhase(cosTheta) * 0.05;
    float sunIntensity = 15.0;
    vec3 sun = SUN_COLOR * pow(sunDot, 1500.0) * sunIntensity;
    vec3 corona = vec3(pow(sunDot, 6.0), pow(sunDot, 8.0), pow(sunDot, 10.0)) * 0.8;
    vec3 scattering = mix(vec3(rayleigh) * vec3(0.5, 0.7, 1.0), vec3(mie) * SUN_COLOR, 0.2);
    float horizon = pow(1.0 - abs(rd.y), 3.0);
    vec3 sky = mix(SKY_COLOR, HORIZON_COLOR, horizon);
    return sky + sun + corona + scattering;
}

vec3 toneMap(vec3 color) {
    color *= 0.8;
    float luminance = dot(color, vec3(0.2126, 0.7152, 0.0722));
    float mappedLuminance = luminance * (1.0 + luminance / (5.0 * 5.0)) / (1.0 + luminance);
    color = color * (mappedLuminance / (luminance + 0.001));
    float saturationAdjust = 1.0 / (1.0 + luminance * 0.3);
    color = mix(vec3(luminance), color, saturationAdjust);
    color = mix(color, vec3(luminance), pow(luminance, 2.0) * 0.2);
    return color;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
    vec3 ro = vec3(0.0, 2.5, -6.0);
    vec3 lookAt = vec3(0.0, 0.4, 10.0);
    vec3 forward = normalize(lookAt - ro);
    vec3 right = normalize(cross(forward, vec3(0.0, 1.0, 0.0)));
    vec3 up = cross(right, forward);
    float fov = 1.0;
    vec3 rd = normalize(forward + right * uv.x * fov + up * uv.y * fov);
    vec2 march = rayMarch(ro, rd);
    vec3 pos = ro + rd * march.x;
    vec3 col;
    if(march.x < MAX_DIST) {
        if(march.y > 0.5) {
            vec3 normal = waterNormal(pos.xz, iTime);
            vec3 reflectDir = reflect(rd, normal);
            float distanceToCamera = length(pos - ro);
            float baseFresnel = pow(1.0 - max(dot(normal, -rd), 0.0), 5.0);
            float distanceFresnel = smoothstep(0.0, WATER_REFLECTION_DISTANCE, distanceToCamera);
            float fresnel = mix(baseFresnel, 1.0, distanceFresnel * 0.8);
            float sunReflection = pow(max(dot(reflectDir, normalize(LIGHT_POS)), 0.0), 128.0);
            sunReflection *= 1.0 + distanceFresnel * 2.0;
            vec3 specular = SUN_COLOR * sunReflection * 3.0;
            vec3 closeWaterColor = vec3(0.1, 0.2, 0.3);
            vec3 farWaterColor = mix(HORIZON_COLOR, SUN_COLOR, 0.3);
            vec3 baseWaterColor = mix(closeWaterColor, farWaterColor, distanceFresnel);
            vec3 waterColor = mix(baseWaterColor, getSkyColor(reflectDir), fresnel);
            col = waterColor + specular;
        } else {
            float caustics = calculateCaustics(pos, iTime);
            float sunShadow = max(dot(normalize(pos - LIGHT_POS), vec3(0.0, 1.0, 0.0)), 0.0);
            col = vec3(0.2) + caustics * vec3(0.4, 0.6, 0.8) + sunShadow * SUN_COLOR * 0.2;
        }
        float fogAmount = 1.0 - exp(-march.x * 0.05);
        col = mix(col, getSkyColor(rd), fogAmount);
    } else {
        col = getSkyColor(rd);
    }
    col = toneMap(col);
    col = pow(col, vec3(0.4545));
    col = col * (1.0 + col) / (1.0 + col * col);
    fragColor = vec4(col, 1.0);
}