6.1 Syntax
6.1.1 General Shader Structure | ||
6.1.2 Variable Types | ||
6.1.3 Structures | ||
6.1.4 Arrays | ||
6.1.5 Variable Classes | ||
6.1.6 Parameter Lists | ||
6.1.7 Constructs |
6.1.1 General Shader Structure
6.1.1.1 Traditional Structure | ||
6.1.1.2 Shader Objects |
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:
- 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). - Member variables can retain shader state. This avoids ad-hoc message passing and other "tricks" widespread in traditional shaders.
- 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.
- The separation of displacement, surface and opacity methods (see below) provides the render with greater opportunities to optimize renderings.
- 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.
|
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:
- Share the result of the computation using message passing (see section Message Passing and Information). This works but complicates the data flow.
- Re-do the same computation in both the surface and displacement shaders. Also works but time is lost in re-computation.
- 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; } } |
6.1.2 Variable Types
6.1.2.1 Scalars | ||
6.1.2.2 Colors | ||
6.1.2.3 Points, Vectors and Normals | ||
6.1.2.4 Matrices | ||
6.1.2.5 Strings | ||
6.1.2.6 Co-Shaders | ||
6.1.2.7 The Void Type |
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:
- Since there is no boolean type in RSL (such as the
bool
keyword in C++), scalars are also used to hold truth values. - Scalars can be promoted to almost any type. For example, a color can be initialized with a scalar:
Ci = 1;
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. */
|
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 */
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 );
NOTE3Delight 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 ofprintf()
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 | ||
6.1.3.2 Declaration and Member Selection | ||
6.1.3.3 Structures as Function Parameters | ||
6.1.3.4 Limitations |
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:
- Note that there is no semicolon at the closing bracket as it is usual in C.
- A structure has no class modifier so each member has to be declared with the appropriate
uniform
orvarying
keyword. If there is no variable class specifier,uniform
is assumed. - Embedded structures do no need initializers, since each structure will have its own member initialization.
- Structures can be declared anywhere in the file. More generally, a structure can be declared anywhere a function can be delcared and same scoping rules apply.
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:
- Structure inheritance and structure member functions.
- Passing structures to RSL plug-ins.
- Functions cannot return structures.
6.1.4 Arrays
6.1.4.1 Static Arrays | ||
6.1.4.2 Resizable 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 | ||
6.1.5.2 Varying Variables | ||
6.1.5.3 Constant Variables | ||
6.1.5.4 Default Class Behaviour |
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).
NOTEThe 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.
NOTEAssigning 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:
- Member variables, declared in shader objects (see section Shader Objects) are
varying
by default. - Variables declared in shader and shader class parameter lists are
uniform
by default. - Variables declared inside the body of shaders are
varying
by default. - 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 | ||
6.1.6.2 Function Parameters |
6.1.6.1 Shader and Shader Class Parameters
This kind of parameters has three particularities:
- 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). - By default, each parameter is uniform (see section Variable Classes).
- 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 | ||
6.1.7.2 Classical Looping Constructs | ||
6.1.7.3 Special Looping Constructs | ||
6.1.7.4 Functions | ||
6.1.7.5 Methods |
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).
NOTEHaving 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
:
- 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
- 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.
|
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 ... */ }
NOTEMessage 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.
NOTESome shadeops contain implicit
illuminance
loops. These shadeops are:specular
,specularstd
,diffuse
andphong
. 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
:
- Non-oriented. Such as a statement is meant to cast light in all directions.
illuminate( point position ) statement
- 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:
- 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. - The
Cl
variable. This variable is the actual color of the light and should be set inside the construct to the desired intensity. - The
Ol
variable. This is the equivalent ofOi
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); } } |
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:
- Non-directional. This form decribes light coming from all points at infinity (such as a light dome).
solar( ) statement
Correctly implementing thesolar
constructs implies integration over all light direction inside shadeops such asspecular()
anddiffuse()
(30) . This is not yet implemented in 3Delight and the statement above is replaced by:solar( I, 0 ) statement
- 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; } } 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.
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 theelse
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 assurface:N
orsurface: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.
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 togather()
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 ); } |
Some noteworthy points about the example above:
- Functions can have multiple exit points.
- Functions can return no value by using the
void
keyword. - Functions can return their values using one or more
output
parameters. - Similarly to the Pascal programming language, functions can be declared inside other functions or in any new scope.
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:
- They are declared inside a shader object (see section Shader Objects).
- They are preceded by the
public
keyword. - Methods are not inlined as usual functions and are callable from other shaders.
- Methods cannot be declared inside other methods.
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.