Writing Atmosphere and Interior Shaders

Writing Atmosphere and Interior Shaders

March, 1997 (Revised April, 2005)


Starting with PhotoRealistic RenderMan 3.7, it is possible to sample lights from within a volume shader. This allows volume effects such as atmospheric haze, smoke, atmospheric shadows, etc. This Applications Note explains the general strategy for writing shaders to produce these effects, and lists a sample shader which implements a smoky appearance. It has been updated with examples for creating these effects via ray-traced or visible point shading.

General Strategy

The general idea behind the smoke effects is to ray march along the incident ray I, sampling illumination and accounting for atmospheric extinction. Typically, this is done with the following algorithm:

 /* Choose an appropriate step size for marching along the ray. */
 total_len = length(I)
     current_pos = P;
     while total_len > 0 do:
             /* sample the smoke density and light at current_pos
              * adjust Ci/Oi to add new light and extinguish due to smoke opacity.
             current_pos += stepsize * normalize(-I);
             total_len -= stepsize;

Volume shaders of this type can be very expensive. The computational expense is proportional to the number of iterations of the while loop, which is determined by the step size and the length of I. Therefore, it is important to choose your stepsize carefully - too large a stepsize will result in banding and quantization artifacts, while too small a stepsize results in very long render times. You will probably need to carefully tune the stepsize on a scene-by-scene basis.

Also remember that atmosphere shaders bind to surfaces, just like surface or displacement shaders. So you must have an object in the background for the atmosphere shader to run. In other words, pixels with no geometry at all "behind" them will not run any atmosphere shaders.

Example Shader: Smoke

Below is a shader that implements such a ray marching algorithm to simulate smoke. If the light sources in the scene cast shadows, you should be able to see the shadows in the smoke.

 * smoke.sl
 * Description:
 *    This is a volume shader for smoke.  Trapezoidal integration is
 *    used to integrate to find scattering and extinction.
 * Parameters:
 *   density - overall smoke density control
 *   integstart, integend - bounds along the viewing ray direction of the
 *          integration of atmospheric effects.
 *   stepsize - step size for integration.  Note that the overall speed
 *          of this shader is inversely proportional to the stepsize.
 *   use_noise - makes the smoke noisy (nonuniform) when nonzero
 *   freq, octaves, smokevary - control the fBm of the noisy smoke
 *   lightscale - multiplier for light scattered toward viewer in volume
 *   debug - if nonzero, copious output will be sent to stderr.

#define snoise(p) (2*noise(p)-1)

smoke (float density = 60;
       float integstart = 0, integend = 1000;
       float stepsize = 0.1;
       float debug = 0;
       float use_noise = 1;
       color scatter = 1;   /* for sky, try (1, 2.25, 21) */
       float octaves = 3, freq = 1, smokevary = 1;
       float lightscale = 15;
  point Worigin = P - I;
  vector incident = vtransform ("shader", I);
  point origin = transform ("shader", Worigin);
  vector IN, WIN;
  float tau;
  color Cv = 0, Ov = 0;           /* net color &; opacity of volume */
  color dC, dO;                   /* differential color &; opacity */
  float ss, dtau, last_dtau;
  color li, last_li, lighttau;
  color scat;
  point PP, PW, Psmoke;
  float smoke, f, i;

  float end = min (length (incident), integend) - 0.0001;

  /* Integrate forwards from the start point */
  float d = integstart + random()*stepsize;
  if (d < end) {
      IN = normalize (incident);
      WIN = vtransform ("shader", "current", IN);
      dtau = 0;
      li = 0;
      ss = min (stepsize, end-d);
      d += ss;

      while (d <= end) {
          PP = origin + d*IN;
          PW = Worigin + d*WIN;
          last_dtau = dtau;
          last_li = li;

          li = 0;
          illuminance (PW, vector(0,0,1), PI) { li += Cl; }
          if (use_noise != 0) {
              Psmoke = PP*freq;
              smoke = snoise (Psmoke);
              /* Optimize: one octave only if not lit */
              if (comp(li,0)+comp(li,1)+comp(li,2) > 0.01) {
                  f = 1;
                  for (i=1;  i < octaves;  i+=1) {
                       f *= 0.5;  Psmoke *= 2;
                       smoke += f*snoise(Psmoke);
              dtau = density * smoothstep(-1,1,smokevary*smoke);
          } else dtau = density;

        /* Our goal now is to find dC and dO, the color and opacity
         * of the portion of the volume covered by this step.
        tau = .5 * ss * (dtau + last_dtau);
        lighttau = .5 * ss * (li*dtau + last_li*last_dtau);

        scat = -tau * scatter;
        dO = 1 - color (exp(comp(scat,0)), exp(comp(scat,1)), exp(comp(scat,2)));
        dC = lighttau * dO;

        /* Now we adjust Cv/Ov to account for dC and dO */
        Cv += (1-Ov)*dC;
        Ov += (1-Ov)*dO;

        ss = max (min (ss, end-d), 0.005);
        d += ss;

  Ci = lightscale*Cv + (1-Ov)*Ci;
  Oi = Ov + (1-Ov)*Oi;

The image below was produced with this very shader.


Compensating for Motion "Dragging" in Volume Shaders

One particular problem with volume shaders can be the appearance of motion "dragging" when using motion blur on objects in scenes with volume or atmosphere shaders.


A standard smokey Atmosphere shader is attached the back wall and floor in this scene. Looks pretty good.


If we put the back wall (only) in motion, notice how the atmosphere starts to blur too, even though visually we might like the smoke to appear fixed around the ball.


If the wall is moving really fast, then the dragging "artifact" becomes very apparent.


In the final picture, the wall is still highly motion-blurred, but now the smoke appears to be still, even though the smoke shader is still attached to the wall. This is accomplished by binding it as a VPAtmosphere to delay the shader execution until after all of the motion samples have been displaced to their final positions, rather than executing the shader at shutter-open time and smearing the results.

    Attribute "identifier" "name" ["theBackWall"]
    MotionBegin [0 1]
      Translate 0 0 0
      Translate 2 0 0
    Surface "texturizer"  "string texturename" ["grid2"]
    VPAtmosphere "smoke"
    Patch "bilinear"  "P" [ 0 1 0   1 1 0  0 0 0   1 0 0 ]

Interior Volume Shaders

Ray-Traced Interior Volume Shaders

Here's a simple scene. images/figures.rayintro/interiorglass1.jpg

Here is a simple glass shader attached to the dragon. Refraction rays are shot from the surface shader using:

gather(..., "volume:Ci", rfrc)

The "volume:" directive specifies that the hit surface color should be modified by any attached volume shaders, if they exist. For this picture, there are no volume shaders..


To make this picture the only change was in the RIB file:

Interior "smoke"

was attached to the dragon. The interior shader is then automatically executed when a refraction ray (using "volume:Ci") is traced from an object that has one attached.


VPInterior Volume Shaders

Here's that dragon again. This time "Opacity .4 .4 .4" is applied to it, and it is using the default surface shader.

Note that this surface shader makes no ray tracing calls.

Getting interior volumetric effects on an arbitrary object like this used to require the surface shader on the "front" to find the "back" analytically, as described in the first smoke example above, or by tracing rays, as described in the second.


In this second image, only an additional "smoke" VPInterior shader has been attached to the dragon, the surface shader is still "defaultsurface". Visible point shading delays volume shader execution on this object until all of the z-samples (front and back "visible points") are known. Visible point shaders are automatically executed, without requiring changes to the surface shader (like Atmospheres).

Surface "defaultsurface"
VPInterior "smoke"
Opacity .4 .4 .4 # so we can see inside
ReadArchive "dragon.rib"
These volumes are computed in 3D, like atmospheres. All volume shaders operate on an interval, along the vector "I", starting at the surface near the camera "(P - I)" and ending at the next surface "P". When there are several VPInterior shaders attached to different objects, the one associated with the interval origin is used by default. images/figures.rayintro/interiorvp3.jpg

Here's a complete, simplistic example of a smoke-filled sphere using the same technique. The smoke shader is the one used in the example above.

Just as with a typical Atmosphere shader, the smoke shader will march along the "I" vector towards a surface point "P" selected by the renderer. The volume shader modifies the existing Ci and Oi, already computed by the surface shader at P, according to its volume function.


The difference here is that the incident vector I does not start at the eye as it would for an Atmosphere, instead it begins with the frontmost z-sample of the object and ends at the rear z-sample. The object must be partially transparent (or have culling otherwise disabled) in order for both the front and back samples to be present in the "visible point list."

##RenderMan RIB
FrameBegin 1
Format 400 234 1
Display "spheresmoke.tiff" "tiff" "rgba"
PixelSamples 5 5
Projection "perspective" "fov" [38]
Translate 0 -2 13
Rotate -20 1 0 0

LightSource "pointlight" 1 "from" [3 6 -6]
        "float intensity" [100]
Attribute "identifier" "name" ["floorwall"]
Surface "paintedplastic"
    "texturename" ["grid.tex"]
Color 0.6 0.6 0.9
Scale 40 40 40
Translate -0.5 0 -0.15
Patch "bilinear" "P"
    [0 0 1  1 0 1  0 0 0  1 0 0]
Translate 0 0 .25
Rotate -70 1 0 0
Patch "bilinear" "P"
    [0 0 1  1 0 1  0 0 0  1 0 0]
  Attribute "identifier" "name" ["smokeball"]
  Color 1 1 1
  Opacity .1 .1 .1
  Surface "defaultsurface"
  VPInterior "smoke" "float stepsize" [.25]
      "float freq" [1.5] "float octaves" [1]
      "float smokevary" [8] "float density" [1]
      "float lightscale" [1]
  Translate 0 3 -2
  Scale 3 3 3
  Sphere 1.0 -1.0 1.0 360.0