Short Contents

6.1 Syntax

6.1.1 General Shader Structure

6.1.1.1 Traditional Structure

Traditional shaders are pre-RSL 2.0 shaders, they exhibit a very simple structure consisting of one main entry point and a certain number of supporting functions (a structure clearly inspired by the C programming language).

{.. Function declarations .. }

[surface|displacement|volume|imager|light] shadername( Parameters )
{
    .... shader body ...
}

The output of such shaders depends solely on their type and is passed back to the renderer through a standard output variable (see section Predefined Shader Variables). Here is a summery for each shader type:

surface
Surface shaders define the color and the opacity (or transparency) of the material by setting the Ci and Oi output variables.
displacement
Displacement shaders can perturb the geometry of the material by modifying the P and N variables.
volume
Volume shaders are executed in-between surfaces or between the eye and some surface (for atmosphere shaders) to simulate various atmospheric effects. They modify Ci and Oi as in surface shaders.
imager
Imager shaders run on the image plane, on every pixel. They modify Ci and alpha variables.
light
Light shaders are slightly more complicated than other shaders in that they also declare an illuminate() construct(22) (see The Illuminate Construct). They operate by setting Cl and L output variables.

6.1.1.2 Shader Objects

The modern structure, introduced in RSL 2.0 specifications, closely resembles C++ or Java classes. The general structures is as follows:

class shadername( ... shader parameters ... )
{
    ... member variables declarations ... 

    ... member methods declarations ... 
};

In a nutshell this new structure (an example is shown in Listing 6.1) provides interesting capabilities and some performance benefits:

  1. Shaders can call methods and access member variables of other shaders (only public variables and methods are accessible, as expected). This feature allows, finally, to build comprehensive shader libraries that are pre-compiled and re-usable. Intra-shader data and method access is performed using the shader variable type (see section Co-Shaders).
  2. Member variables can retain shader state. This avoids ad-hoc message passing and other "tricks" widespread in traditional shaders.
  3. Access to co-shaders encourages a modular, layer-based, development of looks. For example, illumination models can be passed as co-shaders to surface shaders which can take care of the general structure.
  4. The separation of displacement, surface and opacity methods (see below) provides the render with greater opportunities to optimize renderings.
  5. Last but not least, shader objects provide a greater flexibility overall and promising future development perspectives.

Variables and method declarations have the usual form of traditional shaders but can be prefixed by the public keyword. So how does such a class shader define its function (surface, displacement, ... etc) ? By implementing predefined methods as shown in Table 6.1.

public void surface ( output color Ci, Oi; ... )
Declaring this method makes the class usable as a valid surface shader (can be called by RiSurface()).
public void displacement ( output point P; output normal N; ... )
Declaring this method makes the class usable as a valid displacement shader (can be called by RiDisplacement()).
public void volume ( output color Ci, Oi; ... )
Declaring this method makes the class usable as a valid volume shader (can be called by RiInterior() and RiAtmopshere).
public void light(output vector L; output color Cl; ...)
Declaring this method makes the class usable as a valid light shader (can be called by RiLightSource() and RiAreaLight).
public void opacity ( output color Oi; ... )
If this function is declared, 3Delight will call it when evaluating transmission rays (see transmission shadeop). This can lead to a good performance benefit since computing opacity is usually much less expensive than evaluating the entire surface shader.
Table 6.1: Predefined class shader methods.


This shader structure allows the inclusion of both a surface and a displacement entry points. Listing 6.1 demonstrates how this is done and shows the clear benefits of retaining the state of variables in shader objects. In the context of traditional shaders, sharing data between different methods had semantics or was resolved using ad-hoc methods:

  1. Share the result of the computation using message passing (see section Message Passing and Information). This works but complicates the data flow.
  2. Re-do the same computation in both the surface and displacement shaders. Also works but time is lost in re-computation.
  3. Displacement is performed inside the surface shader. This is the most practical solution but sadly it doesn't give the renderer a chance to do some important render-time optimizations.

class plastic_stucco(
    float Ks = .5;
    float Kd = .5;
    float Ka = 1;
    float roughness = .1;
    color specularcolor = 1;
    float Km = 0.05;
    float power = 5;
    float frequency = 10;
    color dip_color = color(.7,.0, .2) )
{
    varying float magnitude = 0;

    public void displacement(output point P; output normal N)
    {
        /* put normalized displacement magnitude in a global variable
           so that the surface method below can use it. */
        point PP = transform ("shader", P);
        magnitude = pow (noise (PP*frequency), power);

        P += Km * magnitude * normalize (N);
        N = calculatenormal (P);
    }

    public void surface(output color Ci, Oi)
    {
        normal Nf =  faceforward( normalize(N), I );
        vector V  = - normalize( I );

        color specular_component =
            specularcolor * Ks * specular(Nf, V, roughness);
        color diffuse_component = Kd * diffuse(Nf);

        /* attenuate the specular component in displacement "dips" */
        specular_component *= (1-magnitude) * (1-magnitude);

        Oi = Os;
        Ci = ( Cs * (Ka * ambient() + diffuse_component) +
            specular_component );

        Ci = mix(dip_color*diffuse_component, Ci, 1-magnitude );

        Ci *= Oi;
    }
}
Listing 6.1: En example class shader computing both surface shading and displacement.

6.1.2 Variable Types

6.1.2.1 Scalars

All scalars in the RenderMan shading language, including integers, are declared using the float keyword. As an example, the following line declares two scalars and sets the value of one of them to 1.

float amplitude = 1.0, frequency;

Some notes about scalars:

6.1.2.2 Colors

The shading language defines colors as an abstract data type: it could be an RGB 3-tuple or an elaborate spectrum definition. But until now, all known implementations only allow a 3-tuple definition in various color spaces. A color definition has the following form (square brackets mean that enclosed expression is optional):

color  color_name = [color "color_space"](x, y, z); 

As an example, the following line declares a color in HSV space:

color foo = color "hsv" (.5, .5, .5);

If the space is not provided it is assumed to be RGB:

color foo = 1; /* set the color to (1,1,1) in RGB space. */

` rgb'
Red, Gree and Blue.
` hsv'
Hue, Saturation and Value.
` hsl'
Hue, Saturation and Lightness.
` xyz'
CIE XYZ coordinates.
` yiq'
NTSC coordinates.
Table 6.2: Supported color spaces.

Operations on colors are: addition, multiplication and subtraction. All operations are performed channel per channel. Transformation of points between the various color spaces is performed using the ctransform() shadeop (see ctransform)

6.1.2.3 Points, Vectors and Normals

Point-like variables are 3-tuples used to store positions, directions and normals in the euclidean 3-space. Such a 3-tuple definition is of the form:

{point|normal|vector} = [ {point|normal|vector} "coordsys"] (x, y, z);

Where `coordsys' can be one of standard coordinate systems (see Table 6.3) or any coordinate system defined by RiCoordinateSystem. As an example, all the following definitions are valid:

point p1 = (1, 1, 1); /* declare a point in current coordinate system */
vector v1 = vector "world" (0,0,0); /* a vector in world coordinates */
normal NN = normal 0; /* normal in current space. Note type promotion */

current
The default coordinate system in which the renderer performs its work. In 3Delight, the `current' space is the same as the `camera' space
object
The coordinate system in which the primitive is declared
shader
The coordinate system in which the shader is declared
world
The coordinate system active at RiWorldBegin
camera
The coordinate system of the camera with positive z pointing forward, positive x pointing to the right and positive y pointing up
[cameraname:]screen
A perspective corrected coordinate system of the camera's image plane. The range is defined by RiScreenWindow. The optional cameraname can be used to specify any camera declared with RiCamera and will transform a point from current space to target camera screen space.
[cameraname:]raster
The 2D projected space of the image. The upper-left corner of the image is (0, 0) with x and y increasing towards their maximum values as specified by RiFormat. The optional cameraname can be used to specify any camera declared with RiCamera and will transform a point from current space to target camera raster space
[cameraname:]NDC
The Normalized Device Coordinates. Like `raster' space but x and y goes from 0 to 1 across the whole image. In other words: NDC(x) = raster(x) / Xres and NDC(y) = raster(y) / Yres. The optional cameraname can be used to specify any camera declared with RiCamera and will transform a point from current space to target camera NDC space.
Table 6.3: Predefined Coordinate Systems

Transforming point-like variables between two coordinate systems is performed using the transform() shadeop (see transform shadeop). It is important to note that that shadeop is polymorphic: it acts differently for points, vectors and normals. The following code snippet transforms a vector from `world' to `current' space:

vector dir = transform( "world", "current", vector(0,1,0) );

The following exampl transforsm a point from current space to matte_paint's camera NDC space (the camera has to be declared in the RIB using RiCamera)

point mattepaint_NDC = transform( "matte_paint:NDC", P );

NOTE

3Delight keeps all points in the `current' coordinate system(23), which happens to be `camera'. This could sometimes lead to confusion:

point p1 = point "world" (0,1,0);
printf( "%p\n", p1 );

One could expect to see (0,1,0) output by printf() but this is generally not the case: since p1 is stored in the `current' coordinate system, the printed value will be the point resulting from the transformation of p1 from `world' to `current'. So if the camera space is offset by (0,-1,0) the output of printf() would be (0,0,0).
A different implementation would have been possible (where points are kept in their respective coordinate systems as late as possible) but that would necessitate run-time tracking coordinate systems along with point-like variables, which is not handy and offer no real advantage.

Operations on Points

point + vector
The results is a point.
point - point
The result is a vector.
point * point
The result is point.
point * vector;
The result is point.

Operations on Vectors and Normals

Any vector type in the following table can be interchanged with a normal.

vector . vector
The result is a scalar (dot product);
vector + vector
vector - vector;
The result is a vector;

On Correctness

The shader compiler will not, by default, enforce mathematical correctness of certain operations. For example, adding two points together makes no real sense geometrically (one should add a vector to a point) but this is still accepted by the shader compiler (although with a warning). Historically, in the very first definitions of the RenderMan Shading Language, there were no vectors nor normals so everything had to be performed on points.

6.1.2.4 Matrices

A matrix in the shading language is a 4x4 homogeneous transformation stored in row-major order(24). A matrix initialization can have two forms:

matrix m = [matrix "space"] scalar;
matrix m2 = [matrix "space"] ( m00, m01, m02, m03, m10, m11, m12, m13,
                               m20, m21, m22, m23, m30, m31, m32, m33 );

The first form sets the diagonal of the matrix to the specified scalar with all the other positions being set to zero. So to declare an identity matrix one would write:

matrix identity = 1;

A zero matrix is declared similarly:

matrix zero = 0;

Exactly as for point-like variables (see section Points, Vectors and Normals, matrices can be declared in any standard or user-defined coordinate system (see Table 6.3):

matrix obj_reference_frame = matrix "object" 1;

Point-like variables can be transformed with matrices using the transform() shadoep (see transform shadeop).

6.1.2.5 Strings

Strings are most commonly used in shaders to identify different resources: textures, coordinate systems, other shaders, ...etc. A string declaration is of the form:

string name = "value";

One can also declare strings like this:

string name = "s1" "s2" ... "sN";

All operations on strings are explained in String Manipulation.

6.1.2.6 Co-Shaders

Co-shaders are both a variable type and an abstract concept defining a modular component. Co-shaders are declared in a shader using the shader keyword and can be loaded using special co-shader access functions as detailed in Co-Shader Access. For example,

shader specular_component = getshader( "specular_instance" );
if( specular_component != null )
    Ci += specular_component->Specular( roughness ) * Ks;

Declares a specular_component shader variable and uses it to load and execute the `specular_instance' co-shader (which could be a Blinn or a Phong specular model depending on the co-shader). The co-shader has to be declared in the scene description using the RiShader() call. Member variables in a co-shader can be accessed using getvar() method:

float Ks;
/* "has_Ks" will be set to 0 if no Ks variable is declared in the co-
   shader. Also note that the last output parameter "Ks" can be omit-
   ted in which case getvar serves to check the existence of the 
   target member variable.  */
float has_Ks = getvar( specular_component, "Ks", Ks );

The arrow operator can be used instead of getvar() to access a member variable in a more compact way:

Ks = specular_component->Ks;

The difference with getvar() is that 3Delight will print a warning if the variable is not declared in the target co-shader. Additionally, the arrow operator will only access variables in a read-only mode and cannot be used to write into variables, this means that to modify a member variable in a target co-shader one has to use a "setter" method.

6.1.2.7 The Void Type

This type is only used as a return type for a shading language function as explained in Functions.

6.1.3 Structures

6.1.3.1 Definition

Structures, or structs in short, are defined in a manner very similar to the C language. The main difference is that each structure member must have a default initializer.

struct LightDescription
{
   point from = (0, 0, 0);
   vector dir = (0, 0, 1);
   varying float intensity = 1;
}

Some observations:

6.1.3.2 Declaration and Member Selection

Defining a structure variable is done as with standard language variables and structure members are accessed using the arrow (->) operator as with C pointers. Using the LightDescription structure declared in Definition, one can write:

LightDescription light_description;
light_description->intensity = 10;

Note that the arrow operator to select structure members is a no-cost operation, compared to the same operator used to access co-shader variables (see section Co-Shaders. It is possible, at declaration time, to override the members of a structure using a constructor:

LightDescription light_description = LightDescription(
    "from", point(1,0,0), "dir", vector(1,1,1) );

Note that the constructor runs at compile time so the member names must be constant strings. Declaring arrays of structures is also trivial:

LightDescription light_descriptions[10] =
     { LightDescription("intensity", 5) };

6.1.3.3 Structures as Function Parameters

Structures are passed to function per reference (no data is copied when calling the function). To modify any member of the structure inside the function it is necessary to use the output keyword. For example:

void TransformLight( output LightDescription ld; matrix trs )
{
    ld->from = transform( trs, ld->from );	
    ld->dir = transform( trs, ld->dir );	
}

6.1.3.4 Limitations

Currently, the following features are not supported:

6.1.4 Arrays

6.1.4.1 Static Arrays

Any shading language type can be used to declare a one-dimensional array (excluding the void type). The general syntax for a static array is:

type array_name[array_size] [= {x, y, z, ...}];
type array_name[array_size] [= scalar];

array_size must be a constant expression and the total number of initializers must be equal or less than array_size(25). When initializing an array with a scalar all array's elements are set to that particular value. For example:

float t[4] = { 0, 1, 2, 3 };
color  n[10] = 1; /* set first element to (1,1,1), the rest to 0 */

Note that the length of the array in the initializer can be omitted, in which case the size is deduced from the right-hand expression:

float t[] = { 1,2,3 }; /* create a static array of size 3. */

The length of arrays can be fetched using the arraylength() shadeop (see arraylength shadeop:

float array_length = arraylength( t );

A particular element in an array can be accessed using squared brackets:

float t[4] = 1;
float elem_2 = t[2];

6.1.4.2 Resizable Arrays

Resizable arrays are declared using a non-constant length or by not providing the array length at all:

color components[]; /* empty */
shader lights[] = getlights();
color light_intensities[ arraylength(lights) ]; /* one per light */

Note that arrays specified without a length as shader parameters are not resizable arrays: their length is fixed when the parameter is initialized. Thus, only locally declared arrays can be resizable. Additionally, initializing an array will define a static array:

float t[] = { 1,2,3 }; /* WARNING: create a *static* array of size 3. */

Resizing Arrays

Resizable arrays can be resized using the resize() shadeop as in:

float A[];
resize(A, 3);

Note that the three elements of A in the example above are not initialized. Arrays can also be resized by assignment:

float A[];
float B[];
resize(A, 3);
B = A; /* B is now of length 3 */

Arrays can be resized by adding or removing elements from their "tail":

float A[];
push( A, 1 );
push( A, 2 ); /* length = 2 */
pop( A ); /* length = 1 */

resize(), push and pop() are further described in Operations on Arrays.

Array's Capacity

Additionally to its length, a dynamic array has a capacity. Declaring a capacity for a dynamic array is solely a performance operation: it allows a more efficient memory allocation strategy. capacity() is described in Operations on Arrays.

6.1.5 Variable Classes

Additionally to variable types, the RenderMan shading language defines variables classes(26). Since the beginning, RSL shaders were meant to run on a multitude of shading samples at a time, a technique commonly called single instruction multiple data or in short: SIMD. SIMD execution was a natural specification for the shading language mainly because the first production oriented RenderMan-compliant renderer implemented a REYES algorithm - a type of algorithm well suited for such grouped execution. An inherent benefit of an SIMD execution pipeline is that the cost of interpretation is amortized: one instruction interpretation is followed by an execution on many samples, this makes interpretation in RSL almost as efficient as compiled shaders.

6.1.5.1 Uniform Variables

Uniform variables are declared by adding the uniform keyword in front of a variable declaration. For example,

uniform float i;

Defines a uniform scalar i. Uniform variables are constant across the entire evaluation sample set. In slightly more practical terms, a uniform variables is initialized once per grid. All variables that do not vary across the surface are good uniform candidates (a good example is a for loop counter). It is important to declare such variables as uniforms since this can accelerate shader execution and lower memory usage(27).

NOTE

The evaluation sample set is a grid in the primary REYES render but is not necessary a grid in the ray-tracer.

6.1.5.2 Varying Variables

Varying variables are variables that can change across the surface and is initialized once per micro-polygon. A perfect example is the u and v predefined shader variables (see section Predefined Shader Variables) or the result of the texture() call (see texture shadeop). To declare a varying variable one has to use the varying keyword:

varying color t;

Declaring varying arrays follows the same logic but one has to make sure a varying is really needed in such a case: a varying array can consume a large amount of memory.

NOTE

Assigning varying variables to uniforms is not possible and the shader compiler will issue a compile-time error. Assigning uniforms to varyings is perfectly legal.

6.1.5.3 Constant Variables

Constant variables were introduced in RSL 2.0 and are meant to declare variables that are initialized only once during the render. This type of variables is further explained in Shader Objects.

6.1.5.4 Default Class Behaviour

If no uniform or varying keyword is used, a default course of action is dictated by the RenderMan shading language. This default behaviour is context depended:

  1. Member variables, declared in shader objects (see section Shader Objects) are varying by default.
  2. Variables declared in shader and shader class parameter lists are uniform by default.
  3. Variables declared inside the body of shaders are varying by default.
  4. Variables declared in function parameter lists inherit the class of the parameter and this behaviour also propagates inside the body of the function. This is possible since RSL functions are inlined, more on this in Functions.

It is recommended not to abuse the varying and uniform keywords where the default behaviour is clear, this makes code more readable.

6.1.6 Parameter Lists

Parameters list can be declared in three different contexts: shader parameters, shader class parameters and function parameters. Shader and shader class parameters have exactly the same form and will be described in the same section whereas function parameters has some specifics and will be described in their own section.

6.1.6.1 Shader and Shader Class Parameters

This kind of parameters has three particularities:

  1. Each parameter must have a default initializer. This is to be expected since all parameters must have a valid state in case they are not specified in the RenderMan scene description (through RiSurface or similar).
  2. By default, each parameter is uniform (see section Variable Classes).
  3. They can be declared as output. Meaning that they can be passed to a display driver (as declared by RiDisplay).

Follows an example of such a parameter list:

surface dummy(
    float intensity = 1, amplitude = 0.5;
    output color specular = 0; ) 
{
    shader body
}

6.1.6.2 Function Parameters

Function parameters have the same form as shader parameters but have no initializer. Another notable difference is that, by default, parameters class is inherited from the argument. For example:

/* note that 'x' has no class specification. */
float sqr( float x; ) { return x*x; }

float uniform_sqr( uniform float x; )  { return x*x; }

surface dummy( float t = 0.5; )
{
    /* function will be inlined in uniform form. */
    Ci = sqr( t );

    /* funciton will be inlined in varying form. */
    Ci += sqr( u );

    /* this will not compile! ('v' is varying but uniform_sqr
       expects a uniform argument) */
    Ci += uniform_sqr( v );
}

More about functions in Functions.

6.1.7 Constructs

6.1.7.1 Conditional Execution

Similarly to many other languages, the conditional block is built using the if/else keyword pair:

if( boolean expression )
    statement
[else
    statement ]

The bracketed else part is optional.

6.1.7.2 Classical Looping Constructs

There are the two classical(28) looping constructs in the shading languag: the for loop and the while loop. Both work almost exactly as in many other languages (C, Pascal, etc...). The general form of a for loop is as follows:

if( expression ; boolean expression ; expression )
    statement

For example:

uniform float i;
for( i=0; i<count; i += 1 )
{
    ... loop body ...
}

Note that i is uniform. This means that the loop counter is the same for all shading samples during SIMD execution. Declaring loop counters as varying makes little sense, usually. Also note that it is not possible to declare a variable in the loop initializing expression.
The while loop has the following structure:

while( boolean expression )
   statement

For example,

uniform float i=0;
while( i<10 )
{
    i = i + 1;
}

The normal execution of the for/while loop can be altered using two special keywords:

continue [l]
This skips the remaining code in the loop and evaluates loop's boolean expression again. Exactly as in the C language. The major difference with C is the optional parameter l that specifies how many loop levels to exit. The default value of l is 1, meaning the currently executing loop.
break [l]
This keyword exits all loops until level l. This is used to immediately stop the execution of the loop(s).
NOTE

Having the ability to exit l loops seems like a nice feature but usually leads to obfuscated code and flow. It is recommended not to use the optional loop level l, if possible.

6.1.7.3 Special Looping Constructs

This section describes constructs that are very particular to the RenderMan shading language. These constructs are meant to describe, in a clear and general way, a fundamental problem in rendering: the interaction between light and surfaces.

The illuminance Construct

This construct is an abstraction to describe an integral over incoming light. By nature, this construct is only meaningful in surface or volume shaders since only in these two cases that one is interested to know about incoming light (an example of a different usage is shown in Baking Using Lights). There are two ways to use illuminance:

  1. Non-oriented. Such a statement is meant to integrate light coming from all directions. In other words, integrate over the entire sphere centered at position.
    illuminance ( [string category], point position, ... )
        statement
    
  2. Oriented. Such a statement will only consider light that is coming from inside the open cone formed by position, axis and angle.
    illuminance ( [string category,]
                  point position, vector axis, float angle, ... )
        statement
    

Light Categories

The optional category variable specifies a subset of lights to include or exclude. This feature is commonly named Light categories. As explained in Predefined Shader Parameters, each light source can have a special __category parameter which lights the categories to which it belongs. Categories can be specified using simple expressions: a series of terms separated by the & or | symbols (logical and, logical or). Valid category terms are described in Table 6.4. If a category is not specified illuminance will proceed with all active light sources.

""
Matches all lights regardless of their category.
"name"
Matches lights that belong to category `name'.
"-name
matches lights that do not belong to category `name', including lights that do not belong to any category.
"*"
Matches every light that belongs to at least one category.
"-"
Matches nothing.
Table 6.4: Valid category terms.

Follows a simple example.

illuminance( "specular&-crowd", P )
{
   /* Execute all lights in the "specular" category but omit
      the ones that are also in the "crowd" category. */
}

Message Passing

illuminance can take additional parameters, as shown in the general form above. These optional parameters are used for message passing and forward message passing. Forward message passing is performed using the special send:light: parameter and is used to set some given light parameter to some given value prior to light evaluation. For example,

uniform float intensity = 2;
illuminance( P, ..., "send:light:intensity", intensity )
{
     ... statements ...
} 

Will set the intensity parameter of the light source to 2 and execute the light (overriding the value in light's parameter's list). The parameter must have the same type and same class (see section Variable Classes) in order for the forward message passing to work.
Getting values back from a light source is done through the same mechanism by using the light: parameter prefix. For example,

/* Some default value in case light source has no "cone_angle" parameter */
float cone_angle = -1;
illuminance( P, ..., "light:cone_angle", cone_angle )
{
    /* do something with the "cone_angle" parameter ... */
}

Will retrieve the cone_angle parameter from the light source.
Both forward and backward message passing can be combined in the same illuminance loop:

/* Some default value in case light source has no "cone_angle" parameter */
uniform float intensity = 2;
float cone_angle = -1;
illuminance( P, ..., "send:light:intensity", intensity,
                     "light:cone_angle", cone_angle )
{
    /* do something with the "cone_angle" parameter ... */
} 
NOTE

Message passing is a powerful feature in the shading language but one has to be aware that improper use could complicate the rendering pipeline fundamentally since it introduces a bi-directional flow of data between light sources and surface shaders.

Working Principles

The mechanics of illuminance are straightforward: loop over all active lights(29) withing the specified category and evaluate them to compute Cl, Ol and L (see section Predefined Shader Variables). These variables are automatically available in the scope of all illuminance loops. Note that the result of light evaluation is cached so that calling illuminance again within the same evaluation context doesn't trigger re-evaluation of light sources. This optimization feature can be disabled using a special `lightcache' argument to illuminate. In the example below, the evaluation light cache will be flushed and the light sources will be re-evaluated.

illuminance( ..., "lightcache", "refresh" )
{
    ... statements ...
}

Flushing the light cache can have a sever performance impact, it is advised not to touch it unless necessary.

NOTE

Some shadeops contain implicit illuminance loops. These shadeops are: specular, specularstd, diffuse and phong. These are all described in Lighting.

The illuminate Construct

The illuminate construct is only defined in light source shaders and serves as an abstraction for positional light casters. In a way, it could be seen as the inverse of an illuminance construct. There are two ways to use illuminate:

  1. Non-oriented. Such as a statement is meant to cast light in all directions.
    illuminate( point position ) 
        statement
    
  2. Oriented. Such a statement will only cast light from a given position and along a given axis and angle. Surface points that are outside the cone formed by <position,axis,angle> will not be lit (Cl will be set to zero, see below).
    illuminate( point position, vector axis, float angle )
        statement
    

Inside the illuminate construct, three standard variables are of interest:

  1. The L variable. This is set by the renderer to Ps - P. This means that L is a vector pointing from light source's position to the point on the surface being shaded. The length of L is thus the distance between the light and the point on the surface.
  2. The Cl variable. This variable is the actual color of the light and should be set inside the construct to the desired intensity.
  3. The Ol variable. This is the equivalent of Oi and describes light opacity. It is almost never used and has been deprecated in the context of shader objects (see section Shader Objects).

Listing Listing 6.2 shows how to write a standard point light. Note that the position given to illuminate is the origin of the shader space (see Table 6.3) and not some parameter passed to the light; this is desirable since the point light can be placed anywhere in the scene using RiTranslate (or similar) instead of specifying a parameter.

light pointlight( float intensity = 1 ; color lightcolor = 1 )
{
    illuminate( point "shader" 0 )
    {
        Cl = intensity * lightcolor / (L.L);
    }
}
Listing 6.2: An example of a standard point light.

The solar Construct

This construct is similar to illuminate but describes light cast by distant light sources. It also has two forms albeit only one is partially supported by 3Delight:

  1. Non-directional. This form decribes light coming from all points at infinity (such as a light dome).
    solar( )
        statement
    
    Correctly implementing the solar constructs implies integration over all light direction inside shadeops such as specular() and diffuse()(30) . This is not yet implemented in 3Delight and the statement above is replaced by:
    solar( I, 0 )
        statement
    
  2. Directional. Describes light coming from a particular direction and covering some given solid angle.
    solar( vector axis, float angle )
        statement
    
    For the same reason as in the case above, 3Delight doesn't consider the angle variable and considers this form as:
    solar( vector axis, 0 )
        statement
    

An example directional light is listed in Listing 6.3. This light source cast lights towards the positive z direction and has to be properly placed using scene description commands to light in any desired direction.

light directionallight( float intensity = 1 ; color lightcolor = 1 )
{
    solar( vector "shader" (0,0,1), 0 )
    {
        Cl = intensity * lightcolor;
    }
}
Listing 6.3: An example directional light using solar.

The gather Construct

This construct explicitly relies on ray-tracing(31) to collect illumination and other data from the surrounding scene elements. In other words, this construct collects surrounding illumination through sampling. Incidentally, gather is well suited to integrate arbitrary reflectance models over incoming indirect light (light that is reflected from other surfaces). The general form of a gather loop is as follow:

gather( string category, point P, dir, angle, samples, ... )
    statement
[else
    statement]

The <P, dir, angle> triplet specifies the sampling cone and samples specifies the number of samples to use (more samples will give more accurate results). The angle should be specified in radians and is measured from the axis specified by dir: an angle of zero implies that all rays are shot in direction dir and an angle of PI/2 casts rays over the entire hemisphere. gather stores its result in the specified optional variables which depend on the specified category. The table below explains all supported categories and related output variables.

samplepattern
This category is meant to run the loop without ray-tracing and without taking any further action but to provide ray's information to the user. This is useful to get the sampling pattern of gather to perform some particular operation. In this mode, one have access to the following output variables :
` ray:origin'
Returns ray's origin.
` ray:direction'
Returns ray's direction. Not necessarily normalized.
` sample:randompair'
Returns a vector which contains two stratified random variables that are well suited for sampling. Only the x and y component of the returned vector are relelevant and the z component is set to zero.
` effectivesamples'
Returns a varying float which is the number of samples effectively used by 3Delight. This may be different from the requested number of samples.
Listing Listing 6.4 combines `samplepattern' and simple transmission queries (see transmission shadeop) to compute the ambient occlusion.

color trans = 0;
uniform float num_samples = 20;
uniform float max_dist = 10;
point ray_origin = 0;
vector ray_direction = 0;
float effectivesamples = 0;
gather( "samplepattern", P, N, PI/2, num_samples,
    "effectivesamples", effectivesamples,
    "ray:direction", ray_direction,
    "ray:origin", ray_origin )
{
}
else
{
    vector normalized_ray_dir = normalize( ray_direction );
    trans += transmission(
        ray_origin, ray_origin + normalized_ray_dir*max_dist );
}
trans /= effectivesamples;
Listing 6.4: An example usage of the `samplepattern' category in gather.

Note that in this mode, it is the else branch of the construct which is executed.
illuminance
In this case, gather uses ray tracing to perform the sampling. Additionally to variables available to the samplepattern category, any variable from surface, displacement or volume shaders can be collected:
` surface:varname'
Returns the desired variable from the context of the surface shader that has been hit by the gather ray. Variables' values are taken after the execution of the shader. The typical variable is surface:Ci but other variables, such as surface:N or surface:P, are perfectly valid(32). Additionally, `varname' can be any output variable declared in the shader.
` displacement:varname'
` volume:varname'
Returns the desired variable from the context of the displacement or volume shader that has been evaluated for the gather ray. All comments for the surface case above also apply here. All returned variables are those encountered at the closest surface to be hit.
` ray:length'
Returns distance to closest hit.

The following example demonstrates how to compute a simple ambient occlusion effect.

float occ = 0;
uniform numSamples = 20;
float smpDiv = 0;
gather( "illuminance", P, Nf, PI/2, numSamples, "effectivesamples", smpDiv )
    ;/* do nothing ... */
else
    occ += 1;

occ /= smpDiv;
environment:environmentname
The `environment' category is used to perform importance sampling on a specified environment map. Importance sampling can be used to implement image based lighting as shown in the `envlight2.sl' shader provided with the 3Delight package. Note that no ray-tracing is performed: importance-sampled rays are returned and it is up to the user to perform ray-tracing if needed. This category unveils three optional parameters (further explained in Image Based Lighting):
` environment:color'
Return the color from the environment map.
` environment:solidangle'
Returns the sampled solid angle in the environment map. This is the size of the sampled region and is usually inversely proportional to the number of sampled rays - more samples will find finer details in the environment map.
` environment:direction'
The direction pointing towards the center of the sampled region in the environment map. Note that `ray:direction' holds a randomized direction inside the region and this direction is suitable for sampling.
An example on how to use these variables is shown in Listing 7.5.
pointcloud:pointcloudfile
Another special form of the gather() construct can be used to gather points in a previously generated point cloud. This is better illustrated in Example 6.1.

    /* Average 32 samples from a point cloud surrounding the current
       point using gaussian filtering.
       Assume point cloud has been saved in "object" space.
    */
    point object_P = transform( "object", P );
    normal object_N = ntransform( "object", N );

    string category = concat( "pointcloud", ":", texturename );

    color surface_color = 1, sum_color = 0;
    float total_atten = 0;
	
    point position = 0;;
    float point_radius;

    uniform float radius = 0.1;

    gather( category, object_P, object_N, PI, 32,
        "point:surface_color", surface_color,
        "radius", radius,
        "point:position", position )
    {
        float dist = distance(object_P, position);

        dist = 2*dist / radius;
        float gaussian_atten = exp( -2. * dist * dist );
        total_atten += gaussian_atten;
        sum_color += gaussian_atten * surface_color;
    }
    /* normalize final color */
    sum_color *= total_atten;
Example 6.1: Using gather() to access point cloud data.

As shown in the above example, specifying `pointcloud:filename' as the category to gather() will give access to the following variables:
` point:position'
Position of the currently processed point
` point:normal'
Normal of the currently processed point
` point:radius'
Radius of the currently processed point
` point:varname'
Any variable stored in the point cloud
Note

  • Points from the point cloud will be listed in gather() from the closest to the furthest, relatively to the provided position P.
  • There is no guarantee that the actual number of points listed will be equal to the number of points requested. This can happen for example if there are not enough points in point cloud (still unlikely) or the provided radius limits the total number of points available in a certain position.
  • Points outside the radius can still be listed if their extent is part of the gather sphere (points in a point cloud are defined by a point and a radius).
  • The N and angle parameter provided to the point cloud version of gather() are not considered.

gather accepts many optional input parameters and these are all explained in Table 6.10. Additionally, gather accepts the `distribution' parameter to sample arbitrary distributions(33), this parameter is described in Table 6.11. gather also supports the `samplebase' parameter as described in Table 6.11. The next code snippet illustrates how to use an environment map as a sampling distribution to compute image based occlusion:

float occ = 0;
uniform numSamples = 20;
gather( "illuminance", P, Nf, PI/2, numSamples,
    "distribution", "lightprobe.tdl" )
{
    ;/* do nothing ... */
}
else
    occ += 1;

occ /= numSamples;

6.1.7.4 Functions

As in many other languages, functions in the RenderMan shading language are re-usable blocks of code that perform a certain task. The general form of an RSL function is as follows:

return_type function_name ( parameters list )
{
    ... function body ... 

    return return_value;
}

In traditional shaders (see section Traditional Structure) functions have to be declared prior to their calling points. In class shaders (see section Shader Objects), functions can be declared anywhere outside the class or inside the class, with functions declared in the class scope being accessible to other shaders. An example function is shown Listing 6.5.

void print_error( string tex; )
    { printf( "error loading texture %s.\n", tex ); }

color tiled_texture( string texture_name, float u, v;
                     float num_tiles_x, num_tiles_y;
                     output float new_u, newv; )
{
    color red() { return color(1,0,0); }

    if( texture_name == "" )
    {
        print_error( texture_name );
        return red();
    }

    /* assuming u & v are in the range [0..1]. */
    new_u = mod(u, 1/num_tiles_x) * num_tiles_x;
    new_v = mod(v, 1/num_tiles_y) * num_tiles_y;

    return texture( texture_name, new_u, new_v );
}
Listing 6.5: An example illustrating the syntax of a RSL function.

Some noteworthy points about the example above:

One particularly useful concept in the RenderMan shading language is that functions are polymorphic(34), meaning that the same function can be delcared with different signatures to accept different parameter types. For example:

float a_plus_b( float a, b; ) { return a+b; }
color a_plus_b( color a, b; ) { return a+b; }

6.1.7.5 Methods

Methods are declared as normal functions with the following differences:

The general form for a RSL method is as follows:

public return_type function_name ( parameters list )
{
    ... function body ... 

    return return_value;
}

For example,

public void surface( output color Ci, Oi; )
{
}

Declares the standard surface() entry point in a shader object.

3Delight 10.0. Copyright 2000-2011 The 3Delight Team. All Rights Reserved.