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.
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.
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.
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:
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.
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
. By scaling s and t by
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 ]
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.
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.
Exercises: