Microsoft DirectX 9.0

Volume Fog Sample


Description

The Volume Fog sample shows the per-pixel density volumetric rendering technique. The fog volume is modeled as a polygonal mesh, and the density of the fog at every pixel is computed by subtracting the front side of the fog volume from the back side. The fog is mixed with the scene by accumulating an in/out test at every pixel, that is, back-facing fog polygons will add, while front-facing ones will subtract. If the value is nonzero, the scene intersects the fog and the scene's depth value is used. To get better results, this sample uses 12 bits of precision by encoding high and low bits in different color channels.

Path

Source: (SDK root)\Samples\C++\Direct3D\VolumeFog

Executable: (SDK root)\Samples\C++\Direct3D\Bin

User's Guide

The following table lists the keys that are implemented. You can use menu commands for the same controls.

KeyAction
JMove object backward on the z-axis.
MMove object forward on the z-axis.
HMove object forward on the x-axis.
KMove object backward on the x-axis.
NMove object forward on the y-axis.
YMove object backward on the y-axis.

Camera controls:

KeyAction
LEFTSlide left
RIGHTSlide right
DOWNSlide down
UPSlide up
WMove forward
SMove backward
NUMPAD8Pitch down
NUMPAD2Pitch up
NUMPAD4Turn right
NUMPAD6Turn left
NUMPAD9Roll clockwise (CW)
NUMPAD7Roll counterclockwise (CCW)

The mouse is used in this sample to control the fog volume.

Programming Notes

Introduction

The article "Volumetric Rendering in Real-Time," printed in the 2001 Game Developers Conference Proceedings, covered the basis of volumetric depth rendering, but at the time of the writing, no pixel-shader-compliant hardware was available. This supplement describes a process designed to achieve two goals: to get more precision out of an 8-bit part, and to allow the creation of concave fog volumes.

Handling Concavity

Computing the distance of fog for the convex case was relatively simple. Recall that the front side of the fog volume was subtracted away from the back side (where the depth is measured in number of units from the camera). Unfortunately, this does not work with concave fog volumes because at any given pixel, it may have two back sides and two front sides. The solution is intuitive and has sound mathematical backing: Sum all of the front sides and subtract them from the summed back sides.

Computing concavity is as simple as adding the multiple front sides and subtracting them from the multiple back sides. Eight bits are not enough for this. Every bit added would allow another summation and subtraction, and allow for more complex fog scenes.

There is an important assumption being made about the fog volume. Is must be a continuous, orientable hull. That is, it cannot have any holes in it. Every ray cast through the volume must enter through hull the same number of times it exits.

Getting Higher Precision

Although most 3-D hardware handle 32 bits, it is really four 8-bit channels. The way most hardware works today, there is only one place where the fog depths could be summed up: the alpha blender. The alpha blender is typically used to blend on alpha textures by configuring the source destination to multiply against the source alpha, and the destination to multiply against the inverse alpha. However, they can also be used to add (or subtract) the source and destination color channels. Unfortunately, there is no way to perform a carry operation here: If one channel would exceed 255 for a color value, it simply saturates to 255. To perform higher bit precision additions on the alpha blending unit, the incoming data has to be formatted in a way that is compatible with the way the alpha blender adds. To do this, the color channels can hold different bits of the actual result, and most importantly, be allowed some overlap in their bits.

This sample uses the following scheme: The red channel will contain the upper eight bits and the blue channel will contain the lower four bits, plus three carry spots. The upper bit should not be used for reasons that are discussed later. So the actual value encoded is: Red times 16 plus Blue. Now, the alpha blender will add multiple values in this format correctly up to eight times before there is any possibility of a carry bit not propagating. This limits the fog hulls to ones that do not have concavity, where looking in any direction a ray might pass in and out of the volume more than eight times. Encoding the bits that will be added cannot be done with a pixel shader. There are two primary limitations. First, the color interpolators are 8 bit as well. Since the depth is computed on a per vertex level, this will not let higher bit values into the independent color channels. Even if the color channel had a higher precision, the pixel shader has no instruction to capture the lower bits of a higher bit value.

The alternative is to use a texture to hold the encoded depths. The advantage of this is twofold. First, texture interpolaters have much higher precision than color interpolaters. And second, no pixel shader is needed for the initial step of summing the front and back sides of the fog volume. It should be possible, on parts with at least 12-bit precision in the pixel shader, to embed the precision in a texture register instead. Unfortunately, most hardware limits the dimensions of textures. For example, 4096 is a typical limitation. This amounts to 12 bits of precision to be encoded in the texture. Twelve bits, however, is vastly superior to 8 bits and can make all the difference in making fog volumes practical. More precision could be obtained by making the texture a sliding window and breaking the object into sizable chunks that would index into that depth, but this sample does not go that far.

Setting it all up

Three important details remain: The actual summing of the fog sides, compensating for objects inside the fog, and the final subtraction. The summing is done in three steps.

First, the scene needs to be rendered to set a z-buffer. This will prevent fog pixels from being drawn that are behind some totally occluding objects. In a real application, this z could be shared from the pass that draws the geometry. The z-buffer is then write disabled so that fog writes will not update it.

After this, the summing is exactly as expected. The application simply draws all the forward facing polygons in one buffer, adding up their results, and then draws all the backward facing polygons in another buffer. There is one potential problem, however. In order to sum the depths of the fog volume, the alpha blend constants need to be set to 1 for the destination and 1 for the source, thereby adding the incoming pixel with the 1 already in the buffer.

Unfortunately, this does not take into account objects inside the fog that are acting as a surrogate fog cover. In this case, the scene itself must be added to the scene since the far end of the fog would have been rejected by the z-test.

At first, this looks like an easy solution. In the article, the buffers were set up so that they were initialized to the depth value of the scene. This way, fog depth values would replace any depth value in the scene if they were in front of it (that is, the z-test succeeds). But, if no fog was present, the scene would act as the fog cover.

This cannot be done for general concavity, however. While technically correct in the convex case, in the concave case there may be pixels at which the fog volumes are rendered multiple times on the front side and multiple sides on the back side. For these pixels, if there was part of an object in between fog layers than the front buffer would be the sum of n front sides, and the back side would be sum of n-1 back sides. But because the fog cover was replaced by the fog, there are now more entry points then exit points. The result is painfully obvious: parts of the scene suddenly loose all fog when they should have some.

The solution requires knowing which scenarios where the w-depth off the scene should be added and which scenarios the w-depth should be ignored. Fortunately, this is not difficult to find. The only situation where the w-depth should be added to the total fog depth are those pixels where the object is in between the front side of a fog volume and its corresponding back side.

The above question can be thought of as asking the question: did the ray ever leave the fog volume? Since the fog hulls are required to be continuous, then if the answer is no then part of the scene must have blocked the ray. This test can be performed by a standard inside/outside test.

To perform an inside/outside test each time a fog pixel is rendered, the alpha value is incremented. If the alpha values of the far fog distances is subtracted to the corresponding point on the near fog distance, then values greater then 1 indicate the ray stopped inside the volume. Values of 0 indicate that the ray left the fog volume.

To set up this test, the alpha buffer of the near and far w-depth buffers must be cleared to 0. Each time a fog pixel is rendered, the alpha will be incremented by the hex value 0x10. This value was used because the pixel shader must perform a 1 or 0 logical operation. A small positive value must be mapped to 1.0 in the pixel shader, a step that requires multiple shifts. Due to instruction count restraints, the initial value has to be at least 0x10 for the shifts to saturate a nonzero value to 1. The rest is straightforward: All the front sides and all the back sides are summed up in their independent buffers. The scene is also drawn in its own buffer. Then all three buffers are ran through the final pass where the w-depth of the scene is added on only if the differences of the alpha values is not 0.

This requires a lengthy pixel shader. Take care to avoid potential precision pitfalls. The following pixel shader performs the required math, although it requires every instruction slot of the pixel shader and nearly every register. Unfortunately, with no carry bit, there is no way to achieve a full 8-bit value at the end of the computation, so it must settle for 8.

ps.1.1
def c1, 1.0f,0.0f,0.0f,0.0f
def c4, 0.0f,0.0f,1.0f,0.0f

tex t0		// Near buffer B
tex t1		// Far buffer A
tex t2      // Scene buffer C

// input:
// b    = low bits (a)  (4 bits) 
// r   = high bits (b) (8 bits)
// intermediate output: 
// r1.b  = (a1 - a2) (can't be greater than 7 bits set)
// r1.r   = (b1 - b2)

sub r1.rgb,t1,t0
+sub_4x r1.a,t0,t1      // If this value is nonzero,  
mov_4x r0.a,r1.a        //   there were not as many backs as 
mad r1.rgb,r0.a,t2,r1   //   front and must add in the scene
dp3 t0.rgba,r1,c4       // Move red component into alpha

// Need to shift r1.rgb 6 bits.  This could saturate
//   to 255 if any other bits are set, but that is fine
//   because in this case, the end result of the subtract 
//   would have to be saturated (you can't 
//   subtract more than 127).
mov_x4 r1.rgb,r1
dp3_x4 t1.rgba,r1,c1   // Move into the alpha
add_x2  r0.a,t0.a,t1.a // The subtract was in 0-127
mov_d2   r0.a,r0.a     // Chop off last bit else banding
+mov r0.rgb,c3         // Load the fog color

This pixel shader gives an alpha value that represents the density of fog and loads the fog-color constant into the color channels. The alpha blending stage can now be used to blend on the fog.

Finally, there is one situation that can cause serious problems: clipping. If a part of the fog volume is clipped away by the camera because the camera is partially in the fog, then part of the scene might be in the fog. Previously, it was assumed the camera was either entirely in, or all the way out of the fog. This might not always be the case.

An alternative solution is to not allow polygons to get clipped. The vertex shader can detect vertices that would get clipped away and snap them to the near clip plane. The following vertex shader clips w-depths to the near clip plane, and z-depths to zero.

// Transform position into projection space
m4x4 r0,v0,c8
max r0.z,c40.z,r0.z     // Clamp to 0
max r0.w,c12.x,r0.w     // Clamp to near clip plane
mov oPos,r0

// Subtract the near clipping plane
add r0.w,r0.w,-c12.x 

// Scale to give us the far clipping plane
mul r0.w,r0.w,c12.y

// Load depth into texture; don't care about y
mov oT0.xy,r0.w

This sample uses the sample framework code that is shared with other samples in the Microsoft® DirectX® software development kit (SDK). You can find the sample framework headers and source code in (SDK root)\DXSDK\Samples\C++\Common\Include and (SDK root)\DXSDK\Samples\C++\Common\Src.



© 2002 Microsoft Corporation. All rights reserved.