Stencil Shadow Volume

Drawing a shadowed scene with this technique involves drawing the scene four times. This shadowing technique is accurate to the pixel but can be computationally intensive.


The first pass prepares the depth buffer. Writing to the color buffer is disabled.

colorMask(red, green, blue, alpha) enables or disables the writing of frame buffer color components.

depthMask(flag) enables or disables writing into the depth buffer. The parameters for these two functions can be false or true.

stencilMask(mask) specifies a bitmask to enable and disable writing of individual bits in the stencil planes. 'mask' is an unsigned int.

stencilMaskSeparate(face, mask) is similar but specifies for a 'face', which can be FRONT, BACK, FRONT_AND_BACK.


The second pass prepares the stencil buffer, using the depth buffer just prepared. Writing to the color buffer and depth buffer is disabled. Face culling is also disabled.The second pass is where things become interesting. To render a shadow volume we extend the silhouette of an occluder. (A silhouette edge on a 3D body projected onto a 2D plane is the collection of points whose outwards surface normal is perpendicular to the view vector.)

This is done by emitting a quad for each silhouette edge.


After all edges have been extended, we seal off the volume by adding the front and back caps. Each triangle that faces the light becomes part of the front cap. For the back cap, we need to extend the vertices of light facing triangle to infinity (along the vector from the light to each vertex) and reverse their order. While a point is extended to infinity along the light vector we can still project it to the near plane.

If the depth test fails when rendering the back-facing polygons of the shadow volume we increment the value in the stencil buffer. If the depth test fails when rendering the front-facing polygons of the shadow volume we decrement the value in the stencil buffer. We do nothing if the depth test passes and the stencil test fails. A point is rendered only if its stencil value is zero.

Stencil Operations Final Stencil Value Drawn?
A +1 +1 -1 -1 0 Yes
B +1 +1 -1 +1 No
C +1 +1 No
D 0 Yes

stencilOp(fail, zfail, zpass) sets the front and back stencil test actions. 'fail' specifies the action to take when the stencil test fails. 'zfail' specifies the action to take when the stencil test passes, but the depth test fails. 'zpass' specifies the action to take when both the stencil test and the depth test pass, or when the stencil test passes and either there is no depth buffer or depth testing is not enabled. 'fail', 'zfail', and 'zpass' can be KEEP, ZERO, REPLACE, INCR, DECR, INVERT, INCR_WRAP, or DECR_WRAP. stencilOpSeparate(face, fail, zfail, zpass) is a similar function but specifies for a 'face' only, which can be FRONT, BACK, or FRONT_AND_BACK.

stencilFunc(func, ref, mask) set the front and back function and reference value for stencil testing. 'func' can be NEVER, ALWAYS, LESS, EQUAL, LEQUAL, GREATER, GEQUAL, NOTEQUAL. 'ref' specifies the reference value for the stencil test. 'mask' specifies a mask that is ANDed with both the reference value and the stored stencil value when the test is done. stencilFuncSeparate(face, func, ref, mask) is a similar function but specifies for a 'face' only, which can be FRONT, BACK, or FRONT_AND_BACK.


The third pass draws on the color buffer, using the stencil buffer just prepared.

depthFunc(func) specifies the function used to compare each incoming pixel depth value with the depth value present in the depth buffer. 'func' can be NEVER, ALWAYS, LESS, EQUAL, LEQUAL, GREATER, GEQUAL, or NOTEQUAL.


The fourth pass draws on the color buffer again. The use of alpha blending gives rise to the addition of ambient lighting.

Because a stencil buffer is not present by default, you need to request for it when getting the context with .getContext() at the beginning.
RESETRUNFULL
// /shared/webgl-library.js
// We are using the w = 0.0 trick to send vertices to infinity,
// but we have to use an infinite projection matrix for this to // work
Matrix4.prototype.frustumInfinite =
    function(left, right, bottom, top, near) {
      var e, rw, rh;
      if (left === right || top === bottom) throw 'null frustum';
      if (near <= 0) throw 'near <= 0';
      rw = 1 / (right - left);
      rh = 1 / (top - bottom);
      e = this.entries;
      e[ 0] = 2 * near * rw;
      e[ 1] = 0;
      e[ 2] = 0;
      e[ 3] = 0;
      e[ 4] = 0;
      e[ 5] = 2 * near * rh;
      e[ 6] = 0;
      e[ 7] = 0;
      e[ 8] = (right + left) * rw;
      e[ 9] = (top + bottom) * rh;
      e[10] = -1;
      e[11] = -1;
      e[12] = 0;
      e[13] = 0;
      e[14] = -2 * near;
      e[15] = 0;
      return this;
};

<!DOCTYPE html><html>
<head>
<script type="text/javascript" src="/shared/webgl-library.js"></script>
<script id="vs" type="x-shader/x-vertex">
   attribute vec4 a_Position;
   attribute vec4 a_Normal;
   attribute vec4 a_Color;
   uniform mat4 u_MvpMatrix;
   uniform vec4 u_LightPos;
   uniform float u_Diffuse;
   uniform float u_Ambient;
   varying vec4 v_Color;
   void main() {
      gl_Position = u_MvpMatrix * a_Position;
      v_Color = vec4(a_Color.rgb * (u_Diffuse * 
                                    dot(a_Normal.xyz, normalize(u_LightPos.xyz)) +
                                    u_Ambient),
                     1.0);
   }
</script>
<script id="vs_shadow" type="x-shader/x-vertex">
   attribute vec4 a_Position;
   attribute vec4 a_Normal;
   uniform mat4 u_MvpMatrix;
   uniform vec4 u_LightPos;
   varying vec4 v_Color;
   void main() {
      gl_Position = u_MvpMatrix * (a_Position.w == 0.0 ||
                           dot(a_Normal.xyz, u_LightPos.xyz) < 0.0 ?
                              vec4(a_Position.xyz * u_LightPos.w - u_LightPos.xyz, 0.0) : 
                              a_Position);
      v_Color = vec4(0.0, 1.0, 1.0, 1.0);
   }
</script>
<script id="fs" type="x-shader/x-fragment">
   #ifdef GL_ES
   precision mediump float;
   #endif
   varying vec4 v_Color;
   void main() {
      gl_FragColor = v_Color;
   }
</script>
<script>
   var sceneRotation = 40.0;
   var Light = { x : -16.0, y : 24.0, z : -4.0, w : 1.0 };       // w = 1.0 for Point light and w = 0.0 for Directional light
   function WebGLStart() {
      var canvas = document.getElementById('cv');
      var gl = canvas.getContext('experimental-webgl', { stencil : true });
      var program = initShaders(gl, "vs", "fs");
      program.a_Position = gl.getAttribLocation(program, 'a_Position');
      program.a_Normal=gl.getAttribLocation(program, 'a_Normal');
      program.a_Color = gl.getAttribLocation(program, 'a_Color');
      program.u_MvpMatrix = gl.getUniformLocation(program, 'u_MvpMatrix');
      program.u_Diffuse = gl.getUniformLocation(program, 'u_Diffuse');
      program.u_Ambient = gl.getUniformLocation(program, 'u_Ambient');
      program.u_LightPos = gl.getUniformLocation(program, 'u_LightPos');
      var programShadow = initShaders(gl, "vs_shadow","fs");
      programShadow.a_Position = gl.getAttribLocation(programShadow, 'a_Position');
      programShadow.a_Normal = gl.getAttribLocation(programShadow, 'a_Normal');
      programShadow.u_MvpMatrix = gl.getUniformLocation(programShadow, 'u_MvpMatrix');
      programShadow.u_LightPos = gl.getUniformLocation(programShadow, 'u_LightPos');
      var plane = initVertexBuffersForPlane(gl);
      var cubeMesh = initVertexBuffersForCubeMesh(gl);
      var tmp = new Matrix4();
      var viewProjMatrix = new Matrix4();
      viewProjMatrix.frustumInfinite(-1.0, 1.0, -1.0, 1.0, 2);
      tmp.lookAt(0.0, 7.0, 9.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
      viewProjMatrix.multiply_matrix(tmp.entries);
      viewProjMatrix.rotate(sceneRotation, 0, 1, 0);      // Initialize
      gl.clearColor(0, 0, 0, 1);
      gl.clearStencil(0);
      gl.enable(gl.DEPTH_TEST);
      gl.depthFunc(gl.LESS);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);      // First pass: depth buffer
      gl.useProgram(program);
      gl.colorMask(false, false, false, false);
      gl.uniform4f(program.u_LightPos, Light.x, Light.y, Light.z, Light.w);
      draw(gl, program, plane, viewProjMatrix);
      drawMesh(gl, program, cubeMesh, viewProjMatrix);      // Second pass: stencil buffer
      gl.useProgram(programShadow);
      gl.enable(gl.STENCIL_TEST);
      gl.disable(gl.CULL_FACE);
      gl.depthMask(false);
      gl.stencilOpSeparate(gl.FRONT, gl.KEEP, gl.INCR_WRAP, gl.KEEP);
      gl.stencilOpSeparate(gl.BACK, gl.KEEP, gl.DECR_WRAP, gl.KEEP);
      gl.stencilFunc(gl.ALWAYS, 0, 0xFF);
      gl.uniform4f(programShadow.u_LightPos, Light.x, Light.y, Light.z, Light.w);
      drawMeshShadowVolume(gl, programShadow, cubeMesh, viewProjMatrix);      // Third pass: color buffer
      gl.useProgram(program);
      gl.colorMask(true, true, true, true);
      gl.enable(gl.CULL_FACE);
      gl.depthMask(true);
      gl.stencilOpSeparate(gl.FRONT, gl.KEEP, gl.KEEP, gl.KEEP);
      gl.stencilOpSeparate(gl.BACK, gl.KEEP, gl.KEEP, gl.KEEP);
      gl.depthFunc(gl.LEQUAL);
      gl.uniform1f(program.u_Diffuse, 0.5);
      gl.uniform1f(program.u_Ambient, 0.0);
      gl.stencilFunc(gl.EQUAL, 0, 0xFF);
      draw(gl, program, plane, viewProjMatrix);
      drawMesh(gl, program, cubeMesh, viewProjMatrix);      // Fourth pass: alpha blending for ambient lighting
      gl.enable(gl.BLEND);
      gl.disable(gl.STENCIL_TEST);
      gl.blendEquation(gl.FUNC_ADD);
      gl.blendFunc(gl.ONE, gl.ONE);
      gl.uniform1f(program.u_Diffuse, 0.0);
      gl.uniform1f(program.u_Ambient, 0.5);
      draw(gl, program, plane, viewProjMatrix);
      drawMesh(gl, program, cubeMesh, viewProjMatrix);
      gl.disable(gl.BLEND);
      gl.disable(gl.DEPTH_TEST);
   }
   var g_modelMatrix = new Matrix4();
   var g_mvpMatrix = new Matrix4();
   function draw(gl, program, o, viewProjMatrix) {
      initAttributeVariable(gl, program.a_Position, o.vertexBuffer);
      initAttributeVariable(gl, program.a_Normal, o.normalBuffer);
      initAttributeVariable(gl, program.a_Color, o.colorBuffer);
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, o.indexBuffer);
      g_mvpMatrix.set(viewProjMatrix);
      g_mvpMatrix.multiply_matrix(g_modelMatrix.entries);
      gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.entries);
      gl.drawElements(gl.TRIANGLES, o.numIndices, gl.UNSIGNED_BYTE, 0);
      gl.disableVertexAttribArray(program.a_Position);
      gl.disableVertexAttribArray(program.a_Normal);
      gl.disableVertexAttribArray(program.a_Color);
   }
   function initAttributeVariable(gl, a_attribute, buffer) {
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.vertexAttribPointer(a_attribute, buffer.num, buffer.type,false, 0, 0);
      gl.enableVertexAttribArray(a_attribute);
   }
   function initVertexBuffersForPlane(gl) {      // Vertex coordinates
      var vertices = new Float32Array([   // v0-v1-v2-v3
          3.0, -1.0, 2.5,    -3.0, -1.0, 2.5,
         -3.0, -1.0, -2.5,    3.0, -1.0, -2.5]);      // Normals
      var normals = new Float32Array([
          0.0, 1.0, 0.0,     0.0, 1.0, 0.0, 
          0.0, 1.0, 0.0,     0.0, 1.0, 0.0]);      // Colors
      var colors = new Float32Array([
         1.0, 1.0, 1.0,   1.0, 1.0, 1.0, 
         1.0, 1.0, 1.0,   1.0, 1.0, 1.0]);      // Indices of the vertices
      var indices = new Uint8Array([0, 2, 1, 0, 3, 2]);  // Utilize Object object to return multiple buffer objects together
      var o = {};
      o.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
      o.normalBuffer = initArrayBufferForLaterUse(gl, normals, 3, gl.FLOAT);
      o.colorBuffer = initArrayBufferForLaterUse(gl, colors, 3, gl.FLOAT);
      o.indexBuffer= initElementArrayBufferForLaterUse(gl, indices, gl.UNSIGNED_BYTE);
      if (!o.vertexBuffer || !o.normalBuffer || !o.colorBuffer || !o.indexBuffer) return null;
       o.numIndices = indices.length;
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
      return o;
   }
   function drawMesh(gl, program, o, viewProjMatrix) {
      initAttributeVariable(gl, program.a_Position, o.vertexBuffer);
      initAttributeVariable(gl, program.a_Normal, o.normalBuffer);
      initAttributeVariable(gl, program.a_Color, o.colorBuffer);
      g_mvpMatrix.set(viewProjMatrix);
      g_mvpMatrix.multiply_matrix(g_modelMatrix.entries);
      gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.entries);
      gl.drawArrays(gl.TRIANGLES, 0, o.count);
      gl.disableVertexAttribArray(program.a_Position);
      gl.disableVertexAttribArray(program.a_Normal);
      gl.disableVertexAttribArray(program.a_Color);
   }
   function dotL(normal) {
      return ((Light.x * normal[0] + Light.y * normal[1] + Light.z * normal[2]));
   } 
   function drawMeshShadowVolume(gl,program,o,viewProjMatrix){
      initAttributeVariable(gl, program.a_Position, o.vertexBuffer);
      initAttributeVariable(gl, program.a_Normal, o.normalBuffer);
      g_mvpMatrix.set(viewProjMatrix);
      g_mvpMatrix.multiply_matrix(g_modelMatrix.entries);
      gl.uniformMatrix4fv(program.u_MvpMatrix, false, g_mvpMatrix.entries);
      gl.drawArrays(gl.TRIANGLES, 0, o.count);
      gl.disableVertexAttribArray(program.a_Position);
      gl.disableVertexAttribArray(program.a_Normal);      // Silhouette detection
      var shadowVolumeEdges = [];
      for(var i = o.edges.length; i--;) {
         var dL0 = dotL(o.edges[i].n[0]);
         var dL1 = dotL(o.edges[i].n[1]);
         o.edges[i].c = dL0 < 0.0;
         if(dL0 * dL1 <= 0.0) shadowVolumeEdges.push(o.edges[i]);
      }      // Silhouette extrusion
      var shadowVolumeVertices = new Float32Array(shadowVolumeEdges.length * 6 * 4);
      for(var i = shadowVolumeEdges.length; i--;) {
         var t;
         shadowVolumeVertices[i * 24 + 19] = 0.0;
         shadowVolumeVertices[i * 24 + 3] = 1.0;
         shadowVolumeVertices[i * 24 + 7] = shadowVolumeVertices[i * 24 + 15] = shadowVolumeEdges[i].c ? 0.0 : 1.0;
         shadowVolumeVertices[i * 24 + 11] = shadowVolumeVertices[i * 24 + 23] = shadowVolumeEdges[i].c ? 1.0 : 0.0;
         for(t = 3; t--;) shadowVolumeVertices[i * 24 + 0 + t] = shadowVolumeEdges[i].v[0][t];
         for(t = 3; t--;) shadowVolumeVertices[i * 24 + 4 + t] = shadowVolumeEdges[i].v[1][t];
         for(t = 3; t--;) shadowVolumeVertices[i * 24 + 8 + t] = shadowVolumeEdges[i].v[1][t];
         for(t = 3; t--;) shadowVolumeVertices[i * 24+12+t] = shadowVolumeEdges[i].v[0][t];
         for(t = 3; t--;) shadowVolumeVertices[i * 24 + 16+t] = shadowVolumeEdges[i].v[1][t];
         for(t = 3; t--;) shadowVolumeVertices[i * 24 + 20+t] = shadowVolumeEdges[i].v[0][t];
      }
      o.shadowBuffer = initArrayBufferForLaterUse(gl, shadowVolumeVertices, 4, gl.FLOAT);
      initAttributeVariable(gl, program.a_Position,o.shadowBuffer);
      g_mvpMatrix.set(viewProjMatrix);
      g_mvpMatrix.multiply_matrix(g_modelMatrix.entries);
      gl.uniformMatrix4fv(program.u_MvpMatrix,false, g_mvpMatrix.entries);
      gl.drawArrays(gl.TRIANGLES,0, shadowVolumeEdges.length*6);
      gl.disableVertexAttribArray(program.a_Position);
   }
   function initVertexBuffersForCubeMesh(gl) {
      var arrVertices = [ [1.0, 1.0, 1.0],   [-1.0, 1.0, 1.0],
                          [-1.0, -1.0, 1.0], [1.0, -1.0, 1.0],
                          [1.0, -1.0, -1.0], [1.0, 1.0, -1.0],
                          [-1.0, 1.0, -1.0], [-1.0, -1.0, -1.0]];
      var arrTriangles = [ {v: [0, 1, 2], n: [0.0, 0.0, 1.0]},
                           {v: [0, 2, 3], n: [0.0, 0.0, 1.0]},
                           {v: [0, 3, 4], n: [1.0, 0.0, 0.0]},
                           {v: [0, 4, 5], n: [1.0, 0.0, 0.0]},
                           {v: [0, 5, 6], n: [0.0, 1.0, 0.0]},
                           {v: [0, 6, 1], n: [0.0, 1.0, 0.0]},
                           {v: [1, 6, 7], n: [-1.0, 0.0, 0.0]},
                           {v: [1, 7, 2], n: [-1.0, 0.0, 0.0]},
                           {v: [7, 4, 3], n: [0.0, -1.0, 0.0]},
                           {v: [7, 3, 2], n: [0.0, -1.0, 0.0]},
                           {v: [4, 7, 6], n: [0.0, 0.0, -1.0]},
                           {v: [4, 6, 5], n: [0.0, 0.0, -1.0]}];
      var color = [1.0, 0.0, 1.0];  // This could be a function, but we have to make sure that we use the same vertices/triangles structures
      var edges = [];
      for(var i = arrTriangles.length; --i;) {
         for(var r = i; r--;) {
            var numShared = 0;
            var last_vi = -1;
            var flip_n = false;
            var edge = {v: [], n: []};
            for(var vi = 3; vi--;) {
               for(var vr = 3; vr--;) {
                 if(arrTriangles[i].v[vi] == arrTriangles[r].v[vr]) {
                   edge.v.push(arrVertices[arrTriangles[r].v[vr]]);
                   edge.n.push(arrTriangles[numShared++ ? r : i].n);
                     if(numShared == 2) {
                        if(vi + 1 != last_vi) edge.n.reverse();
                        edges.push(edge);
                     }
                     last_vi = vi;
                     break;
                  }
               }
            }
         }
      }
      var size = arrTriangles.length * 3 * 3;
      var vertices = new Float32Array(size);
      var normals = new Float32Array(size);
      var colors = new Float32Array(size);
      for(var i = arrTriangles.length; i--;){
         for(var v = 3; v--;) {
            for(var r = 3; r--;) {
               vertices[i * 9 + v * 3 + r] = arrVertices[arrTriangles[i].v[v]][r];
               normals[i * 9 + v * 3 + r] = arrTriangles[i].n[r];
               colors[i * 9 + v * 3 + r] = color[r];
            }
         }
      }  // Utilize Object object to return multiple buffer objects   // together
      var o = {};
      o.edges = edges;
      o.count = arrTriangles.length * 3;
      o.vertexBuffer = initArrayBufferForLaterUse(gl, vertices, 3, gl.FLOAT);
      o.normalBuffer = initArrayBufferForLaterUse(gl, normals, 3, gl.FLOAT);
      o.colorBuffer = initArrayBufferForLaterUse(gl, colors, 3, gl.FLOAT);
      if (!o.vertexBuffer || !o.colorBuffer) return null;
      gl.bindBuffer(gl.ARRAY_BUFFER, null);
      return o;
   }
   function initArrayBufferForLaterUse(gl, data, num, type) {
      var buffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
      gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
      buffer.num = num;
      buffer.type = type;
      return buffer;
   } 
   function initElementArrayBufferForLaterUse(gl, data, type) {
      var buffer = gl.createBuffer();
      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
      gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW);
      buffer.type = type;
      return buffer;
   }
</script>
</head>
<body onload="WebGLStart();">
   <canvas id="cv" style="border: none;" width="500" height="500"></canvas>
</body>
</html>