• Stars
    star
    458
  • Rank 95,591 (Top 2 %)
  • Language
    JavaScript
  • License
    Other
  • Created over 9 years ago
  • Updated over 9 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

💡 phong shading tutorial with glslify

glsl-lighting-walkthrough

final

(live demo)

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:

  • per-pixel lighting
  • flat & smooth normals
  • gamma correction for working in linear space
  • normal & specular maps for detail
  • attenuation for point light falloff
  • Oren-Nayar diffuse for rough surfaces
  • Phong reflectance model for specular highlights

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.

If you have questions, comments or improvements, please post a new issue.

contents

running from source

To run from source:

git clone https://github.com/stackgl/glsl-lighting-walkthrough.git
cd glsl-lighting-walkthrough

npm install
npm run start

And then open http://localhost:9966 to see the demo. Changes to the source will live-reload the browser for development.

To build:

npm run build

code overview

The code is using Babelify for ES6 template strings, destructuring, and arrow functions. It is organized like so:

shaders

glslify is used to modularize the shaders and pull some common functions from npm.

We use a "basic" material for our light indicator, so that it appears at a constant color regardless of depth and lighting:

We use a "phong" material for our torus, which we will explore in more depth below.

There are many ways to skin a cat; this is just one approach to phong shading.

phong

standard derivatives

Our phong shader uses standard derivatives, so we need to enable the extension before we create it. The JavaScript code looks like this:

//enable the extension
var ext = gl.getExtension('OES_standard_derivatives')
if (!ext)
  throw new Error('derivatives not supported')

var shader = createShader(gl, vert, frag)
...

And, in our fragment shader we need to enable it explicitly:

#extension GL_OES_standard_derivatives : enable
precision highp float;

void main() {
  ...
}

The extension is used in two places in our final shader:

vertex shader

white

Our vertex shader needs to pass the texture coordinates and view space position to the fragment shader.

A basic vertex shader looks like this:

attribute vec4 position;
attribute vec2 uv;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

varying vec2 vUv;
varying vec3 vViewPosition;

void main() {
  //determine view space position
  mat4 modelViewMatrix = view * model;
  vec4 viewModelPosition = modelViewMatrix * position;
  
  //pass varyings to fragment shader
  vViewPosition = viewModelPosition.xyz;
  vUv = uv;

  //determine final 3D position
  gl_Position = projection * viewModelPosition;
}

flat normals

flat

If you want flat shading, you don't need to submit normals as a vertex attribute. Instead, you can use glsl-face-normal to estimate them in the fragment shader:

#pragma glslify: faceNormals = require('glsl-face-normal')

varying vec3 vViewPosition;

void main() {
  vec3 normal = faceNormals(vViewPosition);
  gl_FragColor = vec4(normal, 1.0);
}

smooth normals

smooth

For smooth normals, we use the object space normals from torus-mesh and pass them to the fragment shader to have them interpolated between vertices.

To transform the object normals into view space, we multiply them by a "normal matrix" - the inverse transpose of the model view matrix.

Since this doesn't change vertex to vertex, you can do it CPU-side and pass it as a uniform to the vertex shader.

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:

//object normals
attribute vec3 normal;
varying vec3 vNormal;

#pragma glslify: transpose = require('glsl-transpose')
#pragma glslify: inverse = require('glsl-inverse')

void main() {
  ...

  // Rotate the object normals by a 3x3 normal matrix.
  mat3 normalMatrix = transpose(inverse(mat3(modelViewMatrix)));
  vNormal = normalize(normalMatrix * normal);
}

gamma correction

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.

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 can be used for convenience.

#pragma glslify: toLinear = require('glsl-gamma/in')
#pragma glslify: toGamma  = require('glsl-gamma/out')

vec4 textureLinear(sampler2D uTex, vec2 uv) {
  return toLinear(texture2D(uTex, uv));
}

void main() {
  //sample sRGB and account for gamma
  vec4 diffuseColor = textureLinear(texDiffuse, uv);

  //operate on RGB in linear space
  ...
  
  //output final color to sRGB space
  color = toGamma(color);
}

For details, see GPU Gems - The Importance of Being Linear.

normal mapping

normalmap

We can use normal maps to add detail to the shading without additional topology.

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.

  // ... fragment shader ...

  //sample texture and expand to -1 .. 1
  vec3 normalMap = textureLinear(texNormal, uv) * 2.0 - 1.0;

  //some normal maps use an inverted green channel
  normalMap.y *= -1.0;

  //determine perturbed surface normal
  vec3 V = normalize(vViewPosition); 
  vec3 N = perturb(normalMap, normal, -V, vUv);

light attenuation

attenuation

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.

The relevant bits of the fragment shader:

uniform mat4 view;

#pragma glslify: attenuation = require('./attenuation')

void main() {
  ...

  //determine surface to light vector
  vec4 lightPosition = view * vec4(light.position, 1.0);
  vec3 lightVector = lightPosition.xyz - vViewPosition;

  //calculate attenuation
  float lightDistance = length(lightVector);
  float falloff = attenuation(light.radius, light.falloff, lightDistance);

  //light direction
  vec3 L = normalize(lightVector);

  ...
}

Our chosen attenuation function is by Tom Madams, but there are many others that we could choose from.

float attenuation(float r, float f, float d) {
  float denom = d / r + 1.0;
  float attenuation = 1.0 / (denom*denom);
  float t = (attenuation - f) / (1.0 - f);
  return max(t, 0.0);
}

diffuse

diffuse

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.

For rough surfaces, glsl-diffuse-oren-nayar looks a bit better than glsl-diffuse-lambert.

#pragma glslify: computeDiffuse = require('glsl-diffuse-oren-nayar')

  ...

  //diffuse term
  vec3 diffuse = light.color * computeDiffuse(L, V, N, roughness, albedo) * falloff;
  
  //texture color
  vec3 diffuseColor = textureLinear(texDiffuse, uv).rgb;

These shading functions are known as bidirectional reflectance distribution functions (BRDF).

specular

specular

Similarly, we can apply specular with one of the following BRDFs:

Which one you choose depends on the material and aesthetic you are working with. In our case, glsl-specular-phong looks pretty good.

The above screenshot is scaled by 100x for demonstration, using specularScale to drive the strength. The specular is also affected by the light attenuation.

#pragma glslify: computeSpecular = require('glsl-specular-phong')

  ...
  
  float specularStrength = textureLinear(texSpecular, uv).r;
  float specular = specularStrength * computeSpecular(L, V, N, shininess);
  specular *= specularScale;
  specular *= falloff;

final color

final

We now calculate the final color in the following manner.

  ...
  //compute final color
  vec3 color = diffuseColor * (diffuse + light.ambient) + specular;

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.

  ...
  //output color
  gl_FragColor.rgb = toGamma(color);
  gl_FragColor.a   = 1.0;

The final result.

Further Reading

License

MIT. See LICENSE.md for details.

More Repositories

1

shader-school

🎓 A workshopper for GLSL shaders and graphics programming
JavaScript
4,275
star
2

webgl-workshop

🎓 The sequel to shader-school: Learn the WebGL API
JavaScript
1,486
star
3

glsl-transpiler

Transpile GLSL to JS
JavaScript
175
star
4

gl-shader

🎁 WebGL shader wrapper
JavaScript
120
star
5

packages

📦 A list of packages that fall under the stack.gl umbrella
JavaScript
113
star
6

glsl-parser

transform streamed glsl tokens into an ast
JavaScript
98
star
7

gl-mat4

gl-matrix's mat4, split into smaller pieces
JavaScript
79
star
8

gl-now

Create a WebGL context now!
JavaScript
63
star
9

gl-fbo

WebGL framebuffer wrapper
JavaScript
60
star
10

gl-texture2d

WebGL texture wrapper
JavaScript
59
star
11

stackgl.github.io

💻
JavaScript
58
star
12

gl-audio-analyser

Pull audio waveform/frequency data into WebGL for realtime audio visualisation
JavaScript
57
star
13

gl-particles

✨ Convenience module for FBO-driven particle simulations.
JavaScript
57
star
14

gl-vec3

gl-matrix's vec3, split into smaller pieces
JavaScript
53
star
15

gl-geometry

A flexible wrapper for gl-vao and gl-buffer that you can use to set up renderable WebGL geometries from a variety of different formats.
JavaScript
49
star
16

bunny-walkthrough

Draws a 3D bunny to the screen using stack.gl
JavaScript
42
star
17

gl-vec2

gl-matrix's vec2, split into smaller pieces
JavaScript
36
star
18

glslbin

🔮
JavaScript
29
star
19

learning-webgl-01

Learning WebGL Lesson 1 converted from vanilla WebGL to use stack.gl.
JavaScript
28
star
20

snowden

3D mesh of Snowden's Bust.
JavaScript
27
star
21

gl-vao

Vertex array object wrapper for WebGL
JavaScript
24
star
22

gl-buffer

WebGL buffer wrapper
JavaScript
23
star
23

ray-aabb-intersection

Determine the point of intersection between a ray and axis-aligned bounding box (AABB)
JavaScript
23
star
24

gl-shader-core

Core implementation of gl-shader without parser dependencies
JavaScript
22
star
25

glsl-extract

extract uniforms and attributes from glsl programs
JavaScript
19
star
26

gl-reset

Completely reset the state of a WebGL context, deleting any allocated resources
JavaScript
19
star
27

gl-toy

🔮 Quickly create WebGL demos using glslify
JavaScript
19
star
28

gl-magic-uniforms

🎩 Create a magic getter/setter object for a given WebGLProgram's uniforms.
JavaScript
18
star
29

webglew

WebGL Extension Wrangler
JavaScript
17
star
30

gl-mat3

gl-matrix's mat3, split into smaller pieces
JavaScript
17
star
31

gl-state

Saves WebGL context state
JavaScript
17
star
32

gl-post

Simple WebGL post-processing using some pieces from stack.gl
JavaScript
14
star
33

glsl-min-stream

through stream that transforms glsl-parser AST nodes and rewrites variables into shorter forms
JavaScript
14
star
34

contributing

Contribution guidelines for stackgl projects
14
star
35

gl-mesh

Draws static indexed geometry in WebGL
JavaScript
12
star
36

gl-texture2d-read-float

Read out the contents of a floating-point gl-texture2d
JavaScript
11
star
37

gl-clear

A helper WebGL module for clearing the current buffer
JavaScript
11
star
38

lookat-camera

Simple "lookat" camera abstraction built on top of gl-matrix
JavaScript
10
star
39

glsl-deparser

through stream that translates glsl-parser AST nodes into working glsl code
JavaScript
10
star
40

glslify-api

An API and accompanying client for generating glslify shaders in the browser
JavaScript
10
star
41

gl-modules.github.io

modules.gl website
CSS
9
star
42

gl-texture2d-pip

🔲 Preview the contents of a set of gl-texture instances alongside your main render pass.
JavaScript
9
star
43

gl-quat

gl-matrix's quaternion, split into smaller pieces
JavaScript
8
star
44

gl-compare

Visually compare two webgl render loops on the fly
JavaScript
8
star
45

stackgl-readme-css

Reusable CSS for styling README/Markdown content consistently.
HTML
8
star
46

learning-webgl-02

Learning WebGL Lesson 2 converted from vanilla WebGL to use stack.gl.
JavaScript
7
star
47

gl-mat2

gl-matrix's mat2, split into smaller pieces
JavaScript
7
star
48

gl-api

A JSON listing of the WebGL 1.0 API
JavaScript
7
star
49

learning-webgl-03

Learning WebGL Lesson 3 converted from vanilla WebGL to use stack.gl.
JavaScript
7
star
50

gl-flags

Easily change and access your WebGL flags set by gl.enable/gl.disable with minimised overhead.
JavaScript
6
star
51

gl-buffer-snoop

Intercepts uploads to WebGL buffers in order to keep track of their expected value on the GPU.
JavaScript
6
star
52

gl-vec4

gl-matrix's vec4, split into smaller pieces
JavaScript
5
star
53

gl-shader-errors

"Parses" the log output of `gl.getShaderInfoLog`
JavaScript
5
star
54

gl-modules-workshopper

workshopper for the js side of glsl.
4
star
55

discussions

3
star