About Shaders and Functions

About Shaders and Functions

Traditional Shaders

Traditional shaders are introduced by the keywords light, surface, volume, displacement, or imager and are followed by the name of the shader and the statements that comprise its definition. These keywords indicate the type of RenderMan Interface shader that is being defined. The RenderMan Interface uses the shader type to enforce a type match in subsequent calls to RenderMan Interface procedures. For example, it is illegal to declare a shader to be of type light and then instance it using RiSurface.

The arguments to a shader are referred to as its instance variables. All of these variables are required have default values, and are assumed to be uniform unless declared otherwise. Shader instance variables can be changed when a particular shader is instanced from the RenderMan Interface. For example, consider the shader weird:

surface
weird( float a=0.5; varying float b=0.25 )
{
     Ci = color (mod(s,a),  abs(sin(a+b)),  mod(b,t));
}

This surface shader may be referenced through the RenderMan Interface with the RiSurface command. For example,

RiSurface( "weird", RI_NULL );

instances the above shader with all its defaults.

Shader instance variable values can be changed from their defaults by passing their new values through the RenderMan Interface. This first requires that the type of the variable be declared. The declaration for weird would be:

RiDeclare( "a", "uniform float" );
RiDeclare( "b", "varying float" );

Of course, the RiDeclare can be eliminated if in-line declarations are used. In either case, uniform instance variables can be set by passing them in the parameterlist of a shader instance. In the following example a is redefined while b remains equal to its default.

RtFloat  a;
RtFloat  bs[4];
RtPoint  Ps[4];
RiPolygon(4, "P", Ps, "a", (RtPointer)&a, "b", (RtPointer)bs, RI_NULL)

a is a single float and b is an array containing four values, since it is a varying variable. The standard variable "P" is predeclared to be of type varying point.

Shader variables can also be set in geometric primitives. For example, the weird shader variables could be set when defining a primitive:

RtFloat a = 0.3;
RiSurface( "weird", "a", (RtPointer)&a, RI_NULL );

If a geometric primitive sets a shader variable that is defined in none of the shaders associated with that primitive, it is ignored. Variables that are set on geometric primitives override the values set by the shader instance.

Shader instance variables are read-only in the blody of the shader, unless they are declared using the output keyword:

displacement
lumpy( float a=0.5; output varying float height=0 )
{
    float h = a * noise(P);
    P += h * normalize(N);
    N = calculatenormal(N);
    height = h;
}

This displacement shader produces a lumpy surface and also saves the displacement amount in an output variable height. Output variables of one shader may be read by other shaders on the same primitive (see the RSL Functions page) in order to pass information between them.


Functions

Functions are similar to shaders except they can be called from shaders or other function but cannot be instanced from the RenderMan Interface. Functions in the Shading Language are also much like functions in C. For example, the following defines a function normalize that returns a unit vector in the direction V:

point
normalize ( vector V )
{
   return V/length(V);
}

Function calls use the traditional C syntax, except for the issues noted below.

Functions may return any of the basic types (float, color, point, vector, normal, matrix, string). Functions may not return arrays, though they may take arrays as parameters.

Function parameters are passed by reference; in other words, parameters are not copied into private areas of the function. Nevertheless, function parameters are not writable unless specified with the output keyword. For example:

void
normalize_inplace ( output vector V )
{
   V = V/length(V);
}

Functions in the Shading Language cannot be called recursively.

Functions may be polymorphic - that is, you may define multiple functions with the same name, as long as they take different types or numbers or arguments. Functions may even be polymorphic based on return type. For example:

float
snoise ( point p )
{
   return 2 * (float noise(p)) - 1;
}
float snoise( float s, float t )
{
   return 2 * (float noise(s,t)) - 1;
}
vector snoise(point p)
{
   return 2 * (vector noise(p)) - 1;
}

Functions may be declared anyplace that it is legal to declare a variable or have any other language statement, including inside the body of a shaders, inside another function, or inside any other code block. A function may only be called within the lexical scope in which it is declared, and only after its declaration (the same rules that apply to using a declared variables). The only data that functions may access are those passed as parameters, except for variables declared with the extern keyword, which indicates that the variable is in an outer scope rather than that local storage should be allocated. For example:

surface mysurf ( float Knoise = 0.5; ) /* parameter */
{
   color shadernoise (point p)
   {
     extern float Knoise;
     return Knoise * color noise(transform("shader", p));
  }
  /* code for the shader */
  Ci = shadernoise(P) * diffuse(faceforward(normalize(N),I));
}

Shader Class Definitions

Shaders can also be declared as class definitions with member variables and multiple methods. For example, here is the outline of a shader that defines both displacement and surface methods, along with a member variable that allows the surface method to avoid repeating a texture lookup that is performed by the displacement method:

class myshader(string mapname="", ...)
{
    varying color Ct = (1,1,1);

    public void displacement(output point P; output normal N) {
        if (mapnamp != "")
            Ct = texture(mapname, ...);
        ...
    }
    public void surface(output color Ci, Oi) {
        ...
        Ci = Ct * ... ;
    }
}

The sections below describe the syntax of shader class definitions in more detail. For a broader discussion, see the Shader Objects and Co-Shaders application note.

Method definitions

A method definition looks like a function definition, but is preceded by the public keyword. Methods that are called by the renderer must define certain arguments. For example, an RiSurface shader can define these methods:

public void displacement(output point P; output normal N);
public void opacity(output color Oi);
public void surface(output color Ci, Oi);

and the following method can be defined by RiAtmosphere, RiInterior and RiExterior shaders:

public void volume(output color Ci, Oi);

Note that Ci and Oi may not be used as global variables in a shader class definition (unlike in traditional shader definitions). Similarly L and Cl must be defined as output parameters of a light method:

public void light(output vector L; output color Cl);

In anticipation of future relighting tools, the surface method can be replaced by these three methods (any of which may be omitted):

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

The prelighting method typically performs texture lookups and BRDF calculations that are independent of the lights. The lighting method contains the illuminance loops that call the lights, and the postlighting method performs any postprocessing that is necessary after the lights are executed. A future relighting tool can cache the state of the shader after prelighting, and it can cache the contributions of the lights. After a light is interactively modified (e.g. changing its position or intensity), the lighting method can be called with only the modified light, re-calculating its contribution.

User-defined methods

In addition to the standard methods described above, shaders can define methods with arbitrary parameters and return values. For example, here is a shader that provides "getter" and "setter methods" for its internal state:

class myshader()
{
    string state = "ready";

    public string GetState() {
        return state;
    }
    public void SetState(string newstate) {
        state = newstate;
    }
    ...
}

Member variables

Member variables are declared inside the body of a shader class definition using the usual variable declaration syntax, which can include an optional initial value. Member variables can be constant, uniform, or varying. A constant member variable is initialized once, when the instance is first created, and is read-only thereafter. Uniform member variables are typically re-initialized each time a new batch of points is shaded, but not always: the value of a uniform member variable can be computed once and persist indefinitely thereafter. Varying member variables are not valid for more than one execution of a shader, since the number of points usually changes each time the shader is executed.

The initial value specified in a member variable definition must be a constant or a simple constant expression (e.g. "sqrt(2)" or "degrees(PI/2)"). Alternatively, an initialization method (similar to a C++ constructor) may be used to initialize member variables. Two initialization methods may be used: the construct method is executed when the instance is first constructed, and the begin method is called each time the shader is prepared for execution on a new batch of points.

class myshader()
{
    constant float _maxdepth;
    uniform float _raydepth;
    uniform float _sides;
    varying point _origP;

    public void construct() {
        option("trace:maxdepth", _maxdepth);
    }

    public void begin() {
        rayinfo("depth", _raydepth);
        _sides = 2;
        attribute("Sides", _sides);
        _origP = P;
    }

    normal shadingnormal(normal N) {
        normal Ns = normalize(N);
        if (_sides == 2 || _raydepth > 0)
            Ns = faceforward(Ns, I, Ns);
        return Ns;
    }

    public void displacement(output point P; output normal N) {
        normal Ns = shadingnormal(N);
        ...
    }
}

It is more efficient to specify a constant initial value in a member variable definition than to assign it in an initialization method. An initialization method is typically used only when the initial value of a member variable requires calculation, depends on shader parameters, etc.

Scoping

Member variables can be used without extern declarations. In addition, extern declarations are no longer required for shader parameters, although they may still be used for documentation purposes. (The only time extern declarations are required is when a nested function uses variables in an enclosing function.)

Also, note that member variables can be used freely in ordinary (private) functions (not just public methods), provided the functions are defined inside the shader class definition. For example:

class myshader() {
    string state = "ready";

    // This is an ordinary (private) function, not a public method.
    void proceed() {
        state = "executing";  // OK to access member variable.
        ...
    }
    ...
}

Public variables and message passing

Unlike shader parameters, member variables are private by default. Delaring a member variable with a public keyword makes it visible to other shaders:

class myshader(float a = 0) // a is public
{
    float b = 1;            // b is private
    public float c = 2;     // c is public
}

"Message passing" is the traditional term for communication between shaders. For example, a light shader traditionally uses a surface() call to access surface shader parameter values:

float surfaceKd = 0;
surface("Kd", surfaceKd);

Message passing functions now provide access to all the public variables of a shader, namely its parameters and its public member variables.

A more general built-in function called getvar operates on an arbitrary shader:

shader baselayer = getshader("baselayer");
float baseKd = 0;
getvar(baselayer, "Kd", baseKd);

The getvar function operates like the surface function: if the requested variable is not found, it returns zero and does not modify the output parameter. It returns one for success.

The output parameter of getvar can be omitted, in which case it simply checks whether the specified variable exists:

float hasKd = getvar(baselayer, "Kd");

It is also possible to check for the existence of a method:

float hasSurface = hasmethod(baselayer, "surface");

For convenience, an "arrow operator" can also be used to get the value of a member variable or shader parameter:

float baseKd = baselayer->Kd;

When this syntax is used, a warning is reported if the specified variable does not exist or does not have the expected type, in which case it returns zero(s) (or the empty string, etc.) The arrow operator is useful when a "strongly typed" programming style is adopted: rather than testing each variable access for errors, the shader is tested once (e.g. to verify its category), after which it is assumed to satisfy a particular interface.

Optional method parameters

It is often useful to pass additional arguments to a light method. Such parameters must be treated as optional, since not all light methods would necessarily accept those parameters.

To accomplish this, a method definition can specify any number of optional parameters following the required parameters. An optional argument is declared by including a default value in the definition of a formal parameter. For example:

public void foo(float x; float y = 1; float z = 2) {
    ...
}

To avoid ambiguity, optional parameters must follow required parameters (i.e. they cannot be intermingled). Default values of method parameters must be constants (or constant expressions such as MAX_SIZE+1).

A method call passes required parameters by position, whereas optional parameters are passed as name/value pairs. Optional parameters can be passed in any order, and undefined parameters are ignored (but type errors are not ignored.) For example:

bar->foo(0);
bar->foo(0, "y", 3);
bar->foo(0, "z", 4, "y", 3);

Optional parameters are slightly more expensive than required parameters, so they should not be used except when necessary.

For backwards compatibility, method calls with optional arguments also work when calling traditional shaders, except that the optional argument values are passed as shader parameters. For example, the following traditional shader

light foo(float y = 1; float z = 2)
{
    ...
}

could be called as follows:

lights[i]->light(L, Cl, "y", 3, "z", 2);

The area light shader in Shader Objects, Appendix B demonstrates the usefulness of optional method parameters. It defines a light method that takes P as an optional input parameter and produces resizable arrays of colors and directions as output parameters:

public void light( output vector L;
                   output color Cl;
                   point P = (0,0,0);
                   output color lightcolors[] = { };
                   output vector lightdirs[] = { } );

Reserved Parameter Names

The renderer currently uses a flat name space and a single symbol table for both its internal definitions and user-declared parameters (shader arguments, vertex variables). This can cause problems when a shader argument has the same name as an internal renderer symbol, for example, "origin" as used in the Display command, and "origin" as used as a shader parameter. An RiDeclare can be inserted to change the type of "origin" to the type expected by the shader, but this will cause problems if a subsequent Display command is issued with the parameter "option" specified.

Certain names have been defined as parameters for the standard shaders and the appropriate RiDeclare issued for them. It is strongly recommended that the type of these shader parameters are not changed in order to preserve some degree of compatibility with the standard shaders.

Predefined Renderer Symbols

The following names are reserved for the renderer and must never be used as the names for shader parameters:

Cs az epsilon2 nearhither subwindow
N bias0 eyesplits origin t
Ng bias1 flatness print texture
Np binary fov rgb texturememory
Os bucketsize gridarea rgba trimdeviation
P bucketstride gridsize rgbaz z
Pw displacement interleave rgbz

Pz dynamic jitter s

a endofframe merge shader

algorithm epsilon1 name st

Predefined Shader Parameters

The following shader parameters are pre-defined in the renderer and their types should not be changed:

Ka amplitude conedeltaangle lightcolor specularcolor
Kd background distance maxdistance texturename
Kr beamdistribution from mindistance to
Ks coneangle intensity roughness