├── .gitignore ├── README.md ├── package.json └── tutorial.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebGL Shadow Mapping Tutorial 2 | 3 | ## To read the tutorial 4 | 5 | http://chinedufn.com/webgl-shadow-mapping-tutorial/ 6 | 7 | ## To run locally 8 | 9 | ``` 10 | git clone https://github.com/chinedufn/webgl-shadow-mapping-tutorial 11 | cd webgl-shadow-mapping-tutorial 12 | npm install 13 | npm run start 14 | ``` 15 | 16 | ## License 17 | 18 | MIT 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-shadow-mapping-tutorial", 3 | "version": "0.0.1", 4 | "description": "A WebGL shadow mapping tutorial", 5 | "main": "tutorial.js", 6 | "scripts": { 7 | "start": "budo tutorial --open --live", 8 | "test": "standard" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/chinedufn/webgl-shadow-mapping-tutorial.git" 13 | }, 14 | "keywords": [ 15 | "webgl", 16 | "shadow", 17 | "map", 18 | "mapping", 19 | "opengl", 20 | "gl", 21 | "tutorial", 22 | "lighting" 23 | ], 24 | "author": "Chinedu Francis Nwafili ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/chinedufn/webgl-shadow-mapping-tutorial/issues" 28 | }, 29 | "homepage": "https://github.com/chinedufn/webgl-shadow-mapping-tutorial#readme", 30 | "devDependencies": { 31 | "budo": "^10.0.4", 32 | "standard": "^10.0.3" 33 | }, 34 | "dependencies": { 35 | "gl-mat4": "^1.1.4", 36 | "stanford-dragon": "^1.1.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tutorial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Section 1 - Getting set up 3 | */ 4 | 5 | // gl-mat4 helps us to matrix math 6 | var glMat4 = require('gl-mat4') 7 | // standford dragon gives us vertex data for drawing a dragon 8 | var stanfordDragon = require('stanford-dragon/4') 9 | 10 | // We insert our canvas into the page 11 | var canvas = document.createElement('canvas') 12 | canvas.width = 500 13 | canvas.height = 500 14 | var mountLocation = document.getElementById('webgl-shadow-map-tut') || document.body 15 | mountLocation.appendChild(canvas) 16 | 17 | // We get our WebGL context and enable depth testing so that we can tell when an object 18 | // is behind another object 19 | var gl = canvas.getContext('webgl') 20 | gl.enable(gl.DEPTH_TEST) 21 | 22 | // We set up controls so that we can drag our mouse or finger to adjust the rotation of 23 | // the camera about the X and Y axes 24 | var canvasIsPressed = false 25 | var xRotation = Math.PI / 20 26 | var yRotation = 0 27 | var lastPressX 28 | var lastPressY 29 | canvas.onmousedown = function (e) { 30 | canvasIsPressed = true 31 | lastPressX = e.pageX 32 | lastPressY = e.pageY 33 | } 34 | canvas.onmouseup = function () { 35 | canvasIsPressed = false 36 | } 37 | canvas.onmouseout = function () { 38 | canvasIsPressed = false 39 | } 40 | canvas.onmousemove = function (e) { 41 | if (canvasIsPressed) { 42 | xRotation += (e.pageY - lastPressY) / 50 43 | yRotation -= (e.pageX - lastPressX) / 50 44 | 45 | xRotation = Math.min(xRotation, Math.PI / 2.5) 46 | xRotation = Math.max(xRotation, 0.1) 47 | 48 | lastPressX = e.pageX 49 | lastPressY = e.pageY 50 | } 51 | } 52 | 53 | // As you drag your finger we move the camera 54 | canvas.addEventListener('touchstart', function (e) { 55 | lastPressX = e.touches[0].clientX 56 | lastPressY = e.touches[0].clientY 57 | }) 58 | canvas.addEventListener('touchmove', function (e) { 59 | e.preventDefault() 60 | xRotation += (e.touches[0].clientY - lastPressY) / 50 61 | yRotation -= (e.touches[0].clientX - lastPressX) / 50 62 | 63 | xRotation = Math.min(xRotation, Math.PI / 2.5) 64 | xRotation = Math.max(xRotation, 0.1) 65 | 66 | lastPressX = e.touches[0].clientX 67 | lastPressY = e.touches[0].clientY 68 | }) 69 | 70 | /** 71 | * Section 2 - Shaders 72 | */ 73 | 74 | // We create a vertex shader from the light's point of view. You never see this in the 75 | // demo. It is used behind the scenes to create a texture that we can use to test testing whether 76 | // or not a point is inside of our outside of the shadow 77 | var shadowDepthTextureSize = 1024 78 | var lightVertexGLSL = ` 79 | attribute vec3 aVertexPosition; 80 | 81 | uniform mat4 uPMatrix; 82 | uniform mat4 uMVMatrix; 83 | 84 | void main (void) { 85 | gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); 86 | } 87 | ` 88 | var lightFragmentGLSL = ` 89 | precision mediump float; 90 | 91 | vec4 encodeFloat (float depth) { 92 | const vec4 bitShift = vec4( 93 | 256 * 256 * 256, 94 | 256 * 256, 95 | 256, 96 | 1.0 97 | ); 98 | const vec4 bitMask = vec4( 99 | 0, 100 | 1.0 / 256.0, 101 | 1.0 / 256.0, 102 | 1.0 / 256.0 103 | ); 104 | vec4 comp = fract(depth * bitShift); 105 | comp -= comp.xxyz * bitMask; 106 | return comp; 107 | } 108 | 109 | void main (void) { 110 | // Encode the distance into the scene of this fragment. 111 | // We'll later decode this when rendering from our camera's 112 | // perspective and use this number to know whether the fragment 113 | // that our camera is seeing is inside of our outside of the shadow 114 | gl_FragColor = encodeFloat(gl_FragCoord.z); 115 | } 116 | ` 117 | 118 | // We create a vertex shader that renders the scene from the camera's point of view. 119 | // This is what you see when you view the demo 120 | var cameraVertexGLSL = ` 121 | attribute vec3 aVertexPosition; 122 | 123 | uniform mat4 uPMatrix; 124 | uniform mat4 uMVMatrix; 125 | uniform mat4 lightMViewMatrix; 126 | uniform mat4 lightProjectionMatrix; 127 | 128 | // Used to normalize our coordinates from clip space to (0 - 1) 129 | // so that we can access the corresponding point in our depth color texture 130 | const mat4 texUnitConverter = mat4(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); 131 | 132 | varying vec2 vDepthUv; 133 | varying vec4 shadowPos; 134 | 135 | void main (void) { 136 | gl_Position = uPMatrix * uMVMatrix * vec4(aVertexPosition, 1.0); 137 | 138 | shadowPos = texUnitConverter * lightProjectionMatrix * lightMViewMatrix * vec4(aVertexPosition, 1.0); 139 | } 140 | ` 141 | var cameraFragmentGLSL = ` 142 | precision mediump float; 143 | 144 | varying vec2 vDepthUv; 145 | varying vec4 shadowPos; 146 | 147 | uniform sampler2D depthColorTexture; 148 | uniform vec3 uColor; 149 | 150 | float decodeFloat (vec4 color) { 151 | const vec4 bitShift = vec4( 152 | 1.0 / (256.0 * 256.0 * 256.0), 153 | 1.0 / (256.0 * 256.0), 154 | 1.0 / 256.0, 155 | 1 156 | ); 157 | return dot(color, bitShift); 158 | } 159 | 160 | void main(void) { 161 | vec3 fragmentDepth = shadowPos.xyz; 162 | float shadowAcneRemover = 0.007; 163 | fragmentDepth.z -= shadowAcneRemover; 164 | 165 | float texelSize = 1.0 / ${shadowDepthTextureSize}.0; 166 | float amountInLight = 0.0; 167 | 168 | // Check whether or not the current fragment and the 8 fragments surrounding 169 | // the current fragment are in the shadow. We then average out whether or not 170 | // all of these fragments are in the shadow to determine the shadow contribution 171 | // of the current fragment. 172 | // So if 4 out of 9 fragments that we check are in the shadow then we'll say that 173 | // this fragment is 4/9ths in the shadow so it'll be a little brighter than something 174 | // that is 9/9ths in the shadow. 175 | for (int x = -1; x <= 1; x++) { 176 | for (int y = -1; y <= 1; y++) { 177 | float texelDepth = decodeFloat(texture2D(depthColorTexture, fragmentDepth.xy + vec2(x, y) * texelSize)); 178 | if (fragmentDepth.z < texelDepth) { 179 | amountInLight += 1.0; 180 | } 181 | } 182 | } 183 | amountInLight /= 9.0; 184 | 185 | gl_FragColor = vec4(amountInLight * uColor, 1.0); 186 | } 187 | ` 188 | 189 | // Link our light and camera shader programs 190 | var cameraVertexShader = gl.createShader(gl.VERTEX_SHADER) 191 | gl.shaderSource(cameraVertexShader, cameraVertexGLSL) 192 | gl.compileShader(cameraVertexShader) 193 | 194 | var cameraFragmentShader = gl.createShader(gl.FRAGMENT_SHADER) 195 | gl.shaderSource(cameraFragmentShader, cameraFragmentGLSL) 196 | gl.compileShader(cameraFragmentShader) 197 | 198 | var cameraShaderProgram = gl.createProgram() 199 | gl.attachShader(cameraShaderProgram, cameraVertexShader) 200 | gl.attachShader(cameraShaderProgram, cameraFragmentShader) 201 | gl.linkProgram(cameraShaderProgram) 202 | 203 | var lightVertexShader = gl.createShader(gl.VERTEX_SHADER) 204 | gl.shaderSource(lightVertexShader, lightVertexGLSL) 205 | gl.compileShader(lightVertexShader) 206 | 207 | var lightFragmentShader = gl.createShader(gl.FRAGMENT_SHADER) 208 | gl.shaderSource(lightFragmentShader, lightFragmentGLSL) 209 | gl.compileShader(lightFragmentShader) 210 | 211 | var lightShaderProgram = gl.createProgram() 212 | gl.attachShader(lightShaderProgram, lightVertexShader) 213 | gl.attachShader(lightShaderProgram, lightFragmentShader) 214 | gl.linkProgram(lightShaderProgram) 215 | 216 | /** 217 | * Setting up our buffered data 218 | */ 219 | 220 | // Set up the four corners of our floor quad so that 221 | // we can draw the floor 222 | var floorPositions = [ 223 | // Bottom Left (0) 224 | -30.0, 0.0, 30.0, 225 | // Bottom Right (1) 226 | 30.0, 0.0, 30.0, 227 | // Top Right (2) 228 | 30.0, 0.0, -30.0, 229 | // Top Left (3) 230 | -30.0, 0.0, -30.0 231 | ] 232 | var floorIndices = [ 233 | // Front face 234 | 0, 1, 2, 0, 2, 3 235 | ] 236 | 237 | var dragonPositions = stanfordDragon.positions 238 | var dragonIndices = stanfordDragon.cells 239 | // standford dragon comes with nested arrays that look like this 240 | // [[0, 0, 0,], [1, 0, 1]] 241 | // We flatten them to this so that we can buffer them onto the GPU 242 | // [0, 0, 0, 1, 0, 1] 243 | dragonPositions = dragonPositions.reduce(function (all, vertex) { 244 | // Scale everything down by 10 245 | all.push(vertex[0] / 10) 246 | all.push(vertex[1] / 10) 247 | all.push(vertex[2] / 10) 248 | return all 249 | }, []) 250 | dragonIndices = dragonIndices.reduce(function (all, vertex) { 251 | all.push(vertex[0]) 252 | all.push(vertex[1]) 253 | all.push(vertex[2]) 254 | return all 255 | }, []) 256 | 257 | /** 258 | * Camera shader setup 259 | */ 260 | 261 | // We enable our vertex attributes for our camera's shader. 262 | var vertexPositionAttrib = gl.getAttribLocation(lightShaderProgram, 'aVertexPosition') 263 | gl.enableVertexAttribArray(vertexPositionAttrib) 264 | 265 | var dragonPositionBuffer = gl.createBuffer() 266 | gl.bindBuffer(gl.ARRAY_BUFFER, dragonPositionBuffer) 267 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(dragonPositions), gl.STATIC_DRAW) 268 | gl.vertexAttribPointer(vertexPositionAttrib, 3, gl.FLOAT, false, 0, 0) 269 | 270 | var dragonIndexBuffer = gl.createBuffer() 271 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, dragonIndexBuffer) 272 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(dragonIndices), gl.STATIC_DRAW) 273 | 274 | var floorPositionBuffer = gl.createBuffer() 275 | gl.bindBuffer(gl.ARRAY_BUFFER, floorPositionBuffer) 276 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(floorPositions), gl.STATIC_DRAW) 277 | gl.vertexAttribPointer(vertexPositionAttrib, 3, gl.FLOAT, false, 0, 0) 278 | 279 | var floorIndexBuffer = gl.createBuffer() 280 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, floorIndexBuffer) 281 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(floorIndices), gl.STATIC_DRAW) 282 | 283 | /** 284 | * Light shader setup 285 | */ 286 | 287 | gl.useProgram(lightShaderProgram) 288 | 289 | // This section is the meat of things. We create an off screen frame buffer that we'll render 290 | // our scene onto from our light's viewpoint. We output that to a color texture `shadowDepthTexture`. 291 | // Then later our camera shader will use `shadowDepthTexture` to determine whether or not fragments 292 | // are in the shadow. 293 | var shadowFramebuffer = gl.createFramebuffer() 294 | gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer) 295 | 296 | var shadowDepthTexture = gl.createTexture() 297 | gl.bindTexture(gl.TEXTURE_2D, shadowDepthTexture) 298 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) 299 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) 300 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, shadowDepthTextureSize, shadowDepthTextureSize, 0, gl.RGBA, gl.UNSIGNED_BYTE, null) 301 | 302 | var renderBuffer = gl.createRenderbuffer() 303 | gl.bindRenderbuffer(gl.RENDERBUFFER, renderBuffer) 304 | gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, shadowDepthTextureSize, shadowDepthTextureSize) 305 | 306 | gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, shadowDepthTexture, 0) 307 | gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderBuffer) 308 | 309 | gl.bindTexture(gl.TEXTURE_2D, null) 310 | gl.bindRenderbuffer(gl.RENDERBUFFER, null) 311 | 312 | // We create an orthographic projection and view matrix from which our light 313 | // will vie the scene 314 | var lightProjectionMatrix = glMat4.ortho([], -40, 40, -40, 40, -40.0, 80) 315 | var lightViewMatrix = glMat4.lookAt([], [0, 2, -3], [0, 0, 0], [0, 1, 0]) 316 | 317 | var shadowPMatrix = gl.getUniformLocation(lightShaderProgram, 'uPMatrix') 318 | var shadowMVMatrix = gl.getUniformLocation(lightShaderProgram, 'uMVMatrix') 319 | 320 | gl.uniformMatrix4fv(shadowPMatrix, false, lightProjectionMatrix) 321 | gl.uniformMatrix4fv(shadowMVMatrix, false, lightViewMatrix) 322 | 323 | gl.bindBuffer(gl.ARRAY_BUFFER, floorPositionBuffer) 324 | gl.vertexAttribPointer(vertexPositionAttrib, 3, gl.FLOAT, false, 0, 0) 325 | 326 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, floorIndexBuffer) 327 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(floorIndices), gl.STATIC_DRAW) 328 | 329 | gl.bindFramebuffer(gl.FRAMEBUFFER, null) 330 | 331 | /** 332 | * Scene uniforms 333 | */ 334 | gl.useProgram(cameraShaderProgram) 335 | 336 | var samplerUniform = gl.getUniformLocation(cameraShaderProgram, 'depthColorTexture') 337 | 338 | gl.activeTexture(gl.TEXTURE0) 339 | gl.bindTexture(gl.TEXTURE_2D, shadowDepthTexture) 340 | gl.uniform1i(samplerUniform, 0) 341 | 342 | var uMVMatrix = gl.getUniformLocation(cameraShaderProgram, 'uMVMatrix') 343 | var uPMatrix = gl.getUniformLocation(cameraShaderProgram, 'uPMatrix') 344 | var uLightMatrix = gl.getUniformLocation(cameraShaderProgram, 'lightMViewMatrix') 345 | var uLightProjection = gl.getUniformLocation(cameraShaderProgram, 'lightProjectionMatrix') 346 | var uColor = gl.getUniformLocation(cameraShaderProgram, 'uColor') 347 | 348 | gl.uniformMatrix4fv(uLightMatrix, false, lightViewMatrix) 349 | gl.uniformMatrix4fv(uLightProjection, false, lightProjectionMatrix) 350 | gl.uniformMatrix4fv(uPMatrix, false, glMat4.perspective([], Math.PI / 3, 1, 0.01, 900)) 351 | 352 | // We rotate the dragon about the y axis every frame 353 | var dragonRotateY = 0 354 | 355 | // Draw our dragon onto the shadow map 356 | function drawShadowMap () { 357 | dragonRotateY += 0.01 358 | 359 | gl.useProgram(lightShaderProgram) 360 | 361 | // Draw to our off screen drawing buffer 362 | gl.bindFramebuffer(gl.FRAMEBUFFER, shadowFramebuffer) 363 | 364 | // Set the viewport to our shadow texture's size 365 | gl.viewport(0, 0, shadowDepthTextureSize, shadowDepthTextureSize) 366 | gl.clearColor(0, 0, 0, 1) 367 | gl.clearDepth(1.0) 368 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 369 | 370 | gl.bindBuffer(gl.ARRAY_BUFFER, dragonPositionBuffer) 371 | gl.vertexAttribPointer(vertexPositionAttrib, 3, gl.FLOAT, false, 0, 0) 372 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, dragonIndexBuffer) 373 | 374 | // We draw our dragon onto our shadow map texture 375 | var lightDragonMVMatrix = glMat4.create() 376 | glMat4.rotateY(lightDragonMVMatrix, lightDragonMVMatrix, dragonRotateY) 377 | glMat4.translate(lightDragonMVMatrix, lightDragonMVMatrix, [0, 0, -3]) 378 | glMat4.multiply(lightDragonMVMatrix, lightViewMatrix, lightDragonMVMatrix) 379 | gl.uniformMatrix4fv(shadowMVMatrix, false, lightDragonMVMatrix) 380 | 381 | gl.drawElements(gl.TRIANGLES, dragonIndices.length, gl.UNSIGNED_SHORT, 0) 382 | 383 | gl.bindFramebuffer(gl.FRAMEBUFFER, null) 384 | } 385 | 386 | // Draw our dragon and floor onto the scene 387 | function drawModels () { 388 | gl.useProgram(cameraShaderProgram) 389 | gl.viewport(0, 0, 500, 500) 390 | gl.clearColor(0.98, 0.98, 0.98, 1) 391 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 392 | 393 | // Create our camera view matrix 394 | var camera = glMat4.create() 395 | glMat4.translate(camera, camera, [0, 0, 45]) 396 | var xRotMatrix = glMat4.create() 397 | var yRotMatrix = glMat4.create() 398 | glMat4.rotateX(xRotMatrix, xRotMatrix, -xRotation) 399 | glMat4.rotateY(yRotMatrix, yRotMatrix, yRotation) 400 | glMat4.multiply(camera, xRotMatrix, camera) 401 | glMat4.multiply(camera, yRotMatrix, camera) 402 | camera = glMat4.lookAt(camera, [camera[12], camera[13], camera[14]], [0, 0, 0], [0, 1, 0]) 403 | 404 | var dragonModelMatrix = glMat4.create() 405 | glMat4.rotateY(dragonModelMatrix, dragonModelMatrix, dragonRotateY) 406 | glMat4.translate(dragonModelMatrix, dragonModelMatrix, [0, 0, -3]) 407 | 408 | // We use the light's model view matrix of our dragon so that our camera knows if 409 | // parts of the dragon are in the shadow 410 | var lightDragonMVMatrix = glMat4.create() 411 | glMat4.multiply(lightDragonMVMatrix, lightViewMatrix, dragonModelMatrix) 412 | gl.uniformMatrix4fv(uLightMatrix, false, lightDragonMVMatrix) 413 | 414 | gl.uniformMatrix4fv( 415 | uMVMatrix, 416 | false, 417 | glMat4.multiply(dragonModelMatrix, camera, dragonModelMatrix) 418 | ) 419 | 420 | gl.uniform3fv(uColor, [0.36, 0.66, 0.8]) 421 | 422 | gl.activeTexture(gl.TEXTURE0) 423 | gl.bindTexture(gl.TEXTURE_2D, shadowDepthTexture) 424 | gl.uniform1i(samplerUniform, 0) 425 | 426 | gl.drawElements(gl.TRIANGLES, dragonIndices.length, gl.UNSIGNED_SHORT, 0) 427 | 428 | gl.bindBuffer(gl.ARRAY_BUFFER, floorPositionBuffer) 429 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, floorIndexBuffer) 430 | gl.vertexAttribPointer(vertexPositionAttrib, 3, gl.FLOAT, false, 0, 0) 431 | 432 | gl.uniformMatrix4fv(uLightMatrix, false, lightViewMatrix) 433 | gl.uniformMatrix4fv(uMVMatrix, false, camera) 434 | gl.uniform3fv(uColor, [0.6, 0.6, 0.6]) 435 | 436 | gl.drawElements(gl.TRIANGLES, floorIndices.length, gl.UNSIGNED_SHORT, 0) 437 | } 438 | 439 | // Draw our shadow map and light map every request animation frame 440 | function draw () { 441 | drawShadowMap() 442 | drawModels() 443 | 444 | window.requestAnimationFrame(draw) 445 | } 446 | draw() 447 | --------------------------------------------------------------------------------