Last time I promised a splash of colour. The plan was to implement per vertex colouring, but I've got a better idea. Let's do something that Warp3D can't do: procedural/algorithmic "texturing" using shaders.

We're going to write shaders that generate texels on-the-fly instead of reading them from memory. Sure, the old Warp3D could render identical images using "real" textures, but that's just not the same. Procedural textures take up almost no memory (only parameters/coefficients need to be stored). They also have infinite resolution since it's based on mathematics instead of pixels.

Let's get started...

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: W3DNovaTutorial2.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;

Add a 2D scaling constant:

const vec2 posScale = vec2(1.0 / 640, 1.0 / 480);

Finally, replace the "colour = ..." line with:

pos = vertPos * posScale;

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;

const vec2 posScale = vec2(1.0 / 640, 1.0 / 480);

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

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 writes the 2D position 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.

W3DNTut3 basic

Step Four: Ripples

Okay, let's up the ante a bit. 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 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 now generates the image below. That's a lot more interesting.

W3DNTut3 ripples

Step Five: Go Nuts

Let's take this several steps further...

#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.

W3DNTut3 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, fragment shader instructions are executed every pixel, so it does have its own cost. Use too complicated an algorithm and the processing time will take its toll on performance.

Download the code here: W3DNovaTutorial3.lha

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

NOTE2: At the time of publishing (7 July 2016), Warp3D Nova was still in an early prerelease state and the shader compiler can't handle the full GLSL repertoire (e.g., no for loops). This will improve over time. Meanwhile, playing with shaders may result in bumping your head against the limits.