All Lessons
Lesson 5

Lighting and Shading

Code

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

const vsSource = `
  attribute vec4 aVertexPosition;
  attribute vec3 aVertexNormal;
  uniform mat4 uModelViewMatrix;
  uniform mat4 uProjectionMatrix;
  uniform mat4 uNormalMatrix;
  varying highp vec3 vNormal;
  varying highp vec3 vPosition;
  
  void main() {
    gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition;
    vNormal = (uNormalMatrix * vec4(aVertexNormal, 0.0)).xyz;
    vPosition = (uModelViewMatrix * aVertexPosition).xyz;
  }
`;

const fsSource = `
  varying highp vec3 vNormal;
  varying highp vec3 vPosition;
  
  void main() {
    highp vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
    highp vec3 normal = normalize(vNormal);
    highp vec3 viewDir = normalize(-vPosition);
    
    // Ambient
    highp vec3 ambient = vec3(0.2, 0.2, 0.3);
    
    // Diffuse
    highp float diff = max(dot(normal, lightDir), 0.0);
    highp vec3 diffuse = diff * vec3(0.5, 0.7, 1.0);
    
    // Specular
    highp vec3 reflectDir = reflect(-lightDir, normal);
    highp float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    highp vec3 specular = spec * vec3(1.0, 1.0, 1.0);
    
    highp vec3 result = ambient + diffuse + specular;
    gl_FragColor = vec4(result, 1.0);
  }
`;

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 sphere geometry
function createSphere(radius, segments) {
  const positions = [], normals = [], indices = [];
  for (let lat = 0; lat <= segments; lat++) {
    const theta = lat * Math.PI / segments;
    const sinTheta = Math.sin(theta), cosTheta = Math.cos(theta);
    for (let lon = 0; lon <= segments; lon++) {
      const phi = lon * 2 * Math.PI / segments;
      const sinPhi = Math.sin(phi), cosPhi = Math.cos(phi);
      const x = cosPhi * sinTheta, y = cosTheta, z = sinPhi * sinTheta;
      positions.push(radius * x, radius * y, radius * z);
      normals.push(x, y, z);
    }
  }
  for (let lat = 0; lat < segments; lat++) {
    for (let lon = 0; lon < segments; lon++) {
      const first = lat * (segments + 1) + lon;
      const second = first + segments + 1;
      indices.push(first, second, first + 1, second, second + 1, first + 1);
    }
  }
  return { positions, normals, indices };
}

const sphere = createSphere(0.7, 20);

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

const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(sphere.normals), gl.STATIC_DRAW);

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

const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
const vertexPosition = gl.getAttribLocation(shaderProgram, 'aVertexPosition');
const vertexNormal = gl.getAttribLocation(shaderProgram, 'aVertexNormal');
const projectionMatrix = gl.getUniformLocation(shaderProgram, 'uProjectionMatrix');
const modelViewMatrix = gl.getUniformLocation(shaderProgram, 'uModelViewMatrix');
const normalMatrix = gl.getUniformLocation(shaderProgram, 'uNormalMatrix');

let rotation = 0;

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];
}

function render() {
  rotation += 0.005;
  
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  
  const proj = perspective(Math.PI / 4, 1, 0.1, 100);
  const c = Math.cos(rotation), s = Math.sin(rotation);
  const mv = [c,0,s,0, 0,1,0,0, -s,0,c,0, 0,0,-3,1];
  
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.vertexAttribPointer(vertexPosition, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(vertexPosition);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  gl.vertexAttribPointer(vertexNormal, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(vertexNormal);
  
  gl.useProgram(shaderProgram);
  gl.uniformMatrix4fv(projectionMatrix, false, proj);
  gl.uniformMatrix4fv(modelViewMatrix, false, mv);
  gl.uniformMatrix4fv(normalMatrix, false, mv);
  
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.drawElements(gl.TRIANGLES, sphere.indices.length, gl.UNSIGNED_SHORT, 0);
  
  requestAnimationFrame(render);
}

render();

Preview