Use your fingers or mouse to control the model (hold shift key or use mouse wheel to zoom it). You can change N but for large values this script is not very stable (in FireFox and Chrome).
This WebGL compatible demo saves every normal component in two parts (bytes). Less accurate demo with 8-bit normals. Script Demo with the OES_texture_float extension is used below for explanations.

Bump mapping outline

To add fine structure to a smooth model we can use bump mapping. Consider a (small) rectangle. Two axes x, y (so called tangent and binormal along the rectangle sides) and normal z make local coordinate system. Let us add to the rectangle perturbation δz(x,y). Really we need calculate only perturbed normals N(x,y) (in the local coordinate system)
   var pixels = [], tSize = 64;
   for(var i = 0; i < tSize; i++){
     var s = .2*Math.sin(2*i*Math.PI/tSize),  no = Math.sqrt(1 + s*s);
     pixels.push( 0, s/no, 1/no );
and store them in a float texture
   var texture = gl.createTexture();
   gl.bindTexture(gl.TEXTURE_2D, texture);
   gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
   gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, tSize, 0,
     gl.RGB, gl.FLOAT, new Float32Array(pixels));
   gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
   gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

   gl.uniform1i(gl.getUniformLocation(prog, "uTexSamp"), 0);
It is well known that spline patches have natural tangent directions S(u,v)/∂u and S(u,v)/∂v. They are not orthogonal therefore you can use any one of them and make third vector by cross product (it is ambiguously). These two non orthogonal tangents aBinorm1 and aBinorm2 are used in this script.

Now in the vertex shader we move (rotate and translate) our model

<script id="shader-vs" type="x-shader/x-vertex"> 
  attribute vec3 aPos;
  attribute vec3 aNorm;
  attribute vec3 aBinorm1;
  attribute vec3 aBinorm2;
  attribute vec2 aTexCoord;
  uniform mat4 mvMatrix;
  uniform mat4 prMatrix;
  varying vec3 rotDif;
  varying vec3 rotHalf;
  varying vec2 vTexCoord;
  const vec3 dirDif = vec3(0., 0., 1.);
  const vec3 dirHalf = vec3(-.4034, .259, .8776);
void main(void) {
   vTexCoord = aTexCoord;
   gl_Position = prMatrix * mvMatrix * vec4(aPos, 1.);
rotate x, y, z (aBinorm1, aBinorm2, aNorm) vectors
   vec3 rotNorm = (mvMatrix * vec4(aNorm, .0)).xyz;
   vec3 rotBinorm1 = (mvMatrix * vec4(aBinorm1, .0)).xyz;
   vec3 rotBinorm2 = (mvMatrix * vec4(aBinorm2, .0)).xyz;
and project light directions (dirDif, dirHalf) into the rotated local coordinate system
   rotDif = vec3(dot(dirDif,rotBinorm1), dot(dirDif,rotBinorm2),
   rotHalf = vec3(dot(dirHalf,rotBinorm1), dot(dirHalf,rotBinorm2),
to calculate colors in the fragment shader we use dot products of the "local" light directions and normals saved in texture.
<script id="shader-fs" type="x-shader/x-fragment"> 
precision mediump float;
  uniform sampler2D uTexSamp;
  varying vec3 rotDif;
  varying vec3 rotHalf;
  varying vec2 vTexCoord;
void main(void) {
   vec3 Norm = texture2D(uTexSamp, vTexCoord).xyz;
   float i = max( 0., abs(dot(Norm, rotDif)) );
   vec4 color = vec4(i, .5*i, 0., 1.);
   i = 2.*pow( max( 0., abs(dot(Norm, rotHalf)) ), 120.);
   color += vec4(i, i, i, 0.);
   gl_FragColor = color;

Contents     updated 2 Jan 2011