WebGL Lighting
Related Topics: GLSL Shader
- Overview
- Ambient
- Diffuse
- Specular
- Attenuation
- All Together: Phong Shader
- Example: Single Light
- Example: Multiple Lights
Overview



One of important 3D cues is lighting. To make an object in a scene looks 3D, the object needs to be illuminated by the light source. There are 4 components for lighting; ambient, diffuse, specular and attenuation. This page explains these illumination parameters and the mathematical computations how lighting is applied to the objects.
Ambient

Ambient illumination is light that is scattered equally in all directions when it hits on a surface. So, ambient light comes from all directions surrounding an object. And, the object is equally lit from all directions, so it looks like 2D (no shades). It becomes an environmental brightness of the scene.
The calculation of the ambient component is simple. If the light's ambient is and the material ambient is
, then the combined ambient color is the piecewise product of each color component (r, g, b);
In the GLSL fragment shader, we pass the light and material ambient components as unions;
// uniforms in fragment shader
uniform vec4 lightAmbient; // (r, g, b, a)
uniform vec4 materialAmbient; // (r, g, b, a)
...
void main(void)
{
vec4 color;
// compute ambient
color = lightAmbient * materialAmbient;
...
}
Diffuse

Diffuse illumination is light that comes from only the light source, but it reflects equally in all directions where it hits on a surface. So, it appears as matte finish.
The radiant intensity varys proportional to the cosine of the angle between the light vector, and the surface normal,
. This is called Lambert's Cosine Law.
The intensity is calculated by the inner product of the light vector and normal vector; . If
and
are same direction, the intensity is the highest. And it is decreases when the the light vector is tilted away from the surface normal at the vertex point, P.
The diffuse lighting is normally computed in the eye space, so the light position, vertex position and normal vector are transformed to the eye space in the vertex shader. Then, the diffuse illumination is computed with these transformed vectors in the fragment shader.
// in vertex shader
// vertex attributes
attribute vec3 vertexPosition; // vertex pos in object space
attribute vec3 vertexNormal; // normal vector in object space
// uniforms
uniform mat4 matrixModelView;
uniform mat4 matrixNormal;
// varying outputs
varying vec3 positionVec; // vertex position in eye space
varying vec3 normalVec; // normal vector in eye space
...
void main(void)
{
// transform vertex position from object space to eye space
positionVec = (matrixModelView * vec4(vertexPosition, 1.0)).xyz;
// transform the normal vector from object space to eye space
// assume vertexNormal is already normalized
normalVec = (matrixNormal * vec4(vertexNormal, 1.0)).xyz;
...
}
// in fragment shader
// uniforms
uniform vec4 lightPosition; // light pos in the eye space
uniform vec4 lightDiffuse; // (r, g, b, a)
uniform vec4 materialDiffuse; // (r, g, b, a)
// input varyings
varying vec3 positionVec; // vertex position in eye space
varying vec3 normalVec; // normal vector in eye space
...
void main(void)
{
// re-normalize normal vector in eye space
vec3 normal = normalize(normalVec);
// compute light vector in eye space
vec3 light = lightPosition.xyz - positionVec;
light = normalize(light);
// compute diffuse component using Lambert cosine law
float dotNL = max(dot(normal, light), 0.0);
vec4 color = dotNL * materialDiffuse * lightColor;
...
}
The above example shader is using a positional light. If the light source is a directional light such as Sun, then the light vector is simply the normalized light position itself. The direction of the light is same on any vertex position.
Specular

Specular illumination is light that comes from one direction, same as diffuse but it only reflects to a particular direction. So, it makes a shiny spot on the surface. For example, metal or plastic object has high shininess.
A perfect specular spot is only visible where the surface normal is exact halfway between the light vector and the view vector
. However, real-world surfaces are not perfectly smooth, so the specular highlights are blurry.
Phong's reflection model estimates the specular reflectance by the angle between the reflection vector and the view vector with shininess scale, s. If the angle is smaller, the reflection is getting stronger.
GLSL provides a utility function, reflect() to calculate the reflection vector. Or, you can manually calculate it by yourself using vector arithmetic.
The following fragment shader is only calculating the specular illumination using Phong's shading model. You need to pass additional specular colors and shininess scale value to the fragment shader
// In fragment shader
// uniforms
uniform vec4 lightPosition; // light pos in the eye space
uniform vec4 lightSpecular; // (r, g, b, a)
uniform vec4 materialSpecular; // (r, g, b, a)
uniform float materialShininess; // shininess scale
// input varyings
varying vec3 positionVec; // vertex position in eye space
varying vec3 normalVec; // normal vector in eye space
...
void main(void)
{
// re-normalize normal vector in eye space
vec3 normal = normalize(normalVec);
// compute positional light vector in eye space
vec3 light = lightPosition.xyz - positionVec;
light = normalize(light);
// compute view vector in eye space
vec3 view = normalize(-positionVec);
// compute reflection vector: 2 * (N dot L) * N - L
vec3 reflectVec = reflect(-light, normal);
// compute specular component using Phong's model
float dotVR = max(dot(view, reflectVec), 0.0);
vec4 color = pow(dotVR, materialShininess) * lightSpecular * materialSpecular;
...
}

The specular illumination can be computed by Blinn shading model by using the half vector between the light vector and the view vector
, and calculate the reflection intensity using the angle between the half vector
and the normal vector
instead of the reflection vector and the view vector. If the angle
is 0, then it is the highest reflection because the view vector is aligned to the reflection vector. And the intensity decreases while the angle increases.
The half vector can be computed by adding the light and view vectors and normalized.
// Blinn specular model in fragment shader
...
void main(void)
{
// re-normalize normal vector in eye space
vec3 normal = normalize(normalVec);
// compute positional light vector in eye space
vec3 light = lightPosition.xyz - positionVec;
light = normalize(light);
// compute view vector in eye space
vec3 view = normalize(-positionVec);
// compute half vector: (L + V) / |L + V|
vec3 halfVec = normalize(light + view);
// compute specular component using Blinn's model
float dotNH = max(dot(normal, halfVec), 0.0);
vec4 color = pow(dotNH, materialShininess) * lightSpecular * materialSpecular;
...
}
The following screenshots are the comparison between Phong and Blinn's specular models. Blinn's reflections become elliptical shapes where the surface is reflected from a steep angle, for example the sun is reflected at the horizon level.


Attenuation
The light intensity decreases as the distance to the light source increases. In physics, the energy of a point light is inversely propotional to the square of the distance. If the light source is a line, the intensity is inversely propotional to the distance. And, if the light source is a plane, the intensity doesn't decrease even if the distance is infinity.
You can attenuate the light intensity by combining these 3 cases together.
If the light source is directional where its position is with w = 0, it is same as a planer light source. That is the intensity of a directional light doesn't decrease by its distance. Therefore, we don't apply the attenuation for the directional lights.
// attenuation in fragment shader
uniform vec3 lightAttenuation; // attenuation coefficients (k0, k1, k2)
...
void main(void)
{
// compute positional light vector in eye space
vec3 light = lightPosition.xyz - positionVec;
light = normalize(light);
// compute attenuation: 1 / (k0 + k1*d + k2*d*d)
vec3 attFact;
attFact.x = 1.0; // 1
attFact.z = dot(light, light); // dist * dist
attFact.y = sqrt(attFact.z); // dist
float attenuation = 1.0 / dot(lightAttenuation, attFact);
...
// apply attenuation to final color
vec4 color = ...; // ambient + diffuse + specular
gl_FragColor = color * attenuation;
}
All Together: Phong Shader
The following shader is combining all lighting components together; ambient, diffuse, specular and attenuation using Phong reflection model. You can download various versions from the GitHub repository.
// Phong vertex shader
// constants
const float ZERO = 0.0;
const float ONE = 1.0;
// vertex attributes
attribute vec3 vertexPosition; // vertex pos in object space
attribute vec3 vertexNormal; // normal vector in object space
// uniforms
uniform mat4 matrixNormal; // normal vector transform matrix
uniform mat4 matrixModelView; // model-view matrix
uniform mat4 matrixModelViewProjection; // model-view-projection matrix
// output varying variables
varying vec3 positionVec; // vertex position in eye space
varying vec3 normalVec; // normal vector in eye space
void main(void)
{
// transform vertex position to clip space
gl_Position = matrixModelViewProjection * vec4(vertexPosition, ONE);
// transform the normal vector from object space to eye space
normalVec = (matrixNormal * vec4(vertexNormal, ONE)).xyz;
// transform vertex position from object space to eye space
positionVec = vec3(matrixModelView * vec4(vertexPosition, ONE));
}
// Phong Fragment Shader
// constants
const float ZERO = 0.0;
const float ONE = 1.0;
// uniforms
uniform vec4 lightColor;
uniform vec4 lightPosition; // should be in the eye space
uniform vec3 lightAttenuations; // attenuation coefficients (k0, k1, k2)
uniform vec4 materialAmbient; // material ambient color
uniform vec4 materialDiffuse; // material diffuse color
uniform vec4 materialSpecular; // material specular color
uniform float materialShininess; // material specular exponent
// input varying variables
varying vec3 positionVec; // vertex position in eye space
varying vec3 normalVec; // normal vector in eye space
void main(void)
{
// re-normalize varying vars
vec3 normal = normalize(normalVec);
// compute light vector and attenuation
vec3 light;
float attenuation;
// directional light
if(lightPosition.w == ZERO)
{
light = normalize(lightPosition.xyz);
attenuation = ONE;
}
// positional light
else
{
// compute light vector in eye space
light = lightPosition.xyz - positionVec;
// compute attenuation: 1 / (k0 + k1*d + k2*d*d)
vec3 attFact;
attFact.x = ONE; // 1
attFact.z = dot(light, light); // dist * dist
attFact.y = sqrt(attFact.z); // dist
attenuation = ONE / dot(lightAttenuations, attFact);
light = normalize(light);
}
// compute reflected ray vector: 2 * (N dot L) * N - L
vec3 reflectVec = reflect(-light, normal);
// compute view vector (from vertex to camera) in eye space
vec3 view = normalize(-positionVec);
// start with ambient
vec3 color = materialAmbient.rgb;
// add diffuse portion using Lambert cosine law
float dotNL = max(dot(normal, light), ZERO);
color += dotNL * materialDiffuse.rgb * lightColor.rgb;
// add specular portion
float dotVR = max(dot(view, reflectVec), ZERO);
color += pow(dotVR, materialShininess) * materialSpecular.rgb * lightColor.rgb;
// finally, set frag color
// keep alpha as original material diffuse has
gl_FragColor = vec4(color * attenuation, materialDiffuse.a);
}
Example: Single Light
This example explains how to compute lighting for a single positional light. Please see the complete code from the following link.
Fullscreen Demo: test_light.html
GitHub Repo: test_light.html
Example: Multiple Lights
This example explains how to compute lighting with multiple positional lights. Please see the complete code from the following link.
Fullscreen Demo: test_lights.html
GitHub Repo: test_lights.html