For the cupcake assignment, I found that a particular element had a blurry refraction and a color tint to the refractions also. As I've previously mentioned, I am mostly a mental ray user, so I have always been frustrated by the lack of simple access to things like refraction blur and color attenuation/absorption.


Since I am working to become more familiar with RenderMan shader writing, I decided to implement this as a custom shader. This also represents the first step toward my independent project of building a mia_material_x replacement in RenderMan.


Environment reflections with the environment() function are a fast and efficient way to generate reflections without the need of raytracing. The All-Purpose shader makes use of the RenderMan Environment Light object as a controller for various settings and also as a visual representation of an environment sphere. The object can be rotated and the reflections will update correctly in the render. Generally when using the environment() function in a shader, you end up with a text box for typing in a .tex map name and no inherent control to do anything to orient the light. I find this to be non-intuitive and would like to support the same behavior as the All-Purpose shader in future custom shaders.

REFERENCE IMAGES

DISTRIBUTED RAYTRACING

To accomplish both blurry refractions and reflections, we need to trace numerous rays from each point into a sampling solid angle, instead of just tracing one ray along the reflection or refraction vector with trace(). The gather() loop construct in RSL provdes the shader writer with the ability to shoot a distribution of rays into the sampling area. There are plenty of other resources that go into more detail about distributed raytracing and reflection and refraction in general, so I will not spend much time on that here.


Below is an example of a blurry refraction gather() construct.


color hitCi = color(0,0,0);

//sampCone = the sampling area (in radians)
gather("illuminance", P, refrVector, sampCone, refrSamps, "surface:Ci", hitCi) {
	Ci += hitCi;
} else {
	//ray didn't hit a surface - return environment or other operation
}

Ci /= refrSamps;

Obviously larger solid angles will require many more samples. Things get very slow very quickly with high trace depths, though, so a future optimization will be to incorporate importance sampling or some kind of intelligent super-sampling.

ABSORPTION

There are a few different ways to deal with absorption. First, the refraction color can simply be multiplied by a user-input color. In this case, if the value is low, a smokey darkening effect will be created.


gather("illuminance", P, refrVector, sampCone, refrSamps, "surface:Ci", hitCi) {
	Ci += hitCi;
}
Ci /= refrSamps;
Ci *= absorbedColor;

However, the results are not very physically accurate. If we observe dielectric materials in nature, different wavelengths of light attenuate at different rates over distance. This is why a glass of water is nearly clear, but a deep pool or ocean can take on a very blue or green tint depending on the composition of the solution. The same is true for thick sheets of glass.


The thicker glass appears green, while the thinner glass appears clear
The water in the shallow bucket appears much less blue than the pool beside

While light actually attenuates at different rates across the spectrum, it is extremely expensive to implement across a full spectrum of light, so for now we will approximate this with R, G, and B.


This effect is described by Beer's Law, which is simplified to refracted light = incoming light * e^(-extCo * distance). Distance is how far our ray has traveled through an object and the extinction coefficient defines how quickly the light falls off inside the object. Larger extinction coefficients cause light to attenuate at a faster through the material.


The extinction coefficient is multiplied by the user-supplied color value, giving a unique attenuation curve for R, G, and B. We must also be sure to only attenuate the light while inside the surface by testing whether or not the micropolygon is facing the incident ray. Obviously this means we must take great care to have correct normals on geometry using this shader. See the comments below for more details.


color extColor = color(1,0,0);
gather("illuminance", P, refrVector, sampCone, refrSamps,
       "surface:Ci", hitCi, "ray:length" dist) {
	if(-In.Nn > 0) { 
		//Clamp the extinction color to prevent surfaces from being
		//100% color filters or absorb 100% of light
		extColor[0] = max(0.05, min(0.95, extColor[0]));
		extColor[1] = max(0.05, min(0.95, extColor[1]));
		extColor[2] = max(0.05, min(0.95, extColor[2]));
		   
		Ci[0] += exp(-extinction * dist * (1-extColor[0])) * hitCi;
		Ci[1] += exp(-extinction * dist * (1-extColor[1])) * hitCi;
		Ci[2] += exp(-extinction * dist * (1-extColor[2])) * hitCi;	
	} else {
		Ci += hitCi;
	}
}
	
Ci /= refrSamps;
				


I plan to extend this math a bit to allow for some more unusual effects by modifying the shape of the extinction curve.

IMPROVEMENTS

I am continuing to continue working on this shader by optimizing the code, incorporating importance sampling, and adding support for transmission.

ENVIRONMENT LIGHT

This section is very much a work-in-progress and will be expanded as I have time to dive deeper into the functions. Basically, the function is treated very similarly to the environment() call.


uniform shader envLights[] = getlights("category", "environment");
uniform float numEnvLights = arraylength(envLights);
				
//do gather function as usual here
gather("illuminance", P, ....... ) {
	//do operations
	
} else if (numEnvLights > 0) {
	//ray did not hit a surface shader and an environment
	//light exists.  Do environment reflections here
	
	uniform float i;
	for(i=0; i < numEnvLights; i++) {
		color hitCol = 0, missCol = 0;
		envLights[i]->gatherSpecularIllum(P, reflDir, sampCone,
				reflSamps, reflLimit, sImportance, "objectSet",
				maxDist, hitCol, missCol, reflOccl);
		Ci += missCol;
	}
	Ci /= reflSamps;
}

The code we are interested in is the gatherSpecularIllum() function. I am unable to find documentation for this fuction at present, but it appears to act like the gather() block construct, with both hitColor and missColor returned. In this example, the missColor is the environment reflection. This is very hacky and slow, but does work. I will be expanding this in the future.