4. GLSL Lighting

GLSL Lighting

Lighting is an essential part of creating realistic 3D scenes. With GLSL shaders, you can achieve more precise control over lighting, including custom effects. This tutorial introduces per-vertex lighting using GLSL, which follows the same basic lighting idea as the fixed-function pipeline but lets us see and control the calculation ourselves.

In per-vertex lighting, the lighting calculations are done at each vertex and interpolated across the surface of the geometry. This tutorial sets up a directional light source and calculates the diffuse lighting intensity at each vertex. One important thing to keep straight is coordinate space: the compatibility variables we use here are in eye/view space, so the normal and the light direction need to be in that same space before we take their dot product.

Main Program

Here is the main program (main.cpp) that sets up the OpenGL context, initialises the shaders, and renders a rotating sphere with per-vertex lighting.

#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 sphere(void) {
    glRotatef(angle, 1.0, 0.0, 0.0);
    glRotatef(angle, 0.0, 1.0, 0.0);
    glRotatef(angle, 0.0, 0.0, 1.0);
    glutSolidSphere(2, 10, 10);
}

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();
    sphere();
    shader.unbind();

    glutSwapBuffers();
    angle += 0.01f;
}

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 Lighting Example");

    glewInit();
    init();

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

    glutMainLoop();
    return 0;
}

Vertex Shader

The vertex shader calculates the lighting intensity at each vertex based on the diffuse lighting model. This intensity is passed to the fragment shader using a varying variable.

varying float diffuse_value;

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

    // Because w = 0, this is a directional light. Its XYZ value is
    // an eye-space light direction, so normalise it before the dot product.
    vec3 vertex_light_direction = normalize(gl_LightSource[0].position.xyz);

    // Calculate diffuse lighting using vectors in the same coordinate space
    diffuse_value = max(dot(vertex_normal, vertex_light_direction), 0.0);

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

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

Fragment Shader

The fragment shader uses the interpolated diffuse_value to calculate the final pixel colour, creating the appearance of lighting across the surface.

varying float diffuse_value;

void main() {
    // Add the ambient term, then 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;
}

Understanding the Code

  1. Vertex Shader:
    • The dot product between the vertex normal and light direction calculates the intensity of light on the surface.
    • gl_NormalMatrix gives us an eye-space normal, while this directional light is also represented in eye space.
    • normalize makes both vectors unit length, which is critical for the dot product to behave as a lighting cosine.
  2. Fragment Shader:
    • The interpolated diffuse_value creates smooth transitions between the values calculated at each vertex. This is why this technique is called per-vertex lighting, even though the fragment shader still receives the interpolated result.
  3. Lighting Setup:
    • setLighting initialises the diffuse and ambient light colours and sets a directional light using w = 0. The light position/direction is transformed by the current model-view matrix when glLightfv(..., GL_POSITION, ...) is called.

Output Behaviour

When you run this program, you should see a sphere with a gradient lighting effect, brighter where the light hits directly and darker where it falls off. The lighting changes dynamically as the sphere rotates.

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