├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── assets ├── brick-diffuse.jpg ├── brick-normal.jpg └── brick-specular.jpg ├── index.html ├── index.js ├── lib ├── app.js ├── create-sphere.js ├── create-torus.js ├── scene.js └── shaders │ ├── basic.frag │ ├── basic.vert │ ├── madams-attenuation.glsl │ ├── phong.frag │ ├── phong.vert │ └── reid-attenuation.glsl └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | bundle.js 5 | tmp -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | bundle.js 5 | test 6 | test.js 7 | demo 8 | example 9 | .npmignore -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2014 [stackgl](http://github.com/stackgl/) contributors 5 | 6 | *stackgl contributors listed at * 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## glsl-lighting-walkthrough 2 | 3 | [![final](http://i.imgur.com/9kQcKBP.png)](http://stack.gl/glsl-lighting-walkthrough/) 4 | 5 | [(live demo)](http://stack.gl/glsl-lighting-walkthrough/) 6 | 7 | This article provides an overview of the various steps involved in lighting a mesh with a custom GLSL shader. Some of the features of the demo: 8 | 9 | - per-pixel lighting 10 | - flat & smooth normals 11 | - gamma correction for working in linear space 12 | - normal & specular maps for detail 13 | - attenuation for point light falloff 14 | - Oren-Nayar diffuse for rough surfaces 15 | - Phong reflectance model for specular highlights 16 | 17 | It is not intended as a full-blown beginner's guide, and assumes prior knowledge of WebGL and stackgl rendering. Although it is implemented with stackgl, the same concepts and shader code could be used in ThreeJS and other frameworks. 18 | 19 | If you have questions, comments or improvements, please [post a new issue](https://github.com/stackgl/glsl-lighting-walkthrough/issues). 20 | 21 | ## contents 22 | 23 | - [running from source](#running-from-source) 24 | - [code overview](#code-overview) 25 | - [shaders](#shaders) 26 | - [phong](#phong) 27 | - [standard derivatives](#standard-derivatives) 28 | - [vertex shader](#vertex-shader) 29 | - [flat normals](#flat-normals) 30 | - [smooth normals](#smooth-normals) 31 | - [gamma correction](#gamma-correction) 32 | - [normal mapping](#normal-mapping) 33 | - [light attenuation](#light-attenuation) 34 | - [diffuse](#diffuse) 35 | - [specular](#specular) 36 | - [final color](#final-color) 37 | 38 | ## running from source 39 | 40 | To run from source: 41 | 42 | ```sh 43 | git clone https://github.com/stackgl/glsl-lighting-walkthrough.git 44 | cd glsl-lighting-walkthrough 45 | 46 | npm install 47 | npm run start 48 | ``` 49 | 50 | And then open `http://localhost:9966` to see the demo. Changes to the source will live-reload the browser for development. 51 | 52 | To build: 53 | 54 | ```sh 55 | npm run build 56 | ``` 57 | 58 | ## code overview 59 | 60 | The code is using Babelify for ES6 template strings, destructuring, and arrow functions. It is organized like so: 61 | 62 | - [index.js](index.js) - loads images, then boots up the app 63 | - [lib/app.js](lib/app.js) - sets up a WebGL render loop and draws the scene 64 | - [lib/scene.js](lib/scene.js) - sets up textures, positions the light and draws meshes 65 | - [lib/create-sphere.js](lib/create-sphere.js) - create a 3D sphere for the light source 66 | - [lib/create-torus.js](lib/create-torus.js) - creates a 3D torus with a phong shader 67 | 68 | ## shaders 69 | 70 | [glslify](https://github.com/stackgl/glslify) is used to modularize the shaders and pull some common functions from [npm](https://www.npmjs.com/). 71 | 72 | We use a "basic" material for our light indicator, so that it appears at a constant color regardless of depth and lighting: 73 | 74 | - [shaders/basic.frag](lib/shaders/basic.frag) 75 | - [shaders/basic.vert](lib/shaders/basic.vert) 76 | 77 | We use a "phong" material for our torus, which we will explore in more depth below. 78 | 79 | - [shaders/phong.frag](lib/shaders/phong.frag) 80 | - [shaders/phong.vert](lib/shaders/phong.vert) 81 | 82 | There are many ways to skin a cat; this is just one approach to phong shading. 83 | 84 | ## phong 85 | 86 | ### standard derivatives 87 | 88 | Our phong shader uses standard derivatives, so we need to enable the extension before we create it. The JavaScript code looks like this: 89 | 90 | ```js 91 | //enable the extension 92 | var ext = gl.getExtension('OES_standard_derivatives') 93 | if (!ext) 94 | throw new Error('derivatives not supported') 95 | 96 | var shader = createShader(gl, vert, frag) 97 | ... 98 | ``` 99 | 100 | And, in our fragment shader we need to enable it explicitly: 101 | 102 | ```glsl 103 | #extension GL_OES_standard_derivatives : enable 104 | precision highp float; 105 | 106 | void main() { 107 | ... 108 | } 109 | ``` 110 | 111 | The extension is used in two places in our final shader: 112 | 113 | - [glsl-face-normal](https://www.npmjs.com/package/glsl-face-normal) for flat shading (optional) 114 | - [glsl-perturb-normal](https://www.npmjs.com/package/glsl-perturb-normal) for normal-mapping 115 | 116 | ### vertex shader 117 | 118 | ![white](http://i.imgur.com/J24k2iu.png) 119 | 120 | Our vertex shader needs to pass the texture coordinates and view space position to the fragment shader. 121 | 122 | A basic vertex shader looks like this: 123 | 124 | ```glsl 125 | attribute vec4 position; 126 | attribute vec2 uv; 127 | 128 | uniform mat4 projection; 129 | uniform mat4 view; 130 | uniform mat4 model; 131 | 132 | varying vec2 vUv; 133 | varying vec3 vViewPosition; 134 | 135 | void main() { 136 | //determine view space position 137 | mat4 modelViewMatrix = view * model; 138 | vec4 viewModelPosition = modelViewMatrix * position; 139 | 140 | //pass varyings to fragment shader 141 | vViewPosition = viewModelPosition.xyz; 142 | vUv = uv; 143 | 144 | //determine final 3D position 145 | gl_Position = projection * viewModelPosition; 146 | } 147 | ``` 148 | 149 | ### flat normals 150 | 151 | ![flat](http://i.imgur.com/YvuhBGk.png) 152 | 153 | If you want flat shading, you don't need to submit normals as a vertex attribute. Instead, you can use [glsl-face-normal](https://www.npmjs.com/package/glsl-face-normal) to estimate them in the fragment shader: 154 | 155 | ```glsl 156 | #pragma glslify: faceNormals = require('glsl-face-normal') 157 | 158 | varying vec3 vViewPosition; 159 | 160 | void main() { 161 | vec3 normal = faceNormals(vViewPosition); 162 | gl_FragColor = vec4(normal, 1.0); 163 | } 164 | ``` 165 | 166 | ### smooth normals 167 | 168 | ![smooth](http://i.imgur.com/hnYlRG5.png) 169 | 170 | For smooth normals, we use the object space normals from [torus-mesh](https://www.npmjs.com/package/torus-mesh) and pass them to the fragment shader to have them interpolated between vertices. 171 | 172 | To transform the object normals into view space, we multiply them by a "normal matrix" - the inverse transpose of the model view matrix. 173 | 174 | Since this doesn't change vertex to vertex, you can do it CPU-side and pass it as a uniform to the vertex shader. 175 | 176 | Or, you can just simply compute the normal matrix in the vertex step. GLSL ES does not provide built-in `transpose()` or `inverse()`, so we need to require them from npm: 177 | 178 | - [glsl-inverse](https://www.npmjs.com/package/glsl-inverse) 179 | - [glsl-transpose](https://www.npmjs.com/package/glsl-transpose) 180 | 181 | ```glsl 182 | //object normals 183 | attribute vec3 normal; 184 | varying vec3 vNormal; 185 | 186 | #pragma glslify: transpose = require('glsl-transpose') 187 | #pragma glslify: inverse = require('glsl-inverse') 188 | 189 | void main() { 190 | ... 191 | 192 | // Rotate the object normals by a 3x3 normal matrix. 193 | mat3 normalMatrix = transpose(inverse(mat3(modelViewMatrix))); 194 | vNormal = normalize(normalMatrix * normal); 195 | } 196 | ``` 197 | 198 | ### gamma correction 199 | 200 | When dealing with PNG and JPG textures, it's important to remember that they most likely have gamma correction applied to them already, and so we need to account for it when doing any work in linear space. 201 | 202 | We can use `pow(value, 2.2)` and `pow(value, 1.0 / 2.2)` to convert to and from the gamma-corrected space. Or, [glsl-gamma](https://github.com/stackgl/glsl-gamma) can be used for convenience. 203 | 204 | ```glsl 205 | #pragma glslify: toLinear = require('glsl-gamma/in') 206 | #pragma glslify: toGamma = require('glsl-gamma/out') 207 | 208 | vec4 textureLinear(sampler2D uTex, vec2 uv) { 209 | return toLinear(texture2D(uTex, uv)); 210 | } 211 | 212 | void main() { 213 | //sample sRGB and account for gamma 214 | vec4 diffuseColor = textureLinear(texDiffuse, uv); 215 | 216 | //operate on RGB in linear space 217 | ... 218 | 219 | //output final color to sRGB space 220 | color = toGamma(color); 221 | } 222 | ``` 223 | 224 | For details, see [GPU Gems - The Importance of Being Linear](http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html). 225 | 226 | ### normal mapping 227 | 228 | ![normalmap](http://i.imgur.com/cJce72J.png) 229 | 230 | We can use normal maps to add detail to the shading without additional topology. 231 | 232 | A normal map typically stores a unit vector `[X,Y,Z]` in an image's `[R,G,B]` channels, respectively. The 0-1 colors are expanded into the -1 to 1 range, representing the unit vector. 233 | 234 | ```glsl 235 | // ... fragment shader ... 236 | 237 | //sample texture and expand to -1 .. 1 238 | vec3 normalMap = textureLinear(texNormal, uv) * 2.0 - 1.0; 239 | 240 | //some normal maps use an inverted green channel 241 | normalMap.y *= -1.0; 242 | 243 | //determine perturbed surface normal 244 | vec3 V = normalize(vViewPosition); 245 | vec3 N = perturb(normalMap, normal, -V, vUv); 246 | ``` 247 | 248 | ### light attenuation 249 | 250 | ![attenuation](http://i.imgur.com/qZUMbUd.png) 251 | 252 | For lighting, we need to determine the vector from the view space surface position to the view space light position. Then we can account for attenuation (falloff based on the distance from light), diffuse, and specular. 253 | 254 | The relevant bits of the fragment shader: 255 | 256 | ```glsl 257 | uniform mat4 view; 258 | 259 | #pragma glslify: attenuation = require('./attenuation') 260 | 261 | void main() { 262 | ... 263 | 264 | //determine surface to light vector 265 | vec4 lightPosition = view * vec4(light.position, 1.0); 266 | vec3 lightVector = lightPosition.xyz - vViewPosition; 267 | 268 | //calculate attenuation 269 | float lightDistance = length(lightVector); 270 | float falloff = attenuation(light.radius, light.falloff, lightDistance); 271 | 272 | //light direction 273 | vec3 L = normalize(lightVector); 274 | 275 | ... 276 | } 277 | ``` 278 | 279 | Our chosen [attenuation function](lib/shaders/madams-attenuation.glsl) is by Tom Madams, but there are many others that we could choose from. 280 | 281 | ```glsl 282 | float attenuation(float r, float f, float d) { 283 | float denom = d / r + 1.0; 284 | float attenuation = 1.0 / (denom*denom); 285 | float t = (attenuation - f) / (1.0 - f); 286 | return max(t, 0.0); 287 | } 288 | ``` 289 | 290 | ### diffuse 291 | 292 | ![diffuse](http://i.imgur.com/pfqQCN7.png) 293 | 294 | With our light direction, surface normal, and view direction, we can start to work on diffuse lighting. The color is multiplied by falloff to create the effect of a distant light. 295 | 296 | For rough surfaces, [glsl-diffuse-oren-nayar](https://www.npmjs.com/package/glsl-diffuse-oren-nayar) looks a bit better than [glsl-diffuse-lambert](https://www.npmjs.com/package/glsl-diffuse-lambert). 297 | 298 | ```glsl 299 | #pragma glslify: computeDiffuse = require('glsl-diffuse-oren-nayar') 300 | 301 | ... 302 | 303 | //diffuse term 304 | vec3 diffuse = light.color * computeDiffuse(L, V, N, roughness, albedo) * falloff; 305 | 306 | //texture color 307 | vec3 diffuseColor = textureLinear(texDiffuse, uv).rgb; 308 | ``` 309 | 310 | These shading functions are known as [bidirectional reflectance distribution functions](http://en.wikipedia.org/wiki/Bidirectional_reflectance_distribution_function) (BRDF). 311 | 312 | ### specular 313 | 314 | ![specular](http://i.imgur.com/lDimd4U.png) 315 | 316 | Similarly, we can apply specular with one of the following BRDFs: 317 | 318 | - [glsl-specular-blinn-phong](https://www.npmjs.com/package/glsl-specular-blinn-phong) 319 | - [glsl-specular-phong](https://www.npmjs.com/package/glsl-specular-phong) 320 | - [glsl-specular-ward](https://www.npmjs.com/package/glsl-specular-ward) 321 | - [glsl-specular-gaussian](https://www.npmjs.com/package/glsl-specular-gaussian) 322 | - [glsl-specular-beckmann](https://www.npmjs.com/package/glsl-specular-beckmann) 323 | - [glsl-specular-cook-torrance](https://www.npmjs.com/package/glsl-specular-cook-torrance) 324 | 325 | Which one you choose depends on the material and aesthetic you are working with. In our case, `glsl-specular-phong` looks pretty good. 326 | 327 | The above screenshot is scaled by 100x for demonstration, using `specularScale` to drive the strength. The specular is also affected by the light attenuation. 328 | 329 | ```glsl 330 | #pragma glslify: computeSpecular = require('glsl-specular-phong') 331 | 332 | ... 333 | 334 | float specularStrength = textureLinear(texSpecular, uv).r; 335 | float specular = specularStrength * computeSpecular(L, V, N, shininess); 336 | specular *= specularScale; 337 | specular *= falloff; 338 | ``` 339 | 340 | ### final color 341 | 342 | ![final](http://i.imgur.com/ZN5FmKz.png) 343 | 344 | We now calculate the final color in the following manner. 345 | 346 | ```glsl 347 | ... 348 | //compute final color 349 | vec3 color = diffuseColor * (diffuse + light.ambient) + specular; 350 | ``` 351 | 352 | Our final color is going straight to the screen, so we should re-apply the gamma correction we removed earlier. If the color was going through a post-processing pipeline, we could continue operating in linear space until the final step. 353 | 354 | ```glsl 355 | ... 356 | //output color 357 | gl_FragColor.rgb = toGamma(color); 358 | gl_FragColor.a = 1.0; 359 | ``` 360 | 361 | The [final result](http://stack.gl/glsl-lighting-walkthrough/). 362 | 363 | ## Further Reading 364 | 365 | - [Tom Dalling - Modern OpenGL Series](http://www.tomdalling.com/blog/category/modern-opengl/) 366 | - [GPU Gems - The Importance of Being Linear](http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html) 367 | - [Normal Mapping Without Precomputed Tangents](http://www.thetenthplanet.de/archives/1180) 368 | 369 | ## License 370 | 371 | MIT. See [LICENSE.md](http://github.com/stackgl/glsl-lighting-walkthrough/blob/master/LICENSE.md) for details. -------------------------------------------------------------------------------- /assets/brick-diffuse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackgl/glsl-lighting-walkthrough/4b0056966cb2a8cb48b1c6959b0e4e66bf9f1c14/assets/brick-diffuse.jpg -------------------------------------------------------------------------------- /assets/brick-normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackgl/glsl-lighting-walkthrough/4b0056966cb2a8cb48b1c6959b0e4e66bf9f1c14/assets/brick-normal.jpg -------------------------------------------------------------------------------- /assets/brick-specular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackgl/glsl-lighting-walkthrough/4b0056966cb2a8cb48b1c6959b0e4e66bf9f1c14/assets/brick-specular.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | glsl-lighting-walkthrough 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var createApp = require('./lib/app') 2 | var each = require('async-each') 3 | var loadImage = require('img') 4 | 5 | // load our texture maps 6 | var names = ['diffuse', 'normal', 'specular'] 7 | var urls = names.map(x => { 8 | return `assets/brick-${x}.jpg` 9 | }) 10 | 11 | each(urls, loadImage, (err, images) => { 12 | if (err) 13 | throw err 14 | 15 | var app = createApp(images) 16 | document.body.appendChild(app.canvas) 17 | }) 18 | -------------------------------------------------------------------------------- /lib/app.js: -------------------------------------------------------------------------------- 1 | var context = require('webgl-context') 2 | var loop = require('canvas-loop') 3 | var assign = require('object-assign') 4 | var createCamera = require('perspective-camera') 5 | var createScene = require('./scene') 6 | 7 | module.exports = function (images) { 8 | // get a retina-scaled WebGL canvas 9 | var gl = context() 10 | var canvas = gl.canvas 11 | var app = loop(canvas, { 12 | scale: window.devicePixelRatio 13 | }).on('tick', render) 14 | 15 | // create a simple perspective camera 16 | // contains our projection & view matrices 17 | var camera = createCamera({ 18 | fov: Math.PI / 4, 19 | near: 0.01, 20 | far: 100 21 | }) 22 | 23 | // create our custom scene 24 | var drawScene = createScene(gl, images) 25 | 26 | var time = 0 27 | app.start() 28 | 29 | return assign(app, { 30 | canvas, 31 | gl 32 | }) 33 | 34 | function render (dt) { 35 | // our screen-space viewport 36 | var [ width, height ] = app.shape 37 | 38 | time += dt / 1000 39 | 40 | // set WebGL viewport to device size 41 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight) 42 | gl.enable(gl.DEPTH_TEST) 43 | gl.enable(gl.CULL_FACE) 44 | 45 | gl.clearColor(0.04, 0.04, 0.04, 1) 46 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) 47 | 48 | // rotate the camera around origin 49 | var rotation = Math.PI / 4 + time * 0.2 50 | var radius = 4 51 | var x = Math.cos(rotation) * radius 52 | var z = Math.sin(rotation) * radius 53 | camera.identity() 54 | camera.translate([ x, 0, z ]) 55 | camera.lookAt([ 0, 0, 0 ]) 56 | camera.viewport = [ 0, 0, width, height ] 57 | camera.update() 58 | 59 | // draw our scene 60 | drawScene(time, camera) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/create-sphere.js: -------------------------------------------------------------------------------- 1 | var createGeometry = require('gl-geometry') 2 | var createShader = require('gl-shader') 3 | var mat4 = require('gl-mat4') 4 | var icosphere = require('icosphere') 5 | 6 | var glslify = require('glslify') 7 | var vert = glslify('./shaders/basic.vert') 8 | var frag = glslify('./shaders/basic.frag') 9 | 10 | module.exports = function(gl) { 11 | //create our shader 12 | var shader = createShader(gl, vert, frag) 13 | 14 | //set up a sphere geometry 15 | var mesh = icosphere(2) 16 | var geom = createGeometry(gl) 17 | .attr('position', mesh.positions) 18 | .faces(mesh.cells) 19 | 20 | //the model-space transform for our sphere 21 | var model = mat4.create() 22 | var s = 0.05 23 | var scale = [s, s, s] 24 | 25 | var sphere = { 26 | position: [0, 0, 0], 27 | color: [1, 0, 0], 28 | draw: draw 29 | } 30 | 31 | return sphere 32 | 33 | function draw(camera) { 34 | //set up our model matrix 35 | mat4.identity(model) 36 | mat4.translate(model, model, sphere.position) 37 | mat4.scale(model, model, scale) 38 | 39 | //set our uniforms for the shader 40 | shader.bind() 41 | shader.uniforms.projection = camera.projection 42 | shader.uniforms.view = camera.view 43 | shader.uniforms.model = model 44 | shader.uniforms.color = sphere.color 45 | 46 | //draw the mesh 47 | geom.bind(shader) 48 | geom.draw(gl.TRIANGLES) 49 | geom.unbind() 50 | } 51 | } -------------------------------------------------------------------------------- /lib/create-torus.js: -------------------------------------------------------------------------------- 1 | /* 2 | Creates a new 3D torus with its own shader and vertex buffers. 3 | */ 4 | 5 | var createGeometry = require('gl-geometry') 6 | var createShader = require('gl-shader') 7 | var createTorus = require('torus-mesh') 8 | var mat4 = require('gl-mat4') 9 | 10 | //our phong shader for the brick torus 11 | var glslify = require('glslify') 12 | var vert = glslify('./shaders/phong.vert') 13 | var frag = glslify('./shaders/phong.frag') 14 | 15 | module.exports = function(gl) { 16 | var complex = createTorus({ 17 | majorSegments: 64, 18 | minorSegments: 64 19 | }) 20 | 21 | //enable derivatives for face normals 22 | var ext = gl.getExtension('OES_standard_derivatives') 23 | if (!ext) 24 | throw new Error('derivatives not supported') 25 | 26 | //create our shader 27 | var shader = createShader(gl, vert, frag) 28 | 29 | //create a geometry with some vertex attributes 30 | var geom = createGeometry(gl) 31 | .attr('position', complex.positions) 32 | .attr('normal', complex.normals) 33 | .attr('uv', complex.uvs, { size: 2 }) 34 | .faces(complex.cells) 35 | 36 | //our model-space transformations 37 | var model = mat4.create() 38 | 39 | var mesh = { 40 | draw: draw, 41 | light: null, 42 | flatShading: false, 43 | } 44 | 45 | return mesh 46 | 47 | function draw(camera) { 48 | //set our uniforms for the shader 49 | shader.bind() 50 | shader.uniforms.projection = camera.projection 51 | shader.uniforms.view = camera.view 52 | shader.uniforms.model = model 53 | shader.uniforms.flatShading = mesh.flatShading ? 1 : 0 54 | shader.uniforms.light = mesh.light 55 | shader.uniforms.texDiffuse = 0 56 | shader.uniforms.texNormal = 1 57 | shader.uniforms.texSpecular = 2 58 | 59 | //draw the mesh 60 | geom.bind(shader) 61 | geom.draw(gl.TRIANGLES) 62 | geom.unbind() 63 | } 64 | } -------------------------------------------------------------------------------- /lib/scene.js: -------------------------------------------------------------------------------- 1 | /* 2 | Brings together the textures, mesh, and lights into a unified scene. 3 | */ 4 | 5 | var createTorus = require('./create-torus') 6 | var createSphere = require('./create-sphere') 7 | var createTexture = require('gl-texture2d') 8 | var hex = require('hex-rgb') 9 | 10 | var hex2rgb = (str) => { 11 | return hex(str).map(x => x/255) 12 | } 13 | 14 | module.exports = function(gl, images) { 15 | //the 3D objects for our scene 16 | var mesh = createTorus(gl) 17 | var sphere = createSphere(gl) 18 | 19 | 20 | //upload our textures with mipmapping and repeat wrapping 21 | var textures = images.map(image => { 22 | var tex = createTexture(gl, image) 23 | //setup smooth scaling 24 | tex.bind() 25 | tex.generateMipmap() 26 | tex.minFilter = gl.LINEAR_MIPMAP_LINEAR 27 | tex.magFilter = gl.LINEAR 28 | 29 | //and repeat wrapping 30 | tex.wrap = gl.REPEAT 31 | 32 | //minimize distortion on hard angles 33 | var ext = gl.getExtension('EXT_texture_filter_anisotropic') 34 | if (ext) { 35 | var maxAnistrophy = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT) 36 | tex.bind() 37 | gl.texParameterf(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(16, maxAnistrophy)) 38 | } 39 | 40 | return tex 41 | }) 42 | 43 | var [ diffuse, normal, specular ] = textures 44 | 45 | var light = { 46 | falloff: 0.15, 47 | radius: 5, 48 | position: [0, 0, 0], 49 | color: hex2rgb('#ffc868'), 50 | ambient: hex2rgb('#0a040b') 51 | } 52 | 53 | return function draw(time, camera) { 54 | // move our light around 55 | light.position[0] = -Math.sin(time/2)*0.9 56 | light.position[1] = Math.sin(time/2)*0.3 57 | light.position[2] = 0.5+Math.sin(time/2)*2 58 | 59 | // bind our textures to the correct slots 60 | diffuse.bind(0) 61 | normal.bind(1) 62 | specular.bind(2) 63 | 64 | // draw our phong mesh 65 | mesh.light = light 66 | mesh.draw(camera) 67 | 68 | // draw our light indicator 69 | sphere.position = light.position 70 | sphere.color = light.color 71 | sphere.draw(camera) 72 | } 73 | } -------------------------------------------------------------------------------- /lib/shaders/basic.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform vec3 color; 4 | 5 | void main() { 6 | gl_FragColor = vec4(color, 1.0); 7 | } 8 | -------------------------------------------------------------------------------- /lib/shaders/basic.vert: -------------------------------------------------------------------------------- 1 | attribute vec4 position; 2 | uniform mat4 projection; 3 | uniform mat4 view; 4 | uniform mat4 model; 5 | 6 | void main() { 7 | gl_Position = projection * view * model * position; 8 | } 9 | -------------------------------------------------------------------------------- /lib/shaders/madams-attenuation.glsl: -------------------------------------------------------------------------------- 1 | // by Tom Madams 2 | // Simple: 3 | // https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/ 4 | // 5 | // Improved 6 | // https://imdoingitwrong.wordpress.com/2011/02/10/improved-light-attenuation/ 7 | float attenuation(float r, float f, float d) { 8 | float denom = d / r + 1.0; 9 | float attenuation = 1.0 / (denom*denom); 10 | float t = (attenuation - f) / (1.0 - f); 11 | return max(t, 0.0); 12 | } 13 | 14 | #pragma glslify: export(attenuation) -------------------------------------------------------------------------------- /lib/shaders/phong.frag: -------------------------------------------------------------------------------- 1 | #extension GL_OES_standard_derivatives : enable 2 | precision highp float; 3 | 4 | //our custom Light struct 5 | struct Light { 6 | vec3 position; 7 | vec3 color; 8 | vec3 ambient; 9 | float falloff; 10 | float radius; 11 | }; 12 | 13 | varying vec2 vUv; 14 | varying vec3 vViewPosition; 15 | varying vec3 vNormal; 16 | 17 | //import some common functions 18 | #pragma glslify: faceNormals = require('glsl-face-normal') 19 | #pragma glslify: perturb = require('glsl-perturb-normal') 20 | #pragma glslify: computeDiffuse = require('glsl-diffuse-oren-nayar') 21 | #pragma glslify: computeSpecular = require('glsl-specular-phong') 22 | #pragma glslify: attenuation = require('./madams-attenuation') 23 | #pragma glslify: toLinear = require('glsl-gamma/in') 24 | #pragma glslify: toGamma = require('glsl-gamma/out') 25 | 26 | //some settings for the look and feel of the material 27 | const vec2 UV_SCALE = vec2(8.0, 1.0); 28 | const float specularScale = 0.65; 29 | const float shininess = 20.0; 30 | const float roughness = 1.0; 31 | const float albedo = 0.95; 32 | 33 | uniform sampler2D texDiffuse; 34 | uniform sampler2D texNormal; 35 | uniform sampler2D texSpecular; 36 | 37 | uniform int flatShading; 38 | uniform mat4 model; 39 | uniform mat4 view; 40 | 41 | uniform Light light; 42 | 43 | //account for gamma-corrected images 44 | vec4 textureLinear(sampler2D uTex, vec2 uv) { 45 | return toLinear(texture2D(uTex, uv)); 46 | } 47 | 48 | void main() { 49 | //determine the type of normals for lighting 50 | vec3 normal = vec3(0.0); 51 | if (flatShading == 1) { 52 | normal = faceNormals(vViewPosition); 53 | } else { 54 | normal = vNormal; 55 | } 56 | 57 | //determine surface to light direction 58 | vec4 lightPosition = view * vec4(light.position, 1.0); 59 | vec3 lightVector = lightPosition.xyz - vViewPosition; 60 | vec3 color = vec3(0.0); 61 | 62 | //calculate attenuation 63 | float lightDistance = length(lightVector); 64 | float falloff = attenuation(light.radius, light.falloff, lightDistance); 65 | 66 | //now sample from our repeating brick texture 67 | //assume its in sRGB, so we need to correct for gamma 68 | vec2 uv = vUv * UV_SCALE; 69 | vec3 diffuseColor = textureLinear(texDiffuse, uv).rgb; 70 | vec3 normalMap = textureLinear(texNormal, uv).rgb * 2.0 - 1.0; 71 | float specularStrength = textureLinear(texSpecular, uv).r; 72 | 73 | //our normal map has an inverted green channel 74 | normalMap.y *= -1.0; 75 | 76 | vec3 L = normalize(lightVector); //light direction 77 | vec3 V = normalize(vViewPosition); //eye direction 78 | vec3 N = perturb(normalMap, normal, -V, vUv); //surface normal 79 | 80 | //compute our diffuse & specular terms 81 | float specular = specularStrength * computeSpecular(L, V, N, shininess) * specularScale * falloff; 82 | vec3 diffuse = light.color * computeDiffuse(L, V, N, roughness, albedo) * falloff; 83 | vec3 ambient = light.ambient; 84 | 85 | //add the lighting 86 | color += diffuseColor * (diffuse + ambient) + specular; 87 | 88 | //re-apply gamma to output buffer 89 | color = toGamma(color); 90 | gl_FragColor.rgb = color; 91 | gl_FragColor.a = 1.0; 92 | } -------------------------------------------------------------------------------- /lib/shaders/phong.vert: -------------------------------------------------------------------------------- 1 | attribute vec4 position; 2 | attribute vec2 uv; 3 | attribute vec3 normal; 4 | 5 | uniform mat4 projection; 6 | uniform mat4 view; 7 | uniform mat4 model; 8 | 9 | varying vec3 vNormal; 10 | varying vec2 vUv; 11 | varying vec3 vViewPosition; 12 | 13 | //import some common functions not supported by GLSL ES 14 | #pragma glslify: transpose = require('glsl-transpose') 15 | #pragma glslify: inverse = require('glsl-inverse') 16 | 17 | void main() { 18 | mat4 modelViewMatrix = view * model; 19 | vec4 viewModelPosition = modelViewMatrix * position; 20 | 21 | // Pass varyings to fragment shader 22 | vViewPosition = viewModelPosition.xyz; 23 | vUv = uv; 24 | gl_Position = projection * viewModelPosition; 25 | 26 | // Rotate the object normals by a 3x3 normal matrix. 27 | // We could also do this CPU-side to avoid doing it per-vertex 28 | mat3 normalMatrix = transpose(inverse(mat3(modelViewMatrix))); 29 | vNormal = normalize(normalMatrix * normal); 30 | } 31 | -------------------------------------------------------------------------------- /lib/shaders/reid-attenuation.glsl: -------------------------------------------------------------------------------- 1 | // by David Reid - Source: 2 | // https://kookaburragamer.wordpress.com/2013/03/24/user-friendly-exponential-light-attenuation/ 3 | float attenuation(float r, float f, float d) { 4 | return pow(max(0.0, 1.0 - (d / r)), f + 1.0); 5 | } 6 | 7 | #pragma glslify: export(attenuation) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glsl-lighting-walkthrough", 3 | "version": "1.0.0", 4 | "description": "an example of shading in GLSL with glslify", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "budo index.js:bundle.js --live --verbose -- -t babelify -t glslify -p errorify | garnish", 9 | "build": "browserify index.js -t babelify -t glslify | uglifyjs -cm > bundle.js" 10 | }, 11 | "author": { 12 | "name": "Matt DesLauriers", 13 | "email": "dave.des@gmail.com", 14 | "url": "http://github.com/mattdesl" 15 | }, 16 | "dependencies": { 17 | "async-each": "^0.1.6", 18 | "canvas-loop": "^1.0.4", 19 | "gl-geometry": "^1.0.3", 20 | "gl-mat4": "^1.1.3", 21 | "gl-shader": "^4.0.1", 22 | "gl-texture2d": "^2.0.8", 23 | "glsl-diffuse-oren-nayar": "^1.0.2", 24 | "glsl-face-normal": "^1.0.2", 25 | "glsl-gamma": "^2.0.0", 26 | "glsl-inverse": "^1.0.0", 27 | "glsl-perturb-normal": "^1.0.2", 28 | "glsl-specular-phong": "^1.0.0", 29 | "glsl-transpose": "^1.0.0", 30 | "glslify": "^2.1.2", 31 | "hex-rgb": "^1.0.0", 32 | "icosphere": "^1.0.0", 33 | "img": "^1.0.0", 34 | "object-assign": "^2.0.0", 35 | "perspective-camera": "^1.0.0", 36 | "torus-mesh": "^1.0.0", 37 | "webgl-context": "^2.1.2" 38 | }, 39 | "devDependencies": { 40 | "babelify": "^6.0.2", 41 | "browserify": "^10.1.3", 42 | "budo": "^4.0.0", 43 | "errorify": "^0.2.4", 44 | "garnish": "^2.1.3", 45 | "uglify-js": "^2.4.21" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git://github.com/stackgl/glsl-lighting-walkthrough.git" 50 | }, 51 | "keywords": [ 52 | "ecosystem:stackgl" 53 | ], 54 | "homepage": "https://github.com/stackgl/glsl-lighting-walkthrough", 55 | "bugs": { 56 | "url": "https://github.com/stackgl/glsl-lighting-walkthrough/issues" 57 | } 58 | } 59 | --------------------------------------------------------------------------------