WebGL Lighting

←Back

Related Topics: GLSL Shader

Overview

ambient
Ambient only
ambient and diffuse
Ambient + Diffuse
ambient, diffuse and specular
Ambient + Diffuse + Specular

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
Ambient Lighting

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
Diffuse Lighting

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
Phong's Specular Lighting

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 angle is smaller, the reflection is getting stronger.
Phong's specular

GLSL provides a utility function, reflect() to calculate the reflection vector. Or, you can manually calculate it by yourself using vector arithmetic.
Reflection Vector

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;
    ...
}

blinn
Blinn's Specular Lighting

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 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.

Phong Reflection
Phong Reflection
Blinn Reflection
Blinn Reflection

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.
attenuation

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.


decoration Fullscreen Demo: test_light.html
decoration 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.


decoration Fullscreen Demo: test_lights.html
decoration GitHub Repo: test_lights.html

←Back
 
Hide Comments
comments powered by Disqus