RSL 2.0 Shader Objects

The following is a transcription of one of my independent projects while I was a student at SCAD.  The original page is still available here.  With RPS 17 and beyond, most of these features are standard, and I haven't continued development as I no longer have a RenderMan license at home.  I hope this information is useful!

Prior to this project, I had never attempted anything involving combined shading or shader objects. RSL 2.0 introduced two new concepts - shader objects and C-style structs. Both provide benefits to the shading pipeline speed.

First, the new shader objects (or class-based shaders) can contain separate methods for displacement, surface, and opacity in the simplest implementation. This allows for the sharing of data between these methods and more efficient execution by breaking the shading process into separate methods. A key example is the opacity method. In RSL 1.0 shaders, the shader may execute all the way to the end, only to end up with an opacity of 0. Malcolm mentioned this as a scenario with shading of tree leaves, and I suspected then that the speed could be improved by solving opacity first, and then wrapping the rest of the surface shader in a conditional based on the opacity being greater than some threshold. In RSL 2.0, the opacity method executes before the surface method, and will not run the surface shader if the opacity is below the threshold. This way we can accomplish the same behavior without ugly changes to the surface shader code. Additionally, values from these internal methods are cached, offering further speed advantages.

A more advanced version of shader objects utilizes multiple functions to break up the lighting process, as well as an initialization function to precompute values and speed up the shading process. This framework, along with user-created functions, is essential to creating a MIS-aware material in RPS 16.

//A simple example of a class-based setup
class rslTwoShader( shader params ) {
	float classVar;
	stdrsl_ShadingContext m_shadingCtx;
	
	public void construct() {
	}
	public void begin() {
		m_shadingCtx->init(); //initialize data struct
	}
	public void displacement() {
		//Runs first, modifies P and the normal
	}
	public void opacity() { //optional
		//Runs next, gives opportunity to exit before shading
	}
	public void prelighting() { //optional
		//Precompute any non-light-specific values for lighting
		//Useful for interactive relighting efficiency
	}
	public void lighting() {
		//Light-position dependent step, run per light
	}
	public void postlighting() { //optional
		//Any post-processing required
	}
	public void userCreatedFunc() {
	}
}

RSL 2.0 also introduced C-style structs. Structs can be used to store functions and data for reuse across the shading pipeline, and mainly serve to organize code and facilitate reuse. In my case, I used several Pixar structs and a custom struct for my final shader. One good example is Pixar's ShadingContext struct, which stores a myriad of data about the current shading point and provides many utility functions for dealing with that data. The ShadingContext struct is intialized in the begin() method of a class-based shader, and can be used throughout the shading pipeline for easy access to ray depth, the faceforward normal, tangents, etc.

RPS16 PHYSICALLY-PLAUSIBLE-SHADING

RenderMan Pro Server 16's physically-plausible shading makes use of both structs and class-based shaders. These shaders are constructed like any other, but utilize new lighting integrators and new required functions for multiple importance sampling.

First, an overview of the shading process with respect to multiple importance sampling. I have already covered how in some cases it is better for the material to generate the sample directions, and in other cases it is better for the light to provide sample directions to the material. With both lights and shaders, two new functions must be defined in RPS 16 to work with the MIS system.

The generateSamples() method is defined in both the material and light, and is used to store the response of that portion of the final result. In the case of the light, generateSamples() stores the sample directions and the light radiance. In the case of the material, it stores the sample directions and the material response at that direction (the evaluation of the BRDF and the PDF, but not the lighting).

//used as a part of the full shader object
public void generateSamples(string distribution;
		output __radiancesample samples[]) {
	
	//distribution = "specular", "indirectspecular", etc.
	
	if(distribution == "specular") {
		/* append to the samples array - if it already has any,
		they are from the lights */
		
		color materialresponse = 0;
		float materialpdf = 0;
		for(i = start_size; i < size; i+=1) {
			//do my whole BRDF calculation, resulting in:
			matresp = refl * (F*G*D*)/4*(Vn.Nn)*(Ln.Nn);
			matpdf = (D*(H.Nn))/(4*(H.Vn)); //see paper for details
			//store this material response, pdf, light dir and distance
			samples[i]->setMaterialSamples(matresp, matpdf, Ln, 1e30);
		}
	}
}

Next, the evaluateSamples() method must be defined for both the material and the light. In the case of a light, evaluateSamples takes the samples generated by the material (already containing the material response and pdf), and adds the light radiance for that sample direction, thus creating a full sample with lighting and material response. In the case of the material, the sample direction already contains information about the light radiance, and material response and PDF are added to create a full sample.

These samples are stored internally by RPS 16 in a radiance cache and can be reused in the new diffuselighting() and specularlighting() shader methods, delivering speedups in some cases.

float evaluateSamples(string distribution;
	output _radiancesample samples[]) {
	if(distribution == "specular") {
		for(i = 0; i < num_samps; i+=1) {
			//direction provided by light generateSamples()
			vector Ln = samples[i]->direction;
			
			//evaluate BRDF as above
			samples[i]->setMaterialResponse(matresp,matpdf);
		}
	} else if (distribution == "diffuse") {
		//implement lambert or oren-nayar diffuse here
		//right now diffuse is only using light samples since it is
		//inefficient to sample the whole hemisphere of the material
		for(i = 0; i < num_samps; i+=1) {
			float cosTheta = (samples[i]->direction).Nn; //Lambert
			if(cosTheta > 0) {
				matresp = cosTheta * diffuseColor / PI;
			}
			samples[i]->setMaterialResponse(matresp, 0); //pdf = 0
		}
	}
}

There are also two new integrators, directlighting() and indirectspecular(), where these samples are put to use. These functions invoke the generateSamples() and evaluateSamples() functions of both the materials and lights, and internally handle the multiple importance weightings. The directlighting() function includes a "heuristic" parameter to adjust the balance between light and material samples based on different research.

public void lighting(output color Ci, Oi) {
	__radiositysample samples[];
	
	directlighting(this, getlights(), "mis" 1, "heuristic", "veachpower2",
			"materialsamples", samples, "diffuseresult", diff_col,
			"specularresult", spec_col);
	color indir_spec = indirectspecular(this, "materialsamples", samples);
	color indir_diff = indirectdiffuse(P, Nn, num_diffuse_samps);
	
	Ci += (Kd * (indir_diff + diff_col) * (1-fresnel)) + spec_color + indir_spec;
}