Writing Surface Shaders


return to main index



contents

related pages


OVERVIEW

The role of a surface shader is to set the apparent color and opacity of the light reflected from a sample (small region) of the surface of an object. In this section you will learn to use some of the built-in functions of the language, in particular, how to


  • assign colors to shader instance variables and local variables,
  • modify a color by changing a single component of a color variable,
  • use texture coordinates to set a color,
  • use a texture map to set color and opacity,
  • use a function called noise() to achieve special effects with color and opacity.

This tutorial presents several simple shaders that can act as starting points for your own explorations. The sample shaders are not intended to be used for production.


A BASIC SHADER

The experiments on surface shading in this section are based the following source code. Apart from using several intermediate variables, such as ambientcolor, diffusecolor etc., this shader is identical to the standard Pixar 'plastic' shader.



surface test(float Ka = 1,
                   Kd = 0.5, 
                   Ks = 0.7, 
                   roughness = 0.1;
		           color hilite = 1)
{
normal  nf, n;
vector  i;
color   ambientcolor, diffusecolor, speccolor,
        surfcolor = 1.0, surfopac = Os;
  
/* STEP 1 - adjust the normal */
n = normalize(N);       /* unit normal */
nf = faceforward(n, I); /* normal facing forward */
  
/* STEP 2 - assign the apparent surface opacity */
Oi = surfopac;  
  
/* STEP 3 - calculate the lighting components */
 ambientcolor = Ka * ambient();
 diffusecolor = Kd * diffuse(nf);
i = normalize(-I);
 speccolor = Ks * specular(nf, i, roughness) * hilite;
  
/* STEP 4 - assign the apparent surface color */
 Ci = Oi * Cs * surfcolor * (ambientcolor + diffusecolor + speccolor);
}

In the following examples new or modified code is shown underlined.




ASSIGNING COLORS

Colors may be assigned as a single value or three individual values,

color c;   /* declare a variable of data type color */
c = 0.8;                        /* assign one color */
c = color(0.3, 0.1, 0.5);   /* assign three colors */

In the first example, 0.8 is assigned to all the components of the color. In the second example, the function color() is used to set individual values for each of the red, green and blue components. The rgb color space is the default for surface shaders. Colors may also be modified by assigning a value to an individual component of a color,

color c = color(0.3, 0.1, 0.5); /* declare a color */
setcomp(c, 0, 0.9); /* reset red to 0.9 */

In this example, the function setcomp() has been used to change the red component from 0.3 to 0.9. The indices 0, 1 and 2 reference the red, green and blue color components respectively.

Two colors can be mixed using the mix() function.

color c1 = color(0.3, 0.1, 0.5),
      c2 = color(0.6, 0.7, 0.8),
      c3 = mix(c1, c2, 0.5);

In this example, the function mixes 50% of colors c1 and c2 to create a new color, c3;



USING TEXTURE COORDINATES TO SET A COLOR

While each point (actually a small region called a sample) on a 3D object has an x, y and z coordinate, each "point" is also located in a 2D coordinate system called the 'st' texture space. The 'st' texture coordinates are similiar to the measurements of latitude and longitude used to fix a location on the surface of a cartographers map or on our planet.

When a object is texture mapped the 's' and 't' coordinates of each 3D sample being shaded is used to find the corresponding pixels in a texture map. The color of the pixels taken from the texture mape are assigned to the apparent surface color (Ci) of the corresponding region on the 3D object.

The diagram below illustrates the relationship between the default texture coordinates of a quadric cylinder and an image used for texture mapping.

Note: in Maya a default nurbs cylinder is aligned to the vertical y-axis and has its 't' coordinate (and the 'v' of its uv parametric space) "pointing" upwards.



When a surface shader is used by the renderer, otherwise known as being called or instanced, the global variables 's' and 't' store the specific texture coordinates of the region being shaded. By default the values of 's' and 't' are between zero (0.0) and one (1.0). The specific values of 's' and 't' can be used for any purpose - not just for texture mapping.

In this first example the 's' and 't' values are used to assign (bizarre) color values to each part of a surface. Since the components (ie. red, green and blue) of a color are in the range 0.0 to 1.0 the shader can directly control the red and green components use the 'st' values.


surface bizarre(float Ka = 1, 
                      Kd = 0.5, 
                      Ks = 0.7, 
                      roughness = 0.1; 
		              color hilite = 1)
{
normal  nf, n;
vector  i;
color   ambientcolor, diffusecolor, speccolor, 
        surfcolor = 1, surfopac = Os;

n = normalize(N);
nf = faceforward(n, I);

Oi = surfopac;

 ambientcolor = Ka * ambient();
 diffusecolor = Kd * diffuse(nf);
i = normalize(-I);
 speccolor = Ks * specular(nf, i, roughness) * hilite;
  
surfcolor = color(s,t,0);
  
 Ci = Oi * Cs * surfcolor * (ambientcolor + diffusecolor + speccolor);
}

Modify the Surface statement in the tester RIB file, ie.Surface "bizarre".


Bizarre Shader



USING A TEXTURE MAP TO SET COLOR AND OPACITY

The shading language function texture() is used to read information from a texture file. It can return a color or a single floating point number that represents the average value of the red, green and blue components of the pixel(s) corresponding to the 's' and 't' coordinates of the sample being shaded. In the following example, the texture() function is used to convert our basic shader into a texture mapper.


surface texturemapper(float Ka = 1, 
                            Kd = 0.5, 
                            Ks = 0.7,
                            roughness = 0.1;
		                    color hilite = 1;
		                   string texname = "")
{
normal  nf, n;
vector  i;
color   ambientcolor, diffusecolor, speccolor,
        surfcolor = Cs, surfopac = Os;
   
n = normalize(N);
nf = faceforward(n, I);
   
Oi = surfopac;
   
 ambientcolor = Ka * ambient();
 diffusecolor = Kd * diffuse(nf);
i = normalize(-I);
 speccolor = Ks * specular(nf, i, roughness) * hilite;
  
if(texname != "") /* NOTE: no space between the quotes */
    surfcolor = texture(texname);
 	
 Ci = Oi * surfcolor * (ambientcolor + diffusecolor + speccolor);
}		

If you want to apply a texture map as if it were a grayscale image then use this assignment,

if(texname != "")
    surfcolor = float texture(texname);

The word float before the texture function ensures the function will return a single value ie. a floating point number, that is an average of the red, green and blue values of the source image.

Modify the Surface statement in the tester RIB file, ie. Surface "texturemapper" "texname" ["swazi_princess.tx"]

If you are using the RIB statement "MakeTexture" to make a texture you should also uncomment the MakeTexture line at the beginning of the RIB file. Of course you should obtain your own image for a texture map. Just remember to crop it square, ensure its 256x256 pixels in size, or is a multiple thereof, and is saved as a TIFF file. Use LZW compression where possible.



Texture Mapping


In the next example the function texture() derives a single floating point value that is used to modify the opacity of the point being shaded. Again the Surface statement in the test RIB file should be edited ie. Surface "opacitymapper" "traname" ["swazi_princess.tx"]


surface opacitymapper(float Ka = 1, 
                            Kd = 0.5, 
                            Ks = 0.7, roughness = 0.1;
			                   color hilite = 1;
                     string traname = "")
{
normal  nf, n;
vector  i;
color   ambientcolor, diffusecolor, speccolor,
        surfcolor = Cs, surfopac = Os;
  
n = normalize(N);
nf = faceforward(n, I);
  
 ambientcolor = Ka * ambient();
 diffusecolor = Kd * diffuse(nf);
i = normalize(-I);
 speccolor = Ks * specular(nf, v, roughness) * hilite;
  
if(traname != "")
     Oi = float texture(traname) * Os;
else
    Oi = surfopac;
  
 Ci = Oi * surfcolor * (ambientcolor + diffusecolor + speccolor);
}	

Notice the texture() function is forced to return a single floating point value - the average of the red, green and blue components of the 's', 't' location in the texture file. The gray scale value sets the apparent opacity of the surface.

In particular notice the texture value has been multiplied by Os ie. the true surface opacity. If Os is not used in this way the overall opacity of an object to which this shader is applied cannot be animated.



Opacity Mapper



USING THE NOISE() FUNCTION

The noise() function is similiar to the rand() function in the 'C' language except that, depending on the data types it has been given, it can return different values of similiarly varying types. For example,

noise1D = noise(1.2);      /* a single float */
noise2D = noise(1.2, 3.0); /* two  floats */
noise3D = noise(P);        /* a single "point" */
noise4D = noise(P, 1.3);   /* a "point" and a float */

The noise() function can return either a floating point, a point or a color value. Which ever way the noise function is used, remember it returns a floating point value for 1D and 2D noise in the range 0.0 to 1.0, or a point or a color value with components in the range 0.0 to 1.0. To ensure you get the type of data that you expect "out" of the noise function it is best to cast its return value.

For example, even though the variable "noisyColor" is declared as a color data type it is advisable to prefix the noise function with the word "color" if, for no other reason, than it makes it quite clear that you are generating a color value based on the sample of the surface being shaded ie. point P.

color  surfcolor = color noise(P);

Similiarly, even though the variable "noise2D" is declared as a float, its best to be explicit and cast the return value as follows,

float  noise2D = float noise(s, t);

If the casting had been omitted the shader compiler would have returned the appropriate data type based on the data type of the variable receiving the value from the noise function.

On the other hand its quite acceptable to force the value being returned by the noise function into, what might appear at first sight, to be an inappropriate data type. For example,

color  noisyColor = color noise(s, t);

So what would this give us? Since the noise function has been given two floating point numbers (ie. the values of 's' and 't') it will return a single float that "represents" a 2D noise value. The single value is assigned to a data type with three components. As a result, the compiler uses the output value three times to assign the same value to each of the red, green and blue components. In effect we get a grayscale value based on the texture coordinate of the sample being shaded.

In the following example the noise function assign, albeit in a very arbitary way, the RGB components of the apparent color of the surface of an object. Because the input to the noise function is P ie. an xyz location, the blochy coloration of an object to which this shader is attached will depend on where in a scene the object is located.

A very important consideration that must always be kept in mind is that P is defined in the camera coordinate system. The xyz components of P are measured from the origin of the camera. Visually the coloration of an object will appear to be "locked" to the camera ie. move the camera and the colors on the surface of an object will also move! Refer to Sticky 3D Noise to see how colors can be fixed to an object.


surface noise(float Ka = 1,
                    Kd = 0.5, 
                    Ks = 0.7, 
                    roughness = 0.1;
		            color hilite = 1)
{
normal  nf, n;
vector  i;
color   ambientcolor, diffusecolor, speccolor,
        surfcolor = Cs, surfopac = Os;

n = normalize(N);
nf = faceforward(n, I);

Oi = surfopac;

 ambientcolor = Ka * ambient();
 diffusecolor = Kd * diffuse(nf);
i = normalize(-I);
 speccolor = Ks * specular(nf, v, roughness) * hilite;
  
surfcolor = color noise(P);
  
 Ci = Oi * surfcolor * (ambientcolor + diffusecolor + speccolor);
}		



Noise Shader




TEXTURE COORDINATES AND SPECIAL EFFECTS I

The global variables "s" and "t" store the texture coordinates of the point on a surface that is being rendered. The first shader shown below, stripes.sl, illustrates the use of the mod function to setup a series of repeating transparent stripes. The mod function returns the fractional part of "s * frequency" divided by 1.0.

For example, suppose that a point being shaded has a "s" coordinate of 0.8 and "frequency" has been set to 4, then 0.8 x 4 will equal 3.2. By dividing this value by 1.0, the mod function will return 0.2 ie the fractional part of 3.2. The overall effect is to divide the texture space into 4 equal sub-spaces ("ss") that have values ranging from 0.0 to 0.9999. By checking if the point being shaded is within half of the sub-space (0.5) the opacity of the surface is either set to zero ie. transparent or opaque.


surface stripes(float Ka = 1, 
                      Kd = 0.5, 
                      Ks = 0.7, 
                      roughness = 0.1,
                      frequency = 8;
		              color hilite = 1)
{
normal  nf, n;
vector  i;
color   ambientcolor, diffusecolor, speccolor,
        surfcolor = Cs, surfopac = Os;
 float   ss = mod(s * frequency, 1.0);

n = normalize(N);
nf = faceforward(n, I);
  
if(ss < 0.5)
    Oi = 0;
else
    Oi = surfopac;
  
 ambientcolor = Ka * ambient();
 diffusecolor = Kd * diffuse(nf);
i = normalize(-I);
speccolor = specular(nf, v, roughness);
  
 Ci = Oi * surfcolor * (ambientcolor + diffusecolor + speccolor);
}


Stripes Shader



Although the image shown above does not exhibit aliasing, the shader performs very badly at high frequences. For example, in the test RIB file try this value for the frequency parameter,

Surface "stripes" "frequency" 50

TEXTURE COORDINATES AND SPECIAL EFFECTS II

In the next example both "s" and "t" are converted to sub-spaces ie. divided into bands and on the basis of checking their values "Oi" is set to transparent or the true surface opacity (Os). The result is to cut triagular holes in the surface being shaded.


surface triangles(float Ka = 1, 
                      Kd = 0.5, 
                      Ks = 0.7, 
                      roughness = 0.1,
                      frequency = 8;
		              color hilite = 1)
{
normal  nf, n;
vector  i;
color   ambientcolor, diffusecolor, speccolor,
        surfcolor = Cs, surfopac = Os;
 float ss = mod(s * frequency, 1.0);
 float tt = mod(t * frequency, 1.0);
  
n = normalize(N);
nf = faceforward(n, I);
  
if(ss >= tt)
    Oi = 0;
else
    Oi = surfopac;
  
 ambientcolor = Ka * ambient();
 diffusecolor = Kd * diffuse(nf);
i = normalize(-I);
speccolor = specular(nf, v, roughness);
  
 Ci = Oi * surfcolor * (ambientcolor + diffusecolor + speccolor);
}


Triangles Shader




TEXTURE COORDINATES AND SPECIAL EFFECTS III

In this example the distance of the point being shaded from center of each sub-space is checked. The result is to cut circular holes in the surface being shaded.


surface circularholes(float Ka = 1, 
                            Kd = 0.5, 
                            Ks = 0.7, 
                            roughness = 0.1,
                            radius = 0.4, 
                            frequency = 8;
			                   color hilite = 1;
{
normal  nf, n;
vector  i;
color   ambientcolor, diffusecolor, speccolor,
        surfcolor = Cs, surfopac = Os;
  
 float   ss = mod(s * frequency, 1.0);
 float   tt = mod(t * frequency, 1.0);
float   sDist = 0.5 - ss;
float   tDist = 0.5 - tt;
 float   distance = sqrt(sDist * sDist + tDist * tDist);

n = normalize(N);
nf = faceforward(n, I);

if(distance < radius)
    Oi = 0;
else
    Oi = surfopac;
  
 ambientcolor = Ka * ambient();
 diffusecolor = Kd * diffuse(nf);
i = normalize(-I);
speccolor = specular(nf, v, roughness);
  
 Ci = Oi * surfcolor * (ambientcolor + diffusecolor + speccolor);
}


Circles Shader




© 2002-4 Malcolm Kesson. All rights reserved.