Codevember breakdowns Part 1 : Shadow Mapping

林逸文 Yi-Wen LIN
5 min readJan 8, 2018

--

One of my new year resolution is to go back to write some blog posts, so I think I’ll start with sharing some techniques I used in my Codevember last year.

During this month I’ve made a small experiment for each day. It’s very intense so there are couple of tricks / technique I’ve keep reusing them, which I think could be useful in some situations. So I am going to write about these techniques and hopefully it can be helpful someday on your project !

Before we start, in this article I’ll be using my own WebGL tool, but most of the technique are in the math or the shaders, so it doesn’t matter which 3D library you pick. However it does require some knowledge about cameras ( View matrix and Projection matrix) and shaders.

In this first part we are going to talk about the shadow mapping technique. What I am using is just part of it, if you are really interested in shadow mapping here is an execellent tutorial for it :

It’s in opengl but you can easily convert it to WebGL / javascript.

For this breakdown, I’ll walk through on how I create this experiment :

In the experiment first thing you see is a lot of pillars, and then when you rotate to certain point, the face of John Malkovich will appear.

The way I create this effect is to project an image on to the pillars, and for each vertice on the pillar, I check what’s the colour being projected then decide how much I want to scale down the pillar ( white : scale = 1, black : scale = 0) This feels pretty straight forward, with just one question : How do I project the texture on to my pillars ?

This is where the shadow mapping comes in to help. Imagine the point we project our image as a light source, and then we can use the shadow mapping technique to get what’s the colour of the image being projected onto our surface from the light source. Since we have the theory, now let’s implment it.

So the first step is to create bunch of pillars, you can make the model in 3D software or create in code, just make sure you have enough polygons to deform the shape.

Next step is to setup the camera from our point of projection. It’s the view matrix and the projection matrix we are after to create the shadow map matrix :

( Again I am using my own tool, so you’ll need to translate these code into the library you are using. By the way I am using gl-matrix library for the math. The correspond methods are mat4.perspective and mat4.lookAt )

// setup camera from the point of projection
this.cameraPortrait = new CameraPerspective();
this.cameraPortrait.setPerspective(FOV * RAD, ratio, .1, 50);
// setPerspective : mat4.perspective(fov, aspect, near, far)
this.pointSource = vec3.fromValues(0, 0, -radius);
this.cameraPortrait.lookAt(this.pointSource, [0, -1, 0], [0, 1, 0]);
// lookAt : mat4.lookAt(eye, target, up)
// this bias matrix will normalize value from -1 ~ 1 to 0 ~ 1
// which is the texture space we need to sample the texture
this.biasMatrix = mat4.fromValues(
0.5, 0.0, 0.0, 0.0,
0.0, 0.5, 0.0, 0.0,
0.0, 0.0, 0.5, 0.0,
0.5, 0.5, 0.5, 1.0
);
// combine all the matrices together :
// shadow matrix = ProjMatrix * ViewMatrix * BiasMatrix
this.shadowMatrix = mat4.create();
mat4.multiply(this.shadowMatrix, this.cameraPortrait.projection, this.cameraPortrait.viewMatrix);
mat4.multiply(this.shadowMatrix, this.biasMatrix, this.shadowMatrix);

The next step is to get the image project on the surface, first we need to get the shadow coordinate, which is really simple. In the vertex shader :

vShadowCoord   = uShadowMatrix * uModelMatrix * vec4(position, 1.0);

Then we can use this shadowCoord in the fragment shader to get the colour :

varying vec4 vShadowCoord;
uniform sample2D texturePortrait;
void main(void) {
// don't forget to divide by the .w first
vec4 shadowCoord = vShadowCoord / vShadowCoord.w;
// There are 2 ways to get the colour, you can use texture2DProj
// and pass in shadowCoord(vec4)
// vec3 color = texture2DProj(texturePortrait, shadowCoord).rgb;
// Or go with tradition way, get the uv coordinate
vec2 uv = shadowCoord.xy;
vec3 color = texture2D(texturePortrait, uv).rgb;
gl_FragColor = vec4(color, 1.0);
}

Then you should be able to get this :

Now we are nearly there. The last thing I want to add is to scale the pillar based on the image. Since we already know how to get the pixel colour being projected, this is easy: We just need to do the sampling in the vertex shader again then do the deform :

void main(void) {
// I'm using instancing so I need to add the position offset to
// get the final position
vec3 position = aVertexPosition;
vShadowCoord = uShadowMatrix * uModelMatrix * vec4(position + aPosOffset, 1.0);
// get the shadowCoord
vec4 shadowCoord = vShadowCoord/vShadowCoord.w;
vec2 uv = (shadowCoord/shadowCoord.w).xy;

// adjust the uv coord with the image ratio
float t = (uRatio - 1.0) * 0.5;
uv.x /= (1.0 + t);
// get the pixel colour
vec3 color = texture2D(texturePortrait, uv).rgb;
// scale the pillar based on the colur (xz only)
position.xz *= color.r;
// add the position offset back from instancing
position += aPosOffset;

gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(position, 1.0);
vTextureCoord = aTextureCoord;
vNormal = aNormal;
}

And Voilà you get final result. As you can see it’s a simple trick but you can create something really interesting using this. I reused the same technique to create this Codevember experiment:

It’s the same principle and add a simple interaction on it. And also this one is using this technique too :

It’s slightly compilated because I projected 4 textures and need to determine which one need to be shown at the right angle. However that’s just picking the texture to show, the way it project the textures onto the cube is the same.

Here are the source code and the demo of this breakdown. Being able to ‘Project’ a texture onto a surface from any point is a really powerful tool. And it’s really fun to play too ! You can create some really interesting visual and illusion using this trick. I’ve used this a lot in my experiments, all based on this technique :

So here is the part 1 of the breakdowns, hope you enjoy it. The next part will be an extension of this technique where not only to project a texture, but we will try to get the position data using this technique. Stay tuned !

--

--