next up previous contents
Next: Get Your Hands Dirty Up: How Do Several Shaders Previous: Mental Ray   Contents

A Simple Scene With Five Different Shaders

Let's create a very simple test scene and write all kind of shaders for it. In figure 5 you see a torus in front of the camera. The right half of the background leaves space for our imager shader, the left half is covered by a bilinear patch. We will use the same scene for RenderMan and mental ray.

Figure 5: Simple test scene
\includegraphics[scale=1.0]{simplescene.ps}

Let's start with RenderMan. The RIB file is so simple that I will explain it a bit and tell you where to make modifications to integrate the shaders step by step. The original RIB file looks like this:

# shadertest.rib
Exposure 1.0 2.2
Display "shadertest.tiff" "file" "rgba"
Format 508 380 1.0
Projection "perspective" "fov" [ 90 ]
LightSource "pointlight" 0 "intensity" [ 1 ]
WorldBegin
  Surface "plastic"
# Patch
  AttributeBegin
    Translate -10 0 10
    Scale 10 10 10
    Patch "bilinear" "P" 
    [ -1.0 -1.0 0.0 1.0 -1.0 0.0 -1.0 1.0 0.0 1.0 1.0 0.0 ] 
  AttributeEnd
# Torus
  AttributeBegin
    Translate 0 1 5
    Rotate 45 1 0 0
    Torus 3.0 1.0 0.0 360.0 360.0
  AttributeEnd
WorldEnd

The scene defines a light source outside the WorldBegin and WorldEnd block. At the moment we use one of the standard shaders pointlight for the light source shader. Within the WorldBegin and WorldEnd block we define the bilinear patch for the background and the torus for the foreground. The geometry is defined within an AttributeBegin and AttributeEnd block. The transformations are defined within this blocks because we don't want them to affect the other geometry. The effect is limited to this block and restored outside. The geometry itself is more or less defined in a single line. The bilinear patch uses 4 points with x-, y-, and z-coordinates. The torus is defined in a line like this:

Torus rmajor rminor phimin phimax thetamax ...parameterlist...

The major radius rmajor defines the distance of a circular arc which could be used to define the torus by rotating this cross section around the z-axis. The minor radius rminor is the radius of the circular arc and the angles are used in a way that we define a whole torus instead of the more general definition.

Figure 6: RenderMan's torus primitive
\includegraphics[scale=0.5]{ribtorus.ps}

If you render the RIB file with a RenderMan compliant renderer the alpha channel will ``cut out'' the part of the background which is not covered by the bilinear patch. Now let's add the following line before WorldBegin:

...
Imager "background" "background" [ 1 0 0 ]
...
WorldBegin
...

This will fill the background which was ``cut out'' before with a red color. We will write our own imager shader now. So what information do we have within a imager shader? Let's try to visualize some of this information. Table 4 on page [*] shows a full list of predefined imager shader variables.

Please modify the RIB file to use our own shader. You should know by now how to do this. The shader looks at the moment like this:

/* myImager */

imager
myImager()
{
  Ci = color(0.0, 1.0, 0.0);
  Oi = 1;
  alpha = 1;
}

Add the proper lines to your Makefile and render the scene. Well, the result is not what we want -- the whole picture is green now. If we take the alpha value into account we could create a simple ``contour shader'':

/* myImager */

imager
myImager()
{
  if (alpha == 0.0) Ci = color(0.0, 1.0, 0.0);
  else if (alpha != 1.0) Ci = color(1.0, 0.0, 0.0);
  Oi = 1;
  alpha = 1;
}

Let's not talk about if a shader like this would be useful or not. What I wanted to show is that shaders are kind of hard to debug. In theory you can create output lines within a shader, analyze this lines with another program, visualize them etc. but the simplest way is to create a test scene where a certain color like red does no occur and use that color to indicate problem areas.

The next version of the imager shader will show how to access some information which does not come from predefined surface shader variables23:

/* myImager */

imager
myImager()
{
  float xyp[3] = { 1.0, 1.0, 1.0 };
  option("Format", xyp);
  Ci = color(xcomp(P) / xyp[0], ycomp(P) / xyp[1], 0);
  Oi = 1;
  alpha = 1;
}

We get access to the Format line in the RIB file and extract the x- and y-resolution of the rendered picture from there. We use that information to scale the pixel coordinates stored in P so that the result is between zero and one. We use that to color encode the coordinates in the background image.

Figure 7: A canvas imager shader for BMRT
\includegraphics[scale=0.5]{canvas.ps}

It's hard to find a real application for an imager shader. I leave it for you as an exercise to find one. In Pixar's document you will find the standard background shader and an example how to write an imager shader to implement the exposure and quantization process:

imager 
exposure(float gain=1.0, gamma=1.0, one = 255, min = 0, max = 255)
{
  Ci = pow(gain * Ci, 1/gamma); 
  Ci = clamp(round(one * Ci), min, max); 
  Oi = clamp(round(one * Oi), min, max);
}

Beside the fact that this shader will not compile it's still an example that an imager shader could be used to do color correction. I think there are not too many applications for an imager shader but one interesting experiment can be found on the following web page from Katsuaki Hiramitsu:

http://www.edit.ne.jp/~katsu/img_index.htm

Unfortunately the shader does not compile for other renderers beside BMRT and it has the ``knowledge'' about the scene within the imager shader. Anyway, maybe worth to look at it in figure 7.

At least an imager shader can be used to develop patterns or to visualize procedural textures without having any geometry or lights in the RIB file. Once you are happy with your pattern you can apply a similar line in a surface shader and get rid of the imager shader.

Let's just create a simple pattern -- a disk -- as described in the book [3]. In chapters 20 to 22 you find more information about patterns but let's just develop one pattern in the imager shader and apply it later for a surface shader. Here is the source code of the imager shader:

/* myImager */

imager
myImager()
{
  /* some of this variables should be shader parameters */
  float xyp[3];
  float xcoord, ycoord;
  float dist;
  float inDisk;
  float radius = 0.4;
  point centre = point(0.5, 0.5, 0.0);
  point here;
  color inside  = color(0, 0, 1);
  color outside = color(1, 0, 0);
  /* get x- and y-resolution from global options */
  option("Format", xyp);
  /* make sure the coordinates are between 0 and 1 */
  xcoord = xcomp(P) / xyp[0];
  ycoord = ycomp(P) / xyp[1];
  here = point(xcoord, ycoord, 0.0);
  /* how far are we away from the centre? */
  dist = distance(centre, here);
  /* are we inside the disk? */
  if (dist <= radius) inDisk = 1.0;
  else inDisk = 0.0;
  /* use inside or outside color */
  Ci = mix(outside, inside, inDisk);
  /* we don't need the alpha channel for this test */
  Oi = 1;
  alpha = 1;
}

Most of the shader is straight forward and I put some comments inside the shader. There are two functions we haven't talked about yet. The function distance returns the Euclidean distance between two point. You could have defined a vector from centre to here and call the function length for this vector or you could have calculated the Euclidean distance yourself with help of the function sqrt24. The other function is called mix and takes two colors and a float value to mix both colors with the following formula:

$(1-a)*color_1+a*color_2$

Before we apply this to a surface shader we make some modifications to mix the two colors slowly. The following output comes from a Unix program called diff which shows only the difference between two file versions:

11a12
>   float fuzz = 0.1;
14c15
<   color inside  = color(0, 0, 1);
---
>   color inside  = color(1, 1, 0);
25,26c26
<   if (dist <= radius) inDisk = 1.0;
<   else inDisk = 0.0;
---
>   inDisk = 1 - smoothstep(radius - fuzz, radius + fuzz, dist);

I defined a new variable called fuzz and changed the outside color from blue to yellow. It looks nicer to mix the colors yellow and red instead of blue and red. Normally you would use the color set by the user in the RIB file as outside color and the inside color would be a shader parameter but we will do that in a minute. The last change I made is that I use a function called smoothstep which defines a range with its first two parameters and ramps smoothly in between. So if our current position here is less than the radius minus our fuzz distance then we are completely inside the disk and use the inside color only. If the current position is greater than our radius plus the fuzz distance we use the outside color only. Every position in between mixes both colors in a way that it slowly fades from one color two the other.

Now let's apply this technique to a surface shader. We have to change a few things to create a surface shader from the imager shader we got so far:

1c1
< /* myImager */
---
> /* mySurface */
3,4c3,4
< imager
< myImager()
---
> surface
> mySurface()
7,8d6
<   float xyp[3];
<   float xcoord, ycoord;
17,18d14
<   /* get x- and y-resolution from global options */
<   option("Format", xyp);
20,22c16
<   xcoord = xcomp(P) / xyp[0];
<   ycoord = ycomp(P) / xyp[1];
<   here = point(xcoord, ycoord, 0.0);
---
>   here = point(s, t, 0.0);
28,31c22,23
<   Ci = mix(outside, inside, inDisk);
<   /* we don't need the alpha channel for this test */
<   Oi = 1;
<   alpha = 1;
---
>   Oi = Os;
>   Ci = Os * mix(outside, inside, inDisk);

First of all we rename the shader to mySurface, we change the type of the shader to surface, and we get rid of a few variables we don't need anymore because we replace the x- and y-coordinates of the image shader by the texture coordinates s and t. We leave the opacity as it is and apply the color without taking any lighting into account25.

Figure 8: Test scene with imager shader and surface shader
\includegraphics[scale=0.5]{simplescene2.ps}

Before we can render the new scene we change the RIB file to use the new surface shader:

9c9
<   Surface "plastic"
---
>   Surface "mySurface"

See figure 8 for the resulting image. I want to change a few things before I continue with other shader types. Read chapter 22 of the book [3] to understand the concept of Tiling and Repeating Patterns. I want the disk pattern to be repeated several times over the surfaces and I want to take the lighting into account:

4c4,5
< mySurface()
---
> mySurface(float Ka = 1, Kd = 0.5, Ks = 0.5, roughness = 0.1;
>         color specularcolor = 1)
10a12
>   float ss, tt;
15,16c17,23
<   /* make sure the coordinates are between 0 and 1 */
<   here = point(s, t, 0.0);
---
>   color myCs;
>   normal Nf = faceforward(normalize(N), I);
>   /* repeat the pattern */
>   ss = mod(s*10, 1);
>   tt = mod(t*10, 1);
>   /* use ss and tt instead of s and t */
>   here = point(ss, tt, 0.0);
23c30,32
<   Ci = Os * mix(outside, inside, inDisk);
---
>   myCs = Os * mix(outside, inside, inDisk);
>   Ci = Os * (myCs * (Ka * ambient() + Kd * diffuse(Nf)) + 
>            specularcolor * Ks * specular(Nf, normalize(-I), roughness));

The easiest thing to do for the lighting is to take parts from the source code of an already existing shader like the plastic shader and change a few lines in the old version. The shader takes now parameters which are identical to the plastic shader. Instead of assigning the color directly to the Ci output variable I use a new variable named myCs for my surface color.

For the repeating disk pattern I introduce the variables ss and tt. The most important lines for the repeating pattern are the two lines using the mod function. This so-called modulo function takes the first argument and divides it by the second one. The remainder of the division is returned. In this case the remainder is the part behind the dot. The return value of mod(3.7,1) is $0.7$. By scaling s and t by $10$ before we use the mod function we make sure that the disk is repeated ten times in each direction but ss and tt still vary between zero and one.

The resulting image looks far too dark. Instead of changing the surface shader we are writing our own light shader. First we will not take any attenuation over distance into account:

/* myLight */

light
myLight()
{
  Cl = color 1;
}

Let's render a picture with this three shaders. You have to change the RIB file before your start rendering:

7c7
< LightSource "pointlight" 0 "intensity" [ 1 ]
---
> LightSource "myLight" 0

Do you remember section 3.2? This is an ambient light and the surface shader calls it because of the ambient() function which is multiplied by one because of the default value of the shader parameter Ka = 1. Let's try another light source shader which fades from near to far linearly between full intensity to black:

/* myLight */

light
myLight(float intensity = 1, near = 1, far = 10;
        color lightcolor = 1;
        point from = point "shader" (0,0,0))
{
  float len, brightness;

  illuminate (from)
    {
      len = length(L);
      if (len < near) brightness = 1;
      else if (len > far) brightness = 0;
      else brightness = 1 - (len - near) / (far - near);
      Cl = intensity * lightcolor * brightness;
    }
}

If we render again the torus will be lit but the linear patch is in the dark. You have to modify the RIB file again to change the default parameter values for the light source shader:

7c7,9
< LightSource "myLight" 0
---
> Declare "near" "float"
> Declare "far" "float"
> LightSource "myLight" 0 "near" [ 3 ] "far" [ 15 ]

Figure 9: Test scene with imager, surface, and light shaders
\includegraphics[scale=0.5]{simplescene3.ps}

Now a bit of the linear patch is lit but not all as you can see in figure 9. We didn't implement a spotlight shader even though it looks a bit like one. Change for a moment the light shader:

13c13
<       if (len < near) brightness = 1;
---
>       if (len < near) brightness = 0;

The reason for this is that it's hard to find the border to the near value, but simple for the far border. If we make everything outside the near and far range unlit it's easier to find good parameter settings. This leads me to a new exercise for you. Think about a way to find the closest and furthermost point to/from the camera on any surface during rendering. Use this values for the previous light source shader and vary the near and far values from there.

We will write a displacement shader now. A very simple one which just uses the sin function along the s and t texture coordinates to vary the position and the normal before the surface shader gets called:

/* myDisplacement */

displacement
myDisplacement()
{
  float amp = 0.1 * sin(t*20*PI);
  amp += 0.1 * sin(s*20*PI);
  P += amp * normalize(N);
  N = calculatenormal(P);
}

Look at figure 10 for the result if you simply change the RIB file to include the displacement shader without specifying additional attributes for the renderer.

22a23
>     Displacement "myDisplacement"

Actually it's dependent on which renderer you are using. BMRT and AIR do use bump mapping if you don't tell the renderer to use true displacement. Pixie and Angel use always true displacement.

Figure 10: Test scene with bump mapping
\includegraphics[scale=0.5]{bumpmapping.ps}

Let's compare this to figure 11 which was rendered with the same shader but with the following additional attributes:

10a11,12
>   Attribute "render" "truedisplacement" [1]
>   Attribute "displacementbound" "sphere" [1]

For bump mapping you get a strange artefact. Some points on the surface are rendered black because the bump mapping bends the normal so much that it's facing away from the light and the camera. The silhouette of the torus is still the original one because the positions on the surface are not modified by bump mapping. But for some areas where the surface is far enough away from the camera the bump mapping works quite well. Especially if the displacement is very subtly which is true for a lot of images you will render. For example if you render skin it's nice to have little dents to make the surface look not too perfect but the dents are small in size and they do not perturb the original surface too much.

Figure 11: Test scene with true displacement
\includegraphics[scale=0.5]{truedisplacement.ps}

Exercises:

  1. Find a ``good'' application for an imager shader.

  2. Change the last surface shader example so that all parameters which might be important to change from outside are shader parameters with default values.

  3. Use the new surface shader parameters to create variations of the same scene by manipulating the RIB file.

  4. Think about writing a shader to find the closest point to the camera on any surface and the furthermost point from the camera. This is not only useful for the light shader we talked about above but also for finding good near and far clipping planes or to have better values for rendering shadow maps.


next up previous contents
Next: Get Your Hands Dirty Up: How Do Several Shaders Previous: Mental Ray   Contents
Jan Walter 2004-02-09