Renderman shaders evolution

Recently, Pixar has introduced new shading pipeline methods which completely replaced good old shaders which could be described as a single line of code

Ci = Ka*ambient() + Kd*diffuse(N) + Ks*specular(I, N, roughness)

A very linear and intuitive methodology became modular and started to feel more like Object Oriented Programming, which I really enjoyed in Python, but always tried to avoid in shader development. Although, new style shaders better suit Physically Plausible shading and offer several rendering optimizations, they aren’t necessarily the only solution of getting beautiful images.

Nowadays, there are three different ways of writing the shaders for Renderman. This can be somewhat confusing as all of the routes require different programming structures which may output almost identical results. In order to clean up the mess in my head, I decided to sum up all the approaches of writing RSL shaders in this post.

Here we go. Meet the hard working guy Plastic Shader!

traditional Plastic Shader

The Old Plastic Shader render.

The guy comes from the 90s if not 80s. He was shading such great features as Toy Story, Jurassic Park and many other legendary films. The logic behind the shader is very simple. It takes a light vector and a surface normal and calculates a dot product of the given inputs. The resulted dot product defines the Lambertian of a surface.

Here is a code of the shader.

surface simplePlastic_I(
	color diffuseColor = color (0.5);
	color specularColor = color (0.4);
	float specularRoughness = 0.1;
	)
{
	normal Nn = normalize(N);
	normal Nf = faceforward(Nn,I);
	vector V = normalize(-I);
	Oi = Os;
	Ci = diffuse(Nf)*diffuseColor +
             specular(Nf,V,specularRoughness)*specularColor;
	Ci*=Oi;
}

Despite his age, the guy is still widely used in many productions. In the hands of a good artist, the shader can produce incredible images. Moreover such shader logic is still in use in a video games industry and some other offline renders. It is simple, fast and very easy to code and read.

Next, comes the RSL2.0 shader. The guy is a close relative of the first one. Essentially, it does the same thing, but offers more flexibility to a Lighting TD. It supports some basic Object Oriented Programming features such as inheritance and class methods.

mid_guy0001

The RSL 2.0 Plastic Shader render.

The code of the shader is still pretty straightforward. The shader logic is split on different methods which describe surface properties and geometry displacement. The shader is no longer declared as a surface, it is a class now. Being the class, the shader can share various methods with other class shaders.

class simplePlastic_II(
	color diffuseColor = color (0.5);
	color specularColor = color (0.4);
	float specularRoughness = 0.1;
	)
{
public void displacement (output point P; output normal N)  //the displacement method (optional)
	{ }
public void opacity(output color Oi) //the opacity method
	{
	Oi = Os;
	}
public void surface(output color Ci,Oi) //the surface method.
	{
	normal Nn = normalize(N);
	normal Nf = faceforward(Nn,I);
	vector V = normalize(-I);
	Ci = diffuse(Nf)*diffuseColor+
             specular(Nf,V,specularRoughness)*specularColor;
	Ci*=Oi;
	}
}

And finally, the youngest guy in a neighborhood.

new_guy0001

Specular is sampled over the area of the light source which has a rectangular shape.

No doubts, this shader is relatively overcomplicated for being just a Plastic shader, although it has a couple of key differences. First, it uses sampling to calculate the surface’s response to a lighting environment. It supports true area lights, importance sampling and GI cache. This guy is all about physical plausibility and raytracing optimizations. In order to be considered a modern shader, it should include two methods: evaluateSamples() and generateSamples(). These methods care the core functionality of the shader, as irradiance calculations take place in these two methods. The methods are similar to the gather() function, they define a sampling cone angle, sample generation and evaluation based on a BRDF. Most importantly, the shader builds atop of the preceding two shaders. For example, the diffusion is still calculated as a dot product of two vectors and it uses method calls to describe stages and properties of the surface.

#include <stdrsl/RadianceSample.h>
#include <stdrsl/Math.h>

class simplePlastic_III(
    uniform color diffuseColor = color (0.5);
    uniform color specularColor = color (0.5);
    uniform float specularRoughness = 0.5;
    uniform string diffuse_map = "";
    uniform string specularity_map = "";
    )
{
    varying color diffuse_texture;
    varying color diff,spec;
    varying normal Nn;
    uniform float nsampsSpec = 36;
    uniform float nsampsDiffuse = 256;
    uniform float specularThreshold = .005;  
    varying float maxConeAngle = 0;  
    varying float cosMaxConeAngle = 0; 
    varying vector R;
    varying vector Tn = 0;   
    varying vector Bn = 0;   
    varying float conePdf = 0;  
    varying float exponent = 0; 
	
	public void construct(){
	}
	
	public void begin(){
		Nn = normalize(N);
		vector In = normalize(I);
		R = reflect(In,Nn);
		exponent = 1/specularRoughness;
		}
	
	public void prelighting(output color Ci, Oi){
		if (diffuse_map != ""){
			diffuse_texture = texture(diffuse_map);
			}
		else{
			diffuse_texture = 1;
			}
		cosMaxConeAngle = pow(specularThreshold,1/exponent);
		float coneSolidAngle = M_TWOPI*(1 - cosMaxConeAngle);
                conePdf = 1/coneSolidAngle; 
		Tn = normalize(R^Nn);
		Bn = Tn ^ R;
		}
	
	public void lighting(output color Ci, Oi){
		shader lights[] = getlights();
		directlighting(this,lights,"diffuseresult",diff,"specularresult", spec);
		Ci +=diff+spec;
		}

	public void evaluateSamples(string distribution; output __radiancesample samples[]){
		if(distribution == "diffuse"){
			uniform float i; 
			uniform float sample_size = arraylength(samples);
			for (i=0; i<sample_size;i+=1){ 				float NdotL = samples[i]->direction.Nn;
				samples[i]->materialResponse = M_INVPI*NdotL*diffuseColor*diffuse_texture;
				samples[i]->materialPdf = 0;
				}
			}
		else if (distribution == "specular"){
			uniform float i=0;
			uniform float sample_size = arraylength(samples);
			float RdotL = samples[i]->direction.R;
			if (RdotL<cosMaxConeAngle){ 
                                samples[i]->materialResponse = 0;
				samples[i]->materialPdf = 0;
				}
			else{
				float phong = pow(RdotL,exponent);
				samples[i]->materialResponse = specularColor*phong;
				if (nsampsSpec > 0)
					samples[i]->materialPdf = conePdf;
				else
					samples[i]->materialPdf = 0;
					}
			}
		}
	public void generateSamples(string distribution; output __radiancesample samples[]){
		if(distribution == "specular" && nsampsSpec > 0)
        {
                uniform float i=0;
                resize(samples, nsampsSpec);
                for(i=0;i<nsampsSpec;i+=1)
             	    {                      
                    float cosTheta = 1 - random()*(1 - cosMaxConeAngle);
                    float sinTheta = sqrt(1 - cosTheta * cosTheta);
                    float phi = random() * M_TWOPI;
                    vector Ln = (R * cosTheta +
                             Tn * sinTheta * cos(phi) +
                             Bn * sinTheta * sin(phi));
                    samples[i]->direction = Ln;           
                    samples[i]->distance = 1e20;
                    float phong =  pow(cosTheta,exponent);
                    samples[i]->materialResponse = specularColor * phong;
                    samples[i]->materialPdf = conePdf;
		    }
	        }
       }
}

shader-analysisIn conclusion, all of the approaches listed above are widely used in productions and provide great results.The traditional shaders are fast to develop and render. The modern shaders are a lot harder to write, although  they provide accuracy and speed if heavy raytracing is employed.

 

Leave a Reply

The reply will be processed within a day