Gritty Details
Last time, I presented a "simple" algorithm that performed volume ray marching, but I didn't really explain how it was derived. We'll spend this lesson deriving the algorithm and then show some optimizations that can be done to speed up the renderer.
Volume Rendering Derivation
First, credit where it is due. This derivation is due to Dr. Jerry Tessendorf, my advisor. This is a presentation of his derivation from a course in volume rendering.
We first look at the case where there are no lights, but the medium can emit light itself.
The algorithm for ray marching in volume rendering is essentially just the numerical approximation of the following rendering equation

where
is the camera position,
is the pixel ray direction,
is the scattering coefficient.
The field
is the material color of the medium, and
is the density of the medium. The path
is the straight line path from the camera along the ray direction

and
is the length of the path.
Before trying to tackle this, it's useful to notice that the limits of integration
are completely overkill because the interesting, dense material is only arranged in some limited area. We can speed up the render by simply inserting a near distance
and a maximum path length
, so that the integral is

Numerical evaluation begins by dividing up the range
into segments of length
, which we refer to as the ray march step size. We choose this step size to be small enough that we can assume the color and density fields to be constant within the segment. (The fact that this is not true at borders and other areas of high gradient is where aliasing comes in.) Using these segments, we can define a set of ray march positions
with the iterative definition

and the initial condition
. The full integral now reduces to integrating over a collection of segments

The exponential term inside the integral can also be split up into intervals, but the very last interval is being integrated over. So

If we now apply the assumption that the density is constant inside a each interval, then this reduces to

Inserting this back into the integrals over the intervals, and defining

we get the light as

The remaining integral evaluates exactly to

So the whole integral evaluation is

Since we are creating the material color field
to suit visual interest, we can choose to absorb the constant
into the color and simplify more to

After a small bit of massaging and brain exercising, you will see that this is similar to the algorithm presented last time. This is a full ray marching algorithm for light emitted directly from the object, without external light sources.
A fun aside is the fact that simple OpenGL "distance" fog that was/is used to hide small draw distance is basically this exact same process, but assuming that density is constant between to planes parallel to the image plane. In that case, the integral has a closed-form solution!
Adding Lights to the Equation... (All puns intended)
Adding the ability for lights (and shadowing) is not difficult if we derive the algorithm with a variable light field as well.
When there are external lights (i.e. light sources other than the material - the light(s) may actually be located within the volume), then there is a scattering term to account for light travelling from the light source into the volume, scattering and then making its way to the camera. Just a single scatter is considered. For this situation we want to allow that the volume may have two color fields, one for emission of light and one for scattering of light. So the field we have been calling
is now the emissive color, and we now label it
. The color field for scattering we will label 
So now the rendering equation is

The new field
accounts for the light arriving at the point
from all of the external light sources, and scattered into the direction of the camera. This illumination is a color quantity, and interacts with the material color by multiplying it, component by component. That is the meaning of the
symbol.
The illumination function is the sum of the light colors, weighted by the transmissivity of the volume between the point of interest and the position of the light.

where
is the color of light ''i'', and
is the transmissivity of the volume between the current position
and the position of
of light ''i''. The quantity
is the '''scattering cross section''' which determines what fraction of the light scatters in the direction of the camera. The scattering cross section depends on the relative angle between the ray march direction and the direction of the light from the current position, i.e. on the inner product between
and the direction vector pointing to the light:
.
While the phase function is an important physical quantity, for out purposes it is convenient to ignore it as a constant that we can omit from the problem.
The transmissivity function is

with a ray march path

The conversion of the integral to a ray march proceeds exactly the same as for the emission only case, and produces effectively the same ray march algorithm, with only a change to the material color in the accumulation. With this, we have the exact same algorithm as presented last time. This accounts for shadowing as well. One big question now is how to speed up the light attenuation...
Deep Shadow Maps
One neat trick we can employ entails creating what is called a Deep Shadow Map, or DSM for short. The DSM is basically a grid that will store a precomputed light attenuation so that when we need it for rendering, we can simply sample the grid. This has a number of advantages. If the density is static, but the camera is moving, then we can simply compute the DSM once and re-use that lighting information for each frame. We can also use it to separate the resolution of the lighting from the underlying density. If we want fast lower-quality lighting, then we can compute a lower resolution DSM. If you want colored lighting, then the DSM can also store colored attenuation. That extension should be pretty simple...
To actually compute the DSM, we simply need to create a grid by visiting each voxel and storing the value we would have computed for lighting directly. Then afterwards, all we need to do at render-time is sample from this grid instead of performing ray marching. A really slick implementation of this will actually compute a DSM voxel's value only if it hasn't been computed yet and then reuse the computed results, but we'll stick to the simpler approach for this. Additionally, if your light rays are parallel to a face of the DSM grid, then you can vastly speed up this computation by simply using neighboring cells during integration. Perhaps this is useful if you're simulating a light which is very far away. (Sun rays going through fog near a hill for example.)
I invite you to take a look at the tutorial code again for this lesson as it should clear up most questions about all of this.
Volume Shadowing on Polygons
It's convenient now to notice that we also have a method for computing the volume shadowing effects onto a polygon. If we imagine a polygonal surface, a density above, and then a light source above that, we would imagine that the density would cast a shadow onto the polygon. This can be easily computed now. The light that reaches the polygonal surface is simply read from the DSM just as it would be for the volume rendering! This is how the lighting for the polygonal plane beneath the smoke was computed in the following video.
All for Now
I know there was a lot of actual new concepts in this lesson, but I had a lot of people asking for the theory behind this. Hopefully the derivation was clear and informative. If there are specific topics that anyone would like for me to discuss, I'm all ears. I'm going to be uploading a simple OpenCL volume tracer/fluid simulation system to GitHub soon. I'll probably give a talk on the details of GPU volume marching then. (And then maybe a tutorial on fluid simulation some time? Anybody interested?)
Here's a video of the output from the sample code for this lesson:
Again, the code is available on GitHub. Enjoy!




