7. Texturing in GLSL

GLSL Texturing

Textures bring life and detail to your 3D objects, allowing you to map 2D images onto surfaces. In this tutorial, we’ll explore how to integrate textures into GLSL shaders, covering key concepts like the use of sampler2D and texture coordinates.

Introduction to sampler2D

In GLSL, textures are accessed using sampler uniforms. A sampler2D does not contain the image itself; its integer value selects the texture image unit that the shader will sample. A value of 0 means texture unit GL_TEXTURE0, 1 means GL_TEXTURE1, and so on. The texture object itself is bound to that unit by the OpenGL application.

It’s worth noting that GLSL also supports sampler1D, sampler3D, and specialised samplers for shadow maps and cube maps, but in this tutorial, we’ll focus on sampler2D to keep things simple.

Main Program

Our main program will:

  1. Load a texture.
  2. Bind that texture object to texture unit 0.
  3. Tell the shader sampler to read from texture unit 0.
  4. Pass texture coordinates through the vertex shader to the fragment shader.

Here’s the complete 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 shader;    // Shader program
GLuint texture;   // Texture ID

GLfloat angle = 0.0;  // Rotation angle for teapot

GLuint LoadTexture(const char *filename, int width, int height) {
    GLuint texture;
    unsigned char *data;
    FILE *file;

    // Load RAW image data
    file = fopen(filename, "rb");
    if (file == NULL) return 0;
    data = (unsigned char *)malloc(width * height * 3);
    fread(data, width * height * 3, 1, file);
    fclose(file);

    // Generate and bind texture
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    // Set texture parameters
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    // Generate texture
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
    free(data);

    return texture;
}

void init(void) {
    glEnable(GL_DEPTH_TEST);  // Enable depth testing
    glEnable(GL_LIGHTING);    // Enable lighting
    glEnable(GL_LIGHT0);      // Enable light source 0

    shader.init("shader.vert", "shader.frag"); // Initialise shaders
    texture = LoadTexture("texture.raw", 256, 256); // Load texture
}

void cube(void) {
    glRotatef(angle, 1.0, 0.0, 0.0); // Rotate on the X-axis
    glRotatef(angle, 0.0, 1.0, 0.0); // Rotate on the Y-axis
    glRotatef(angle, 0.0, 0.0, 1.0); // Rotate on the Z-axis

    // Bind our texture object to texture unit 0
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture);

    // The shader is already bound when cube() is called from display().
    // Tell the sampler uniform to read from texture unit 0.
    int texture_location = glGetUniformLocation(shader.id(), "color_texture");
    glUniform1i(texture_location, 0);

    glutSolidTeapot(2.0); // Render teapot
}

void display(void) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear buffers
    glLoadIdentity();
    gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); // Set camera

    shader.bind();
    cube();
    shader.unbind();

    glutSwapBuffers();
    angle += 0.01f; // Increment rotation angle
}

void reshape(int w, int h) {
    if (h == 0) h = 1;
    glViewport(0, 0, (GLsizei)w, (GLsizei)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); // Enable double buffering and depth
    glutInitWindowSize(500, 500);
    glutCreateWindow("GLSL Texturing");

    glewInit();  // Initialise GLEW
    init();

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

    glutMainLoop();
    return 0;
}

Vertex Shader

The vertex shader’s role is to pass texture coordinates from OpenGL to the fragment shader. To do this, it writes the texture coordinates into gl_TexCoord[0].

How It Works:

  1. Reading Texture Coordinates:
    The variable gl_MultiTexCoord0 holds the texture coordinates supplied by OpenGL for the first texture unit (active texture 0).
  2. Writing to gl_TexCoord:
    The vertex shader writes the texture coordinates to gl_TexCoord[0], making them available to the fragment shader.
  3. Result:
    By passing the coordinates from gl_MultiTexCoord0 to gl_TexCoord[0], we allow GLSL to handle multitexturing setups seamlessly.

Here’s the full vertex shader:

void main() {
    // Pass texture coordinates to the fragment shader
    gl_TexCoord[0] = gl_MultiTexCoord0;

    // Transform vertex position to clip space
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

Fragment Shader

The fragment shader uses the sampler2D uniform variable to sample the texture colour and applies it to the object.

Key Points:

  1. Defining the Texture Variable:
    The line uniform sampler2D color_texture; declares a sampler. The value we assign with glUniform1i tells it which texture unit to sample; it is not the texture object ID itself.
  2. Sampling the Texture:
    The function texture2D(color_texture, gl_TexCoord[0].st) fetches the colour of the texture at the given texture coordinates.
  3. Returning the Final Colour:
    The sampled texture colour, a vec4 containing RGBA values, is assigned to gl_FragColor for rendering.

Here’s the complete fragment shader:

uniform sampler2D color_texture; // 2D texture uniform

void main() {
    // Sample texture and assign it as the fragment colour
    gl_FragColor = texture2D(color_texture, gl_TexCoord[0].st);
}

The old fixed-function call glTexEnvf(GL_TEXTURE_ENV, ...) is deliberately gone here. When a shader samples the texture, the fragment shader decides what to do with that sampled colour; the old fixed-function texture-environment mode is not what controls this result.

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