All Lessons
Lesson 8

Normal Mapping

Code

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

const vsSource = `
  attribute vec4 aPosition;
  attribute vec2 aTexCoord;
  attribute vec3 aTangent;
  attribute vec3 aNormal;
  uniform mat4 uModelView;
  varying highp vec2 vTexCoord;
  varying highp vec3 vTangent;
  varying highp vec3 vBitangent;
  varying highp vec3 vNormal;
  varying highp vec3 vPosition;
  
  void main() {
    gl_Position = aPosition;
    vTexCoord = aTexCoord;
    vNormal = aNormal;
    vTangent = aTangent;
    vBitangent = cross(aNormal, aTangent);
    vPosition = aPosition.xyz;
  }
`;

const fsSource = `
  varying highp vec2 vTexCoord;
  varying highp vec3 vTangent;
  varying highp vec3 vBitangent;
  varying highp vec3 vNormal;
  varying highp vec3 vPosition;
  uniform sampler2D uNormalMap;
  
  void main() {
    highp vec3 normalMap = texture2D(uNormalMap, vTexCoord).rgb * 2.0 - 1.0;
    
    highp mat3 TBN = mat3(vTangent, vBitangent, vNormal);
    highp vec3 normal = normalize(TBN * normalMap);
    
    highp vec3 lightDir = normalize(vec3(1.0, 1.0, 2.0));
    highp vec3 viewDir = normalize(-vPosition);
    
    highp float diff = max(dot(normal, lightDir), 0.0);
    highp vec3 diffuse = diff * vec3(0.8, 0.5, 0.3);
    
    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);
    
    highp vec3 ambient = vec3(0.2);
    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 procedural normal map
function createNormalMap(gl, size) {
  const data = new Uint8Array(size * size * 4);
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const idx = (y * size + x) * 4;
      const nx = Math.sin(x * 0.5) * 0.5;
      const ny = Math.sin(y * 0.5) * 0.5;
      const nz = Math.sqrt(1 - nx*nx - ny*ny);
      data[idx] = (nx * 0.5 + 0.5) * 255;
      data[idx + 1] = (ny * 0.5 + 0.5) * 255;
      data[idx + 2] = (nz * 0.5 + 0.5) * 255;
      data[idx + 3] = 255;
    }
  }
  const texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  return texture;
}

const positions = [-0.7, 0.7, 0, -0.7, -0.7, 0, 0.7, -0.7, 0, 0.7, 0.7, 0];
const texCoords = [0, 0, 0, 1, 1, 1, 1, 0];
const tangents = [1,0,0, 1,0,0, 1,0,0, 1,0,0];
const normals = [0,0,1, 0,0,1, 0,0,1, 0,0,1];
const indices = [0, 1, 2, 0, 2, 3];

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

const texBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);

const tanBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, tanBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(tangents), gl.STATIC_DRAW);

const normBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), 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 texAttrib = gl.getAttribLocation(shaderProgram, 'aTexCoord');
const tanAttrib = gl.getAttribLocation(shaderProgram, 'aTangent');
const normAttrib = gl.getAttribLocation(shaderProgram, 'aNormal');

const normalMap = createNormalMap(gl, 64);

gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
gl.vertexAttribPointer(posAttrib, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(posAttrib);

gl.bindBuffer(gl.ARRAY_BUFFER, texBuffer);
gl.vertexAttribPointer(texAttrib, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(texAttrib);

gl.bindBuffer(gl.ARRAY_BUFFER, tanBuffer);
gl.vertexAttribPointer(tanAttrib, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(tanAttrib);

gl.bindBuffer(gl.ARRAY_BUFFER, normBuffer);
gl.vertexAttribPointer(normAttrib, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(normAttrib);

gl.useProgram(shaderProgram);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, normalMap);
gl.uniform1i(gl.getUniformLocation(shaderProgram, 'uNormalMap'), 0);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

Preview