RSL
Introduction to Class-Based Shaders


return to main index



Introduction

This tutorial provides an introduction to the writing of class-based shaders. The notes were prepared using prman 13.5.2. The primary source of information on this topic is the Pixar document,

    DOCS/prman_technical_rendering/AppNotes/ShaderObjects.html

That document refers to enhancements to the shading language that enable shaders to be written in an object oriented style of programming (OOP). Because the term "object" can be generically applied to (almost) anything stored in the memory of a computer the combination of new and existing RSL terminology can be confusing. This tutorial refers to "traditional shaders" and "class-based shaders" in an attempt to distinguish the older shader programming techniques from the new OOP style in which a shader is "wrapped" or "packaged" within a class.

For the purpose of demonstrating the basics of the object oriented features of the RenderMan Shading Language, two shaders from the tutorial "RSL: Shader to Shader Messaging" are used as "starting points" for the development of a couple of variations of a class-based shader.


Basic Code - Traditional Shaders

The combined effect of the shaders in listings 1 and 2 are shown below. The displacement shader, hills.sl, assigns bumpiness to an object. The surface shader, snow.sl, based on its use of the Rsl displacement() function to querry the value of the hump variable of the displacement shader, decides which color to assign to the surface. Both shaders have deliberatrly been kept simple so that the reader can more easily see the correspondence between the original code of listings 1 and 2 and its later use in the class-based shaders.



Figure 1


Listings 1 and 2 (hills.sl, snow.sl)


displacement hills(float  Km = -0.1,
                          Kf = 8;
    output varying float  hump = 0)
{
normal n = normalize(N);
  
hump = noise(transform("shader",P) * Kf);
P = P - n * (hump - 0.5) * Km;
N = calculatenormal(P);
}
surface snow(float Kd = 0.8,
                   snow_ht = 0.5)
{
normal  n = normalize(N), 
        nf = faceforward(n, I);
float   hump = 0;
color   surfcolor = Cs;
  
// Querry the displacement shader 
if(displacement("hump", hump) == 1) {
    if(hump >= snow_ht)
        surfcolor = 1; 
    }
color  diffusecolor = Kd * diffuse(nf); 
Oi = Os;
Ci = Oi * surfcolor * diffusecolor;
}

The rib file used to render figure 1 is shown in listing 3. It should be noted the surface shader, despite changes to the value of the "Kf" parameter of the displacement shader, correcly colorizes (ignore the aliasing) the bumps irrespective of their location on the surface of the sphere. Message passing ensures the coordinated behavior of the shaders.


Listing 3 (snowOnHills.rib)


Display "untitled" "it" "rgba"
Format 250 250 1
Projection "perspective" "fov" 40
ShadingRate 1
  
Translate  0 0 3
Rotate -30 1 0 0
Rotate 0   0 1 0
Scale 1 1 -1
WorldBegin
    LightSource "pointlight" 1 "intensity" 45 "from" [3 3 3]
    TransformBegin
        Surface "snow" "snow_ht" 0.5
        Displacement "hills" "Kf" 8
        
        Attribute "bound" "displacement" [0.1]
        Color 0.341 0.266 0.184
        Sphere 1 -1 1 360
    TransformEnd
WorldEnd


Basic Code - Class Based Shader

Listing 4 gives the first "cut" of a class-based shader that mimics the behavior of hills.sl and snow.sl. The first thing to notice is that the use of the reserved word class gives no indication of what "kind" of shader is being implemented. In contrast, the source code of a traditional shader immediately "declares" what it is implementing by the use of a reserved word such as surface, displacement, light etc.

Only upon further inspection of snowOnHills.sl do we discover that it encapsulates two items of functionality. It can perform displacement shading and surface shading, both of which are implemented by special functions known, in the terminology of Object Oriented Programming (OOP), as methods. A class-based shader, such as snowOnHills, is not required to implement both of these methods but in doing so it ensures that data such as n and hump can be shared its methods.


Listing 4 (snowOnHills.sl)


class snowOnHills(float Kd = 1,
                        Km = -0.1,
                        Kf = 8,
                        snow_ht = 0.5)
{
varying float  hump = 0;
varying normal n = 0;
  
public void displacement(output point P; output normal N) {
    n = normalize(N);
    hump = noise(transform("shader", P) * Kf);
    P = P - n * (hump - 0.5) * Km;
    N = calculatenormal(P);
    }
  
public void surface(output color Ci, Oi) {
    n = normalize(N); 
    normal nf = faceforward(n, I);
    color   surfcolor = Cs;
    
    if(hump >= snow_ht)
        surfcolor = 1;
    color  diffusecolor = Kd * diffuse(nf); 
    Oi = Os;
    Ci = Oi * surfcolor * diffusecolor;
    }
}


Assigning the Shader Object in a Rib File

If the implementation of snowOnHills contained only a displacement method it would be obvious that a rib file that referenced the shader (object) should do so as follows,

    Displacement "snowOnHills" "Km" -0.1 "Kf" 8

However, snowOnHills has both displacement and surface shading capabilities, so it is less obvious how it should be referenced in a rib file. As shown in figures 2 and 3 using it as a Displacment shader or a Surface shader yields very different results.



Figure 2

Using the shader object as a Displacement shader ie.

Displacement "snowOnHills"
Surface "plastic"


Figure 3

Using the shader object as a Surface shader ie.

Surface "snowOnHills"

Specularity is absent from figure 3 because the surface method does not perform a specular lighting calculation. The shader can be adapted to take advantage of (re-lighting) rendering efficiencies that might possibly be introduced in future versions of Pixar's software. This is the subject of the next section.


Factored Lighting

Listing 5 replaces the surface() method with the following,

    public void prelighting (output color Ci, Oi)
    public void lighting    (output color Ci, Oi)
    public void postlighting(output color Ci, Oi)

Apart from the declaration of diffusecolor and surfcolor as member (ie. shared) variables, the functionality of the new shader object is the same as the previous version. Using the new shader object in a rib file is the same as figure 3 ie.

    TransformBegin
        Surface "snowOnHills"
        Attribute "bound" "displacement" [0.1]
        Color 0.341 0.266 0.184
        Sphere 1 -1 1 360
    TransformEnd

Listing 5 (factored lighting)


class snowOnHills(float Kd = 1,
                        Km = -0.1,
                        Kf = 8,
                        snow_ht = 0.5)
{
varying float  hump = 0;
varying normal n = 0;
varying color  diffusecolor = 0;
varying color  surfcolor = 1;
  
public void displacement(output point P; output normal N) {
    n = normalize(N);
    hump = noise(transform("shader", P) * Kf);
    P = P - n * (hump - 0.5) * Km;
    N = calculatenormal(P);
    }
public void prelighting(output color Ci, Oi) {
    if(hump >= snow_ht)
        surfcolor = 1;
    }
public void lighting(output color Ci, Oi) { 
    diffusecolor = diffuse(n) * Kd;
    }
public void postlighting(output color Ci, Oi) { 
    Oi = Os;
    Ci = Oi * Cs * surfcolor * diffusecolor;
    }
}


Using Co Shaders

For the purposes of illustrating the use of co shaders, the version of snowOnHills in this section does not use factored lighting. Although a "co shader" is part of the shading pipeline it is not a shader as such - at least not in the sense that it can be assigned and used, by itself, to shade an object. Instead, it implements one or more methods that can be called upon to perform calculations on behalf of a class-based shader (ie. shader object). For example, listing 6 provides the code for a co shader that "returns" white only for micro-polygons of a bump that are facing upward.


Listing 6 (Co shader)


class hillColor()
{
public void getColor(normal dir; float KsnowLine, altitude; output color c;) {
    vector objectDir = transform("world", dir);
    // Facing upward, therefore, show snow!
    if(ycomp(objectDir) >= 0 && altitude >= KsnowLine)
        c = 1;
    else
        c = Cs;
    }
}

A co shader friendly version of snowOnHills is shown next.


Listing 7


class snowOnHills(float Kd = 1,
                        Km = -0.1,
                        Kf = 8,
                        snow_ht = 0.5;
                string  co_shader = "")
{
varying float  hump = 0;
varying normal n = 0;
  
public void displacement(output point P; output normal N) {
    n = normalize(N);
    hump = noise(transform("shader", P) * Kf);
    P = P - n * (hump - 0.5) * Km;
    N = calculatenormal(P);
    }
  
public void surface(output color Ci, Oi) {
    n = normalize(N); 
    normal nf = faceforward(n, I);
    color   surfcolor = Cs;
    if(co_shader != "")
        {
        shader shd = getshader(co_shader);
        shd->getColor(n, snow_ht, hump, surfcolor);
        }
    else
        {
        if(hump >= snow_ht)
        surfcolor = 1;
        }
    color  diffusecolor = Kd * diffuse(nf); 
    Oi = Os;
    Ci = Oi * surfcolor * diffusecolor;
    }
}

Using a co-shader in a rib file is relatively straightforward. For example, RmanTools can write the appropriate rib statement - figure 4.



Figure 4


As shown below, Cutter inserts a comment that names of the public method(s) implemented by the co-shader. It also provides a generic name ("local_name") by which the co-shader can be referenced by the shader object that will make use of it ie. snowOnHills. It is best to change the generic name to something that is descriptive of the purpose of the co-shader.

    TransformBegin
        # Public method: getColor() 
        Shader "hillColor" "locale_name"
                "foo" 1.0
        Surface "snowOnHills" "co_shader" ["locale_name"]        
        Attribute "bound" "displacement" [0.1]
        Color 0.341 0.266 0.184
        Sphere 1 -1 1 360
    TransformEnd

The effect of the co-shader can be seen in figure 5.



Figure 5


Why Use Co-Shaders?

In the context of the relatively simple code for snowOnHills, it makes very little sense to use a co-shader for the calculation of the surface color. However, that is true only because the code has been deliberately kept simple for the sake of the tutorial. A reason for using a co-shader is its "plug-and-playness". For example, without making any changes to the snowOnHills shader, a different effect can be achieved merely by substituting another co-shader. The main point to note is that snowOnHills expects to call a co-shader that implements a public method with this signature,

    public void getColor(normal; float, float; output color)

Therefore, any co-shader that has a public method of the "form" expected by snowOnHills can be deployed. Listing 8 gives the code for a different co-shader that can be used by snowOnHills.


Listing 8 (drift.sl)


class drift(vector snowDrift = vector(0,1,0))
{
vector snowDir;
  
public void construct() {
    snowDir = transform("object", normalize(snowDrift));
    }
        
public void getColor(normal dir; float KsnowLine, altitude; output color c;) {
    vector objectDir = transform("object", normalize(dir));
    
    // Calculate the dot product to decide if we're 
    // facing the direction of the snow.
    if(objectDir.snowDir >= 0)
        c = 1;
    else
        c = Cs;
    }
}



Figure 6
Setting a snow direction.


The rib that produced figure 6 was edited as follows,

    TransformBegin
        # Public method: getColor() 
        Shader "drift" "locale_name" "snowDrift" [1 1 0]
        Surface "snowOnHills" "co_shader" ["locale_name"]    
        Attribute "bound" "displacement" [0.1]
        Color 0.341 0.266 0.184
        Sphere 1 -1 1 360
    TransformEnd



© 2002- Malcolm Kesson. All rights reserved.