RenderMan Surface Shader

From K-3D

Revision as of 02:46, 3 August 2009 by Tshead (Talk | contribs)
(diff) ← Older revision | Current revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Global Variables

The following table lists the set of global variables that can be used by Surface shaders.

Name Type Storage Description
Ci color varying Apparent color of the surface (output)
Oi color varying Apparent opacity the surface (output)
 
Cs color varying True surface color (input)
Os color varying True surface opacity (input)
P point varying Surface position
dPdu vector varying Change in surface position along u
dPdv vector varying Change in surface position along vy
N normal varying Surface shading normal
Ng normal varying Surface geometric normal
u,v float varying Surface parameters
du,dv float varying Change in surface parameters
s,t float varying Surface texture coordinates
L vector varying Incoming light ray direction
Cl color varying Incoming light ray color
Ol color varying Incoming light ray opacity
E point uniform Position of camera
I vector varying Direction of a ray stricking a surface point
ncomps float uniform Number of color components
time float uniform Current shutter time
dtime float uniform The amount of time covered by this shading sample.
dPdtime vector varying How the surface position P is changing per unit time, as described by motion blur in the scene.

Geometry of Shading

             Surface Normal (*) <- Light Source
                          \  |
                           \ | /
 +-----+       Ray Angle    \|/ 
 | Cam ||] - - - - - - - - - / <- Surface
 +-----+                    / 
                           /

The geometry is characterized by the surface position P which is a function of the surface parameters (u,v). The rate of change of surface parameters are available as (du,dv). The parametric derivatives of position are also available as dPdu and dPdv. The actual change in position between points on the surface is given by:

  P(u+du) = P + dPdu * du 

and

  P(v+dv) = P + dPdv * dv

The calculated geometric normal perpendicular to the tangent plane at P is Ng. The shading normal N is initially set equal to Ng unless normals are explicitly provided with the geometric primitive. The shading normal can be changed freely; the geometric normal is automatically recalculated by the renderer when P changes, and cannot be changed by shaders. The texture coordinates are available as (s,t).

The optical environment in the neighborhood of a surface is described by the incident ray I and light rays L. The incoming rays come either directly from light sources or indirectly from other surfaces. The direction of each of these rays is given by L; this direction points from the surface towards the source of the light. A surface shader computes the outgoing light in the direction

Shading Algorithm

The purpose of a surface shader is to:

  • Calculate the apparent color of a surface, the color of the light leaving an object.
  • Calculate the apparent opacity of a surface.

This mean that each surface shader have its own illumination model.

Basic Shader

Here's one of the simplest surface shaders possible.

 surface first_example()
 {
    Oi = Os;
    Ci = Os * Cs;
 }

This is in fact the same as the standard constant shader. First we declare that it's a surface shader with the keyword surface, then we give it a name first_example, it doesn't have any instance variables so we leave the paranthesis empty. Next we copy the input opacity value to the output and copy the input color to the output while multiplying with the opacity. Using the multiply operator ('*') together with color values is refered to as color filtering, corresponding to the absorption of light by a material.

We could also have just ignored the input and assign our own choosen color, in this case blue, to the output (not that this is any particular useful shader).

 surface second_example()
 {
    Oi = Os;
    Ci = Os * color(0, 0, 1);
 }

Or we could use an instance variable to control the shader in some way.

 surface third_example( color a = color(0.5, 0.5, 0.5) )
 {
    Oi = Os;
    Ci = Os * (Cs + a);
 }

Using the addition operator ('+') is the same as mixing two light sources.

Developing Shaders with K-3D

Writing shaders is an highly iterative process, meaning that you write some shader code, compile it and render some kind of preview, then repeat the process by going back and tweaking the code some more.

Using K-3D to speed up the development is easy. First create a skeleton shader and save it to a file.

 surface test_shader()
 {
    Ci = Cs;
 }

Next we run k3d-sl2xml on the shader file to create as meta file that K-3D use to load shaders and create a user interface for them. Note that k3d-sl2xml only supports preprocessed sources as input, so you need to run the shader source through a C preprocessor:

$ cpp -E -P -x c test_shader.sl | k3d-sl2xml > test_shader.sl.slmeta

Then fire up K-3D, select the MaterialShader object and choose our new test_shader to be used. Then create some kind of geometry, maybe a sphere or a Newell primitive, and apply the material on it.

At last we're ready to start write our shader code. Just edit the test_shader.sl file in your favourite text editor and saving your changes, then hit render in K-3D and it will compile the shader and render the preview.

Just note that you can't add any instance variables to the above shader without first running k3d-sl2xml and restarting K-3D for them to turn up as properties in the Object Properties panel. However, while developing a shader you can just declare them as local variables and set their values at the top of the shader, changing them to instance variables when the shader is finished and running sl2xml again.

It may seem counterproductive to run sl2xml another time, but this is just a workaround since you may not always know exactly what instance variables you want or need. And this way you can keep the test_shader we created above around as a playground for shader development.

Another approach is to add all the instance variables you may need at the start, that way you do not need to keep running k3d-sl2xml. One very good reason to avoid having to rerun k3d-sl2xml is that it will overwrite your customisations of the .sl.slmeta file. Most shaders will need to have their .slmeta file customised.

You can use output to engine.log to help debug your shader.

if (debug=="y")
{
  printf("<debug shadername='myshader' shadertype='surface'>%p\t%f\t%f\t%c</debug>\n", P, depth(P), d, Ci);
}

If you have a string var debug you can set it to "y" in k3d when you want data output into engine.log. The tag pair is to help separate your data from the other data that may be in the log.

An example of using such data can be seen here.

Header Files

K-3D also come with a set of convenient shading language header files.

Note: This page a work in progress! The following examples will use header files not yet checked into CVS or currently shipped with K-3D.

Illumination

Local Illumination

So far the examples above have just been rendered flat, it didn't matter if there was any light present in the scene, and if there was it didn't affect the material. Which makes it pretty dull in the long run.

How materials respond to light that hit it is actually very important for their appearance. For example, a simple painted surface respond to light by reflecting a part of the lights spectrum (a red painted surface reflect the red chunk of the spectrum) while a shiny object reflect nearly all light. So to make your shder look like plastic, metal, mud, etc. it has to respond to light just like that material.

This light repsonse is usually refered to as illumination model. And to make life a little bit easier there are a whole lot of illumination models available to choose from that can be reused in your shaders. The most common model is some form of BRDF (Bidirectional Reflection Distribution Function), like "plastic" or "shiny metal". The name of the models is just descriptive and the "plastic" illumination model can be used to render many non-plastic materials that just share the same characteristics.

 #include "illumination.h"
 surface illumination_example( float Ka=1, Kd=0.8, Ks=0.5, roughness=0.1; )
 {
     /* adjust the normal */
     point Nf = faceforward(normalize(N), I);
     /* illumination */
     Oi = Os;
     Ci = Os * PlasticBRDF(Nf, Cs, Ka, Kd, Ks, roughness);
 }

In the example above we begin with making a copy of the surface normal and use the viewing angle to ensure the normal oints towards the camera. Then we extend our basic constant shader by using the plastic illumination model.

Layered Illumination

How to create surface shaders that correctly layer structures of different illumination models using the Kubelka-Munk model. (To be written...)

Patterns

So far our example have produced a single colored material that respond to light roughly like plastic. But very few materials in real life is truly that uniform as our material, even one colored things has variations in them. Ok, there wouldn't be any big point in having surface shaders if we couldn't also produce a big range of patters in addition to mixing colors and responding to light in fancy ways. So now it's high time to create some nice patterns,

Regular Patterns

Transitions

The natural first step away from having a single colored material would be to have a two colored material. So the simplest pattern we can possibly think of would be a transition between two colors.

Transitions is the basic building block since pretty much all pattern making is about transiting between colors in different ways. For that we have functions like step, pulse and the variations on them.

The function step(a, x) acts like a switch jumping from 0 to 1 when the variable, x, is greater than some threshold, a. In the example below the surface switch from black to white when the texture coordinate s become 0.5 or higher.

 surface step_example()
 {
    Ci = Os * step(0.5, s);
 }

The function smoothstep(a, b, x) is similar to step, but provides a smooth transition from 0 to 1. When the variable x is greater than the value a the function starts changing from 0 to 1. By the time the variable x reaches the value b, the transition has completed and the value of smoothstep is 1.

 surface smoothstep_example()
 {
    Ci = Os * smoothstep(0.45, 0.55, s);
 }

To make it really easy to avoid aliasing K-3D provide an anti-aliased version of smoothstep called filtersmoothstep. It takes one extra argument that need to be calculated using a macro called filterwidth.

 #include "pattern.h"
 surface filtersmoothstep_example()
 {
    Ci = Os * filtersmoothstep(0.45, 0.55, s, filterwidth(s));
 }

While a step go from 0 to 1 a pulse represents a temporary transition defined over some range. That is, it make a transition from 0 to 1, hold it there for some time, and then make a transition back to 0 again.

 #include "pattern.h"
 surface pulse_example()
 {
    Ci = Os * pulse(0.25, 0.75, s);
 }

Just as with the step functions pulse also has a brother called smoothpulse to provide smooth transitions.

 #include "pattern.h"
 surface smoothpulse_example()
 {
    Ci = Os * smoothpulse(0.23, 0.27, 0.73, 0.77, s);
 }

And to avoid any aliasing we also have the anti-aliased versions filterpulse and filtersmoothpulse taking the same extra argument discussed above.

Tiling

Ok, so now we can create two colored thingys that is supperior our previous one color thingys. However, surfaces with two color fields isn't what most people mean when they say "nice pattern". So our next step will be to start repeat our transitions.

The mod function is probably the most important building block for generating periodic patterns. Because when mod is used to modify the input to another function, f, the result is that function f gets repeated. That mean we can use mod to repeat our transitions over an entire surface. This is called tiling.

Instead of using texture coordinates (s, t) which vary from 0 to 1 over the entire surface, we use tile texture coordinates (ss, tt) which vary from 0 to 1 within a single tile to compute a tile pattern. These coordinates can be computed using mod directly or through a convenience macro called repeat.

 #include "pattern.h"
 surface tile_example()
 {
    float ss = repeat(s, 4);
    Ci = Os * pulse(0.25, 0.75, ss);
 }

The white band with black borders will get repeated four times in the shader above.

When repeat is used, each tile can be considered in the same way since each tile will have the same tile texture coordinates, one tile cannot be distinguished from another using ss and tt alone. Sometimes however it is useful to determine "which" tile we are currently shading so that we can introduce alterations to individual tiles.

Computing the row and column (the tile coordinate) of the current tile is easy using a convenience macro called whichtile. In the example below frquency (sfreq, tfreq) is the number of tiles horizontaly and verticaly.

 #include "pattern.h"
 surface whichtile_example()
 {
    float sfreq = 2;
    float tfreq = 2;
    float ss = repeat(s, sfreq);
    float tt = repeat(t, tfreq);
    float col = whichtile(s, sfreq);
    float row = whichtile(t, tfreq);
    color surface_color = color(1,1,1);
    if (odd(col) && even(row))
       surface_color = color(1,0,0);
    else if (odd(row) && even(col))
       surface_color = color(1,0,0);
    Ci = Os * surface_color;
 }

The two handy convenience macros odd and even can be used to test if the current tile is in an even or odd row/column and then shift or not shift texture or coordinates appropriately.

Layering

After we have learned to create patterns with tiling, repeating them side by side, its time to start layering, stacking them on top of each other.

By using layering we can decompose any complicated, multicolored, pattern into individual layers by braking it down into simpler parts that can be understood. The shader below isn't particular multicolored nor complicated but it shows pretty much the basics of layering.

 surface layer_example()
 {
    color layer_color;
    float layer_alpha;
    /* background */
    color surface_color = color(1,1,1);
    /* first layer */
    layer_color = color(1,0,0);
    layer_alpha = pulse(0.25, 0.75, s);
    surface_color = mix(surface_color, layer_color, layer_alpha);
    /* second layer */
    layer_color = color(0,0,1);
    layer_alpha = pulse(0.25, 0.75, t);
    surface_color = mix(surface_color, layer_color, layer_alpha);
    /* output */
    Oi = Os;
    Ci = Os * surface_color;
 }

We will always define the layers from back to front and composite each layer as we go. The accumulated color of all layers defined so far will be stored in a variable called surface_color. Initially, we'll assign a value directly to surface_color which will indicate the color of the background layer, of course this can just as well be controlled by an instance variable or the global variable Cs. Then for each foreground layer we use the layer_color and layer_alpha to represent color and opacity for the current layer, reusing them in each layer. And lastly we will composite the layer on top if the previous defined layers using mix.

Transforming

When it is necessary to create patterns at angles, particular scales, or positions, it is usually easier to transform the texture coordinates rather than the pattern itself. Texture coordinate transformations are easy, while generating patterns is usually more difficult.

Transforming texture coordinates has an "inverse" effect on the appearance of the pattern. For example, if you scale the texture coordinates by a factor of 2, the frequency of the pattern will double and the pattern will appear twice as small. Likewise, positive translations will move the pattern up and to the left (the opposite direction) and positive clockwise rotations will rotate the pattern counter-clockwise.

A macro called rotate2d makes it easy to rotate 2D texture coordinates.

 #include "pattern.h"
 surface rotate_example()
 {
    color surface_color;
    color layer_color;
    float layer_alpha;
    float ss, tt;
    /* rotate texture coordinates */
    rotate2d(s, t, radians(45), 0.5, 0.5, ss, tt);
    /* background */
    surface_color = color(1,1,1);
    /* first layer */
    layer_color = color(1,0,0);
    layer_alpha = pulse(0.35, 0.65, ss);
    surface_color = mix(surface_color, layer_color, layer_alpha);
 
    /* second layer */
    layer_color = color(0,0,1);
    layer_alpha = pulse(0.35, 0.65, tt);
    surface_color = mix(surface_color, layer_color, layer_alpha);
    /* output */
    Ci = Os * surface_color;
 }

There's also the convenience macros translate2d and scale2d (yes they are simplistic, and mostly there for completeness.)

Another type of transformation are converting the texture coordinates to polar coordinates, useful for generating radially shaped patterns. The convenience macro topolar2d do exactly that.

Shapes

Rectangular shapes can be made by intersecting (multiplying) a vertical pulse and a horizontal pulse. There are already rect and filterrect functions implementing this shape generator.

 #include "pattern.h"
 surface rect_example()
 {
    /* background */
    color surface_color = color(1,1,1);
    /* rectangle */
    color rect_color = color(1,0,0);
    float rect_alpha = rect(s, t, 0.05, 0.75, 0.1, 0.7, 0.025);
    surface_color = mix(surface_color, rect_color, rect_alpha);
    /* output */
    Ci = Os * surface_color;
 }

Disks and rings can be generated using distance and either smoothstep (for a disk) or pulse (for a ring). If the distance between the current shading point and the center of the disk is less than the radius of the disk, then the current shading point is inside the disk. The idea for rings is identical, except that a pulse is used instead of smoothstep. There are already disk, filterdisk, ring and filterring functions implementing this type of shape generator.

 #include "pattern.h"
 surface disk_example()
 {
    /* background */
    color surface_color = color(1,1,1);
    /* disk */
    color disk_color = color(1,0,0);
    float disk_alpha = disk(s, t, 0.35, 0.025, (0.5, 0.5, 0));
    surface_color = mix(surface_color, disk_color, disk_alpha);
    /* output */
    Ci = Os * surface_color;
 }

Lines are generated like disks except that the distance is computed from the current shading point to the nearest point on the line using ptlined. We have line and filterline implementing this type of shape generator.

 #include "pattern.h"
 surface line_example()
 {
    /* background */
    color surface_color = color(1,1,1);
    /* line */
    color line_color = color(1,0,0);
    float line_alpha = line(s, t, 0.1, 0.025, (0.25, 0.15, 0), (0.85, 0.7, 0));
    surface_color = mix(surface_color, layer_color, layer_alpha);
    /* output */
    Ci = Os * surface_color;
 }

With boolean operations can we create complex shapes from union, intersection, or difference of two simpler shapes. There are four convenience macros defined for this purpose boolunion, booldifference, boolintersection and boolcomplement.

 #include "pattern.h"
 surface boolean_example()
 {
    /* background */
    color surface_color = color(1,1,1);
    /* shapes */
    float bool_rect = rect(s, t, 0.05, 0.75, 0.1, 0.7, 0.025);
    float bool_disk = disk(s, t, 0.35, 0.025, (0.5, 0.5, 0));
    /* boolean operation */
    color layer_color = color(1,0,0);
    float layer_alpha = booldifference(bool_rect, bool_disk);
    surface_color = mix(surface_color, layer_color, layer_alpha);
    /* output */
    Ci = Os * surface_color;
 }

Irregular Patterns

Irregular patterns have stochastic components, meaning they are pseudo-random, functions appear to have random output but are actually repeatable given the same input.

Noise

The basic building block for stochastic functions is the noise function.

 #include "noise.h"
 surface noise_example()
 {
    Ci = Os * float noise(s * 5, t * 5);
 }

The input of noise is commonly manipulated using simple transformations such as shifting by an offset or scaling by some frequency.

 noise(x + offset)
 noise(x * frequency)
 noise(x * frequency + offset) * amplitude

It's not always convenient to have a noise function with strictly positive outputs. For that reason K-3D has a family of signed noise functions, snoise, filtersnoise, snoise2, vsnoise and filtervsnoise.

 #include "noise.h"
 surface snoise_example()
 {
    Ci = Os * snoise2(s, t);
 }

The noise function has a tendency to generate output values which lie near the value 0.5. The uniformly distributed noise functions udnoise and udnoise2 can be used instead of noise to generate values which are more uniformly distributed over some range. A uniform distribution of values is particularly useful when randomly varying some pattern.

 surface udnoise_example()
 {
    Ci = Os * udnoise2(s, t, 0, 1);
 }

A commonly used mechanism in stochastic patterns is turbulence. Turbulence is created by summing multiple noise functions with varying amplitude and frequency. There are many ways to create turbulence but K-3D ships with its own standard anti-aliased turbulence function.

 #include "noise.h"
 surface turbulence_example()
 {
    Ci = Os* turbulence(P, filterwidthp(P), 2, 10, 2);
 }

There are also functions for fractional Brownian motion and Voronoi cell noise (a.k.a. Worley noise).

Perturbing

Irregular shapes can be made by adding irregularities to regular patterns to make them less mechanical and more interesting. This kind of perturbation is achieved by adding pseudo-random values to otherwise constant or regularly varying variables which control the shapes in the regular pattern. The random values are generated using noise with the current surface point or texture coordinates as inputs.

 #include "pattern.h"
 #include "noise.h"
 surface perturbing_example()
 {
 float noifreq = 5;    /* frequency of the perturbation */
 float noiscale = 0.4; /* amount of perturbation */
 /* create base noise */
 float base_noise = noise(s * noifreq, t * noifreq);
 /* perturb texture coordinates  */
 ss = s + snoise(base_noise + 912) * noiscale;
 tt = t + snoise(base_noise + 333) * noiscale;
 /* continue as usual... */
 float ss = repeat(ss, 2);
 float tt = repeat(tt, 2);
 color layer_color = color(1,0,0);
 float layer_alpha = ring(ss, tt, 0.35, 0.1, 0.1, (0.5, 0.5, 0));
 Ci = Os * mix(surface_color, layer_color, layer_alpha);
 }

Some components you might want try to add noise to:

  • texture coordinates
  • rotation angle
  • colors

Bombing

Another way to add randomness to regular patterns is to use a technique called bombing. Bombing generates random placement patterns.

We start out with a regular tiled pattern. Then we randomly move the position of each tile. So the thing that distinguishes it from a perturbed regular pattern, is that the shifting is based on the tile coordinates.

 surface bombing_example()
 {
    float ss = repeat(s, 2);
    float tt = repeat(t, 2);
    float col = whichtile(s, 2);
    float row = whichtile(t, 2);
    /* create base noise */
    float base_noise = noise(col * 10 + 0.5, row * 10 + 0.5);
    /* jitter tiles */
    float ss = ss + udnoise(base_noise * 1183, -0.35, 0.35);
    float tt = tt + udnoise(base_noise * 999, -0.35, 0.35);
    /* continue as usual... */
    color layer_color = color(1,0,0);
    float layer_alpha = rect(ss, tt, 0.05, 0.75, 0.1, 0.7, 0.025);
    surface_color = mix(surface_color, layer_color, layer_alpha);
 }

Some attributes you might want try to add noise to:

  • location
  • orientation
  • shape
  • color
  • existence

Texturemaps

How to create surface shaders that use texturemaps. (To be written...)

References

Personal tools