GLSL: Per-Pixel Lighting (PPL)

In OpenGL’s fixed function pipeline, lighting (N dot L) is computed per-vertex and interpolated over the surface of the polygon. The result is decent when using high-poly models and can be further disguised by texturing. Per-vertex lighting is, however, unacceptable on some low-poly models such as single-quad floors or walls. In this case, you must resort to using baked textures or per-pixel lighting.

Using PPL, lighting (N dot L) is computed per-pixel. Regardless of the polygon density, the light “falls off” naturally. Minor artifacts may remain if an object is particularly low-poly and approximates a curved surface such as a sphere. There is a performance hit when using PPL but that cost is minimized on newer hardware.

Vertex Shader

The vertex shader is executed for each vertex in the OpenGL pipeline. GLSL’s ftransfrom() convenience function is used transform the vertex position into model-view-projection space. We transform the normal into world normal space and the vertex in the world space so that the fragment shader can compute the lighting vector. The keyword varying allows a variable to be accessed in the fragment shader.

varying vec3 normal;
varying vec3 position;

void main()
{
	gl_Position = ftransform();

	normal = gl_NormalMatrix * gl_Normal;
	position = gl_ModelViewMatrix * gl_Vertex;
}

Fragment Shader

The fragment shader is executed for each fragment (pixel). Whatever is assigned to gl_FragColor will be the resulting color of the pixel. The varying variables that were defined in the vertex shader are interpolated between vertices. Therefore, even though “position” is only calculated for each vertex, it is interpolated and actually corresponds to the position of each fragment.

In the fragment shader, we calculate the light vector which is the light position minus the fragment position. We can then determine the diffuse component by taking the dot product of the normal vector and the light vector.

varying vec3 normal;
varying vec3 position;

void main()
{
	vec3 light = gl_LightSource[0].position - position;

	gl_FragColor = dot(normalize(normal), normalize(light));
}

Note: This fragment shader is a simple example and does not take into account any light properties (ambient, diffuse, specular, attenuation) or material properties (color, texture), and only uses one light (LIGHT0).

C/C++ Implementation

Using GLSL shaders in C/C++ is straightforward. During initialization, both the vertex and fragment shaders must be compiled and then linked together into a “program.”

// load and compile the ppl vertex shader
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
const GLchar* vertexShaderSource =
	readFile("..\\..\\Content\\ppl_v.glsl");
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
free((void*)vertexShaderSource);

// load and compile the ppl fragment shader
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
const GLchar* fragmentShaderSource =
	readFile("..\\..\\Content\\ppl_f.glsl");
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
free((void*)fragmentShaderSource);

// create and link the ppl shader program
pplProgram = glCreateProgram();
glAttachShader(pplProgram, fragmentShader);
glAttachShader(pplProgram, vertexShader);
glLinkProgram(pplProgram);

Once the shaders are compiled and linked, you can enable/disable the shader program by calling glUseProgram.

void onKeyboard(unsigned char key, int x, int y)
{
	switch (key)
	{
		case '1':
			glUseProgram(NULL);
			break;

		case '2':
			glUseProgram(pplProgram);
			break;
	}
}

See Also

Download: bin, src

Tags: , , , , , , , , , , , , , , , , , ,

Comments are closed.