The Basics of 3D Lighting

6837d514b487de395be51432d9cdd078
0
TheNut 179 Mar 10, 2012 at 10:19 webgl shaders lighting

Interactive Shader Effects Series

  • Effects
    • Blending Layers
    • Convolution Filters (Emboss, Edge Detection)
    • Curves and Levels
    • Depth of Field
    • Film Grain
    • Heat Waves
    • Old Film
  • Lighting
  • Materials
    • Skin
    • Snow and Ice
    • Water
  • Miscellaneous
    • Transition Effects
  • Procedural
    • 2D, 3D, 4D Noise
    • Sky / Cloud Generation
    • Terrain

Introduction

In the first article in a series of articles about popular shader effects, we go over the basics of 3D lighting through an interactive WebGL demo. You'll learn about the lighting formula using ambient, diffuse and specular reflection, light attenuation, and spotlight effects.

The interactive article is structured into five parts: In the first tab, you can try out various lighting effects using an interactive WebGL demo. The shader code that powers these lighting effects are showin the second and third tab. The fourth tab goes over the theory in more detail. Finally, the last tab contains the full source code to the demo.

 

Start WebGL Demo

In order to run this demo, you must meet the following requirements.

  • You are running the latest version of Mozilla Firefox, Google Chrome, or Safari.
  • You have a WebGL compatible video card with the latest drivers.
  • Your video card is not blacklisted. You can check the current blacklist on Khronos.

Some browsers may require additional configuration in order to get WebGL to run. If you are having problems running this demo, visit the following sites.

Ambient Colour

 
 
 

Diffuse Colour

 
 
 

Specular Colour

 
 
 

Shininess

 

Light Colour

 
 
 

Attenuation

 
 
 

Shading Type

Gouraud
Phong

/// <summary>
/// Light source structure.
/// <summary>
struct LightSource
{
    vec3 Position;
    vec3 Attenuation;
    vec3 Direction;
    vec3 Colour;
    float OuterCutoff;
    float InnerCutoff;
    float Exponent;
};


/// <summary>
/// Material source structure.
/// <summary>
struct MaterialSource
{
    vec3 Ambient;
    vec4 Diffuse;
    vec3 Specular;
    float Shininess;
    vec2 TextureOffset;
    vec2 TextureScale;
};


/// <summary>
/// Attributes.
/// <summary>
attribute vec3 Vertex;
attribute vec2 Uv;
attribute vec3 Normal;


/// <summary>
/// Uniform variables.
/// <summary>
uniform mat4 ProjectionMatrix;
uniform mat4 ViewMatrix;
uniform mat4 ModelMatrix;
uniform vec3 ModelScale;

uniform int NumLight;
uniform LightSource Light[4];
uniform MaterialSource Material;

/// <summary>
/// Gets or sets whether vertex lighting (Gouraud Shading) or
/// fragment lighting (Phong Shading) is used.
/// <summary>
uniform int ShadingType;


/// <summary>
/// Varying variables.
/// <summary>
varying vec4 vWorldVertex;
varying vec3 vWorldNormal;
varying vec2 vUv;
varying vec3 vViewVec;
varying vec4 vColour;


/// <summary>
/// Vertex shader entry.
/// <summary>
void main ()
{
    vWorldVertex = ModelMatrix * vec4(Vertex * ModelScale, 1.0);
    vec4 viewVertex = ViewMatrix * vWorldVertex;
    gl_Position = ProjectionMatrix * viewVertex;
    
    vUv = Material.TextureOffset + (Uv * Material.TextureScale);
    
    vWorldNormal = normalize(mat3(ModelMatrix) * Normal);
    
    vViewVec = normalize(-viewVertex.xyz);
    
    if ( ShadingType == 0 )
    {
        vColour = vec4(Material.Ambient, 0.0);
        for (int i = 0; i < 4; ++i)
        {
            if ( i >= NumLight )
                break;
            
            // Calculate diffuse term
            vec3 lightVec = normalize(Light[i].Position - vWorldVertex.xyz);
            float l = dot(vWorldNormal, lightVec);
            if ( l > 0.0 )
            {
                // Calculate spotlight effect
                float spotlight = 1.0;
                if ( (Light[i].Direction.x != 0.0) || (Light[i].Direction.y != 0.0) || (Light[i].Direction.z != 0.0) )
                {
                    spotlight = max(-dot(lightVec, Light[i].Direction), 0.0);
                    float spotlightFade = clamp((Light[i].OuterCutoff - spotlight) / (Light[i].OuterCutoff - Light[i].InnerCutoff), 0.0, 1.0);
                    spotlight = pow(spotlight * spotlightFade, Light[i].Exponent);
                }
                
                // Calculate specular term
                vec3 r = -normalize(reflect(lightVec, vWorldNormal));
                float s = pow(max(dot(r, vViewVec), 0.0), Material.Shininess);
                
                // Calculate attenuation factor
                float d = distance(vWorldVertex.xyz, Light[i].Position);
                float a = 1.0 / (Light[i].Attenuation.x + (Light[i].Attenuation.y * d) + (Light[i].Attenuation.z * d * d));
                
                // Add to colour
                vColour.xyz += ((Material.Diffuse.xyz * l) + (Material.Specular * s)) * Light[i].Colour * a * spotlight;
            }
        }
        
        vColour.w = Material.Diffuse.w;
    }
}
    
    

#ifdef GL_ES
    precision highp float;
#endif


/// <summary>
/// Light source structure.
/// <summary>
struct LightSource
{
    vec3 Position;
    vec3 Attenuation;
    vec3 Direction;
    vec3 Colour;
    float OuterCutoff;
    float InnerCutoff;
    float Exponent;
};


/// <summary>
/// Material source structure.
/// <summary>
struct MaterialSource
{
    vec3 Ambient;
    vec4 Diffuse;
    vec3 Specular;
    float Shininess;
    vec2 TextureOffset;
    vec2 TextureScale;
};


/// <summary>
/// Uniform variables.
/// <summary>
uniform int NumLight;
uniform LightSource Light[4];
uniform MaterialSource Material;

/// <summary>
/// Gets or sets whether vertex lighting (Gouraud Shading) or
/// fragment lighting (Phong Shading) is used.
/// <summary>
uniform int ShadingType;


/// <summary>
/// Varying variables.
/// <summary>
varying vec4 vWorldVertex;
varying vec3 vWorldNormal;
varying vec2 vUv;
varying vec3 vViewVec;
varying vec4 vColour;


/// <summary>
/// Fragment shader entry.
/// <summary>
void main ()
{
    if ( ShadingType == 1 )
    {
        // vWorldNormal is interpolated when passed into the fragment shader.
        // We need to renormalize the vector so that it stays at unit length.
        vec3 normal = normalize(vWorldNormal);
    
        vec3 colour = Material.Ambient;
        for (int i = 0; i < 4; ++i)
        {
            if ( i >= NumLight )
                break;
            
            // Calculate diffuse term
            vec3 lightVec = normalize(Light[i].Position - vWorldVertex.xyz);
            float l = dot(normal, lightVec);
            if ( l > 0.0 )
            {
                // Calculate spotlight effect
                float spotlight = 1.0;
                if ( (Light[i].Direction.x != 0.0) || (Light[i].Direction.y != 0.0) || (Light[i].Direction.z != 0.0) )
                {
                    spotlight = max(-dot(lightVec, Light[i].Direction), 0.0);
                    float spotlightFade = clamp((Light[i].OuterCutoff - spotlight) / (Light[i].OuterCutoff - Light[i].InnerCutoff), 0.0, 1.0);
                    spotlight = pow(spotlight * spotlightFade, Light[i].Exponent);
                }
                
                // Calculate specular term
                vec3 r = -normalize(reflect(lightVec, normal));
                float s = pow(max(dot(r, vViewVec), 0.0), Material.Shininess);
                
                // Calculate attenuation factor
                float d = distance(vWorldVertex.xyz, Light[i].Position);
                float a = 1.0 / (Light[i].Attenuation.x + (Light[i].Attenuation.y * d) + (Light[i].Attenuation.z * d * d));
                
                // Add to colour
                colour += ((Material.Diffuse.xyz * l) + (Material.Specular * s)) * Light[i].Colour * a * spotlight;
            }
        }
        
        gl_FragColor = clamp(vec4(colour, Material.Diffuse.w), 0.0, 1.0);
    }
    else
    {
        // Set colour
        gl_FragColor = vColour;
    }
}
  
  

Basics of Lighting

Abstract

This shader demonstrates the basic lighting formula using ambient, diffuse and specular reflection, light attenuation, and spotlight effects. This article will describe the basics behind lighting and cover the differences between Gouraud and Phong shading techniques.

Diffuse Reflection

We see objects because objects reflect some of the light energy they receive back into the environment. This is illustrated in the following diagram.

The light ray reflects at multiple angles within +-90 degrees of the surface normal. A mathematician by the name Johann Lambert in 1760 discovered this property of diffuse reflection and deduced the following formula.

[1]

Where

is the unit normal vector of the surface

is the unit direction vector from a point on the surface to the light source.

is the intensity of the light source.

is the calculated intensity of the diffusely reflected light.

If you assume the intensity of your light source remains at a constant of 1.0, then the formula reduces to a simple dot product between the surface normal and the direction vector from the surface to the light source. The dot product between two vectors will produce a positive value if both vectors are within 90 degrees of each other, 0 if the vectors are perpendicular to each other, or a negative value if both vectors are greater than 90 degrees apart. If both vectors are of unit length (ie: normalized), then the dot product will return a value -1.0 <= ID <= 1.0. If the value is less than or equal to 0, then that part of the object is shaded. If the value is greater than 0, then that part of the object is illuminated based on the diffuse intensity value calculated for that particular point. To apply a diffuse colour to the object, you use the following formula.

Where

is the red, green, and blue diffuse colour of the object

is your Lambert diffuse intensity value.

is the final diffuse colour.

If you have multiple light sources, you simply sum the diffuse reflection contributions from each light source and clamp the result to your colour depth. Shaders use floating point values, so you would clamp the result between 0.0 and 1.0.

Improving Diffusion Reflection

Constant diffuse factor vs bump map

The diffuse reflection formula discussed in this article and demonstrated in the WebGL demo use a constant diffuse value. This produces a smooth surface that lacks any texture such as cracks, bumps, pores, scratches, etc. In real life, all surfaces exhibit irregularities causing light to reflect at different angles. This is illustrated in the following diagram.

This can be represented using a high number of displaced polygons, but that comes with a computationally expensive process. A more efficient way to improve diffuse reflection quality is to use bump maps. Bump maps are image files that contain the perturbed surface normal vectors encoded in RGB space. With a bump map, you calculate the diffuse intensity value for the surface normal as discussed above in addition to calculating the diffuse intensity value of the perturbed normal stored in the bump map. You then combine (multiply) the two in order to produce the final intensity value. The following demonstrates what a bump map in RGB space looks like and what the final lighting calculations would produce.

From left to right: Displacement map, bump map conversion, final lighting result

This topic goes outside the scope of this article, but it is mentioned to give you insight how to improve the visual quality of diffuse reflection. Topics you can investigate include dot 3 bump mapping (aka normal mapping) and the more advanced parallax bump mapping, which adds the illusion of depth by factoring in the viewing angle.

Specular Reflection

Specular reflection is what gives you that shinny look that you often see on billiard balls, leather sofas, or metal surfaces. It can be really shinny and give off a bright glare or it can be really dull and lower the contrast of a particular object. There are many empirical formulae for calculating specular reflection, but this shader focuses on the more popular Phong reflection model. The Phong reflection model was developed by Bui Tuong Phong at the University of Utah in 1973[2]. It is calculated using the following formula.

Where

is the unit reflection vector calculated from the surface normal and the light direction vector.

is the unit light direction vector

is the unit surface normal vector

is the unit view direction vector

is the shininess term. A high value produces a smaller glare while a low value produces a larger glare.

is the calculated intensity of the specular reflected light.

Unlike diffuse reflection where light reflects at multiple angles, specular lighting only reflects at one angle, which is relative to the surface normal and light direction vectors. The dot product between the reflected light vector and view vector will produce a value clamped to the range 0.0 <= IS <= 1.0. From this we deduce that a specular highlight will be at its strongest when the reflected light vector is aligned with the view direction vector and at its weakest when the reflected light vector is greater than or equal to 90 degrees from the view vector. The formula to apply the intensity value to the specular colour is defined below.

Where

is the red, green, and blue specular colour of the object

is your calculated specular intensity value.

is the final specular colour.

As with diffuse reflection, if you have multiple light sources you simply sum the specular reflection contributions from each light source and clamp the result to your colour depth.

Improving Specular Reflection

Using a constant specular colour throughout an object does not always work out well. For example, the human body doesn't shine equally across all body parts. A bald head is more likely to give off a polished shine than your arms or hands. A sweaty body will also give off more shine compared to a dry body. In order to define these areas and treat them differently, you need to create and supply your shader with a specular map. This map will control the specular intensities that are permitted for a particular pixel on your object. The following example demonstrates the rendering differences with and without a specular map.

From left to right: Face without specular map, specular map, face with specular map

In addition to specular maps, normal maps also help bring out specular reflections. The bumpiness of the material will alter the direction light reflects off the surface, giving the following result.

Specular reflection due to bump mapping

This is a common technique applied to rendering oceans. To reduce the polygon count, the waves of an ocean are rendered using low and high frequency normal maps. The specular highlights produced by these normal maps produces a more realistic body of water. This topic goes outside the scope of this article, but it is mentioned to give you insight how to improve the visual quality of specular reflection.

Ambient Lighting

Sphere rendered without ambient lighting and with ambient lighting.

When light is reflected off a surface, that light can be further reflected by other objects in the area, which then other objects reflect that light and so on until the energy dissipates. This is known as ambient lighting (or background lighting) and it plays a crucial role in generating realistic imagery. The most basic way to represent ambient lighting is to illuminate the object as a whole with a constant value, but this can create less than realistic images. When combined with smart texturing to cover up the obvious constant ambient factor, it can sometimes be enough to improve the image quality.

Since ambient is additional lighting from the environment, the ambient colour is added to the result of the lighting equation. This produces the following formula.

Where

is the red, green, and blue ambient colour value to add.

is the remainder of the lighting formula that will sum diffuse, specular, attenuation, and spotlights

is the final colour of the pixel.

Improving Ambient Lighting

Diffuse reflection uses bump maps to improve quality and specular reflection uses specular maps. Ambient quality can be improved in a similar way by using ambient occlusion maps. Ambient occlusion is an empirical technique for determining how much ambient lighting a pixel on an object receives based on its view of the outside world. These maps even include the amount of shadowing received due to light sources being occluded by other objects.

Image on the left rendered without AO and with AO on the right.

Ambient occlusion maps are calculated based on the assumption objects remain stationary. If an object is transformed, then the calculated ambient values are no longer valid. One way around that is to use screen space ambient occlusion (SSAO), which is a real-time technique applied in a fragment shader to approximate ambient occlusion values based on the recorded depth values. It requires a fair amount of processing power to calculate however.

This topic goes outside the scope of this article, but it is mentioned to give you insight how to improve the visual quality of ambient lighting.

Attenuation

Light dissipates over the distance it travels and more so when it bumps into other particles such as dust, walls, gases, etc. This visual trait can be simulated by multiplying the calculated light intensity value by an attenuation factor that will reduce the intensity of light based on the distance it travelled from the light source to the surface of the object. This is represented using the following formula.

Where

is an attenuation constant.

is the calculated linear attenuation and is the linear constant used in that calculation.

is the calculated quadratic attenuation andis the quadratic constant used in that calculation.

is the distance between the surface of the object and the light source.

is the calculated attenuation factor that will multiply the light intensity value.

Each attenuation factor has a certain effect on luminosity. The constant attenuation factor doesn't dim the light over distance, but instead adjusts its intensity. Linearly attenuating the light source is approximate to a light source that emits in a vacuum. Quadratic attenuation dims the light more per distance travelled, which is typical in an environment where light collides with many particles. This is often used to simulate light sources such as flashlights and lanterns.

Spotlights

Spotlights extend the lighting equation by cutting off the light source after a certain angle. This is shown in the formula below.

 

 

Where

is the unit light direction vector.

is the unit direction vector from the light source to the vertex.

is the spotlight exponent, which increases or decreases the spotlight's brightness factor.

is the calculated angle between the light source and the vertex.

 

This is similar to the diffuse reflection formula except you are examining the situation from the light's point of view instead of the vertex point of view. If the vertex lies within the cone of influence, it will be lit, otherwise it will be shaded. In addition to the above, sometimes you want a sharp edge and other times you want a nice falloff. The effect is illustrated below.

 

Spotlight without falloff on the left and with falloff on the right

 

A falloff can be calculated by factoring an inner ring and an outer ring in the spotlight's cone of influence. This is illustrated below.

 

The area inside the inner ring is fully lit, whereas the falloff area will dim the luminosity as you approach the outer ring. This can be added to the spotlight formula.

 

 

Where

is the cosine of the outer cutoff angle. Ex: cos(45.0 * PI / 180.0).

is the cosine of the inner cutoff angle, which must be less than or equal to the outer cutoff angle.

is the calculated spotlight angle from the previous equation.

is the calculated spotlight intensity clamped to the range 0.0 and 1.0.

 

Putting it all Together

We've covered diffuse reflection, specular reflection, ambient lighting, attenuation, and spotlights. When combining all these elements, we get the final lighting formula.

 

 

Where

is the ambient component.

is the diffuse component.

is the specular component.

is the light colour.

is the attenuation factor.

is the spotlight factor.

is the final colour for that pixel.

Shading Techniques

There are two ways to apply the lighting formula.

Gouraud Shading

Gouraud shading is an interpolation technique whereby the colour values calculated at the vertices of a polygon are linearly interpolated to fill the rest of the polygon. This is computationally inexpensive as lighting calculations are performed at the vertices rather than at each pixel, which is the case with Phong shading. There is some quality loss when using a low-polygon model however. Both diffuse and specular reflections can appear blocky due to interpolating between so few polygons. An example of this is shown below.

The first image demonstrates specular highlights with Gouraud shading. The second image demonstrates specular highlights with Phong shading. Since there are more pixels than polygons when rendering the image, the linearly interpolated values calculated in the vertex shader display quality loss. If however you have an object with a sufficiently high polygon count, typically a 1:1 polygon per pixel ratio, then you effectively eliminate the problem.

Here is the same object rendered with Gouraud shading, but with a sufficiently higher polygon count. In this particular case, the performance benefits of Gouraud shading are eliminated by the fact that more memory and vertex processing are required to produce this level of quality.

Phong Shading

Phong shading, not to be confused with the Phong reflection model, is an interpolation technique whereby the surface normal is interpolated over the polygon in a fragment shader for purposes of shading the object. By interpolating normals instead of colour values, as is the case with Gouraud shading, you end up with a higher quality image at the expense of additional computations per pixel. One thing to note is that linearly interpolating normals will not guarantee unit length throughout. You must renormalize the normal vector in order to restore this property. Failing to do so will result in improper lighting intensities when interpolating normals with large angular differences, such as the vertex normals at each corner in a cube.

References

1. Wikipedia Editors (2012-02-10). "Lambertian reflectance". Wikipedia. Retrieved 2012-02-11.

2. Wikipedia Editors (2011-11-27). "Phong shading" . Wikipedia. Retrieved 2012-02-11.

The source code for this project is made freely available for download. The ZIP package below contains both the HTML and JavaScript files to replicate this WebGL demo.


The source code utilizes the Nutty Open WebGL Framework, which is an open sourced, simplified version of the closed source Nutty WebGL Framework. It is released under a modified MIT license, so you are free to use if for personal and commercial purposes.


Download Source

2 Replies

Please log in or register to post a reply.

431a1faa046ac210405712b7e0d842f2
0
mad_god 101 Jul 10, 2012 at 14:42

There is a slight mistake in specular element calculation. In vertex shader you determine view vector as follows:

vWorldVertex = ModelMatrix * vec4(Vertex * ModelScale, 1.0); vec4 viewVertex = ViewMatrix * vWorldVertex; vViewVec = normalize(-viewVertex.xyz);

This is not correct and will give you Vertex->Camera vectors in View space, which you later use with the vertex normal and light vector in worlds space.

Instead you can transfer uniform camera position into the vertex shader and calculate the correct Vertex-Camera vector in the World space:

… uniform vec3 vec3CameraWorldPos; … vViewVec = normalize(vec3CameraWorldPos - vWorldVertex);

Another method is to use inverted view matrix on (0,0,0) vector but I believe it would be slower.

Great article btw.

12e2f9b1c3559347c69eae62fe17e89f
0
heretique 101 Jan 29, 2013 at 15:47

Well for me these Shader Effects Series are the best tutorials I found on the web. Clean, concise, well presented and with live demos, what else one could ask?

Ah…we need MORE :D