FumeFX to RenderMan - Shading

The following is a transcription of one of my independent projects while I was a student at SCAD.  The original page is still available here.  Unfortunately, I haven't had much time or inclination to further develop this as I haven't really done anything with an fx simulation in a couple of years.  Perhaps some of the information here is useful to someone!

SHADING THE FIRE

The visible color of most fire is based on a physical property called black-body radiation. A black body is a surface that absorbs all electromagnetic radiation. When a black body is heated to a constant temperature, it emits black body radiation, some of which is in the visible spectrum. The wavelengths and intensity of this radiation are defined by temperature alone. Therefore, there is a defined 'color' for a black body at a certain temperature.

The soot particles in fire are considered black bodies, and an obvious property of fire fluid simulation is temperature. If fire simulations are conducted with realistic temperature values (or converted later), the black body radiation spectrum can be applied to the fire particles to produce a realistic flame rendering. In my case, I am using a color spline with 10 values approximating the ramp.

Brightness of actual fire is often not taken to account in rendering of fire fluid simulations. The actual brightness is defined by the Stefan-Boltzmann law [2], which is quite involved but basically boils down to intensity being proportional to the temperature raised to the fourth power. I used the normalized temperature of the fire along the color gradient as the base and multiply the result by a user controlled constant. The result of the fourth power is still in the normalized range, and the constant gives the user direct control over the maximum intensity of the fire.

float scaleFactor = pow((normalizedSplinePosition),4)*fireExponent;
color fireColor = colorSplineValue * scaleFactor;

Fire opacity control is provided by a user controlled constant, which is multiplied by the normalized position on the temperature gradient. This way, the hotter the fire, the more opaque the fire, and the fire fades off nicely as the temperature cools. This is not entirely accurate, but is provided to offer more directibility, as it can still sometimes be difficult to dial in the contributions of the fire and smoke. I would like to extend this functionality in a more elegant way than providing a list of float values for a spline curve. A spline curve would also assist with a separate issue where the cooler fire (red) is generally so transparent as to not be visible at all, and I expect to add this in a future version.

FireOi = normalizedSplinePosition * fireOpacityVal;
FireCi = FireOi * fireColor;

It is often quite difficult to nail temperatures exactly in a fluid simulation, so the shader also includes the ability to multiply the temperature values up or down by a user controlled value to dial in a look.

SHADING THE SMOKE

The smoke shading process is much simpler than the fire; currently the user only has access to a color and opacity control. FumeFX considers a voxel full of smoke to have a value of 1, though these values can be greater than 1 to indicate more dense smoke. The user opacity value is multiplied by the smoke density from the voxel grid, so it acts as an 'opacity per 1 unit density' control. Color is currently just controlled via a color swatch, and color/opacity are not affected by temparature or other factors at this time.

A great resource for fire and smoke shading methods is the course notes from the Siggraph 2011 course Production Volume Rendering [1].

MULTIPLE SCATTERING

Single scattering is the effect of light penetrating into a volume along the light vector and attenuating over distance based on the density of the volume. Since this attenuation only depends on light direction, this is basically accomplished with deep shadows, where the accumulated light attenuation is stored in the shadow map and accessed via the illuminate(P) call from the shader.

Single scattering is great for light sources on the smoke but doesn't really account for fire illuminating the smoke, or for illumination from light sources bouncing around in the smoke cloud. To be economical, this multiple scattering requires the use a pre-computed point cloud. Writing the point cloud is quite simple, and can be done at a larger shading rate than the final renders.

//PtcFile contains the file path for the point cloud writing
//4 channels written: _area, _radiosity, _extinction, Cs

if(OI != color(0) && PtcFile != "") {
	//Check opacity to avoid writing the whole volume box
	float area = area(P, "dicing");
	bake3d(PtcFile, "_area,_radiosity,_extinction,Cs", P, N,
			"interpolate", 1,
			"_area", area, "_radiosity", bakeColor, 
			"_extinction", OI, "Cs", bakeColor);
}

This first step simply stores the radiance at each shading point in the volume where opacity is above zero. In my shader, I added controls to only scatter the fire light, so depending on the value of this check box the radiance from light sources may or may not be included in the point cloud. This way the user is able to gain up the resulting scattered light without washing out the entire smoke cloud from direct lighting.

The first point cloud is not sufficient for indirect lighting. There are two methods to generate an indirect lighting solution from this point cloud - the indirectdiffuse() call and the ptfilter external application.

Generally the indirectdiffuse() function is used as an efficient gather() loop and is raytraced, but it also has support for a point-based mode. In point-based mode, the shader evaluates the indirect lighting on the shading point at render time, though I found this to be extremely slow and opted not to use this method. I would like to revisit this in the future to explore any potential speedups I may have missed.

For my shader I utilized the ptfilter external application. Ptfilter is included with RenderMan Pro Server and performs a variety of operations on point clouds, from color bleeding and subsurface scattering to volume indirect lighting. In my initial implementation, I wrote the point clouds out via a low quality render, and then ran the following command in the shell:

ptfilter -filter volumecolorbleeding -clamp 1 -sortbleeding 1\
    old_point_cloud.ptc new_point_cloud.ptc

Ptfilter creates a new point cloud with the indirect illumination calculations baked in. This new point cloud can be used in the shader via a 3d texture lookup. The biggest problem with this workflow is the large number of steps and annoying back and forth between Maya, command line prman and ptfilter. I noticed some time ago with subsurface scattering that this ptfilter command could be built into the RMS workflow as a part of a pre-pass, but had no idea if it was possible to add my own passes to this system.