Rendering Water

with WebGL

Jonas Wagner, local.ch

Contents

Goal

Characteristics

of water

Waves

Reflection

Refraction

Reflectance

Absorbtion

Scattering

Caustics

Implementing

the shader

OpenGL ES 2.0 Pipeline (simplified)

The Vertex Shader

attribute vec3 position;

uniform mat4 modelTransform;
uniform mat4 worldViewProjection;

varying vec3 worldPosition;
varying vec4 projectedPosition;

void main(){
    vec4 worldPosition4 = modelTransform*vec4(position, 1.0);
    worldPosition = vec3(worldPosition4);
    projectedPosition = gl_Position = worldViewProjection*worldPosition4;
}

Waves

Waves

vec4 getNoise(vec2 uv){
    vec2 uv0 = (uv/103.0)+vec2(time/17.0, time/29.0);
    vec2 uv1 = uv/107.0-vec2(time/-19.0, time/31.0);
    vec2 uv2 = uv/vec2(897.0, 983.0)+vec2(time/101.0, time/97.0);
    vec2 uv3 = uv/vec2(991.0, 877.0)-vec2(time/109.0, time/-113.0);
    vec4 noise = (texture2D(normalSampler, uv0)) +
                 (texture2D(normalSampler, uv1)) +
                 (texture2D(normalSampler, uv2)) +
                 (texture2D(normalSampler, uv3));
    return noise*0.5-1.0;
}

Lighting

void sunLight(const vec3 surfaceNormal, const vec3 eyeDirection, float shiny, float spec, float diffuse,
              inout vec3 diffuseColor, inout vec3 specularColor){
    vec3 reflection = normalize(reflect(-sunDirection, surfaceNormal));
    float direction = max(0.0, dot(eyeDirection, reflection));
    specularColor += pow(direction, shiny)*sunColor*spec;
    diffuseColor += max(dot(sunDirection, surfaceNormal),0.0)*sunColor*diffuse;
}

Putting it together

void main(){
    vec4 noise = getNoise(worldPosition.xz);
    vec3 surfaceNormal = normalize(noise.xzy*vec3(2.0, 1.0, 2.0));

    vec3 diffuse = vec3(0.0);
    vec3 specular = vec3(0.0);

    vec3 worldToEye = eye-worldPosition;
    vec3 eyeDirection = normalize(worldToEye);
    sunLight(surfaceNormal, eyeDirection, 100.0, 2.0, 0.5, diffuse, specular);

    gl_FragColor = vec4((diffuse+specular+vec3(0.1))*vec3(0.3, 0.5, 0.9), 1.0);
}

Reflections

Reflections

BUT

there are no clipping planes in WebGL!

Clipping using discard

if(worldPosition.y > clip) {
        discard;
      }
    

Drawbacks

Clipping using oblique frustums

graph.pushUniforms();
gl.cullFace(gl.FRONT);

var worldView = graph.uniforms.worldView,
    projection = mat4.set(graph.uniforms.projection, this._projection),
    p = this._viewPlane,
    q = this._q,
    c = this._c,
    w = -graph.uniforms.eye[1];

mat4.multiplyVec4(worldView, this._plane, p);
p[3] = w;
graph.uniforms.worldView = mat4.multiply(graph.uniforms.worldView, this._worldView, this._worldView_);

q[0] = (sign(p.x) + projection[8]) / projection[0];
q[1] = (sign(p.y) + projection[9]) / projection[5];
q[2] = -1;
q[3] = (1.0+projection[10] ) / projection[14];

var dotpq = p[0]*q[0] + p[1]*q[1] + p[2]*q[2] + p[3]*q[3];
c = vec4.scale(p, 2.0/dotpq);

projection[2] = c[0];
projection[6] = c[1];
projection[10] = c[2] + 1.0;
projection[14] = c[3];

graph.uniforms.worldViewProjection = mat4.multiply(projection, this._worldView_, this._worldViewProjection);
graph.uniforms.projection = projection;

www.terathon.com/lengyel/Lengyel-Oblique.pdf

Reflection Texture

Reflection Shader

float dist = length(worldToEye);

vec2 screen = (projectedPosition.xy/projectedPosition.z + 1.0)*0.5;

float distortionFactor = max(dist/100.0, 10.0);
vec2 distortion = surfaceNormal.xz/distortionFactor;
vec3 reflectionSample = vec3(texture2D(reflectionSampler, screen+distortion));

gl_FragColor = vec4(color*(reflectionSample+vec3(0.1))*(diffuse+specular+0.3)*2.0, 1.0);

Remember?

Fresnel Equations

Schlicks approximation

float theta1 = max(dot(eyeDirection, surfaceNormal), 0.0);
float rf0 = 0.02;
float reflectance = rf0 + (1.0 - rf0)*pow((1.0 - theta1), 5.0);
vec3 albedo = mix(color*diffuse*0.3, (vec3(0.1)+reflectionSample*0.9+specular), reflectance);
albedo = aerialPerspective(albedo, dist, rayDirection);
gl_FragColor = vec4(albedo, 1.0);

Approximating Scattering

vec3 scatter = max(0.0, dot(surfaceNormal, eyeDirection))*vec3(0.0, 0.1, 0.07);
vec3 albedo = mix(color*light*0.3+scatter, (vec3(0.1)+reflectionSample), reflectance);

Refraction

Just like reflection

vec3 refractionSample = vec3(texture2D(refractionSampler, screen - distortion));
vec3 albedo = mix((scatter+(refractionSample*diffuse))*0.3, (vec3(0.1)+reflectionSample*0.9+specular), reflectance);

Absorbtion

Absorbtion

Absorbtion

float depth = length(worldPosition-eye);
float waterDepth = min(refractionSample.a-depth, 40.0);
vec3 absorbtion = min((waterDepth/35.0)*vec3(2.0, 1.05, 1.0), vec3(1.0));
vec3 refractionColor = mix(vec3(refractionSample)*0.5, color, absorbtion);

2D Water

Summary

Questions?

Did you like this presentation?

Get the slides

http://29a.ch/slides/2012/webglwater/

Credits

Phong Shading Picture by Brad Smith,
Waves by wili_hybrid,
Reflection by indianhillbilly,
Refraction by juska,
Reflectance by dicktay2000,
Absorbtion by mikebaird,
Caustics by philippawarr,
Blue Water by whl.travel.

/

#