All Lessons
Lesson 10

Realistic Water Simulation

Code

const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
gl.enable(gl.DEPTH_TEST);

const vsSource = `
  attribute vec3 aPosition;
  uniform float uTime;
  uniform mat4 uProjection;
  varying highp vec3 vPosition;
  varying highp vec3 vNormal;
  
  void main() {
    vec3 pos = aPosition;
    
    float wave1 = sin(pos.x * 3.0 + uTime) * 0.1;
    float wave2 = sin(pos.z * 2.5 - uTime * 0.7) * 0.08;
    float wave3 = sin((pos.x + pos.z) * 4.0 + uTime * 1.5) * 0.05;
    
    pos.y += wave1 + wave2 + wave3;
    
    float dx = cos(pos.x * 3.0 + uTime) * 0.3;
    float dz = -cos(pos.z * 2.5 - uTime * 0.7) * 0.2;
    
    vec3 tangent = normalize(vec3(1.0, dx, 0.0));
    vec3 bitangent = normalize(vec3(0.0, dz, 1.0));
    vec3 normal = cross(tangent, bitangent);
    
    vPosition = pos;
    vNormal = normal;
    
    gl_Position = uProjection * vec4(pos, 1.0);
  }
`;

const fsSource = `
  varying highp vec3 vPosition;
  varying highp vec3 vNormal;
  uniform highp float uTime;
  
  void main() {
    highp vec3 normal = normalize(vNormal);
    highp vec3 viewDir = normalize(vec3(0.0, 1.0, 2.0) - vPosition);
    highp vec3 lightDir = normalize(vec3(1.0, 1.5, 0.5));
    
    // Fresnel effect
    highp float fresnel = pow(1.0 - max(dot(viewDir, normal), 0.0), 3.0);
    
    // Water colors
    highp vec3 deepColor = vec3(0.0, 0.2, 0.4);
    highp vec3 shallowColor = vec3(0.0, 0.5, 0.7);
    highp vec3 waterColor = mix(deepColor, shallowColor, fresnel);
    
    // Lighting
    highp float diff = max(dot(normal, lightDir), 0.0);
    highp vec3 reflectDir = reflect(-lightDir, normal);
    highp float spec = pow(max(dot(viewDir, reflectDir), 0.0), 128.0);
    
    highp vec3 color = waterColor * (0.3 + diff * 0.7);
    color += vec3(1.0) * spec * 0.8;
    color = mix(color, vec3(0.7, 0.9, 1.0), fresnel * 0.3);
    
    gl_FragColor = vec4(color, 0.9);
  }
`;

function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  return shaderProgram;
}

function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  return shader;
}

// Create water grid
const gridSize = 50;
const positions = [];
const indices = [];

for (let z = 0; z < gridSize; z++) {
  for (let x = 0; x < gridSize; x++) {
    const px = (x / (gridSize - 1) - 0.5) * 4;
    const pz = (z / (gridSize - 1) - 0.5) * 4;
    positions.push(px, 0, pz);
  }
}

for (let z = 0; z < gridSize - 1; z++) {
  for (let x = 0; x < gridSize - 1; x++) {
    const i = z * gridSize + x;
    indices.push(i, i + 1, i + gridSize);
    indices.push(i + 1, i + gridSize + 1, i + gridSize);
  }
}

const posBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

const indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);

const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
const posAttrib = gl.getAttribLocation(shaderProgram, 'aPosition');
const timeUniform = gl.getUniformLocation(shaderProgram, 'uTime');
const projUniform = gl.getUniformLocation(shaderProgram, 'uProjection');

function perspective(fov, aspect, near, far) {
  const f = 1.0 / Math.tan(fov / 2);
  return [f/aspect,0,0,0, 0,f,0,0, 0,0,(far+near)/(near-far),-1, 0,0,(2*far*near)/(near-far),0];
}

const proj = perspective(Math.PI / 4, 1, 0.1, 100);
let time = 0;

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

function render() {
  time += 0.02;
  
  gl.clearColor(0.1, 0.1, 0.2, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
  gl.vertexAttribPointer(posAttrib, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(posAttrib);
  
  gl.useProgram(shaderProgram);
  gl.uniform1f(timeUniform, time);
  gl.uniformMatrix4fv(projUniform, false, proj);
  
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
  
  requestAnimationFrame(render);
}

render();

Preview