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
 
1
// Fragment shader
2
// By: Dr. Wayne Brown, Spring 2016
3
4
precision mediump int;
5
precision mediump float;
6
7
varying vec4 v_vertex_color;
8
varying vec2 v_Texture_coordinate;
9
10
//-------------------------------------------------
11
// Calculate a color based on the texture coordinates
12
vec4 gradient(vec2 tex_coords) {
13
  float s = tex_coords[0];
14
  float t = tex_coords[1];
15
  vec3 red = vec3(1.0, 0.0, 0.0);
16
  float percent;
17
18
  percent = s;
19
  return vec4(red * percent, 1.0);
20
}
21
22
//-------------------------------------------------
23
void main() {
24
  gl_FragColor = gradient(v_Texture_coordinate);
25
}
26
27
./gradient/gradient.html

Gradient texture mapping experiments.

Please use a browser that supports "canvas"
Animate








Shader errors, Shader warnings, Javascript info
Shader '../lib/shaders/shader30.vert' has been downloaded.
In _initializeRendering: 1 of 3 files have been retrieved
Shader '../lib/shaders/shader31.frag' has been downloaded.
In _initializeRendering: 2 of 3 files have been retrieved
Model '../lib/models/textured_cube_faces.obj' has been downloaded.
Found these material MTL files: 0
In _initializeRendering: 3 of 4 files have been retrieved
Materials file '../lib/models/textured_cube_faces.mtl' has been downloaded.
In _initializeRendering: 4 of 4 files have been retrieved
All files have been retrieved!
Created model: Cube
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
39
 
1
// Fragment shader
2
// By: Dr. Wayne Brown, Spring 2016
3
4
precision mediump int;
5
precision mediump float;
6
7
varying vec4 v_vertex_color;
8
varying vec2 v_Texture_coordinate;
9
10
//-------------------------------------------------
11
// Calculate a pattern based on the texture coordinates
12
float checkerboard(vec2 tex_coords, float scale) {
13
  float s = tex_coords[0];
14
  float t = tex_coords[1];
15
16
  float sum = floor(s * scale) + floor(t * scale);
17
  bool isEven = mod(sum,2.0)==0.0;
18
  float percent = (isEven) ? 1.0 : 0.0;
19
20
  return percent;
21
}
22
23
//-------------------------------------------------
24
// Calculate a color based on a pattern defined
25
// by the texture coordinates
26
vec4 overlay(vec2 tex_coords) {
27
  vec3 red = vec3(1.0, 0.0, 0.0);
28
29
  float percent = checkerboard(tex_coords, 3.0);
30
31
  return vec4(red * percent, 1.0);
32
}
33
34
//-------------------------------------------------
35
void main() {
36
  gl_FragColor = overlay(v_Texture_coordinate);
37
}
38
39
./checkerboard/checkerboard.html

Checkerboard texture mapping experiments.

Please use a browser that supports "canvas"
Animate








Shader errors, Shader warnings, Javascript info
Model '../lib/models/textured_cube_faces.obj' has been downloaded.
Found these material MTL files: 0
In _initializeRendering: 1 of 4 files have been retrieved
Shader '../lib/shaders/shader32.frag' has been downloaded.
In _initializeRendering: 2 of 4 files have been retrieved
Shader '../lib/shaders/shader30.vert' has been downloaded.
In _initializeRendering: 3 of 4 files have been retrieved
Materials file '../lib/models/textured_cube_faces.mtl' has been downloaded.
In _initializeRendering: 4 of 4 files have been retrieved
All files have been retrieved!
Created model: Cube
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
44
 
1
// Fragment shader
2
// By: Dr. Wayne Brown, Spring 2016
3
4
precision mediump int;
5
precision mediump float;
6
7
varying vec4 v_vertex_color;
8
varying vec2 v_Texture_coordinate;
9
10
float PI = 3.141592653589793;
11
12
//-------------------------------------------------
13
// Calculate a pattern based on the texture coordinates
14
float circular_gradient(vec2 tex_coords, float scale) {
15
  float s = tex_coords[0];
16
  float t = tex_coords[1];
17
18
  float percent = abs(sin(s * scale*PI)) * abs(sin(t * scale*PI));
19
  return percent;
20
}
21
22
//-------------------------------------------------
23
// Calculate a color based on a pattern defined
24
// by the texture coordinates
25
vec4 overlay(vec2 tex_coords) {
26
  vec3 red = vec3(1.0, 0.0, 0.0);
27
28
  const int n = 1;
29
  float scale = 1.0;
30
  float percent = 0.0;
31
  for (int j=0; j<n; j++) {
32
    percent += (1.0/float(n)) * circular_gradient(tex_coords, scale);
33
    scale *= 2.0;
34
  }
35
36
  return vec4(red * percent, 1.0);
37
}
38
39
//-------------------------------------------------
40
void main() {
41
  gl_FragColor = overlay(v_Texture_coordinate);
42
}
43
44
./circular_overlays/circular_overlays.html

Circular overlays texture mapping experiments.

Please use a browser that supports "canvas"
Animate








Shader errors, Shader warnings, Javascript info
Shader '../lib/shaders/shader33.frag' has been downloaded.
In _initializeRendering: 1 of 3 files have been retrieved
Model '../lib/models/textured_cube_faces.obj' has been downloaded.
Found these material MTL files: 0
In _initializeRendering: 2 of 4 files have been retrieved
Shader '../lib/shaders/shader30.vert' has been downloaded.
In _initializeRendering: 3 of 4 files have been retrieved
Materials file '../lib/models/textured_cube_faces.mtl' has been downloaded.
In _initializeRendering: 4 of 4 files have been retrieved
All files have been retrieved!
Created model: Cube
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
141
 
1
//=========================================================================
2
// Source: https://raw.githubusercontent.com/ashima/webgl-noise/master/src/classicnoise2D.glsl
3
//
4
// GLSL textureless classic 2D noise "cnoise",
5
// with an RSL-style periodic variant "pnoise".
6
// Author:  Stefan Gustavson (stefan.gustavson@liu.se)
7
// Version: 2011-08-22
8
//
9
// Many thanks to Ian McEwan of Ashima Arts for the
10
// ideas for permutation and gradient selection.
11
//
12
// Copyright (c) 2011 Stefan Gustavson. All rights reserved.
13
// Distributed under the MIT license. See LICENSE file.
14
// https://github.com/ashima/webgl-noise
15
//
16
17
precision mediump int;
18
precision mediump float;
19
20
vec4 mod289(vec4 x)
21
{
22
  return x - floor(x * (1.0 / 289.0)) * 289.0;
23
}
24
25
vec4 permute(vec4 x)
26
{
27
  return mod289(((x*34.0)+1.0)*x);
28
}
29
30
vec4 taylorInvSqrt(vec4 r)
31
{
32
  return 1.79284291400159 - 0.85373472095314 * r;
33
}
34
35
vec2 fade(vec2 t) {
36
  return t*t*t*(t*(t*6.0-15.0)+10.0);
37
}
38
39
// Classic Perlin noise
40
float cnoise(vec2 P)
41
{
42
  vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
43
  vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
44
  Pi = mod289(Pi); // To avoid truncation effects in permutation
45
  vec4 ix = Pi.xzxz;
46
  vec4 iy = Pi.yyww;
47
  vec4 fx = Pf.xzxz;
48
  vec4 fy = Pf.yyww;
49
50
  vec4 i = permute(permute(ix) + iy);
51
52
  vec4 gx = fract(i * (1.0 / 41.0)) * 2.0 - 1.0 ;
53
  vec4 gy = abs(gx) - 0.5 ;
54
  vec4 tx = floor(gx + 0.5);
55
  gx = gx - tx;
56
57
  vec2 g00 = vec2(gx.x,gy.x);
58
  vec2 g10 = vec2(gx.y,gy.y);
59
  vec2 g01 = vec2(gx.z,gy.z);
60
  vec2 g11 = vec2(gx.w,gy.w);
61
62
  vec4 norm = taylorInvSqrt(vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)));
63
  g00 *= norm.x;
64
  g01 *= norm.y;
65
  g10 *= norm.z;
66
  g11 *= norm.w;
67
68
  float n00 = dot(g00, vec2(fx.x, fy.x));
69
  float n10 = dot(g10, vec2(fx.y, fy.y));
70
  float n01 = dot(g01, vec2(fx.z, fy.z));
71
  float n11 = dot(g11, vec2(fx.w, fy.w));
72
73
  vec2 fade_xy = fade(Pf.xy);
74
  vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
75
  float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
76
  return 2.3 * n_xy;
77
}
78
79
// Classic Perlin noise, periodic variant
80
float pnoise(vec2 P, vec2 rep)
81
{
82
  vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
83
  vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
84
  Pi = mod(Pi, rep.xyxy); // To create noise with explicit period
85
  Pi = mod289(Pi);        // To avoid truncation effects in permutation
86
  vec4 ix = Pi.xzxz;
87
  vec4 iy = Pi.yyww;
88
  vec4 fx = Pf.xzxz;
89
  vec4 fy = Pf.yyww;
90
91
  vec4 i = permute(permute(ix) + iy);
92
93
  vec4 gx = fract(i * (1.0 / 41.0)) * 2.0 - 1.0 ;
94
  vec4 gy = abs(gx) - 0.5 ;
95
  vec4 tx = floor(gx + 0.5);
96
  gx = gx - tx;
97
98
  vec2 g00 = vec2(gx.x,gy.x);
99
  vec2 g10 = vec2(gx.y,gy.y);
100
  vec2 g01 = vec2(gx.z,gy.z);
101
  vec2 g11 = vec2(gx.w,gy.w);
102
103
  vec4 norm = taylorInvSqrt(vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)));
104
  g00 *= norm.x;
105
  g01 *= norm.y;
106
  g10 *= norm.z;
107
  g11 *= norm.w;
108
109
  float n00 = dot(g00, vec2(fx.x, fy.x));
110
  float n10 = dot(g10, vec2(fx.y, fy.y));
111
  float n01 = dot(g01, vec2(fx.z, fy.z));
112
  float n11 = dot(g11, vec2(fx.w, fy.w));
113
114
  vec2 fade_xy = fade(Pf.xy);
115
  vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
116
  float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
117
  return 2.3 * n_xy;
118
}
119
120
//=========================================================================
121
// Fragment shader
122
// By: Dr. Wayne Brown, Spring 2016
123
124
varying vec4 v_vertex_color;
125
varying vec2 v_Texture_coordinate;
126
127
//-------------------------------------------------
128
// Calculate a color based on a pattern defined
129
// by the texture coordinates
130
vec4 noise(vec2 tex_coords) {
131
132
  float percent = (1.0 + cnoise(tex_coords)) / 2.0;
133
134
  return vec4(percent, percent, percent, 1.0);
135
}
136
137
//-------------------------------------------------
138
void main() {
139
  gl_FragColor = noise(v_Texture_coordinate);
140
}
141
./noise/noise.html

Noise texture mapping experiments.

Please use a browser that supports "canvas"
Animate








Shader errors, Shader warnings, Javascript info
Shader '../lib/shaders/shader30.vert' has been downloaded.
In _initializeRendering: 1 of 3 files have been retrieved
Model '../lib/models/textured_cube_faces.obj' has been downloaded.
Found these material MTL files: 0
In _initializeRendering: 2 of 4 files have been retrieved
Shader '../lib/shaders/shader34.frag' has been downloaded.
In _initializeRendering: 3 of 4 files have been retrieved
Materials file '../lib/models/textured_cube_faces.mtl' has been downloaded.
In _initializeRendering: 4 of 4 files have been retrieved
All files have been retrieved!
Created model: Cube
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
145
 
1
//=========================================================================
2
// Source: https://raw.githubusercontent.com/ashima/webgl-noise/master/src/classicnoise2D.glsl
3
//
4
// GLSL textureless classic 2D noise "cnoise",
5
// with an RSL-style periodic variant "pnoise".
6
// Author:  Stefan Gustavson (stefan.gustavson@liu.se)
7
// Version: 2011-08-22
8
//
9
// Many thanks to Ian McEwan of Ashima Arts for the
10
// ideas for permutation and gradient selection.
11
//
12
// Copyright (c) 2011 Stefan Gustavson. All rights reserved.
13
// Distributed under the MIT license. See LICENSE file.
14
// https://github.com/ashima/webgl-noise
15
//
16
17
precision mediump int;
18
precision mediump float;
19
20
vec4 mod289(vec4 x)
21
{
22
  return x - floor(x * (1.0 / 289.0)) * 289.0;
23
}
24
25
vec4 permute(vec4 x)
26
{
27
  return mod289(((x*34.0)+1.0)*x);
28
}
29
30
vec4 taylorInvSqrt(vec4 r)
31
{
32
  return 1.79284291400159 - 0.85373472095314 * r;
33
}
34
35
vec2 fade(vec2 t) {
36
  return t*t*t*(t*(t*6.0-15.0)+10.0);
37
}
38
39
// Classic Perlin noise
40
float cnoise(vec2 P)
41
{
42
  vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
43
  vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
44
  Pi = mod289(Pi); // To avoid truncation effects in permutation
45
  vec4 ix = Pi.xzxz;
46
  vec4 iy = Pi.yyww;
47
  vec4 fx = Pf.xzxz;
48
  vec4 fy = Pf.yyww;
49
50
  vec4 i = permute(permute(ix) + iy);
51
52
  vec4 gx = fract(i * (1.0 / 41.0)) * 2.0 - 1.0 ;
53
  vec4 gy = abs(gx) - 0.5 ;
54
  vec4 tx = floor(gx + 0.5);
55
  gx = gx - tx;
56
57
  vec2 g00 = vec2(gx.x,gy.x);
58
  vec2 g10 = vec2(gx.y,gy.y);
59
  vec2 g01 = vec2(gx.z,gy.z);
60
  vec2 g11 = vec2(gx.w,gy.w);
61
62
  vec4 norm = taylorInvSqrt(vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)));
63
  g00 *= norm.x;
64
  g01 *= norm.y;
65
  g10 *= norm.z;
66
  g11 *= norm.w;
67
68
  float n00 = dot(g00, vec2(fx.x, fy.x));
69
  float n10 = dot(g10, vec2(fx.y, fy.y));
70
  float n01 = dot(g01, vec2(fx.z, fy.z));
71
  float n11 = dot(g11, vec2(fx.w, fy.w));
72
73
  vec2 fade_xy = fade(Pf.xy);
74
  vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
75
  float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
76
  return 2.3 * n_xy;
77
}
78
79
// Classic Perlin noise, periodic variant
80
float pnoise(vec2 P, vec2 rep)
81
{
82
  vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0);
83
  vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0);
84
  Pi = mod(Pi, rep.xyxy); // To create noise with explicit period
85
  Pi = mod289(Pi);        // To avoid truncation effects in permutation
86
  vec4 ix = Pi.xzxz;
87
  vec4 iy = Pi.yyww;
88
  vec4 fx = Pf.xzxz;
89
  vec4 fy = Pf.yyww;
90
91
  vec4 i = permute(permute(ix) + iy);
92
93
  vec4 gx = fract(i * (1.0 / 41.0)) * 2.0 - 1.0 ;
94
  vec4 gy = abs(gx) - 0.5 ;
95
  vec4 tx = floor(gx + 0.5);
96
  gx = gx - tx;
97
98
  vec2 g00 = vec2(gx.x,gy.x);
99
  vec2 g10 = vec2(gx.y,gy.y);
100
  vec2 g01 = vec2(gx.z,gy.z);
101
  vec2 g11 = vec2(gx.w,gy.w);
102
103
  vec4 norm = taylorInvSqrt(vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)));
104
  g00 *= norm.x;
105
  g01 *= norm.y;
106
  g10 *= norm.z;
107
  g11 *= norm.w;
108
109
  float n00 = dot(g00, vec2(fx.x, fy.x));
110
  float n10 = dot(g10, vec2(fx.y, fy.y));
111
  float n01 = dot(g01, vec2(fx.z, fy.z));
112
  float n11 = dot(g11, vec2(fx.w, fy.w));
113
114
  vec2 fade_xy = fade(Pf.xy);
115
  vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x);
116
  float n_xy = mix(n_x.x, n_x.y, fade_xy.y);
117
  return 2.3 * n_xy;
118
}
119
120
//=========================================================================
121
// Fragment shader
122
// By: Dr. Wayne Brown, Spring 2016
123
124
uniform float u_Scale;
125
uniform float u_S_factor;
126
uniform float u_T_factor;
127
128
varying vec4 v_vertex_color;
129
varying vec2 v_Texture_coordinate;
130
131
//-------------------------------------------------
132
// Calculate a color based on a pattern defined
133
// by the texture coordinates
134
vec4 noise(vec2 tex_coords) {
135
136
  float percent = (1.0 + pnoise(u_Scale * tex_coords, vec2(u_S_factor, u_T_factor))) / 2.0;
137
138
  return vec4(percent, percent, percent, 1.0);
139
}
140
141
//-------------------------------------------------
142
void main() {
143
  gl_FragColor = noise(v_Texture_coordinate);
144
}
145
./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
Shader '../lib/shaders/shader30.vert' has been downloaded.
In _initializeRendering: 1 of 3 files have been retrieved
Model '../lib/models/textured_cube_faces.obj' has been downloaded.
Found these material MTL files: 0
In _initializeRendering: 2 of 4 files have been retrieved
Shader '../lib/shaders/shader35.frag' has been downloaded.
In _initializeRendering: 3 of 4 files have been retrieved
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