10.6 - Texture Mapping Using Procedures

Texture mapping is a technique for specifying a unique color for every fragment that composes a triangle. The colors come from a mapping, which is a function that converts a set of inputs into an output value. There are two basic ways this can be done:

This lesson introduces the basic ideas behind the second mapping technique: how to calculate a color. Since the calculations are typically put into a separate function, this type of texture mapping is called procedural texture mapping.

Overview

Image based texture mapping and procedural based texture mapping are not competing techniques; they are complimentary techniques that each has appropriate uses. In fact, the two techniques are often used together to create more realistic surfaces. Here are some advantages and disadvantages to procedural texture mapping.

Advantages:

  • Procedural texture mapping requires much less memory as compared to image based texture mapping. There is no image to download or store in RAM or in the GPU’s memory.
  • The issues of “magnification” and “minification” go away. Procedural texture mapping works correctly at any and all scales.
  • Procedural texture maps can generate a wide variety of patterns with small tweaks in their calculations. To get the same effect with image based texture mapping would require a separate image for each pattern.

Disadvantages:

  • Procedural texture maps are calculated at rendering time for each individual fragment. If the calculations are complex, rendering speeds become slower.
  • The calculations required for procedural texture mapping can be complex and it can be difficult to modify the equations to achieve the specific effect you are trying to create.
  • Procedural texture mapping creates patterns. If you need something specifically applied to the surface of a model, such as a road sign containing words, image based texture mapping is the best approach.

Procedural texture mapping converts input values into a color. The input values can be anything related to a triangle’s attributes, such as its location, orientation, diffuse color, etc.. However, we typically don’t want the surface properties of a model to change because of it’s location and/or orientation. For example, you don’t want the wood grain in a model to change as a piece of wood is moved in a scene; the wood grain should look the same from any position or angle. You can use texture coordinates as the inputs for calculating a procedural texture map color because texture coordinates typically do not change as a model is transformed. Or you can use the geometry of a model before any transformations are applied to it.

Procedural texture mapping is performed by fragment shader programs. There is no limit to the complexity of such programs, but added complexity means slower rendering. If you need to render various models with different procedural texture maps, it is more efficient to implement a separate shader program for each rendering. The alternative is to use an if statement in a single shader program to select the appropriate procedural texture map at rendering time, but this slows down all rendering.

Software Overview

The basic steps to create a procedural texture mapping are as follows:

  1. When building the model:
    1. Assign an appropriate texture coordinate, (s,t), to each vertex of a triangle. (This can be skipped if the geometry of a model is used for texture mapping inputs.)
  2. JavaScript pre-processing for a canvas rendering:
    1. (None)
  3. JavaScript setup each time a model is rendered using a procedural texture map:
    1. Select the correct shader program with gl.useProgram().
  4. Shader program
    1. In the vertex shader, create a varying variable that will interpolate the texture coordinates across the surface of a triangle. (Or interpolate some other property of the model across the face.)
    2. In the fragment shader, use the texture coordinates (or some other interpolated value) to calculate a color.

As you can see, this is much simpler than image based texture mapping. Basically everything related to procedural texture mapping is performed in a fragment shader program. The remainder of this lesson introduces three techniques on which procedural texture maps are created:

  1. Gradients
  2. Overlaid patterns
  3. Noise (Randomness)

To fully understand these techniques you need to spend some time with each demonstration program below and perform the suggested experiments. And don’t hesitate to make up you own experiments!

1. Gradients

A smooth transition from one color into another color is commonly called a “gradient color”. There are many variations on this simple idea. The following demo program renders a simple cube where each side of the cube has texture coordinates that range from 0.0 to 1.0 across each face. Study the fragment shader and then modify it with each of the suggested gradient functions below. A description of each gradient function hopefully makes it clear how the function works.

Show: Code Canvas Run Info
./gradient/gradient.html

Gradient texture mapping experiments.

Please use a browser that supports "canvas"
Animate








Shader errors, Shader warnings, Javascript info
Open this webgl program in a new tab or window

Gradient Experiments

The fragment shader in the demo program above uses the s component of the texture coordinates as a percentage of the face’s color. The face’s color is “hardcoded” as red in the fragment shader but the face’s color could have come from an attribute variable of the triangle. Note that the operation red * percent is a component by component multiply because red is a vector and s is a scalar (a single value). That is, the result of red * percent is a new vector (red[0]*percent, red[1]*percent, red[2]*percent).

Experiment by trying the following code modifications. Hit the “Re-start” button after each change to see the results. If you introduce errors in the fragment shader program, error messages will be displayed in the “Run Info” display area below the canvas window and in the JavaScript console window.

  • Change the variable red to a different color.
  • Use the t component of the texture coordinates. That is, percent = t;. Notice how the gradient now switches directions to move across the face instead of up the face.
  • Use the s component of the texture coordinates, but reverse its direction. That is, percent = 1.0 - s;. Notice how the gradient now switches directions.
  • Try percent = s + t;. This produces values between 0.0 and 2.0. All values greater than 1.0 are clamped to 1.0. This produces the saturated triangle that is a solid color. Every location on the face where the (s+t) is greater then 1.0 will get the full color.
  • Try percent = (s+t)/2.0;. This scales the sum to always be between 0.0 and 1.0 and produces a nice gradient across the face.
  • Try percent = (s+t)/3.0;. This scales the sum to always be between 0.0 and (2/3) and produces a nice gradient across the face, but the color never saturates to full color.
  • Try percent = s * t;. This produces percentages between 0.0 and 1.0, but the values are not linear. Therefore the gradient only saturates the color in one corner.
  • Try percent = sin(s);. Remember that the trig functions always use radians, so this is calculating the sine of angles between 0.0 and 1.0 radian (57.2958 degrees). The color never becomes saturated because the percentages are between 0.0 and 0.84.
  • Try percent = sin(s * PI/2.0);. This calculates a percentage between 0.0 and 1.0 because the s component is scaled to values between 0.0 and pi/2 (90 degrees). PI is not a defined constant in the shader language, so you need to define it like this: float PI = 3.141592653589793;
  • Try percent = sin(s * 2.0*PI);. This calculates percentages between 1.0 and -1.0 because it scales s to be angles between 0.0 and 360.0 degrees (2*PI). All values below 0.0 are clamped to 0.0, which produces the area that is solid black.
  • Try percent = abs(sin(s * 2.0*PI));. This calculates percentages between 0.0 and 1.0 because of the abs() function which takes the absolute value of its argument. Notice the nice two “color bands”. What happens when 2.0*PI becomes 3.0*PI? What happens for n*PI?
  • Try percent = sin(s * 2.0*PI) * sin(t * 2.0*PI);. This produces gradient circles. If you add the abs() function you will get uniform circles. If you change the 2.0 factor to other values, you get that many circles. Try removing the 2.0 factor.

All of the above experiments calculate a percentage value and then scale the base color by that percentage. You can make a gradient between two different colors by using the percentage, percent, and what’s left over, (1.0-percent), to scale two colors like this.

vec3 red = vec3(1.0, 0.0, 0.0);
vec3 blue = vec3(0.0, 0.0, 1.0);

percent = abs(sin(s * 2.0*PI));
return vec4( red * percent + blue * (1.0-percent), 1.0);

Please experiment with two color gradients.

Hopefully you get the idea of gradients! Think of the possibilities! The above experiments have not even come close to the possible variations.

You can “generalize” gradient texture maps by making parts of the calculations be uniform variables that are set at render time. For example, the function percent = abs(sin(s * n*PI)); produces “n” strips of color over a face. You could make n be a uniform variable, uniform float n;, and set its value in your JavaScript initialization code before you render.

2. Overlaid Patterns

The second basic technique for creating procedural texture maps if to create a basic pattern and then overlay it on top of itself at different scales. This will make more sense as you work though some examples.

Checkerboard Pattern

The following WebGL demo program creates a checkerboard pattern. Study the fragment shader program below and notice that the function called checkerboard calculates whether a texture coordinate is part of a “white” or “black” tile of the pattern. The function returns 0.0 or 1.0. The overlay function calculates a color using the percentage returned from checkerboard. The scale factor passed to the checkerboard function determines the number of tiles in the checkerboard pattern. Change the scale factor in line 29 several times to see the results.

Show: Code Canvas Run Info
./checkerboard/checkerboard.html

Checkerboard texture mapping experiments.

Please use a browser that supports "canvas"
Animate








Shader errors, Shader warnings, Javascript info
Open this webgl program in a new tab or window

We want to create a pattern that is a combination of this pattern at different scales. There are many ways the patterns can be combined and we will examine just a few of them. To make sure the idea is clear, modify the fragment shader to calculate the checkerboard pattern twice and then take 50% of each calculation. (Hint: Use copy/paste to avoid typing errors.)

float percent = 0.5 * checkerboard(tex_coords, 2.0) +
                0.5 * checkerboard(tex_coords, 3.0);

Try that again using 3 different scales:

float percent = 0.33 * checkerboard(tex_coords, 2.0) +
                0.33 * checkerboard(tex_coords, 3.7) +
                0.33 * checkerboard(tex_coords, 7.0);

Hopefully you see the pattern. We can write a loop to create this sum of patterns like this. Note that loops in WebGL GLSL must have a constant for their loop control variable. That is why n is declared as a const (constant) value.

// Set the number of patterns to overlay
const int n = 5;

// Set the starting scale of the pattern
float scale = 2.0;

float percent = 0.0;
for (int j=0; j<n; j++) {
  percent += (1.0/float(n)) * checkerboard(tex_coords, scale);

  // Increase the scale of the pattern
  scale *= 2.0;
}

Notice that there are three “parameters” in this code: n is the number of patterns to overlay, scale is controlling the scale of each pattern, and the (1.0/float(n)) fraction is taking an equal percentage of each pattern. That gives us many options for controlling this texture map. Experiment with various ways to modify the scales, such as:

  • scale += 1.0; (values will be 2, 3, 4, 5, etc.)

  • scale += 2.0; (values will be 2, 4, 6, 8, etc.)

  • scale += 2.5; (values will be 2, 4.5, 7, 9.5, etc.)

  • scale *= 2.0; (values will be 2, 4, 8, 16, etc.)

  • scale *= 3.0; (values will be 2, 6, 24, 72, etc)

  • scale = table[j];, where

    float table[5];
    table[0] = 2.0;
    table[1] = 3.7;
    table[2] = 4.1;
    table[3] = 8.3;
    table[4] = 12.7;
    

Also experiment with different values for n.

Now we have the most complex part of this idea, the combination part. There are many ways to combine the different scaled patterns. Here are some combination techniques to try:

  • Alternately add or subtract the values at each scale. This can be done by flipping a sign value inside the loop like this:

    const int n = 5;
    float scale = 2.0;
    float percent = 0.0;
    float sign = 1.0;
    for (int j=0; j<n; j++) {
      percent += sign * checkerboard(tex_coords, scale);
      scale *= 2.0;
      sign = -sign;
    }
    
  • Instead of using an equal percentage of each scaled texture, use a weighted percentage that treats some of the patterns as more important. The following code uses percentages of (1/2), (1/4), (1/8), etc. using the formula 1.0/pow(2,j+1). (If you wanted to treat the smaller scales as more important, you could reverse the percentages, 1.0/pow(2,n-j)

    const int n = 5;
    float scale = 2.0;
    float percent = 0.0;
    for (int j=0; j<n; j++) {
      percent += (1.0/pow(2.0,float(j+1))) * checkerboard(tex_coords, scale);
      scale *= 2.0;
    }
    

Circular Gradient

Let’s combine the overlays of a “circular gradient” pattern at different scales. Experiment with the various “parameters” to an overlay using this new basis:

  • n, the number of patterns to overlay,
  • scale, the various scales of the basic pattern, and
  • (1.0/float(n)), the combination technique.
Show: Code Canvas Run Info
./circular_overlays/circular_overlays.html

Circular overlays texture mapping experiments.

Please use a browser that supports "canvas"
Animate








Shader errors, Shader warnings, Javascript info
Open this webgl program in a new tab or window

Other basic patterns could be used in place of the checkerboard or circular_gradient functions. Feel free to experiment by modifying or replacing those functions.

3. Noise (Randomness)

A third technique for a basic texture is to use randomness. A random number generator is a standard part of most programming language libraries, but there is no built-in “random” function in WebGL GLSL. Therefore we will use a third party function written by Ashima Arts. We will use his 2D cnoise() and pnoise() functions without going into the math behind them because the math is non-trivial and it is not critical to our overview discussion.

In general, it is not easy for computers to create “randomness”. They are typically designed to create “pseudo-random” sequences of numbers that “appear” random, but are actually very predicable. For computer graphics this is extremely important. We want to generate a random texture on a face, but we want the same “randomness” every time we render the face. If we can’t get the same randomness, the texture would change on each rendering, which would not be visually appealing in most cases.

Please study the fragment shader in the following WebGL demo program. Skip over the “noise” generator code in lines 1-119 and concentrate on the fragment shader code in lines 120-140. Notice that we are using a percentage value returned from a “noise” generator to create shades of gray by using the percentage value for each color component, i.e., (percent, percent, percent). This helps you better visualize the “noise”.

Show: Code Canvas Run Info
./noise/noise.html

Noise texture mapping experiments.

Please use a browser that supports "canvas"
Animate








Shader errors, Shader warnings, Javascript info
Open this webgl program in a new tab or window

Ashima’s cnoise() function returns a random value between -1.0 and +1.0. To use this value for a color component we add 1.0 to get the values in the range 0.0 to 2.0 and then divide by 2.0 to get percentages between 0.0 and 1.0. This is purely to visualize the random pattern. You may or may not want to do that for other texture mapping situations. Notice that the values returned by cnoise() are not random in the sense of a random number generator. Rather, they provide a random change in gradient over the surface of a face. This change in gradient can be scaled by multiplying the texture coordinates by a scale factor. Try this variants to line 132:

  • float percent = (1.0 + cnoise(2.0 * tex_coords)) / 2.0;
  • float percent = (1.0 + cnoise(3.2 * tex_coords)) / 2.0;
  • float percent = (1.0 + cnoise(10.0 * tex_coords)) / 2.0;

Notice that the random pattern is not periodic as the scale increases, and that you get a more complex pattern for larger scale factors.

Perlin Noise

Ashima’s pnoise() function implements Perlin noise. This function requires an extra parameter which is a vector of 2 “perturbation” factors. The following WebGL demo program allows you to vary these two factors to investigate the possibilities of Perlin noise.

Show: Code Canvas Run Info
./perlin_noise/perlin_noise.html

Perlin noise texture mapping experiments.

Please use a browser that supports "canvas"
scale: 1.00, factors: (0.000, 0.000)
scale : 0.5 +10.0
s_factor: -1.0 +1.0
t_factor: -1.0 +1.0
Animate
Shader errors, Shader warnings, Javascript info
Open this webgl program in a new tab or window

Note that you can better visualize the changes to the Perlin noise function if you increase the scale of the pattern. The sliders that control the s and t factors can be changed in fine detailed using your keyboard arrow keys (after you move a specific slider and make it the active slider.)

As you experiment with the s and t factors notice that:

  • The pattern will morph for small changes in the factors, but then jump from one pattern to a different pattern. The factors do no change the pattern smoothly at all scales.
  • You get different patterns if you could take the abs() of the pnoise() function (instead of adding 1.0 and dividing by 2.0).
  • Selecting the s and t factors for a desired visual effect is more of an “art” than a “science.”

Other Noise Functions

Other noise functions you might like to investigate at some future time include:

Summary

We have looked at three basic ways to calculate colors across the surface of a face: gradients, overlays of patterns at different scales, and noise. In the next section we will investigate combining these ideas to create interesting and complex textures.

Glossary

procedural texture mapping
Calculate a color for a fragment based on some input values.
image based texture mapping
Get a color for a fragment from a 2D image based on texture coordinates, (s,t).
gradient
In mathematics, an increase or decrease in the magnitude of a property. In computer graphics, an increase or decrease in a color value across the surface of a face.
overlaid patterns
Given a pattern that can be created a various scales, combine multiple instances of the pattern at different scales to create a texture map.
noise
In physics, a random disturbance that obscures or reduces the clarity of a signal. In computer graphics, a perturbation of inputs to simulate randomness.
Next Section - 10.7 - Texture Mapping Using Procedures - Continued