Chapter 5. Custom Volumetric Shading

The built-in shaders, described earlier in section “Using the Built-in Shaders” in Chapter 4, implement the more common volumetric shading models. These built-in shaders are cross-platform and are optimized for SGI visualization systems. To implement less common shaders and to support more graphics platforms, OpenGL Volumizer allows you to write custom shaders.

All lighting and shading has to be implemented in a fragment processing pipeline. The built-in shaders all use multiple passes with OpenGL's fixed-function pipeline. In your custom shaders, you can also use this multipass, fixed-function pipeline approach, or you can use per-fragment computations for programmable GPUs.

This chapter describes how you can write your own custom shaders. The supported custom shaders can be characterized as follows:

In general, OpenGL Volumizer provides the following functions:

The following sections describe what you must provide to implement custom shaders.

Multipass, Callback-Based Shaders

Using multiple passes with different OpenGL states set before the various passes, the built-in shaders render the polygonized geometry. Note that a built-in shader, not the application, handles the multiple passes and OpenGL states. The polygonized geometry is essentially the set of triangles corresponding to each slice of the volume data. Rendering each slice using different OpenGL states allows you to create shading effects that are otherwise difficult to generate using the OpenGL hardware.

TMRenderAction provides a built-in shader class that allows you to write your own custom, multipass shaders. The vzTMShader class provides such an interface to apply arbitrary shading to shapes being rendered using the render action. The class provides a set of callbacks that are invoked by the shader while rendering the polygonized geometry. These callbacks can be used to set the appropriate OpenGL state for the rendered geometry. There are three types of callbacks used by vzTMShader:

  • Shape callbacks

  • Slice callbacks

  • Bind-Parameter callbacks

The section describes these callbacks and provides guidelines for writing your custom shaders.

Shape and Slice Callbacks

The shape callbacks are invoked before and after the current shape is rendered. The shader typically uses the pre-render shape callbacks to set the OpenGL state, which does not change while rendering the shape. This might include enabling 3D textures, binding lookup tables, and the like. The pre-render shape callback can also be used to do error checking. For example, the shader can verify the list of required parameters in the current appearance (described later in this section). The post-render shape callback can be used to restore the OpenGL state modified by the shader.

The slice callbacks are invoked before each slice of the polygonized geometry is rendered. These callbacks are used to set the OpenGL state, which needs to be modified for every slice. The OpenGL state includes the currently bound texture, enabling and disabling blending, modifying the color mask, and the like.

The shape and slice callbacks can be set using the following methods (see man pages for vzTMShader for more details):

void setShapeCallbacks(vzTMShaderCB preCB, vzTMShaderCB postCB, void* userData);
void setSliceCallbacks (int numCB, vzTMShaderCB* callbacks, void* userData);

The vzTMShaderCB callback is defined as the following:

void (*vzTMShaderCB)(vzTMShaderData *shaderData);

Bind-Parameter Callbacks

The shader can request TMRenderAction to “bind” the shader parameters using the vzTMBindParameterCB callback function, defined as the following:

bool (*vzTMBindParameterCB)(const char *name, vzTMShaderData *shaderData, vzTMShaderOp operation = VZ_TM_BIND);

The bind-parameter callbacks accept the following three arguments: the name of the parameter to be bound, shader data, and the shader operation to be applied on the given parameter. If the shader operation is VZ_TM_ENABLE or VZ_TM_DISABLE, the corresponding OpenGL state is simply enabled or disabled, respectively. The default shader operation is VZ_TM_BIND, which binds the given parameter.

The bind-parameter callbacks can be invoked from the shape or slice callbacks to bind the appropriate parameters. The bind-parameter callbacks return a true value if the call succeeds. Otherwise, it returns a false value. The vzTMShaderData argument passed to the callbacks contains shading information. The vzTMShaderData argument has the following data members:

vzTMBindParameterCB bindVolumeTextureCB; 
vzTMBindParameterCB bindLookupTableCB; 
vzAppearance* appearance; 
void* userData; 

TMRenderAction primarily manages two OpenGL resources for the application. These resources are the shader parameters corresponding to the texture data (usually vzParameterVolumeTexture) and the transfer functions (vzParameterLookupTable). These parameters can be bound using the bindVolumeTextureCB and bindLookupTableCB callbacks. The appearance structure can be used to access the other parameters required by the shader.

While the two bind-parameter callbacks can be used to bind the appropriate parameters, the current appearance can be used to access the other parameters required by this shader. The user-data pointer can be used to store arbitrary user data that the shader might need. The shader-data pointer needs to be passed back to the render action as an argument to the bind-parameter callbacks.

The following code shows an example of a shader that blends two volume textures together. This is done using two slice callbacks that bind the different volume textures. The following are pre-render and post-render shape callbacks that enable and disable 3D texturing, respectively:

void preShape(vzTMShaderData *shaderData) {
shaderData->bindVolumeTextureCB("volume", shaderData, VZ_TM_ENABLE);
}
void postShape(vzTMShaderData *shaderData) {
shaderData->bindVolumeTextureCB("volume", shaderData,VZ_TM_DISABLE);
}

The following are the two slice callbacks that bind the different volume textures:

void pass1(vzTMShaderData *shaderData) {
    if(!shaderData->bindVolumeTextureCB(“volume”, shaderData))
       vzError::error(VZ_OPERATION_FAILED,”Could not bind volume                  texture `volume'”);
}
void pass2(vzTMShaderData *shaderData) {
    if(!shaderData->bindVolumeTextureCB(“volume2”, shaderData))
       vzError::error(VZ_OPERATION_FAILED,”Could not bind volume        texture `volume2'”);
}

Guidelines for Creating Your Shaders

Use the following guidelines when creating your shaders:

  • Try to minimize the number of state settings in the callbacks. For example, do not bind the same parameter repeatedly in the slice callbacks if this is not necessary.

  • All the shaders need to have a parameter of type vzParameterVolumeTexture and name volume. TMRenderAction uses this parameter to compute the number of slices from the geometry ROI and sampling rate.

  • Each appearance can have any number of volume texture parameters but only one lookup table parameter.

  • The data ROI and geometry ROI of all the volume textures in the appearance should be the same. Different data ROI and geometry ROI would work correctly if the shape is not bricked internally.

Hardware-Specific, GPU-Based Shaders

Rendering volume datasets differs from polygonal rendering. The traditional OpenGL lighting and shading models for polygonal rendering do not apply to rendering volume datasets. In the context of OpenGL Volumizer, you can think of shaders as a programmable interface to the GPU. All lighting and shading has to be implemented in the fragment processing pipeline using a fragment program (per-fragment computations).

GPU-based shaders have the following features and constraints:

  • Placement of the run-time shading description in a text file

  • Use of application parameters

  • Application management of the OpenGL state

  • Platform support of the OpenGL ARB_fragment_program extension or the following OpenGL Shading Language (GLSL) extensions:

    • ARB_shader_objects

    • ARB_shading_language_100

    • ARB_fragment_shader

    • ARB_vertex_shader

    To check for the presence of this extension on Linux systems, enter the following:

    $ glxinfo | grep -i extension
    

OpenGL Volumizer provides the following classes to interface with GPUs:

  • vzTMRenderAction

  • vzTMFragmentShader

  • vzTMFragmentProgram

Chapter 4, “Texture Mapping Render Action” describes the vzTMRenderAction class. This section describes the other two classes.

The vzTMFragmentShader Class

The vzTMRenderAction and vzTMFragmentShader classes provide the necessary interface in OpenGL Volumizer for GLSL. To describe the per-fragment computations to be done by a fragment shader, you can specify the fragment shader using the the OpenGL ARB_fragment_shader extensions.

The vzTMFragmentShader class provides a static  load() method for loading fragment shaders written in the language specified by the ARB_shading_language_100 extension.

For the complete specification of the ARB_fragment_shader extension, see the following file:

http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_shader.txt

The following code creates a fragment shader by loading it from file fileName:

// Load the fragment shader file `fileName'.
vzTMFragmentShader *shader = vzTMFragmentShader::load(fileName);

In addition to specifying the fragment shader, the application also needs to provide parameters to be used by the fragment shader. The application needs to set the parameters as part of the current appearance.

The vzTMRenderAction class then manages the parameters that are to be used in the fragment shader transparently. For example, if the application has to set volume texture and light parameters for use in fragment shader, then it can be done from the application as shown in the following:

vzAppearance *appearance = new vzAppearance(shader); shader->unref();

// The shader is the vzTMFragmentShader object which was returned 
// after loading the shader from the fragment shader filename
// you specify.

appearance->setParameter(“volume”, texture);
texture->unref();

// 'volume' is the texture which is to be used.

float lightdir[3] = {1,0,0};
vzParameterVec3f *light = new vzParameterVec3f(lightdir);
appearance->setParameter(“lightdir”,light);
light->unref();

// `lightdir' is light direction parameter which is to be used 
// in the fragment shader program.

Unlike the vzTMFragmentProgram class, the vzTMFragmentShader class does not require you to set callbacks for binding parameters.

Fragment Shader Examples

Example 5-1 is a simple fragment shader example.

Example 5-1. A Simple Fragment Shader

// This shader performs mult operation of two volumes.
void main(void){
vec4 color,color1;
// sampling color from `volume' texture
color = texture3D(volume,volume_TexCoord.stp);
// sampling color from `volume2' texture
color1 = texture3D(volume2,volume2_TexCoord.stp);
// Setting the final color of a fragment
gl_FragColor = color * color1;
}

As you can see, volume_TexCoord is being used to refer the texture coordinates for the texture coordinates for the volume texture instead of the corresponding gl_TexCoord[] .

This is because, in the case of vzTMFragmentShader class, the texture is internally bound to the individual texture units. Hence, for use in fragment shaders, the example has the variable volume_TexCoord of type vec4f for acessing the texture coordinates for the corresponding volume texture.

You do not need to know the texture unit on which volume is bound. If there are more than one volume texture, then param_TexCoord can be used to access the texture coordinates, where param is the name of the bound volume texture.

Similarly, in order to access the texture dimesions in your shader program, use param_TexDims, which is of type vec3f, and param, the name of the bound parameter. For example, if param is volume, then then texture dimensions can be accessed through volume_TexDims.

Example 5-2 is an example of a complex fragment shader. It computes gradient using backward differencing and uses it to compute final fragment color.

Example 5-2. A Complex Fragment Shader

void main (void)
{
// Temporary variables
// (x+1, y+1, z+1) samples

vec3 gradient;
vec3 gradient_back;

// (x, y, z) sample

vec4 value;

// Current sample position

vec3 coord = vec3(volume_TexCoord);

// Size of each voxel along each axes

vec3 size = vec3(1.0/volume_TexDims[0],
1.0/volume_TexDims[1],
1.0/volume_TexDims[2]);

// Sample “volume” at (x, y, z)

value = texture3D(volume, coord);

// Compute gradient using forward differencing with
// values at (x+1), (y+1), (z+1) positions

gradient.r = texture3D(volume, (coord + vec3(size[0], 0, 0))).r;
gradient.g = texture3D(volume, (coord + vec3(0, size[1], 0))).r;
gradient.b = texture3D(volume, (coord + vec3(0, 0, size[2]))).r;

// Compute gradient using backward differencing with
// values at (x+1), (y+1), (z+1) positions

gradient_back.r = texture3D(volume, (coord - vec3(size[0], 0, 0))).r;
gradient_back.g = texture3D(volume, (coord - vec3(0, size[1], 0))).r;
gradient_back.b = texture3D(volume, (coord - vec3(0, 0, size[2]))).r;
gradient = (gradient - gradient_back)/2.0;
gradient = normalize(gradient);

// Use alpha value from “lookup_table”

gl_FragColor.a = texture1D(lookup_table, value.a).a;

// Use Color value from gradient

gl_FragColor.rgb = gradient.rgb;

}


Note: The variables volume_TexCoord and volume_TexDims are internal OpenGL Volumizer variables.


You can use the XML file format to load and initialize appearances with fragment shaders. For more information, see section “Loading Fragment Shaders” in Chapter 8.

The vzTMFragmentProgram Class

The vzTMRenderAction and the vzTMFragmentProgram classes provide the OpenGL Volumizer interface to the ARB_fragment_program extension. The OpenGL Volumizer programming model uses the following three callbacks to structure the shading:

Callback 

Description

Pre-shader callback 

Sets the OpenGL state.Binds non-texture parameters.

Texture unit callback 

Binds the texture parameters for a texture unit.

Post-shader callback 

Restores the OpenGL state.

The following code illustrates the structure:

void preShader(){ 
   // bind `diffuse' parameter 
   // set other OpenGL state 
} 
void unit1() { 
   // bind `lookup_table' parameter 
} 
void unit2() { 
   // bind `volume' parameter, e.g. fMRI 
} 
void unit3() { 
   // bind `volume2' parameter, e.g. CT 
} 
void postShader() { 
   // restore OpenGL state 
}

To provide the per-fragment computations, you use a fragment program using the OpenGL ARB_fragment_program extension.

The TMFragmentProgram provides a static load() method for loading fragment programs written in the language specified by the ARB_fragment_program extension. For the complete specification of the ARB_fragment_program extension, see http://oss.sgi.com/projects/ogl-sample/registry/ARB/fragment_program.txt. The following code creates a TMFragmentProgram by loading it from file fileName:

// Load the fragment program file `fileName'
vzTMFragmentProgram *fp = vzTMFragmentProgram::load(fileName);

In addition to specifying the fragment program, the application also needs to provide the shader parameters that are used by the fragment program. These parameters can be accessed inside the fragment program either using textures or local program parameters. The TMRenderAction manages the textures used in the program transparently, but the application needs to bind them to the appropriate texture units using the multitexture callbacks. The application can also set other OpenGL state or shader parameters required by the fragment program in the pre-shape callback. See Example 5-3 for a sample program.

You can also use the XML file format to load and initialize appearances with fragment programs. For more information, see section “Loading Fragment Programs” in Chapter 8.

Fragment Program Examples

Example 5-3 shows a simple fragment program that takes the two textures volume and volume2 as input and then computes and renders their per-voxel product.

Example 5-3. A Simple Fragment Program

!!ARBfp1.0 
# Temporary variables 
TEMP volume; 
TEMP volume2; 
# Sample textures `volume' and `volume2' 
TEX volume1, fragment.texcoord[0], texture[0], 3D; 
TEX volume2, fragment.texcoord[1], texture[1], 3D; 
# Multiply the results of `volume' and `volume2' 
MUL result.color, volume2, volume; 
END

Notice that the volume texture is the 3D texture bound on texture unit 0 while the volume2 texture is bound on unit 1. This can be initialized using the following multitexture callbacks:

// Allocate memory for the multitexture callbacks
vzTMShaderCB *multiTexCB = new vzTMShaderCB[2];

// Initialize the multitexture callbacks for the two texture units
multiTexCB[0] = texUnit0;
multiTexCB[1] = texUnit1;

// Set the callbacks for the shader.
fp->setMultiTextureCallbacks(2, multiTexCB, fp);

The multitexture callbacks used in the preceding code sample are very similar to those used by the vzTMShader class and might look like the following:

void texUnit0(vzTMShaderData *data) {

    // enable texture `volume'
    data->bindVolumeTextureCB(“volume”, data, VZ_TM_ENABLE);

    // bind texture `volume1'
    data->bindVolumeTextureCB(“volume”, data));
}

void texUnit1(vzTMShaderData *data) {
    // enable texture `volume2'
    data->bindVolumeTextureCB(“volume2”, data, VZ_TM_ENABLE);

    // bind texture `volume2'
    data->bindVolumeTextureCB(“volume2”, data));
}


Note: Modern graphics systems do not support texture lookup tables (TLUTs) in the hardware. However, LUTs can be implemented using dependent textures, where the sampled texel values of the volume texture can be used as the texture coordinates for sampling the 1-dimensional, dependent texture.

Example 5-4 shows how to implement LUTs using a dependent texture.

Example 5-4. Implementing LUTs Using Dependent Textures

!!ARBfp1.0
# Temporary variable for storing sample volume texel values
TEMP volume;
# Sample 3D “volume” texture bound on texture unit 1
TEX volume, fragment.texcoord[1], texture[1], 3D;
# Sample 1D “lookup_table” texture bound on texture unit 0
# Use values from above instruction as texture coordinates
TEX result.color, volume, texture[0], 1D;
END

Example 5-5 shows a more complex fragment program that uses a conditional to apply a per-voxel threshold to selectively remove parts of the rendered volume.

Example 5-5. A Complex Fragment Program

!!ARBfp1.0 
# threshold for tag shader 
PARAM thresh = {0.5, 0.5, 0.5, 0.5}; 
# temporary variable for `tag' texture 
TEMP tag; 
# sample the `tag' texture 
TEX tag, fragment.texcoord[1], texture[1], 3D; 
# subtract threshold value from the `tag' value 
SUB tag, tag, thresh; 
# if tag value is smaller than threshold, kill fragment 
KIL tag; 
# sample `volume' and output resulting color 
TEX result.color, fragment.texcoord[0], texture[0], 3D; 
END

Note that the blending is controlled by the OpenGL state, which is set by the application before the shader is called.

A set of GLSL shaders and fragment programs are installed under VZROOT/src/shaders/. As an exercise, you can modify them to generate different visual effects.