The next step after "hello triangle" tutorials is usually adding per-vertex colour, but I'm going to do something different. I'm going to give you a glimpse at the power and flexibility that programmable shaders give you. In this tutorial we'll generate colours entirely on the GPU. It's something called "procedural textures."

Textures are images that are wrapped around a 3D model to give it more detail (i.e., texture). Procedural textures are generated on-the-fly by the GPU instead of reading them from an image in memory. As such, they take up almost no memory (only parameters/coefficients need to be stored). Another advantage is that they have infinite resolution since they're based on mathematical formulae rather than an array of pixels.

Step One: Download the Previous Tutorial's Code

To make life easier we're going to start with the previous tutorial's code, and modify its shaders. So download that now. Here's a direct link: GLTutorial2.lha

Step Two: Changing the Vertex Shader

The fragment shader needs texture coordinates, and it's the vertex shader's task to deliver them. For simplicity we'll use the normalized 2D position, i.e., the position scaled so that it's in the range [0,1] (zero to one inclusive). Only a few vertex shader modifications are required.

First, change the colour output parameter to:

out vec2 pos;

Next, the position needs to be rescaled from [-1,1] to the range [0,1] before sending it to the fragment shader. Replace the "colour = ..." line with:

pos = 0.5 * vertPos + vec2(0.5);

The line above performs a per-vector-element multiply, scaling the input position to the desired range.

The full vertex shader is:

#version 140

in vec2 vertPos;

out vec2 pos;

void main() {
	pos = 0.5 * vertPos + vec2(0.5);
	gl_Position = vec4(vertPos, 0.0, 1.0);
}

NOTE: In OpenGL the y-axis starts at the bottom of the screen rather than the top, and that's why the triangle's bottom vertex is red and the top goes from green to yellow.

Step Three: A Simple Gradient

The real magic happens in the fragment shader. Let's start with something simple. Change the fragment shader to:

#version 140

in vec2 pos;

void main() {
	gl_FragColor = vec4(pos, 0.0, 1.0);
}

This simply takes the scaled 2D position generated in the vertex shader, and writes it to the red and green channels, generating a smooth gradient. Recompile and run the program to see the result (below). Congratulations! You just created your first procedural texture. It's rather boring, though.

GLTutorial3 basic

Step Four: Ripples

Okay, let's make something more interesting. Try the following fragment shader:

#version 140

in vec2 pos;

const float M_PI = 3.14159;
const float M_4PI = 4 * M_PI;

void main() {
	vec2 adjPos = 8 * (pos - vec2(0.5));
	float radial = dot(adjPos, adjPos);
	float red = abs(adjPos.x);
	float green = 0.5 * sin(radial * M_4PI) + 0.5;
	gl_FragColor = vec4(red, green, 0.0, 1.0);
}

Line by line, the code above does the following:

  • Calculates a scaled position (adjPos) in the range [-4,4]
  • Calculates the radial distance from (0,0) to adjPos squared
  • Sets the red colour to be the absolute value of adjPos' x-axis coordinate
  • Sets green to be a radial ripple using the trigonometric sine function
  • Outputs the calculated colour (with blue = 0.0, and alpha = 1.0)

NOTE: If you're worried about the sine function's overhead; GPUs have dedicated sine & cosine instructions (at least the AMD Southern Islands GPUs do).

With this shader the program generates the image below. Much more interesting, right?

GLTutorial3 ripples

Step Five: Go Bonkers

Let's make this more extreme...

#version 140

in vec2 pos;

const float M_PI = 3.14159;
const float M_2PI = 2 * M_PI;

void main() {
	vec2 adjPos = M_2PI * (pos - vec2(0.5));
	float base = sin(adjPos.x) * cos(adjPos.y);
	float mid = base + 0.25 * sin(4.0 * adjPos.x - M_2PI) * cos(5.0 * adjPos.y);
	float high = mid + 0.125 * sin(8.0 * adjPos.x) * cos(10.0 * adjPos.y);
	gl_FragColor = 2.0 * abs(fract(10.0 * (vec4(base * high, mid * high, high, 1.0))) - 0.5);
}

The code above uses trigonometric functions at multiple frequencies to generate something quite complex (see below). The key to the banding is the fract function, while abs makes it smoother. I encourage you to experiment with the shader code. Try adjusting values and/or forumulae. Visually seeing what code changes do gives you a feel for what how it works.

GLTutorial3 complex

Conclusion

This tutorial has given a taste of procedural texturing. Don't get the impression that it's all about bright abstract patterns, though. Procedural textures can render real-world things such as: fractals, clouds, marble, etc.

Procedural textures have two key advantages over regular textures: they take up almost no memory, and they have infinite resolution. Having said that, they're not without code. fragment shader instructions are executed every pixel, so using too complicated an algorithm will take its toll on performance.

Download the code here: GLTutorial3.lha

NOTE: The downloadable code includes all three of the fragment shaders listed above. It uses the last shader by default. The other two shaders can be used by passing their file name as a parameter (e.g., GLTutorial3 AlgCol2D_ripples.frag).