5. GLSL Per Pixel Lighting

GLSL Per-Pixel Lighting

Per-pixel lighting is a technique where lighting calculations are performed for each pixel rather than at each vertex. 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 visualize the effect.

Main Program

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

#if (defined(__MACH__) && defined(__APPLE__))
#include <cstdlib>
#include <OpenGL/gl.h>
#include <GLUT/glut.h>
#include <OpenGL/glext.h>
#else
#include <cstdlib>
#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 color variables
GLfloat dlr = 1.0, dlg = 1.0, dlb = 1.0;

// Ambient light color 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 color to red
    glutSolidTeapot(2);
}

void setLighting(void) {
    GLfloat DiffuseLight[] = {dlr, dlg, dlb};
    GLfloat AmbientLight[] = {alr, alg, alb};
    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) {
    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 light position and the transformed normal to the fragment shader for per-pixel lighting calculations.

varying vec3 vertex_light_position;
varying vec3 vertex_normal;

void main() {
    // Transform the normal into world coordinates
    vertex_normal = normalize(gl_NormalMatrix * gl_Normal);

    // Transform the light position into world coordinates
    vertex_light_position = normalize(gl_LightSource[0].position.xyz);

    // Pass the vertex color 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’s normals to world coordinates.
Light Transformation: The light position is normalized and passed to the fragment shader for interpolation.
Color Passing: `gl_FrontColor` ensures that vertex colors 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_position;
varying vec3 vertex_normal;

void main() {
    // Normalize the interpolated normal and light position
    vec3 normal = normalize(vertex_normal);
    vec3 light_direction = normalize(vertex_light_position);

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

    // Set the final fragment color
    gl_FragColor = gl_Color * diffuse_value;
}

Explanation:

Normalization: Ensures that interpolated normals and light directions remain unit vectors for accurate calculations.
Diffuse Lighting: The dot product between the normal and light direction determines the light intensity at the pixel.
Fragment Color: The final color is calculated by multiplying the diffuse intensity by the vertex color.

Benefits of Per-Pixel Lighting

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

Download Links

If you have any questions, feel free to email me at swiftless@gmail.com.

  • March 25, 2010
  • 10