VSFX 755 - Procedural 3D Shader Programming

Renderman Slim Flame Shader v2.0 - New and Improved


Project Summary:
Improving and retrofiting a flame shader that I wrote for a previous class in Slim. This version will have corrected opacity and will drive the intensity of Maya lights via a shade op gathering averaged surface opacity.

Results:
Click on the image to the right to see a final video. See a before-and-after comparison video here. You can see the new Slim tree here.



Flame error 2 Flame error 1

Problem 1 - Backwards Opacity

While I was mostly happy with the results of my original flame shader, there were some aspects of it that weren't quite on. Particularly, when portions of the flame break away the background behind it is darkened thru the transparent portion. The images to the right shows a before and after comparison of aframe where this occurs. Click on the image on the far right to see a clearer demonstration. The right half of the image shows the scene with the point lights turned on the left side off. In both instances you can see where the transparent top of the flame has instead darkened the background and taken on the reverse of its usual color.

The plane in both cases carries the same shader as the flame and shows a matching portion with the same error. NOTE: The shader is based on S/T space and the plane is 'upside-down' compared to the flame, so the error portions of it appear towards the bottom. I guessed that this was caused by errors in the opacity calculations and, after some testing, that proved correct. Test renders of a simple scene showed that if the opacity of a surface went negative it would reverse its color and multiply into the background behind it, darkening it rather than merely being transparent.

To solve this I added an SL Box to my Slim tree immediately before the opacity was passed into the shader and used a simple clamp function to make sure the opacity never went below 0 or above 1. Testing this showed that the renders now looked correct, transparent areas were now showing the background correctly.

Problem 2 - Disjointed Lighting

Now, with the most conspicuous problem solved there was something else that I wanted to tackle. In the previous version of the flame, 3 point lights within the flame shape flickered to simulate changes in the light given off by it. However, these were merely driven by noise fuctions based on time; they had no real connection to the flame itself and its current brightness. Though this approximated the effect fairly well it bothered me that the two were disjointed and there were points in the animation where it was readily apparent. I wanted to find a way to link the two so that the overall opacity of the flame, i.e. how solid or broken thru it was, would drive the intensity of the lights. This is actually how it happens in the real world because the less the flame is broken up the more light it gives off.

Capturing the opacity information for the light to use causes a catch-22. In order for the shader to evaluate each surface micropoly it must first perform the lighting calculations for that scene. However the light needs the surface opacity to drive its intensity. A vicious cycle if ever there was one. I thought of various ways around the problem but none seemed particularly feasible. There was no way to link the noise function driving the opacity with the one driving the light intensity because they were two different types performing two different calculations. The flame noise was based on S/T space and being determined by the renderer; the light noise was temporal and being calculated by Maya at the start of each frame.

The Solution

After posting my query on the Renderman forums and receiving no reply (as I write this the post has been read 41 times without so much as a 'no idea' reply) I turned to Professor Malcolm Kesson. I mentioned that it would still work if there was some way to gather the opacity information of the current frame and store it for the next frame in the render to use. True the intensity of the light would be one frame behind the flame but the changes happen slowly and gently enough that it would not be noticable. After some serious head-scratching between the two of us he suggested using a shade op, written in C, to gather the opacity information into a text file that Maya would access at the start of the next frame. Because the shade op is written using an older method, it currently requires that the renders be restricted to a single thread, be it for a Slim preview or a full render in Maya. Failing to do so will cause the program to crash out the instant the render is finished.
#include <stdio.h>
#include <stdlib.h>
#include <shadeop.h>

FILE *fptr = NULL;
int count = 0;
float totalR = 0,
      totalG = 0,
      totalB = 0;
float totalArea = 0;

SHADEOP_INIT(averageOpacity_init) {
      return NULL;
}

SHADEOP_CLEANUP(averageOpacity_cleanup) {
      if(count > 0) {
           fprintf(fptr, "%1.4f %1.4f %1.4f\n", totalR/count,totalG/count,totalB/count);
           fprintf(fptr, "%1.3f\n", totalArea);
           fprintf(fptr, "%d\n", count);
      }
      fflush(fptr);
      fclose(fptr);
      fptr = NULL;
      count = 0;
      totalR = 0;
      totalG = 0;
      totalB = 0;
      totalArea = 0;
}

// Here we define the "signature" of the averageOpacity() RSL function
SHADEOP_TABLE(averageOpacity) =
{
      { "float averageOpacity (color, float, string)", "averageOpacity_init", "averageOpacity_cleanup" },
      { "" }
};

// Here we implement the core functionality of our custom function
SHADEOP (averageOpacity) {
      float *opacity = (float*)argv[1];
      float area = *(float*)argv[2];

      // Name of the output text file
      STRING_DESC *description = (STRING_DESC *)argv[3];

      if(fptr == NULL) {
           fptr = fopen(description->s, "w");
      }

      totalR += opacity[0];
      totalG += opacity[1];
      totalB += opacity[2];
      totalArea += area;
      count++;
      return 0;
}

With the shade op written, I added code to the opacity-clamp SL Box in Slim to run the op with each shader call. Embedding it this way allowed me to run the op on only the top, flickering portion of the flame so that it's varying opacity would not be diluted by the base of the flame which remains constant. As you can see by the code below the shade op captures not only opacity but also total surface area of the flame. This could be useful as a multiplier for the opacity, providing further control over the intensity. For now, it is not used. Also, currently the opacity is greyscale but in situations where the values vary across R,G, and B the op will capture them separetly so that colored transparency can affect the light color.
/* float */
result = clamp(v1,0,1);

float a = area(P, "dicing");
color c = color(result,result,result);
averageOpacity(c, a, "...directory path.../averageOpacity.txt");

After a few tests thru Slim the op seemed to be functioning correctly and predicatably. I moved on to writing code that would allow Maya to access and interpret the information stored in the generated text file. This involved using some of the expressions that previously controlled the lights and modifying the rest to use the new opacity info. Notes regarding the code are found in commented blocks.
//OLD intensity drivers
//base_lightShape.intensity = `clamp .2 .4 (abs(noise(time)))`;
//mid_lightShape.intensity = `clamp .5 .7 (abs(noise(time*4)))`;
//top_lightShape.intensity = `clamp .6 1 (abs(noise(time*8)))`;

//movement drivers are retained
top_light.translateY = (noise(time*3)/4.5)+5.75;
top_light.translateZ = noise(time*2)/5+.1;

mid_light.translateY = (noise(time*3)/12)+4.8;
mid_light.translateZ = noise(time*2)/10;

//---------------------------------------
//New code for shade op integration

$myTime = `currentTime -q`;

//set path to the opacity text file
$path = "/stuhome/vsfx755/averageOpacity.txt";

//open the file for reading
int $fileid = fopen($path, "r");
string $line = fgetline($fileid);
int $linenumber = 1;
float $R,$G,$B;

if ($myTime == 1){
   //since the shade op will not have run before the first frame renders
   //we set light intensity to some starting value
   top_lightShape.intensity = .9;
   mid_lightShape.intensity = .6;
   base_lightShape.intensity = .3;
}else{
   while(size($line) > 0) {
      //if we are on the first line (containing the three opacity values)
      //we need to split it up into the three separate values
      if($linenumber == 1) {
         string $data[];
         //splits the line based on white space, returns the number of resulting items
         int $numitems = tokenize($line, $data);
         if($numitems == 3) {
            $R = $data[0];
            $G = $data[1];
            $B = $data[2];
            $avgValue = ($R + $G + $B)/3;
            //modifiers for the opacity value to bring the intensities to appropriate levels             $topValue = $avgValue * 4;
            $midValue = $avgValue * 2.5;
            $baseValue = $avgValue;
            //set light intensities to clamped values so that there are no anomalies             top_lightShape.intensity = `clamp .6 1.5 $topValue`;
            mid_lightShape.intensity = `clamp .5 1.1 $midValue`;
            base_lightShape.intensity = `clamp .2 .4 $baseValue`;
         }
      }
      //for now these lines are unnecessary and are included only to keep the while loop from       //repeating forever, future versions could use the data within to further modify the lights
      print($line);
      $line = fgetline($fileid);
      $linenumber += 1;
   }
};
//at first this command was not included, this resulted in maya filling its internal file
//descriptor table and running out of memory, preventing any further updates to the information
fclose $fileid;

Results

Click the thumbnails below to see movies demonstrating the results. The first is the new flame shader with lights driven by the shade op. The second is a before and after comparison of the shader, slight speed changes have been made to the flickering of the new one. Note how in the old version (left) the lights do flicker convincingly but are disjointed from the changes of the flame itself. Note also the background being darkened by the incorrect opacity calculations.



Conclusions

• Though the current version of the shade op requires rendering on only one thread, 1K renders still took only a few seconds per frame proving the shader to be extremely lightweight even with the op running. It requires no new calculations since it is only gathering information that has already been rendered. Future versions would include a rewritten op with support for multi-threading, something that should be a fairly straightforward change.

• Since both area and color information are captured by the op, both can be used to further refine lights that vary more in size or shade.

• Additional support should be added for multiple flames so that they can each call the op without writing over each others' information. This may be as simple as using shader instances but will require further testing.