JavaScript EditorFree JavaScript Editor     Ajax Editor 

Main Page
Previous Page
Next Page

15.7. Wood

We can do a fair approximation of wood with this approach as well. In Advanced Renderman, Apodaca and Gritz describe a model for simulating the appearance of wood. We can adapt their approach to create wood shaders in the OpenGL Shading Language. Here are the basic ideas behind the wood fragment shader shown in Listing 15.8.

  • The wood is composed of light and dark areas alternating in concentric cylinders surrounding a central axis.

  • Noise is added to warp the cylinders to create a more natural looking pattern.

  • The center of the "tree" is taken to be the y-axis.

  • Throughout the wood, a high-frequency grain pattern gives the appearance of wood that has been sawed, exposing the open grain nature of the wood.

The wood shader uses the same vertex shader as the other noise-based shaders discussed in this chapter.

15.7.1. Application Setup

The wood shaders don't require too much from the application. The application is expected to pass in a vertex position and a normal per vertex, using the usual OpenGL entry points. In addition, the vertex shader takes a light position and a scale factor that are passed in as uniform variables. The fragment shader takes a number of uniform variables that parameterize the appearance of the wood.

The uniform variables needed for the wood shaders are initialized as follows:


0.0, 0.0, 4.0




0.6, 0.3, 0.1


0.4, 0.2, 0.07










0.5, 0.1, 0.1





15.7.2. Fragment Shader

Listing 15.8 shows the fragment shader for procedurally generated wood.

Listing 15.8. Fragment shader for wood

varying float LightIntensity;
varying vec3  MCposition;

uniform sampler3D Noise;

uniform vec3  LightWood;
uniform vec3  DarkWood;
uniform float RingFreq;
uniform float LightGrains;
uniform float DarkGrains;
uniform float GrainThreshold;
uniform vec3  NoiseScale;
uniform float Noisiness;
uniform float GrainScale;

void main()
    vec3 noisevec = vec3(texture3D(Noise, MCposition * NoiseScale) *
    vec3 location = MCposition + noisevec;

    float dist = sqrt(location.x * location.x + location.z * location.z);
    dist *= RingFreq;

    float r = fract(dist + noisevec[0] + noisevec[1] + noisevec[2]) * 2.0;

    if (r > 1.0)
        r = 2.0 - r;

    vec3 color = mix(LightWood, DarkWood, r);

    r = fract((MCposition.x + MCposition.z) * GrainScale + 0.5);
    noisevec[2] *= r;
    if (r < GrainThreshold)
        color += LightWood * LightGrains * noisevec[2];
        color -= LightWood * DarkGrains * noisevec[2];
    color *= LightIntensity;
    gl_FragColor = vec4(color, 1.0);

As you can see, we've parameterized quite a bit of this shader through the use of uniform variables to make it easy to manipulate through the application's user interface. As in many procedural shaders, the object position is the basis for computing the procedural texture. In this case, the object position is multiplied by NoiseScale (a vec3 that allows us to scale the noise independently in the x, y, and z directions), and the computed value is used as the index into our 3D noise texture. The noise values obtained from the texture are scaled by the value Noisiness, which allows us to increase or decrease the contribution of the noise.

Our tree is assumed to be a series of concentric rings of alternating light wood and dark wood. To give some interest to our grain pattern, we add the noise vector to our object position. This has the effect of adding our low frequency (first octave) noise to the x coordinate of the position and the third octave noise to the z coordinate (the y coordinate won't be used). The result is rings that are still relatively circular but have some variation in width and distance from the center of the tree.

To compute where we are in relation to the center of the tree, we square the x and z components and take the square root of the result. This gives us the distance from the center of the tree. The distance is multiplied by RingFreq, a scale factor that gives the wood pattern more rings or fewer rings.

Following this, we attempt to create a function that goes from 0 up to 1.0 and then back down to 0. We add three octaves of noise to the distance value to give more interest to the wood grain pattern. We could compute different noise values here, but the ones we've already obtained will do just fine. Taking the fractional part of the resulting value gives us a function that ranges from [0,1). Multiplying this value by 2.0 gives us a function that ranges from [0,2). And finally, by subtracting 1.0 from values that are greater than 1.0, we get our desired function that varies from 0 to 1.0 and back to 0.

We use this "triangle" function to compute the basic color for the fragment, using the built-in mix function. The mix function linearly blends LightWood and DarkWood according to our computed value r.

At this point, we would have a pretty nice result for our wood function, but we attempt to make it a little better by adding a subtle effect to simulate the look of open-grain wood that has been sawed. (You may not be able to see this effect on the object shown in Color Plate 25.)

Our desire is to produce streaks that are roughly parallel to the y-axis. So we add the x and z coordinates, multiply by the GrainScale factor (another uniform variable that we can adjust to change the frequency of this effect), add 0.5, and take the fractional part of the result. Again, this gives us a function that varies from [0,1), but for the default values for GrainScale (27.0) and RingFreq (4.0), this function for r goes from 0 to 1.0 much more often than our previous function for r.

We could just make our "grains" go linearly from light to dark but we try something a little more subtle. We multiply the value of r by our third octave noise value to produce a value that increases nonlinearly. Finally, we compare our value of r to the GrainThreshold value (default is 0.5). If the value of r is less than GrainThreshold, we modify our current color by adding to it a value we computed by multiplying the LightWood color, the LightGrains color, and our modified noise value. Conversely, if the value of r is greater than GrainThreshold, we modify our current color by subtracting from it a value we computed by multiplying the DarkWood color, the DarkGrains color, and our modified noise value. (By default, the value of LightGrains is 1.0, and the value of DarkGrains is 0, so we don't actually see any change if r is greater than GrainThreshold.)

You can play around with this effect and see if it really does help the appearance. It seemed to me that it added to the effect of the wood texture for the default settings I've chosen, but there probably is a way to achieve a better effect more simply.

With our final color computed, all that remains is to multiply the color by the interpolated diffuse lighting factor and add an alpha value of 1.0 to produce our final fragment value. The results of our shader are applied to a bust of Beethoven in Color Plate 25.

Previous Page
Next Page

JavaScript EditorAjax Editor     JavaScript Editor