├── .gitignore
├── .npmignore
├── assets
├── brick-diffuse.jpg
├── brick-normal.jpg
└── brick-specular.jpg
├── lib
├── shaders
│ ├── basic.frag
│ ├── basic.vert
│ ├── reid-attenuation.glsl
│ ├── madams-attenuation.glsl
│ ├── phong.vert
│ └── phong.frag
├── create-sphere.js
├── app.js
├── create-torus.js
└── scene.js
├── index.html
├── index.js
├── LICENSE.md
├── package.json
└── README.md
/.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
--------------------------------------------------------------------------------
/assets/brick-diffuse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackgl/glsl-lighting-walkthrough/HEAD/assets/brick-diffuse.jpg
--------------------------------------------------------------------------------
/assets/brick-normal.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackgl/glsl-lighting-walkthrough/HEAD/assets/brick-normal.jpg
--------------------------------------------------------------------------------
/assets/brick-specular.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackgl/glsl-lighting-walkthrough/HEAD/assets/brick-specular.jpg
--------------------------------------------------------------------------------
/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/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)
--------------------------------------------------------------------------------
/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/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.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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-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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## glsl-lighting-walkthrough
2 |
3 | [](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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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.
--------------------------------------------------------------------------------