Skip to content

Phong Lighting Model

Martin Prazak edited this page Oct 28, 2018 · 1 revision

Lighting models in computer graphics have evolved significantly over the years, improving on modeling accuracy of real-world materials and lighting.

Some of the earliest models include the Lambertian diffuse model, Phong reflection model and Blinn-Phong shading model. While simple, these models are far from obsolete - they still form the primary models used in real-time graphics (games and CAD applications).

In this tutorial, we'll make our way through these models in GLSL, using a simple example with a single moving lightsource.

Initial setup

We will start by bringing in two simple setups from the opengl toolbar. We will add a time node, pass it to a render/uniforms/float node (renamed to "time"), and then further to render/uniforms/viewport and to the uniforms input of the render/draw nodes. In the two loaders, we will bring in the examples/fsu_models/teapot.obj as our test model, and examples/sphere_lowres.obj to visualise the light position.

alt text

To make the "light" (the sphere object, for now) orbit the teapot, let's change the vertex shader of the sphere to:

#version 140

in vec3 P;

uniform mat4 iProjection;
uniform mat4 iModelView;

uniform float time;

void main() {
	// calculate light position in world space, based on time
	vec3 light_pos = vec3(sin(time * 3.14), 0, cos(time * 3.14)) * 20.0;

	vec4 pos4 = vec4(P + light_pos, 1);
	gl_Position = iProjection * iModelView * pos4;
}

This will change the sphere's position to appear to "orbit" the teapot at the origin. Let's also change the fragment shader to simply return white:

#version 140

out vec4 color;

void main() {
	color = vec4(1);
}

This leads to the following setup, with the timeline controlling the sphere position. Hitting spacebar will make the sphere orbit the teapot, with half an orbit per second.

alt text

Lambertian reflectance

The first part of our lighting model is the diffuse reflection, modeled using simple Labertian reflectance. This model describes a diffuse surface as a surface that reflects the incoming light in all directions uniformly. As a consequence, this term does not require the knowledge of the viewer's position - if a surface is visible, its reflectance will be determined fully by its position, facing direction (i.e., normal vector) and the vector of incoming light source.

The Lambertian reflectance ID is described in terms of the incoming light vector L, normal vector N, surface diffuse colour CD and incoming light intensity IL as:

This equation is used in a vector form, for the vec3 of RGB colours. Given our knowledge of the light position (see above), we can replace the vertex shader of our teapot with:

#version 140

out vec3 colour;

in vec3 P;
in vec3 N;

uniform mat4 iProjection;
uniform mat4 iModelView;
uniform mat4 iModelViewNormal;

uniform float time;

// constant "colour" of the surface
vec3 diffuse_color = vec3(1, 0.5, 0.5);

void main() {
	// light position, as a circular trajectory
	vec3 light_pos = vec3(sin(time * 3.14), 0, cos(time * 3.14)) * 20.0;
	// surface position (scaling down the teapot)
	vec3 surface_pos = P * 0.1;

	// light direction from the surface
	vec3 light_vector = normalize(light_pos - surface_pos);
	// surface normal (as read from the input file)
	vec3 normal = normalize(N);

	// lambertial term
	colour = vec3(dot(light_vector, normal)) * diffuse_color;

	// perspective projection for rasterisation
	gl_Position = iProjection * iModelView * vec4(surface_pos, 1);
}

As we are computing the colour in the vertex shader, our fragment shader then just needs to pass this colour on:

#version 140

out vec4 result;

in vec3 colour;

void main() {
	result = vec4(colour, 1);
}

Leading to our teapot with diffuse shading:

alt text

Phong specular term

The Phong reflection model enhances the Lambertian diffuse reflection by a specularity term IS, combined additively with the diffuse term ID.

The specularity term models the light IS that is directly reflected from a light towards the viewer, with a simple model of surface roughness via "shininess" parameter α:

with V representing the view vector (from the surface towards the camera), CS the surface specular colour / coefficient (white for most common realistic reflections), IL the incoming luminance, and R the reflection vector, computed as:

The R vector describes the direction in which a perfectly specular material would reflect a ray coming from the direction L:

The view vector represents the vector from the shaded point to the camera. To obtain the camera position in world space, we simply need to invert the modelview transformation.

Adding all this to our shader, we obtain:

#version 140

out vec3 colour;

in vec3 P;
in vec3 N;

uniform mat4 iProjection;
uniform mat4 iModelView;
uniform mat4 iModelViewNormal;

uniform float time;

// constant "colour" of the surface
vec3 diffuse_color = vec3(1, 0.5, 0.5);
// "shininess" parameter
float alpha = 4.0;

void main() {
	// light position, as a circular trajectory
	vec3 light_pos = vec3(sin(time * 3.14), 0, cos(time * 3.14)) * 20.0;
	// surface position (scaling down the teapot)
	vec3 surface_pos = P * 0.1;

	// light direction from the surface
	vec3 light_vector = normalize(light_pos - surface_pos);
	// surface normal (as read from the input file)
	vec3 normal = normalize(N);

	// lambertial term
	colour = vec3(max(0.0, dot(light_vector, normal))) * diffuse_color;

	// reflection vector
	vec3 reflection = 2.0*dot(light_vector, normal)*normal - light_vector;
	// camera position, as the inverse of the scene transformation
	vec3 campos = vec3(inverse(iModelView) * vec4(0,0,0,1));
	// view vector, determined from the modelview matrix and surface position
	vec3 view = normalize(campos + surface_pos);

	// phong reflective term
	colour += pow(max(0.0, dot(reflection, view)), alpha);

	// perspective projection for rasterisation
	gl_Position = iProjection * iModelView * vec4(surface_pos, 1);
}

And, as a result, we get the full Phong reflectance model:

alt text

Gourand and Phong shading

So far, we have written all our code in the vertex shader, leaving the fragment shader only to interpolate the resulting colour. This closely resembles the legacy fixed function pipeline's handling of material properties (using glMaterial calls and related API).

Shading computed per-vertex is often referred to as the Gourand shading. In this model, per-vertex colours are interpolated in screen space (i.e., in rendered pixels), which for sharp highlights create discontinuity artifacts, highlighting the polygonal structure of the displayed model:

alt text

An alternative method is to perform majority of reflectance and shading computation in the fragment shader, implementing Phong shading (not to be confused with Phong reflectance model). While this method produces better results, it requires significantly more computation per fragment, making it much more computationally expensive. However, modern GPUs have no problems handling this complexity.

To implement Phong shading, we will move most of our shading core from the vertex shader to the fragment shader. Our vertex shader will only pass data to the fragment shader:

#version 140

out vec3 fragPos;
out vec3 fragNorm;

in vec3 P;
in vec3 N;

uniform mat4 iProjection;
uniform mat4 iModelView;
uniform mat4 iModelViewNormal;

void main() {
	// pass through vertex position and normal, in world space
	fragPos = P * 0.1;
	fragNorm = N;

	// perspective projection for rasterisation
	gl_Position = iProjection * iModelView * vec4(fragPos, 1);
}

Our fragment shader will now contain most of the lighting computation:

#version 140

out vec4 colour;

in vec3 fragPos;
in vec3 fragNorm;

uniform mat4 iProjection;
uniform mat4 iModelView;
uniform mat4 iModelViewNormal;

uniform float time;

// constant "colour" of the surface
vec3 diffuse_color = vec3(1, 0.5, 0.5);
// "shininess" parameter
float alpha = 4.0;

void main() {
	// light position, as a circular trajectory
	vec3 light_pos = vec3(sin(time * 3.14 / 5), 0, cos(time * 3.14 / 5)) * 20.0;

	// light direction from the surface
	vec3 light_vector = normalize(light_pos - fragPos);
	// surface normal (as read from the input file)
	vec3 normal = normalize(fragNorm);

	// lambertial term
	vec3 result = vec3(max(0.0, dot(light_vector, normal))) * diffuse_color;

	// reflection vector
	vec3 reflection = 2.0*dot(light_vector, normal)*normal - light_vector;
	// camera position, as the inverse of the scene transformation
	vec3 campos = vec3(inverse(iModelView) * vec4(0,0,0,1));
	// view vector, determined from the modelview matrix and surface position
	vec3 view = normalize(campos + fragPos);

	// phong reflective term
	result += pow(max(0.0, dot(reflection, view)), alpha);

	// convert the resulting colour to vec4
	colour = vec4(result, 1);
}

Phong shading removes the shading artifacts of the Gourand shading method, resulting in a more fragment-accurate result:

alt text

This improvement allows us to have a much sharper highlights (corresponding to a more mirror-like surface). For example, changing the α parameter to 100 leads to the following result:

alt text

Finally, the final setup of this tutorial:

alt text