5. GLSL Per Pixel Lighting

GLSL Per-Pixel Lighting

Per-pixel lighting is the common name for a technique where the lighting calculation is moved from the vertex shader into the fragment shader. Technically the shader runs per generated fragment, not literally once for every physical screen pixel, but “per-pixel lighting” is the name you will see everywhere. This approach produces smoother and more realistic lighting, especially on curved surfaces or high-polygon models. In this tutorial, we’ll demonstrate how to set up per-pixel lighting using GLSL.

We’ll create shaders that calculate lighting in the fragment shader and render a rotating teapot to visualise the effect.

Main Program

The main program initialises the OpenGL context, loads the shaders, and renders a teapot with per-pixel lighting. Below is the implementation:

#include <cstdlib>

#if defined(__MACH__) && defined(__APPLE__)
#include <OpenGL/gl.h>
#include <GLUT/glut.h>
#include <OpenGL/glext.h>
#else
#include <GL/glew.h>
#include <GL/gl.h>
#include <GL/glut.h>
#include <GL/glext.h>
#endif

#include "shader.h"

// Shader instance
Shader shader;

// Rotation angle
GLfloat angle = 0.0;

// Diffuse light colour variables
GLfloat dlr = 1.0, dlg = 1.0, dlb = 1.0;

// Ambient light colour variables
GLfloat alr = 0.0, alg = 0.0, alb = 0.0;

// Light position variables
GLfloat lx = 0.0, ly = 1.0, lz = 1.0, lw = 0.0;

void init(void) {
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    shader.init("shader.vert", "shader.frag");
}

void teapot(void) {
    glRotatef(angle, 1.0, 0.0, 0.0);  // Rotate on X-axis
    glRotatef(angle, 0.0, 1.0, 0.0);  // Rotate on Y-axis
    glRotatef(angle, 0.0, 0.0, 1.0);  // Rotate on Z-axis
    glColor4f(1.0, 0.0, 0.0, 1.0);   // Set teapot colour to red
    glutSolidTeapot(2);
}

void setLighting(void) {
    GLfloat DiffuseLight[] = {dlr, dlg, dlb, 1.0f};
    GLfloat AmbientLight[] = {alr, alg, alb, 1.0f};
    GLfloat LightPosition[] = {lx, ly, lz, lw};

    glLightfv(GL_LIGHT0, GL_DIFFUSE, DiffuseLight);
    glLightfv(GL_LIGHT0, GL_AMBIENT, AmbientLight);
    glLightfv(GL_LIGHT0, GL_POSITION, LightPosition);
}

void display(void) {
    glClearColor(0.0, 0.0, 0.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

    setLighting();

    shader.bind();
    teapot();
    shader.unbind();

    glutSwapBuffers();
    angle += 0.1f;
}

void reshape(int w, int h) {
    if (h == 0) h = 1;
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(60, (GLfloat)w / (GLfloat)h, 1.0, 100.0);
    glMatrixMode(GL_MODELVIEW);
}

int main(int argc, char **argv) {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
    glutInitWindowSize(500, 500);
    glutCreateWindow("GLSL Per-Pixel Lighting Example");

    glewInit();
    init();

    glutDisplayFunc(display);
    glutIdleFunc(display);
    glutReshapeFunc(reshape);

    glutMainLoop();
    return 0;
}

Vertex Shader

The vertex shader passes the transformed normal and light direction to the fragment shader for the per-fragment lighting calculation. Just like the previous tutorial, both values need to be in the same coordinate space. In this compatibility example that space is eye/view space.

varying vec3 vertex_light_direction;
varying vec3 vertex_normal;

void main() {
    // Transform the normal into eye/view space
    vertex_normal = gl_NormalMatrix * gl_Normal;

    // w = 0 makes this a directional light. The compatibility light
    // value is in eye/view space, matching the normal above.
    vertex_light_direction = gl_LightSource[0].position.xyz;

    // Pass the vertex colour to the fragment shader
    gl_FrontColor = gl_Color;

    // Transform the vertex position
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

Explanation:

  • Normal Transformation: gl_NormalMatrix converts the object normal into eye/view space.
  • Light Direction: Because w = 0, gl_LightSource[0].position.xyz represents a directional light in that same eye/view space. We pass it through and normalise after interpolation.
  • Colour Passing: gl_FrontColor ensures that vertex colours are passed to the fragment shader.

Fragment Shader

The fragment shader calculates the lighting for each pixel using the interpolated normal and light position.

varying vec3 vertex_light_direction;
varying vec3 vertex_normal;

void main() {
    // Interpolation changes vector lengths, so normalise again here
    vec3 normal = normalize(vertex_normal);
    vec3 light_direction = normalize(vertex_light_direction);

    // Calculate diffuse lighting using the dot product
    float diffuse_value = max(dot(normal, light_direction), 0.0);

    // Add ambient light and the diffuse contribution
    vec4 ambient = gl_Color * gl_LightSource[0].ambient;
    vec4 diffuse = gl_Color * gl_LightSource[0].diffuse * diffuse_value;
    gl_FragColor = ambient + diffuse;
}

Explanation:

  • Normalisation: Interpolated vectors are not guaranteed to stay unit length, so we normalise them in the fragment shader before using the dot product.
  • Diffuse Lighting: The dot product between the normal and light direction determines the light intensity at the pixel.
  • Fragment Colour: The final colour is calculated by multiplying the diffuse intensity by the vertex colour.

The difference from the last tutorial is small in code, but important visually: instead of interpolating one finished lighting value between vertices, we interpolate the vectors and do the actual dot product for each fragment. That gives curved surfaces a much smoother result.

Benefits of Per-Pixel Lighting

  • Improved Visual Quality: Lighting is calculated for every pixel, resulting in smoother shading and fewer artefacts.
  • Dynamic Effects: Enables detailed and realistic lighting effects for complex models.

Source & downloads

Questions or corrections? If something doesn’t work or you spot something I’ve got wrong, leave a comment below. These tutorials have been around a long time, so useful corrections are always welcome!

  • March 25, 2010
  • 10