Shader Objects and Co-shaders

Shader Objects and Co-shaders

January, 2009

Introduction

Shaders can now be objects 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 m_Ct = (1,1,1);

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

The introduction of shader objects provides many new capabilities and performance benefits:

  • Shaders can retain state in member variables, facilitating certain kinds of caching and baking (without resorting to plugins) and eliminating the need for certain kinds of message passing.
  • The shading pipeline has been generalized and optimized for shaders with multiple methods. For example, if a surface shader provides an opacity method, the renderer can use it to determine visibility prior to surface shading.
  • Shaders can call methods and access member variables of other shaders. For example, an illuminance loop can be reformulated as a for loop that calls the light method of each light shader, and a light shader can call arbitrary methods in the surface shader. Complicated illumination models can be refactored to employ multiple methods (rather than multiple illumination loops), yielding greater flexibility and higher performance.
  • Scene descriptions can now include co-shaders that allow custom shading pipelines to be expressed in the shading language itself.
    • Layers of a surface appearance can be expressed as separate shader instances that are combined by a shader that calls the displacement and surface methods of each layer. This allows some aspects of the appearance to be modified without recompilation and complex appearances to be constructed from modular building blocks.
    • Non-traditional shaders, such as "lights" that modify surface characteristics, can be expressed in a less ad hoc manner. For example, a color-correction "light" can be reformulated as a co-shader that is called directly by the surface shader, rather than a light that is invoked during illumination.

The sections that follow describe these capabilities in more detail. An extended example (an area light shader) is presented in the Appendix.


Member variables

Persistent data is useful in many kinds of shaders. For example, surface shaders often recompute uniform values that do not depend on data from geometric primitives. Such values can now be computed once, when the instance is first created, and saved in member variables.

A shader definition can now have a syntax that is similar to a C++ class definition. A shader class definition declares shader parameters and contains member variables, functions, and methods. For example:

class myshader(... params ...)
{
    // member variables
    uniform float m_y = 1;
    varying float m_z = 2;

    // method definition
    public void surface(output color Ci, Oi) {
        ...
    }
}

Methods

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;
                         output vector dPdtime /* optional */);
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 are no longer global variables (although they are still supported 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);

Of course, traditional shaders can be used alongside method-based shaders. Backwards compatibility is discussed in more detail in a later section.

Pipeline Methods

The renderer must be able to invoke each shader. For traditional shaders, there was implicitly one entry point and all code was run. In RSL 2.0 there are pipeline methods, which the renderer may invoke directly. A light shader will need a light() method, and a displacement shader must provide a displacement() method. Surface shaders are more complex, and there are a number of methods that may be invoked (the simplest of which is a single surface() method). For more information see the シェーディング Pipeline documentation.

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 m_state = "ready";

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

The use of such methods is the subject of the next section.


Co-shaders

The method-based shading pipeline is a fixed pipeline. Custom shading pipelines can be implemented using co-shaders. The term itself is analogous to "co-routines", implying cooperative computation by modular components. Co-shaders are simply shader objects with no explicitly specified purpose; they are not executed directly by the renderer, but rather are called from other shaders. Co-shader objects are created like lights in RIB, by specifying the shader name, a handle name, and any desired parameter values:

Shader "basic" "baselayer" "float Ks" [.5]
Shader "rust" "rustlayer" "float Ks" [0]

Co-shader objects are accumulated in a list that is similar to the light list. The usual scoping rules apply: additions to the current list of co-shaders are local to the current attribute scope.

In the shading language, a co-shader can be obtained from its handle, yielding a shader (which might be null if the handle was not found). Method calls use the syntax:

shader baselayer = getshader("baselayer");
if (baselayer != null)
    baselayer->surface(Ci, Oi);

Other ways of obtaining co-shaders are described below. First, let's revisit member variables in more detail


Member Variables, Revisited

Member variables can have a "storage specifier" that is one of: constant, uniform, or varying. The storage of a member variable affects its lifetime, when it can be accessed, and whether it can contain varying data (values that are different for each point in the batch being shaded). The differences between the storage classes are described below.

Constant Members

A constant member variable has a lifetime equal to that of the shader. It may be initialized inline. For example:

constant float m_f = 0;

This causes m_f to be set to 0 before construct() runs.

Alternatively, constant member variables can be initialized in your shader's construct method. Generally, this approach is preferred both for clarity as well as to express more complex intialization expressions. If you choose to initialize member variables in the construct method, it is unnecessary to supply an inline initializer. For example:

constant float m_primId;

public void construct() {
    attribute("user:primId", m_primId);
}

Constant members can only be assigned to during initialization or construct(). Thereafter they are immutable.

Please see the User construct() method section for more details regarding the implications of making attribute() or getshader() calls during the construct() method.

Constant member variables are most useful when combined with construct() in order to save computations that do not depend on primitive variables. Such computations need only be performed once and can later be reused for each batch of points. The resulting efficiency gains can be signficant.

The read-only nature of constant variables outside of the construct() call also prevents potential shader-writing errors and presents the renderer with a potential opportunity for optimization.

Constant member variables represent a measurable cost in memory and bandwidth and so should be used in conditions where this cost is warranted. For the common case of simple scalar numeric constants we suggest that pre-processor #define directives be favored over constant member variables.

In this example, we amortize the cost of the getshader() function over all calls to the surface() method.

class constantMembers(uniform string shaderHandle = "") {
     constant shader m_otherShader = null;

     public void construct() {
         if (shaderHandle != "") {
             m_otherShader = getshader(shaderHandle);
         }
     }

     public void surface(output color Ci,Oi) {
         //... use otherShader
         if (m_otherShader != null)
             m_otherShader->surface(Ci,Oi);
     }
}

Uniform Members

Uniform member variables also have a lifetime equal to that of the shader. Unlike constant members, uniforms are writable in any method. They therefore are appropriate for uses such as:

  • saving values that depend on uniform primitive variables
  • tracking internal state across shader pipeline methods

Uniform members may have an inline initializer:

uniform float m_haveComputedPattern = 0;

and this will run just before construct() and again before``begin()`` in the pipeline. This form of initialization is offered as a convenience to programmers. For nontrivial cases, we recommend that initialization be explicitly provided in the begin() method.

It is important to note however that in RPS 14.0 this initializer will run, regardless of whether the variable was set during construct(). Simply put, initializers provide a syntactic shorthand for resetting uniform member variables for each new batch of points. Often this is unnecessary, as the value will be computed subsequently anyway (see Varying Members).

In a future release we anticipate reducing the frequency of execution of inline initialization to that of constant member variables.

If no initializer is provided, the value of the uniform member variable is undefined. In RPS 14.0, uniform members retain their value across shader pipeline invocations, but we strongly advise against relying on this behavior.

This example emphasizes the subtlety of per-invocation initializers for uniform member variables.

// example 1: uniform inline initializers are run prior to begin!
uniform float m_f = 0;
uniform float m_g;
public void construct() {
    m_f = 1;
    m_g = 1;
}
public void begin() {
    // m_f is 0 here
    // m_g is 1 here
}

// example 2:  array resize operations
uniform float m_fa[] = {};
uniform float m_ga[];
public void construct() {
    resize(m_fa,1);
    m_fa[0] = 99;
    resize(m_ga,1);
    m_ga[0] = 99;
}
public void begin() {
    // arraylength(m_fa) is again 0
    // arraylength(m_ga) is still 1
    // m_ga[0] is still 99
}

The cached_spot example below shows the use of a uniform member variable as a mechanism for tracking state. Here is a sketch of a uniform variable used in combination with a uniform primitive variable.

 class temperature_dependent (
     uniform float temperature = 0;
     float numFrequencySamples = 100;
     float maxFrequency = 1e6;
 ) {
     constant float m_frequencies[];
     uniform float m_energyPerFrequency[];

     void construct() {
         resize(m_frequencies, numFrequenceSamples);
         resize(m_energyPerFrequency, numFrequenceSamples);

         uniform float i;
         for (i=0; i < numFrequencySamples; i += 1) {
             m_frequencies[i] = (i*maxFrequency)/numFrequencySamples;
         }
     }

     public void begin() {
         // sample the frequency spectra
         uniform float n = arraylength(m_frequencies), i;
         for (i = 0; i < n; i += 1) {
             uniform float fr = m_frequencies[i];
             uniform float t1 = 2*h*fr*fr*fr/c*c;
             uniform float t2 = 1/exp((h*fr)/(k*temperature));
             m_energyPerFrequency[i] = t1*t2;
         }
     }

     public void displace(output point P; output normal N) {
         // use energies at infra red spectrum to control displacement
     }

     public void surface(output color Ci,Oi) {
         // sum energies to form output emissive color
     }
}

Varying Members

Varying member variables are not valid for more than one execution of a shader, since the number of points can change each time the shader is executed. Varying members are therefore only valid while shading a batch of points. This means they come into existence just before begin() is called and are destroyed just after shading the batch of points is complete.

The initial value in a member variable definition (if specified) must be a constant or a simple constant expression (sqrt(2), degrees(PI/2), ...). Varying members become accessible only at begin() and are not accessible during construct().

Varying data supplied with an initializer will be set to that value just before begin() runs. However, it is not always efficient to supply such a value, especially if the varying data is subsequently and unconditionally computed (therefore overwriting the default-initialized value).

The use of varying members is primarily to communicate varying data (e.g. generated patterns or lighting results) between various phases of the pipeline.

For example, patterns often affect both displacement and the surface color, and it is desirable to share the generation of those patterns between both the displacement phase of the pipeline and the surface shading part.

class myshader()
{
    constant float m_maxdepth;
    uniform float m_raydepth;
    uniform float m_sides;
    varying point m_origP;
    varying float m_ridgeHeight;

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

    public void begin() {
        rayinfo("depth", m_raydepth);
        m_sides = 2;
        attribute("Sides", m_sides);
        m_origP = P;
    }

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

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

        m_ridgeHeight = expensiveToComputePattern(s,t,P);
    }

    public void surface(output color Ci,Oi) {

        // reuse the pattern generation to generate color
        computeカラーAndOpacityBasedOnRidge(m_ridgeHeight,Ci,Oi);
    }
}

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, or depends on shader parameters, etc.

Summary:

  • The presence or absence of an inline initializer for a member may change the frequency with which it is initialized.
  • The exact frequency with which initializers run might change in the future.
  • If you need a variable to be initialized per batch of points, do so in begin().
  • If a variable can be initialized in construct() it is preferable to do so, as it is generally more optimal.
  • Explicitly writing initializations in the construct() or begin() methods is preferable to writing them inline.

For further details, please see:

Scoping

Member variables can be used without extern declarations. In addition, extern declarations are no longer required for shader parameters, although they still may 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 m_state = "ready";

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

To avoid confusion, we recommend that a naming convention (such as m_myvar) be used to distinguish between member variables and local variables.

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 m_b = 1;            // m_b is private
    public float m_c = 2;     // m_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 (0) and does not modify the output parameter. It returns one (1) 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, et cetera). 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.

The shader compiler does not know the types of variables defined by other shaders, so the arrow operator cannot be used in contexts where the expected type is ambiguous (e.g. passed to an overloaded operator or function).

Note that the arrow operator cannot be used to modify a public variable; a method call should be used instead:

baselayer->m_state = 0;          // Error
baselayer->SetState(0);          // OK

Despite the new lightweight syntax, these operations are expensive, requiring a hash table lookup, a run-time type test, and a data copy. The shader compiler is not yet capable of optimizing repeated uses of the same public variable, so users should code conservatively. For example:

if (foo->m_x > 0)
    y *= foo->m_x;    // inefficient: repeats lookup and copy.

float x = foo->m_x;   // better: avoids duplicating work.
if (x > 0)
    y *= x;

Iterating with Co-shaders and ライト

As mentioned previously, co-shader objects are created like lights in RIB, and they are accumulated in a list that is similar to the light list. The entire co-shader list (or a subset, filtered by category) can be obtained using the getshaders() function, which returns a variable-length array:

shader layers[] = getshaders();
uniform float i, n = arraylength(layers);
for (i = 0; i < n; i += 1) {
    color layerC, layerO;
    layers[i]->surface(layerC, layerO);
    ...
}

As this example demonstrates, shaders are first-class values: they can be stored in arrays, passed to functions, compared for equality, etc. They can also be declared as shader parameters (strings specifying shader handles are used at the RIB level to select the shader for these parameters). See Shaders as parameters for more information on this feature.

ライト can also be used as co-shaders. A particular light instance can be obtained by specifying its handle:

shader mylight = getlight("mylight");

The entire light list can also be obtained by the getlights() function (which can filter by category, like getshaders). This can provide a more flexible alternative to illuminance loops. For example, diffuse illumination can be calculated as follows:

shader lights[] = getlights("category", "diffuse");
uniform float i, n = arraylength(lights);
for (i = 0; i < n; i += 1) {
    vector L;
    color Cl;
    lights[i]->light(L, Cl);
    C += Cl * max(0,Nn . normalize(-L));
}

Note that the L vector computed by a light method points from the light to the surface; illuminance loops automatically flip the L vector, but when a light method is called directly this is the caller's responsibility.

Explicit illumination loops such as this are not equivalent to illuminance loops in one important respect: the renderer does not attempt to cache the results of executing the light. This can be more of a benefit than a drawback, since illuminance caching is ad hoc and can deliver incorrect results (unless one is careful to invalidate the cache in certain situations).

Another advantage of an explicit illumination loop is that it gives the caller more control over execution of the light. For example, the caller can inspect various properties of the light to determine whether it should be executed, or to determine which method should be executed. Higher performance can also be achieved by using method parameters instead of shader parameters to convey inputs and outputs, since method parameters are passed by position. This avoids hash-table lookups and allows outputs to be computed "in place", rather than being copied from shader outputs.


Predefined Shader Objects

Special global variables called surface and light can be used to access the current surface shader and the current light. (The current light can be used only within an illuminance loop.) This allows access to shader parameters, member variables, and arbitrary methods:

public void light(output vector L; output color Cl) {
    ...
    float surfaceKd = surface->Kd;
    if (surfaceKd > 0)
        surface->GetMoreInfo(...);
    ...
}

The current shader is also available as a global variable called this, which is useful when calling a co-shader that performs a callback:

baselayer->coroutine(..., this);

Do not use this to access member variables or call methods in the current shader, since it introduces an expensive level of indirection and defeats compile-time type checking and inlining:

if (this->Kd > 0)           // No! Just use Kd.
    this->proceed(...);     // No! Just call proceed(...)

The only exception to this rule is for recursive method calls, which actually work. They cost the same as ordinary co-shader method calls.

The special global variable caller refers to the shader that made the call to the current method (this could be null if the current shader was invoked directly by the renderer). The caller variable allows a light to discover the shader that invoked lighting if, for example, lighting was invoked by a co-shader. It can also be useful if a co-shader method wishes to know the invoking shader.


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 after 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 the Appendix 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 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[] = { } );

Backwards compatibility

Many of the new features described in this document are orthogonal and can be incrementally adopted. A traditional shader definition, such as a surface shader, is generally equivalent to a class definition with a single method.

  • Traditional shaders can be used as co-shaders: RiShader can create co-shader instances from any kind of shader definition.
  • The main method of a traditional shader is called "surface", "light", etc. and can be called from other shaders.
  • The new getvar function can be used to access parameters of traditional shaders.
  • The new arrow operator ("foo->bar") works on traditional shader parameters.
  • Old message passing functions (e.g. lightsource) can be used to access shader parameters and member variables of shader objects.

However, RiSurface calls treat the old and new styles of shader definitions differently. Only shaders with class definitions are executed using the new method-based pipeline.

The new light() method can be written with more than two required arguments. Normally there would only be two - the output vector L and the output color Cl, but it may may be useful to add more when additional communication is required between the light and the surface. In such cases - where the arguments are required (are not specified as optional arguments) the renderer cannot and will not attempt to invoke the light as part of the diffuse(), specular() or ambient() shadeops. PRMan will also not invoke the light during illuminance loops. Instead is expected that you will invoke the light by obtaining a reference to it using getlights() and invoke it using the method call syntax.


User construct() Method

As noted above, it is much more efficient to initialize member variables during construct() than later in begin(). Member variables of constant storage are writable in construct, allowing more complex initialization than inline default values permit.

Varying member variables, including those structs that contain varying data, are inaccessible during construct.

It is also important to note that construct runs independently of a batch of points and therefore primitive variables will not be bound during construct. That means that any shader parameters that might eventually be bound to a primitive variable will have their default (shader instance argument) value during construct.

A user-provided construct() is guaranteed to run once and only once for given shader instance.

A shader instance is, broadly speaking, equivalent to the instance declaration of a shader in Ri. However, a shader specified in Ri may in certain cases result in more than one instance. This can happen when a shader accesses attributes in its construct method. For example, if a shader "foo" refers to attributes during construct,

Shader "foo" "foohandle"

AttributeBegin
        # gprim 1
AttributeEnd

AttributeBegin
        # gprim 2
AttributeEnd

it will result in two instances. If shader "foo" does not access attributes in construct it will result in one instance (and one construct call), provided the vertex variables of the two gprims are compatible. Accessing RIB options in the construct method does not result in more than one instance.

Access to attributes (via one of attribute(), getshader(), getshaders() etc) can be thought of as "specializing" the shader to work only with objects that have compatible sets of other shaders and compatible attributes.

Users should, however, not rely on the frequency at which construct runs relative to a given shader declaration in Ri.

Shaders returned via getshader(), getshaders() and friends are guaranteed to be constructed. A warning is issued for the cycle when a shader gets others that in turn get the original shader.

  • Important

    For RSL plugin authors:

    Do not assume that construct() will run in the same thread that shading will occur in.

    Shaders may be used in multiple threads, so thread-local data should be used as a caching mechanism. Assumptions that thread-local data has been initialized by a shader may not hold true - even for the same instance of the shader, i.e. even if a shader writes to thread-local data in a DSO shadeop during begin() there is no guarantee that the shader will be in the same thread when surface() is invoked.


User begin() Method

A user begin() method provides a place to initialize data that is valid only while shading a given batch of points. For example, varying data would fall into this category because it is inaccessible during construct().

begin() may run many times for a given shader instance, whereas construct() will run once.

begin() will only be run after all shaders are constructed. Accessing other shaders during begin will cause them to be begun as necessary.

Access to attributes outside of construct() does not "specialize" the shader instance (see above). So special care should be taken if values are stored into uniform member variables without initializer. In particular, storing a shader reference in a uniform member and expecting the same set of shaders to be available for the next batch of points is likely to be problematic. Equivalently, care must be taken with attribute lookups stored in a member during begin() or later. If the attribute() call is not unconditionally re-executed for each batch of points, the member might no longer reflect the true attribute value.


More on the Order of Method Calls

By way of further explanation of member variable lifetimes, here is a description of the order in which members are initialized and user methods are called.

Once

  1. all uniform member initializers are run (if the member has an initializer) this includes intialization of structs
  2. all arrays are set up with initial length
  3. shaders passed via the parameter list are resolved
  4. user construct() is called

Many times

  1. uniform member initializers are re-run (if the member has an initializer)
  2. varying member initializers are run (if the member has an initializer)
  3. attempts are made to re-resolve any member variables of type shader
  4. user begin() is called

Shaders As Parameters

Shader handles may be passed via the parameter list of a shader. This may be useful to construct graphs of shaders statically in rib avoiding the need to perform a getshader() to bind the set of co-shaders together. For example:

class masterShader(shader helperShader = null) {
       //...

In Ri, the shader helperShader would be specified as a co-shader handle. The shader helperShader will be bound to a matching co-shader, which does not have to be in scope when the shader is bound to a gprim.

This allows lights to have locally scoped helper co-shaders:

AttributeBegin
        Shader "helper" "helperhandle"
        LightSource "foo" "lighthandle" "string helperShader" "helperhandle"
AttributeEnd
Illuminate "lighthandle" 1

Shaders referenced via the parameter list will be constructed before the referring shader is, and likewise will be begun before the referring shader.

Please see Appendix E: Dynamism and Tight binding for a light shader with blockers for an comparison of shaders passed via the parameter list with dynamically determined shaders (using getshader()).


Conclusion

Shader objects are a convenient way to retain state and factor functionality into separate methods. The RenderMan shading pipeline can take advantage of explicitly defined shading stages, for example by executing the opacity method but postponing the bulk of shading until visibility has been determined. Member variables allow state to be shared between different stages of the shading pipeline in a principled and efficient way. Furthermore, method calls and message passing of member variables allow complicated illumination models to be expressed more clearly with improved performance. Finally, co-shaders allow custom shading pipelines to be expressed in the shading language itself.


Appendix A: Glossy surface with area light

//
//
// A surface with Ward isotropic glossy reflection, illuminated
// by area light sources.
// An example of the new shader object system in PRMan 13.5.
//

#include "normals.h"
#include "wardglossy.h"

//
// Surface shader class definition
//
class glossy_area( // テクスチャ param
                  string texturename = "";
                  // Ward diffuse and glossy param
                  float Kd = 0.5, Kg = 0.5;
                  float alpha_u = 0.05;
                  // Russian roulette threshold
                  float RRthreshold = 0.01; )
{
    // Ward isotropic glossy BRDF
    public void surface(output color Ci, Oi) {
        color refl, sumrefl = 0;
        color trans;
        normal Ns = shadingnormal(N);
        vector In = normalize(I);
        vector lightDir;
        color tex = 1;
        float dot_i, maxrefl;
        uniform float s;

        shader lights[] = getlights();
        uniform float i, nlights = arraylength(lights);

        // テクスチャ
        if (texturename != "")
            tex = texture(texturename);

        // For each light source ...
        for (i = 0; i < nlights; i += 1) {

            // Get lightcolors, lightdirs from light.
            vector L;                   // unused, but required argument
            color Cl;                   // also unused.
            color lightcolors[];        // note use of resizable arrays
            vector lightdirs[];
            lights[i]->light(L, Cl,
                             "P", P,
                             "lightcolors", lightcolors,
                             "lightdirs", lightdirs);
            uniform float nsamples = arraylength(lightcolors);

            // Evaluate glossy (and diffuse) BRDF for the light
            // directions
            color reflcoeffs[nsamples];
            evaluateWardGlossy(P, Ns, In,
                               Kd * tex * Cs, color(Kg), alpha_u,
                               nsamples, lightdirs, reflcoeffs);

            // Accumulate illumination; shoot shadow rays only where
            // they matter
            for (s = 0; s < nsamples; s += 1) {

                // Compute cosine term
                lightDir = normalize(lightdirs[s]);
                dot_i = lightDir . Ns;

                // Reflected light is the product of incident
                // illumination, a cosine term, and the BRDF
                refl = lightcolors[s] * dot_i * reflcoeffs[s];

                // Compute shadow -- but only if the potentially
                // reflected light is bright enough to matter.  This
                // is sometimes an important optimization!  For
                // example, no shadow rays need to be traced at points
                // with a black texture and no glossy highlight or if
                // the light source is dark-ish.  The point is that we
                // need information both from the surface and the
                // light source to determine whether shadow rays need
                // to be traced.
                maxrefl = max(refl[0], refl[1], refl[2]);
                if (maxrefl > RRthreshold)
                    trans = transmission(P, P + lightdirs[s]);
                else if (maxrefl > RRthreshold * random())
                    // Russian roulette
                    trans = transmission(P, P + lightdirs[s]);
                else
                    trans = 1; // don't bother tracing shadow rays

                sumrefl += trans * refl;
            }
        }

        // Set Ci and Oi
        Ci = sumrefl * Os;
        Oi = Os;
    }
}
//
// normals.h
//
// Compute normalized shading normal with appropriate orientation.
// We ensure that the normal faces forward if Sides is 2 or if the
// shader evaluation is caused by a ray hit.
//
normal
shadingnormal(normal N)
{
    normal Ns = normalize(N);
    uniform float sides = 2;
    uniform float raydepth;
    attribute("Sides", sides);
    rayinfo("depth", raydepth);
    if (sides == 2 || raydepth > 0)
        Ns = faceforward(Ns, I, Ns);
    return Ns;
}
//
// wardglossy.h
//
// Evaluate Ward's isotropic glossy BRDF for the given light directions
//
void
evaluateWardGlossy( vector P, Nn, In;
                    color Cd, Cg;
                    float alpha;
                    uniform float samples;
                    vector lightdirs[];
                    output color reflcoeffs[] )
{
    vector lightDir, reflDir, h;
    float alpha2, dot_i, dot_r;
    float delta, e, c;
    uniform float s;

    alpha2 = alpha*alpha;
    reflDir = -In; // direction toward eye
    dot_r = reflDir . Nn;

    for (s = 0; s < samples; s += 1) {
        lightDir = normalize(lightdirs[s]);
        dot_i = lightDir . Nn;
        if (dot_i <= 0 || dot_r <= 0) {
            reflcoeffs[s] = 0;
        } else {
            reflcoeffs[s] = Cd; // diffuse comp
            // (cosine term is elsewhere)
            h = normalize(reflDir + lightDir);
            delta = acos(h . Nn);
            e = tan(delta) / alpha;
            c = 1 / sqrt(dot_i * dot_r);
            reflcoeffs[s] +=  //  glossy comp
                Cg * c * exp(-e*e) / (4 * PI * alpha2);
        }
    }
}

Appendix B: Simple area light shader

//
// rectarealight.sl - Simple area light source shader.
// An example of the new shader object system in PRMan 13.5.
// Per Christensen, 2007.
//
// Generate (unstratified) illumination directions on rectangular  area light
// source
//

class
rectarealight(
       float intensity = 1;
       color lightcolor = 1;
       uniform float samples = 16;
       point center = point "shader" (0,0,0); // center of rectangle
       vector udir = vector "shader" (1,0,0); // axis of rectangle
       vector vdir = vector "shader" (0,1,0)) // axis of rectangle
{
    public void light( output vector L;         // unused
                       output color Cl;         // unused
                       point P = (0,0,0);
                       output color lightcolors[] = { };
                       output vector lightdirs[] = { } )
    {
       vector rnd;
       point samplepos;
       float rnd1, rnd2;
       uniform float s;

       resize(lightcolors, samples);   // note use of resizable arrays
       resize(lightdirs, samples);

       for (s = 0; s < samples; s += 1) {
           rnd1 = 2 * random() - 1;
           rnd2 = 2 * random() - 1;
           samplepos = center + rnd1 * udir + rnd2 * vdir;
           lightdirs[s] = samplepos - P;
           lightcolors[s] = intensity * lightcolor / samples;
       }

       // Clear L and Cl, even though they're unused.
       L = (0,0,0);
       Cl = (0,0,0);
    }
}

Appendix C: Area light example RIB

FrameBegin 0
Display "glossypot.tif" "tiff" "rgba"
Projection "perspective" "fov" 25
Translate 0 0 12

WorldBegin
# make objects visible to transmission rays
Attribute "visibility" "int transmission" 1

LightSource "rectarealight" 1
      "intensity" 2
      "center" [-10 10 -13]
      "udir" [2.5 0 0] "vdir" [0 0 2.5]
      "samples" 16

LightSource "rectarealight" 2
      "intensity" 0.2
      "center" [30 30 -10]
      "udir" [1.5 0 0] "vdir" [0 0 1.5]
      "samples" 4

Surface "glossy_area" "texturename" "grid.tex"
      "Kd" 0.3 "Kg" 0.7 "alpha_u" 0.1
      "RRthreshold" 0.01

    AttributeBegin
      Translate 0 -1 0
      Rotate -90  1 0 0
      #ReverseOrientation
      ジオメトリ "teapot"   # standard "Utah" Bezier patch teapot
    AttributeEnd

WorldEnd
FrameEnd

Appendix D: Caching spotlight example

Here is an example of a caching spotlight shader. Generally it is desirable (where possible) to avoid calling a light multiple times. However, depending on how you factor your shaders, this might not be done easily. The shader below caches the result of previous invocations (on the same batch of points) to avoid this being an issue.

Note that this provides the most benefit when the light is invoked directly via its shader->light() call, and that is done multiple times. The standard diffuse() and specular() calls already implement a caching mechanism.

class cached_spot(
       float  intensity = 1;
       color  lightcolor = 1;
       point  from = point "shader" (0,0,0);   /* light position */
       point  to = point "shader" (0,0,1);
       float  coneangle = radians(30);
       float  conedeltaangle = radians(5);
       float  beamdistribution = 2;)
   {
       constant float m_cosoutside, m_cosinside;
       constant vector m_A;

       // note that we could supply default values
       // here but it would be pointless as we would
       // overwrite them
       varying color m_cachedCl;
       varying vector m_cachedL;
       // avoid uninitialized warnings
       varying point m_cachedPs = 0;

       // note the initialization of cacheValid in begin()
       // to make tha cache invalid each new batch of points
       uniform float m_cacheValid;

       public void begin() {
           m_cacheValid = 0;
       }

       // compute things that won't change
       public void construct() {
           m_cosoutside = cos(coneangle);
           m_cosinside = cos(coneangle-conedeltaangle);
           m_A = (to - from) / length(to - from);
       }

       public void light(output vector L; output color Cl) {
           float  atten, cosangle;

           // We aim to return the same values without computation
           // effort if the light is invoked more than once on
           // the same batch of points
           if (m_cacheValid == 0 || m_cachedPs != Ps) {
               illuminate(from, m_A, coneangle) {
                   cosangle = L.m_A / length(L);
                   atten = pow(cosangle, beamdistribution) / (L.L);
                   atten *= smoothstep(m_cosoutside, m_cosinside, cosangle);
                   Cl = atten * intensity * lightcolor;
               }
               // save the values
               m_cachedL = L;
               m_cachedCl = Cl;
               m_cachedPs = Ps;
               m_cacheValid = 1;
           } else {
               // return previously computed result
               L = m_cachedL;
               Cl = m_cachedCl;
           }
       }
   }

Appendix E: Dynamism and Tight binding for a light shader with blockers

This example factors a spot shader into a basic light and a set of attenuator coshaders. These attenuators can be a fundamental property of the light - such coshaders are passed via the parameter list of the basic light and exhibit tight binding.

Note that such coshaders when passed parameter-list do not need to be in scope once they have been passed to the light which will invoke them. In the example below, Illuminate is used to turn on the spot light which refers to the coshader. The coshader is no longer in scope and is available to the spotlight only.

To demonstrate the difference between 'tight' binding which occurs when shaders are passed via the parameter list, and the possibilities of dynamic binding, examine the coshaders in scope for the ground plane.

Here we have two additional attenuators which will be dynamically looked up by the light. This allows the light to be attenuated specially for certain objects it shines upon. In this case we apply some distance based attenuation as well as have the light cast shadows. Note that the teapot however does not have hard shadows.

// attenuated_spot.sl

class attenuated_spot(
      float  intensity = 1;
      color  lightcolor = 1;
      point  from = point "shader" (0,0,0);   /* light position */
      point  to = point "shader" (0,0,1);
      float  coneangle = radians(30);
              shader attenuators[] = {};
      )
 {
      constant vector m_A;

      // compute things that won't change
      public void construct() {
          m_A = (to - from) / length(to - from);
      }

      public void light(output vector L; output color Cl) {
          float  atten = 1;
                      uniform float i,nAttenuators = arraylength(attenuators);

                      shader localAttenuators[] = getshaders("category","lightblockers");
                      uniform float nLocalAttenuators = arraylength(localAttenuators);

                      illuminate(from, m_A, coneangle) {
                              // global attenuators
                              for (i=0;i<nAttenuators;i+=1) {
                                      varying float curAtten =
                                              attenuators[i]->attenuate(from,L,m_A);
                                      atten *= curAtten;
                              }
                              // local attenuators
                              for (i=0;i<nLocalAttenuators;i+=1) {
                                      varying float curAtten =
                                              localAttenuators[i]->attenuate(from,L,m_A);
                                      atten *= curAtten;
                              }
                              Cl = atten * intensity * lightcolor;
                      }
      }
  }
// coneangle_attenuate.sl

class coneangle_attenuate(
        float  coneangle = radians(30);
        float  conedeltaangle = radians(5);
        float  beamdistribution = 2;
        string __category = "lightblockers";
    )
{
        constant float m_cosoutside, m_cosinside;

        // compute things that won't change
        public void construct() {
            m_cosoutside = cos(coneangle);
            m_cosinside = cos(coneangle-conedeltaangle);
        }

        public float attenuate(point from; vector L; vector axis) {
            float atten;
            float cosangle = L.axis / length(L);
            atten = pow(cosangle, beamdistribution);
            atten *= smoothstep(m_cosoutside, m_cosinside, cosangle);
            return atten;
        }
}
// distance_attenuator.sl

class distance_attenuator(
    float falloff_power = 2;
    float distance_scale = 1;
    float cuton = 0;
    float cutonwidth = 0.1;
    float cutoff = 1e9;
    float cutoffwidth = 0.2;
    string __category = "lightblockers";
) {

    public float attenuate(point from; vector L; vector axis) {
        float d = distance_scale*length(L);
        float atten = 1/pow(d,falloff_power);

        atten *= smoothstep(cuton,cuton+cutonwidth,d);
        atten *= 1-smoothstep(cutoff,cutoff+cutoffwidth,d);

        return atten;
    }
}
// traceshadow_attenuator.sl

class traceshadow_attenuator(
    string __category = "lightblockers";
    float samples = 16;
    float bias = 0.1;
) {

    public float attenuate(point from; vector L; vector axis) {
        color t = transmission(from,from+L,"bias",bias,"samples",samples);
        return (t[0]+t[1]+t[2])/3;
    }
}
# Attenuated spot example RIB

Attribute "user" "float uselocalatten" [1]

FrameBegin 0

  Format 400 300 1
  シェーディングInterpolation "smooth"
  PixelSamples 4 4
  Display "attenuated_spot" "it" "rgba"
  Projection "perspective" "fov" 25
  Translate 0 0 12

  WorldBegin

    Attribute "visibility" "int transmission" [1]

    AttributeBegin
        Shader "coneangle_attenuate" "angleatten"
        LightSource "attenuated_spot" "attenspot"
            "float intensity" [2]
            "point from" [2 2 -1]
            "point to" [0 0 0] "float coneangle" [30]
            "string[1] attenuators" ["angleatten"]
    AttributeEnd
    Illuminate "attenspot" 1

    # Teapot
    AttributeBegin
      Surface "plastic"
      Translate 0 -1 0
      Rotate -90 1 0 0
      Scale 0.5 0.5 0.5
      ジオメトリ "teapot"
    AttributeEnd

    # Ground plane
    AttributeBegin
      # Have the ground plane lighting
      # falloff with distance
      IfBegin "$user:uselocalatten == 1"
          Shader "distance_attenuator" "localatten"
            "float falloff_power" [1]
            "float distance_scale" [0.5]
            "float cuton" [1.8]
          Shader "traceshadow_attenuator" "localatten2"
      IfEnd

      Surface "plastic"
      Translate 0 -1 0
      Scale 10 10 10
      Polygon "P" [ -1 0 1  1 0 1  1 0 -1  -1 0 -1 ]
    AttributeEnd

  WorldEnd
FrameEnd