This chapter presents a programming template for Volumizer applications. Rigorously following this template is not required when creating your own Volumizer application. The template does, however, provide a programming framework in which to understand the Volumizer API.
This chapter explains the most commonly-used elements of the Volumizer API in the context of creating an application. Chapter 2, “Basic Concepts in Volumizer,” presented the concepts behind the API described in this chapter.
The structure of this chapter is parallel to Chapter 4, “Sample OpenGL Volumizer Application,” in which a full code example is explained. By understanding the API calls presented in this chapter, you can better understand the complete code example in the next chapter.
Developers working with the Volumizer API can be divided into two groups:
Those who want to use the supplied Volumizer application with their own image database.
Those who want to create their own Volumizer application.
Developers in the first group need only to learn about file input and output. OpenGL Volumizer provides several loaders in sample applications. If none of these loaders work, then you need to create your own loader. For help doing that, refer to “Creating Custom Loaders”.
![]() | Note: The examples are provided as illustrative and often do not provide full functionality. |
The other parts of this chapter present topics relevant to the second group of developers. The basic steps you take to create your own OpenGL Volumizer application are discussed in the following sections:
Selecting Optimal Brick Parameters
Defining Appearance (voBrickSetCollection)
Allocating Storage for Voxel Data
Reading Voxel Data from Disk
Working with the Brick Data
Optimizing Appearances
Defining Geometry (voIndexedTetraSet)
Allocating Storage for Transient Polygons
Polygonizing a Volume
State Sorting
Loading Voxel Data into Texture Cache
Drawing Polygonized Volume
“Step 5: Using Lookup Tables” (optional)
This section provides a conceptual overview of the programming template. The sections following this one describe in much more detail how to accomplish each of the programming tasks discussed in this section.
For more information about any of the concepts in this section, see Chapter 2, “Basic Concepts in Volumizer.”
When creating a Volumizer application, you perform the following tasks:
Define a volumetric shape:
Define appearance.
Define geometry.
Render the shape:
Sample the shape along a set of surfaces, often planes parallel to the viewport.
In this process, called polygonization, the surfaces are clipped to the boundaries of the volume's geometry; as a result, the clipped planes become polygons.
The polygons are then clipped to brick boundaries to take advantage of hardware acceleration. A brick is the amount of texture that can be cached into a machine's texture memory.
Depth sort the set of polygons from back to front.
Map textures onto the polygons from the voxel information and composite the polygons together in the frame buffer to produce the final image.
The polygonization process converts the volume shape into a set of semi-transparent textured polygons that can be handed back to applications. These polygons are in no way special (other than being semi-transparent) and can subsequently be mixed with other polygons in a scene.
The following sections explain how these concepts are implemented by the Volumizer API.
The first step in creating a Volumizer application is to include all necessary OpenGL, Motif (or other Graphical User Interface API), and C++ header files and to declare global variables. Most applications need to include the following header files to assure that all Volumizer-specific declarations are present:
#include <vo/GeometryActions.h> #include <vo/AppearanceActions.h> |
These header files include a number of other Volumizer headers. The other header files are listed in “glwSimpleVolume.cxx”.
To define appearance, use the following procedure:
Different hardware platforms provide varying functionality and feature sets making it difficult for application developers to write portable, efficient, and maintainable code. To insulate the API proper from machine dependencies, several helper routines are provided that localize such dependencies, such as voAppearanceActions::getBestParameters(). Its purpose is to suggest optimal values for several machine dependent variables:
interpolationType can have the following values:
enum voInterpolationType { DEFAULT, _2D, _3D }; |
_2D and _3D select two-dimensional and three-dimensional textures as internal voxel representation, respectively. Setting this parameter to DEFAULT selects _3D on machines that support hardware three-dimensional texture mapping, _2D otherwise.
renderingMode can have the following values:
enum voRenderingMode{ DEFAULT, MONOCHROME, COLOR }; |
The value depends on the requested rendering mode: gray scale (MONOCHROME) or RGBA.
partialBrickType determines how to handle volumes that do not divide evenly into bricks whose dimensions are powers of two. The values of the enumerant are of type voPartialBrickTypeScope and can be one of the following values:
enum voPartialBrickType { DEFAULT, TRUNCATE, AUGMENT, AUGMENT_TIGHT }; |
DEFAULT | Same as TRUNCATE. | |
TRUNCATE | Truncate each brick dimension to the largest power of two smaller than the volume. For example, a 323 brick size would be recommended for a 603 volume. | |
AUGMENT | Pad the original volume with empty space to the nearest power of two. For example, a 643 brick size would be suggested for a 423 volume. | |
AUGMENT_TIGHT |
|
dataType describes the data type of the voxel data. The values of the enumerant are of type voDataTypeScope and can be one of the following values:
enum voDataType { DEFAULT, UNSIGNED_BYTE, BYTE, UNSIGNED_BYTE_3_3_2_EXT, UNSIGNED_SHORT, SHORT, UNSIGNED_SHORT_4_4_4_4_EXT, UNSIGNED_SHORT_5_5_5_1_EXT, UNSIGNED_INT, INT, UNSIGNED_INT_8_8_8_8_EXT, UNSIGNED_INT_10_10_10_2_EXT, FLOAT }; |
diskDataFormat is of type voExternalFormatTypeScope and reflects the format of the data on disk (typically, LUMINANCE, INTENSITY or RGBA).
internalFormat the format used internally by the texture subsystem to maintain voxel data. It can be one of the following values:
enum voInternalFormatType{ DEFAULT, INTENSITY8_EXT, LUMINANCE8_EXT, LUMINANCE8_ALPHA8_EXT, RGBA8_EXT, RGB8_EXT, RGBA4_EXT, DUAL_LUMINANCE_ALPHA8_SGIS, DUAL_INTENSITY8_SGIS, QUAD_LUMINANCE8_SGIS }; |
externalFormat is the format that the application uses to pass the voxel data to the texture subsytem. The values of the enum are of type voExternalFormatTypeScope and can be one of the following values:
enum voExternalFormatType { DEFAULT, INTENSITY, LUMINANCE_ALPHA, LUMINANCE, RGBA, ABGR_EXT }; |
xBrickSize, yBrickSize, and zBrickSize are the dimensions that specify the optimal brick size. If possible, the brick size matches the volume size.
subdivideMode, the optional last parameter, specifies the brick subdivision strategy used to attain the optimal brick size. The possible values are 1, 2, and 3:
1 (the default) | The z dimension is divided by 2 until a single brick fits into texture memory. | |
2 | The largest dimension at each pass through a loop is divided by 2 until a single brick fits into texture memory. | |
3 | The x, y, and z sizes are alternately divided by 2 until a single brick fits into texture memory. |
Based on the input parameters, the getBestParameters() method determines the optimal internalFormat, externalFormat, and brick sizes for a given platform. For example, given the disk format of INTENSITY, a rendering mode of MONOCHROME and brick sizes of [256, 256, 256], and an SGI OCTANE with 4 MB of texture memory, the method returns LUMINANCE_ALPHA for externalFormat, LUMINANCE8_ALPHA8_EXT for internalFormat, and [128, 128, 64] for brick sizes. The same call, with identical input parameters returns LUMINANCE, INTENSITY8_EXT, and [256, 256, 256], respectively, on an SGI Onyx2 IR with 64 MB of texture memory.
getBestParameters() does not create anything, but merely suggests suitable parameters to use with subsequent calls. The application can overwrite the suggested values with the understanding that some changes may affect performance or the validity of subsequent operations.
For example, overwriting the internal format results in an additional format conversion every time the volume is downloaded. Or, for example, increasing the total size of a brick is likely to cause failure during the first download because of memory overrun. It is okay, however, to make bricks smaller.
Once the optimal brick parameters are selected, they can be used to construct an instance of a voBrickSetCollection given the voxel array dimensions, brick sizes, voxel formats, and interpolation type to encapsulate the appearance, as follows:
voBrickSetCollection ( int xVolumeSize, int yVolumeSize, int zVolumeSize, int xBrickSize, int yBrickSize, int zBrickSize, voPartialBrickType _fractionalBricks, int _overlap, voInternalFormatType _internalFormat, voExternalFormatType _externalFormat, voDataType _dataType, voInterpolationType _interpolationType); |
A brick set collection is a collection of brick sets. The brick set collection contains the voxel data and all information about the data types, formats, and memory layout of the brick sets in the collection.
![]() | Note: This constructor does not allocate memory for voxel data. |
For more information about voBrickSetCollection, see “Brick Set Collections”. For a discussion of advanced topics in appearance, see Chapter 7, “Volumetric Appearance.”
Each brick holds an array of voxel data. To allocate voxel memory for all of the brick sets in the brick set collection, interate over the bricks using the following nested loop:
Applications that prefer to manage their own data can set the data pointer of the brick directly to point to the memory area that contains the voxel data by calling the setDataPtr() member method. In either case, the application is responsible for memory management. In particular, the voxel memory has to be deallocated with a call to voAppearanceActions::dataFree() or some application-specific way once the application is done using it.
Applications read voxel data from the disk brick by brick. To read a brick from a disk into host memory (not texture memory, which is covered in “Read Brick Data from Disk”), you can use one of the supplied IFL loaders, for example:
The above routine reads a set of voxels that fall within a subvolume, determined by the arguments, from any file supported by the Image Format Library (IFL), for example, a three-dimensional TIFF file. Another loader provided with the API reads a brick of voxels from a stack of “raw” two-dimensional images:
int myReadBrickRaw(char **fileNames, void *data, int xBrickOrigin, int yBrickOrigin, int zBrickOrigin, int xBrickSize, int yBrickSize, int zBrickSize, int xVolumeSize, int yVolumeSize, int zVolumeSize, int headerLength, int bytesPerVoxel) |
For more information about stacks of two-dimensional images, see “Stack of Two-Dimensional Textures”.
A “raw” two-dimensional image is a voxel stream in row-major order possibly proceeded with a fixed-size header (the content of which is ignored).
OpenGL Volumizer provides several loaders. However, if you find that your data set cannot be read by any supplied Volumizer sample application, you need to make your own loader. “Creating Custom Loaders” provides code for a loader that you can modify to handle your data.
All bricks of a brick set can be read in using the following construct:
voBrickSetIterator brickSetIter(aVolume->getCurrentBrickSet()); for (voBrick * brick; brick = brickSetIter();) { int xBrickOrigin, yBrickOrigin, zBrickOrigin; int xBrickSize, yBrickSize, zBrickSize; void *vdata = brick->getDataPtr(); // get brick sizes -- they may different than those requested brick->getBrickSizes(xBrickOrigin, yBrickOrigin, zBrickOrigin, xBrickSize, yBrickSize, zBrickSize); // read the data; OK to use brick data area as a temp buffer myReadBrickIfl(dataFileName, vdata, xBrickOrigin, yBrickOrigin, zBrickOrigin, xBrickSize, yBrickSize, zBrickSize, xVolumeSize, yVolumeSize, zVolumeSize); } |
Multiple copies of the brick sets with different memory layouts can be read this way, or voAppearanceActions::volumeMakeTransposed() can be used to create the remaining copies.
Once you read the brick data from disk into host memory, you might need to:
To scale the voxel values within a brick from an application-specific dynamic range to a standard range (such as <0,255> for unsigned char, <0,65535> for unsigned short, and <0.0,1.0> for floats), you use the following voAppearanceActions method on each brick:
static int dataScaleRange( voTexture3D* aBrick, float& loValue, float& hiValue) |
loValue and hiValue are the minimum and maximum values, respectively, for the original range. For example, while working with 12-bit unsigned short data, the application should use values of 0.0 and 4095.0, respectively.
In certain situations the disk data format does not match the external format, for example, when the data format is INTENSITY on disk and the external (that is, application-side) format is LUMINANCE_ALPHA. In this situation, each voxel within the brick needs to be duplicated so the sequence I1I2I3I4... becomes I1I1I2I2I3I3I4I4.... The following voAppearanceActions method converts the brick data to the format requested in the constructor:
static int dataConvert(voTexture3D* aTexture3D, void* data, voExternalFormatType diskFormat); |
voTexture3D and voExternalFormatType are defined in “Step 2: Define Appearance” .
Converting data may require additional buffer space, such as in the case where the converted voxel data occupies twice as much memory area as the original voxels. Applications can choose to use an additional buffer to facilitate the conversion, as follows:
// select next brick voBrick *brick = ... // IO buffer unsigned char data[BIG_ENOUGH]; // allocate storage for a brick (void)voAppearanceActions::dataAlloc(brick); // read the data into buffer myReadBrickIfl(fileName, data, minX, minY, minZ, xBrickSize, yBrickSize, zBrickSize); // replicate to the desired externalFormat voAppearanceActions::dataConvert(brick,data, diskDataFormat); |
dataConvert(), however, can perform most conversions using the brick data area for intermediate storage. In this approach, voxel data is read directly into the data area of the brick, and is converted in place, for example:
// allocate storage for each brick unsigned char *data = voAppearanceActions::dataAlloc(brick); // read the data; OK to use brick data area as a temporary buffer myReadBrickIfl(fileName, data, minX, minY, minZ, xBrickSize, yBrickSize, zBrickSize); // replicate to the desired externalFormat voAppearanceActions::dataConvert(brick,data,diskDataFormat); |
Using the brick data area for intermediate storage is preferable because, in some cases, it may minimize the amount of data copying.
In some instances, the API needs to maintain multiple copies of the same data set with different memory layouts, for example, to avoid sampling artifacts while using two-dimensional texturing.The API enables you to create the copies using the following method:
static int volumeMakeTransposed(voBrickSetCollection* aVolume); |
This action is a no-op for three-dimensional volumes using tri-linear interpolation that were defined with voInterpolationTypeScope::_3D. However, you should always include this call during initialization to assure platform independence.
Voxel data can be optimized for better performance in the following ways:
Interleave bricks
Create texture objects
To enable appearance optimization, set the last argument in voAppearanceActions::volumeOptimize() to one or more of the following values logically ORd together:
For example, for full optimization, use a statement similar to the following:
voAppearanceActions::volumeOptimize(myBrickSetCollection, BEST_PERFORMANCE ); |
For more information about interleaving bricks, see “Interleaving Bricks”.
For more information about texture objects, see “Creating Texture Objects”.
To define a volume, an application needs to define both its appearance and its geometry. Volumetric geometry is defined by a set of tetrahedra. For example, a cube can be minimally represented as a set of five tetrahedra as shown inFigure 3-1.
A volume is generally tessellated into a collection of tetrahedra. Volumizer provides a voIndexedTetraSet object type to represent such collections. The vertex coordinates of the tetrahedra are indexed because the same set of coordinates can be shared by multiple tetrahedra. Rather than record the same point for multiple tetrahedra, index pointers for each of the tetrahedra point to one set of vertex coordinates, as shown in Figure 3-2.
For example, consider a solid sphere represented by 100 tetrahedra, all of which share a vertex in the center of the sphere. Instead of having 100 sets of the same coordinates, one index from each tetrahedra can point at one vertex coordinate substantially reducing memory requirements for storing vertex coordinates.
Each group of four indices associated with voIndexedTetraSet form a single tetrahedron. For example, in the following array, each row represents a tetrahedron; there are five tetrahedra in the array in total defined by 20 indices:
static int cubeIndices[] = { 0, 2, 5, 7, 3, 2, 0, 7, 1, 2, 5, 0, 2, 7, 6, 5, 5, 4, 0, 7, }; |
These indices point into the vertex data array. The array of vertex coordinates might look like the following:
float cubeVertices[] = { 0, 0, 0, xSize, 0, 0, xSize, ySize, 0, 0, ySize, 0, 0, 0, zSize, xSize, 0, zSize, xSize, ySize, zSize, 0, ySize, zSize }; |
One can create an instance of volumetric geometry describing a minimal tessellation of a cube using the following call:
voIndexedTetraSet *aTetraSet = new voIndexedTetraSet(cubeVertices, 8, 3, cubeIndices, 20); |
To display a volume, the tetrahedra that make up the volume are polygonized into a set of polygons, called faces, by slicing them with a family of sampling surfaces, for example, planar surfaces parallel to the viewport. Each face is a textured slice of the volume clipped to brick and volume boundaries. To prepare for the results of polygonization, you need to create a set of faces equal to the number of bricks times the number of faces in each brick.
When a volume is polygonized, it is sliced into faces. Each face is a textured slice of the volume clipped to brick and volume boundaries. To handle the faces, use voVertexData and voIndexedFaceSet.
voVertexData is an array of records that holds per-vertex information for a list of vertices, defined as follows:
voVertexData(int _countMax, int _valuesPerVertex, float* data=NULL) |
Each record describes a set of properties of a vertex, such as vertex coordinates, colors, normals, texture coordinates, and optional user-defined data.
_countMax is the number of vertices.
_valuesPerVertex indicates how many floats describe a vertex.
The number of properties per vertex is application dependent. The information may include vertex coordinates, colors, textures, normals, and user-defined per-vertex scalar or vector properties. All the properties are resampled and clipped during the polygonization process. Vertex coordinates (three floats) are required; optional data include colors, texture coordinates, and arbitrary user-supplied data. By convention, the order of per-vertex data is:
User
Texture coordinates
Colors
Vertex coordinates
The texture coordinates do not have semantics associated with them directly. By convention, however, the texture coordinates are specified in voxels; for example, in a 2563 volume the last vertex has coordinates [255, 255, 255].
The allocated storage is guaranteed to be contiguous in memory. Optionally, the application can pass a pre-allocated array of floating point values to be used by reference, that is, no copy of data is made. Keep in mind, however, that voVertexData's destructor deletes the data storage only if voVertexData's constructor allocated it. Therefore, if an application passes a pre-allocated array to a constructor, it is the application's responsibility to delete this storage.
For more information about voVertexData, see “voVertexData”.
Each polygon resulting from polygonization is called a face. Polygonization creates a set of parallel faces, which are generally coplanar polygons. voIndexedFaceSet specifies a collection of indexed polygons, defined as follows:
voIndexedFaceSet (int _countMaxV, int _valuesPerVertex, int _countMaxI); voIndexedFaceSet (voVertexData* _vertexData, int _countMaxI) |
_countMaxV is the number of vertices in the face set.
_valuesPerVertex indicates how many floats describe a vertex.
_countMaxI is the number of indices in the face set.
_vertexData is a pointer to a data structure that holds per-vertex information for a list of vertices.
Each polygon in the face set is described by a group of indices. The first index within a group specifies the number of the vertices in the polygon; the following indices point to individual vertex records.
For more information about voIndexedFaceSet, see “voIndexedFaceSet”.
To iterate through a voIndexedFaceSet, use voIndexedFaceSetIterator, as described in “voIndexedSetIterator”.
A volumetric shape is drawn in three phases.
The geometry representing the shape is polygonized by sampling it with a family of surfaces.
The resulting polygons are sorted by state and depth.
The polygons are composited from back-to-front (or front-to-back) into the frame buffer.
The following sections describe these steps in greater detail.
To render a volumetric shape, the set of tetrahedra representing the volume's geometry needs to be polygonized. This is accomplished by “slicing” the volume along a family of sampling surfaces. In the simplest case, the sampling surfaces form a set of planes parallel to the viewport. Each such plane is intersected with each tetrahedron resulting in a single polygon (a triangle or a a quadrangle). This polygon needs to be clipped to the brick boundaries. The number of polygons produced is equal to or less than the number of bricks. Each of the resulting polygons is at most a 10-gon (clipping an n-gon to a box produces at most an (n+6)-gon). This whole procedure is accomplished with help of polygonize().
int polygonize( voIndexedTetraSet *aTetraSet, voBrickSet *aBrickSet, GLdouble modelMatrix[16], GLdouble projMatrix[16], voSamplingMode samplingMode, voSamplingSpace samplingSpace, int &samplesNumber, float samplingPeriod[3], voIndexedFaceSet ***aIndexedFaceSet); |
polygonize() returns a set of polygons. There is one polygon set for each sampling surface (i.e., depth) and per each brick. Therefore, there are samplesNumber * bricksNumber polygons that get generated. An instance of voIndexedFaceSet stores the polygons resulting from the polygonization process:
voIndexedfaceSet *faces[brickNumber][sampleNumber]
voGeometryActions::polygonize() performs the following tasks:
Slices the tetrasets into sets of planes parallel to the viewport, as shown in Figure 3-3.
Clips each plane of the tetraset to brick boundaries.
The voFaceSets in Figure 3-3 are shown as belonging to two bricks stacked vertically. Faces are colored differently depending on which brick they belong to.
The following sections discuss each of the arguments of polygonize().
aTetraSet describes the set of tetras to be polygonized. aBrickSet provides the coordinates of the bricks for clipping.
modelMatrix and projMatrix describe the current position and orientation of the volume and the viewing parameters. They help determine the orientation of the sampling surfaces. They can be obtained from the following OpenGL state:
GLdouble modelMatrix[16]; GLdouble projMatrix[16]; glGetDoublev(GL_MODELVIEW_MATRIX, modelMatrix); glGetDoublev(GL_PROJECTION_MATRIX, projMatrix); |
samplingMode determines what type of sampling surface family to use. The following are valid values:
enum voSamplingMode{ DEFAULT, VIEWPORT_ALIGNED, AXIS_ALIGNED, SPHERICAL, }; |
DEFAULT allows the API to pick a family that is optimized for performance.
Figure 3-4 shows the graphical implication of these values.
VIEWPORT_ALIGNED selects planes parallel to the viewport. This is the optimal sampling strategy in terms of rendering quality on systems which support three-dimensional texture mapping (it requires properly sampled and filtered oblique slices through the volume). On systems that do not support three-dimensional texture mapping this mode may not be supported or may be slow.
AXIS_ALIGNED samples volumes along planes that are aligned with the primary axes (X,Y,Z) of the voxel cube. Three copies of the volume (each sliced in one of the primary directions) may be required. This type of sampling allows machines that do not support three-dimensional texture mapping to render volumes. In this mode, the axis that is most parallel to the line of sight is selected.
It is possible to request this mode on a machine that does support three-dimensional texture mapping to improve performance: the polygonize action does not have to be invoked nearly as often as for in AXIS_ALIGNED mode and the texture data is accessed in a more predictable, cache-friendly fashion. There is no quality degradation in this case, as one can still request tri-linear interpolation (providing that three-dimensional textures are perspectively corrected).
SPHERICAL (not currently supported) allows for sampling of the data set with a family of concentric spherical shells centered at the camera position. This simulates more accurately ray casting in wide-angle perspective camera, for example, during fly-throughs. This mode is not currently implemented in Volumizer. Applications may choose to implement their own if need arises.
In Figure 3-5, a volume is viewed from one of its corners. The volume is polygonized with sampling planes parallel to the viewport (left) and the primary axes (right). The volume consists of two bricks coded in green and red.
During the polygonization process samplesNumber (e.g., 256) different sampling surfaces will be used to sample the volume. By default, they are evenly distributed over the whole sampling space. samplingSpace determines what “sampling space” is and how to compute the bounds (e.g., the locations of the first and last surface). Here are the allowable values:
enum voSamplingSpace { DEFAULT, VIEW_VOLUME, OBJECT, PRIMITIVE, }; |
Figure 3-6 shows the graphical implication of these values.
VIEW_VOLUME divides the view frustum into samplesNumber evenly distributed samples. For example, in the VIEWPORT_ALIGNED sampling mode, the first plane coincides with the ZFar clipping plane, and the last with ZNear. The disadvantage of this technique is that typically some of the sampling surfaces never intersect the object being sampled. Similarly, translating the object though the frustum results in subtle sampling artifacts. On the other hand, it produces a uniform, static sampling grid which makes merging multiple volumes and overlapping transparent geometry somewhat easier.
OBJECT divides the bounding box of the volume, rather than the whole viewing frustum, into samplesNumber of evenly distributed samples. Therefore, this option adjusts to the position and orientation of the volume to assure optimal sampling rate.
samplesNumber has overloaded meaning. If samplingPeriod is NULL, samplesNumber determines how many sampling surfaces to use during the polygonization process. If samplingPeriod is not NULL, samplesNumber determines the upper bound of the number of sampling surfaces. Regardless of the value of samplingPeriod, no more than samplesNumber of sampling planes are generated. This functionality can guard against buffer overflows.
samplesNumber should be set in accordance with the Nyquist sampling limit. The higher the sampling rate, the higher the quality (to a point), but the slower the performance.
If samplingPeriod is NULL, it is ignored. Otherwise, it is used to change the number of sampling surfaces depending on the position, orientation of the surfaces, and viewing parameters.
For example, requesting samplingPeriod of (1.0, 1.0, 1.0) under voSamplingSpaceScope::OBJECT sampling results in SIZE samples when the volume is head on, and sqrt(3) × SIZE when a view of a cube SIZE on the size along the major diagonal is requested. Requesting a samplingPeriod of 0.5 assures that the Nyquist criterion is satisfied in that direction.
The number of samples computed is always clamped to the samplesNumber parameter to avoid unexpected overflows. In either case, samplesNumber is modified to reflect the actual number of slicing planes used.
Once the volume is polygonized it needs to drawn. Because volumes are generally semi-transparent, all processing must take place in visibility-sorted order. In the case of a multi-brick volume, the bricks are processed one at a time:
Each brick is selected.
The selected brick is loaded into the texture cache.
The polygonization faces that fall within the brick are drawn.
The next brick is selected for processing.
To assure proper results, the bricks need to be sorted from back-to-front (or front-to-back). This is done to minimize the graphics API state changes. Use the following method for state sorting:
voSortAction aSortAction( aVolume->getCurrentBrickSet(), modelMatrix, projMatrix); |
The bricks can then be accessed in visibility-sorted order, as follows:
voBrickSetCollection *aVolume; int brickSortedNo = aSortAction[brickNo]; voBrick *aBrick = aVolume->getCurrentBrickSet()->getBrick(brickSortedNo); |
This code returns brick number brickNo in visibility-sorted order.
Once the volume is polygonized, and its bricks are sorted by depth, the application needs to draw the resulting polygons for each brick in order. Before that happens, you need to establish a graphics state by doing the following:
To enable texture mapping, the applications can use voAppearanceActions::textureEnable(), where the argument, interpolationType, is one of the following:
enum voInterpolationType{ DEFAULT = -1, _2D, _3D }; |
If the application did not explicitly specify per-vertex texture coordinates, a one-to-one mapping from vertex coordinates to voxel coordinates is assumed. In this case, it is convenient to use texgen functionality of OpenGL to compute the texture coordinates from the vertex coordinates. To enable this feature use
if (!hasTextureComponent(interleavedArrayFormat)) voAppearanceActions::texgenEnable(). |
To define the blending function, use OpenGL directly before any drawing happens:
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); |
Once the graphics state was set up all the transient geometry generated by the polygonize() can be drawn:
// Iterate over all bricks. for (brickNo = 0; brickNo < BrickCount; brickNo++) { int brickSortedNo = aSortAction[brickNo]; voBrick *aBrick = aVolume->getCurrentBrickSet()->getBrick(brickSortedNo); // Update texgen equation for the current brick. if (!hasTextureComponent(interleavedArrayFormat)) voAppearanceActions::texgenSetEquation(aBrick); // load the texture from host to texture memory voAppearanceActions::textureBind(aBrick); // iterate over all sampling planes for (int binNo = 0; binNo < samplesNumber ; binNo++) { voGeometryActions::draw( aPolygonSetArray[brickSortedNo][binNo], interleavedArrayFormat); } } |
Lookup table values control the opacity and colors of volumes. For example, you might choose to vary the opacity of skin such that when it becomes transparent, you can see through the skin to muscular or skeletal systems. Or you might choose to color parts of a volume differently, for example, you might enable the user to color a tumor differently from its surroundings.
voLookupTable is an abstract class that is used as a base class for both pre- and post-interpolation lookups. voLookupTable() constructs a lookup table in which the external and internal values of the table are defined in the argument of the method, as follows:
voLookupTable (GLenum _internalFormat, int _width, GLenum _externalFormat, GLenum _dataType, void* _data=NULL); |
The arguments are defined in “Step 2: Define Appearance”.
voTextureLookupPre() performs a pre-interpolation lookup, which occurs in the image pipeline as the texture is loaded into the texture memory. Typically this class implements functionality provided in hardware with GL_COLOR_TABLE_SGI or any other tables along the imaging pipeline. The constructor is defined as follows:
voLookupTablePre(GLenum _internalFormat, int _width, GLenum _externalFormat, GLenum _dataType, void* _data); |
The arguments are defined in “Step 2: Define Appearance”.
For changes caused by the lookup table to take effect, pre-interpolation lookups require reloading of the entire volume, even if it is already in texture memory. For this reason, pre-interpolation lookups are generally slower than post-interpolation lookups.
Pre-interpolations lookups are used to replace default functionality.
voTextureLookupPost() performs a post-interpolation lookup, which occurs during the rasterization phase when the image is on its way from the texture cache to the frame buffer. Typically this class implements functionality provided in hardware with GL_TEXTURE_COLOR_TABLE_SGI. The constructor is defined as follows:
voLookupTablePost(GLenum _internalFormat, int _width, GLenum _externalFormat, GLenum _dataType, void* _data); |
The arguments are defined in “Step 2: Define Appearance”.
Volumes do not need to be reloaded for post-interpolation changes to take effect. For this reason, post-interpolation lookups are generally faster than pre-interpolation lookups.
Post -interpolations lookups are used to alter default functionality.
The results of pre- and post-interpolation lookups are usually different. Make sure to choose the one appropriate to your intent.
For example, in post-interpolation lookup, two values, 100 and 200, are linearly interpolated and the resulting values (spanning the range 100-200) are looked up. Alternatively, in pre-interpolation lookup, the two values, 100 and 200, are looked up first, and the looked-up values are interpolated. If the values in a lookup table are linear, the results of pre- and post-interpolation lookups are identical; with a non-linear table, however, the results of pre- and post-interpolation differ.
Some platforms may impose restrictions on the use of post-interpolation lookups. For example, it is not possible to do a full RGBA post-interpolation lookup on Indigo2 Impact where the table length is limited to 256 entries.
Example 3-0 shows an example of a table lookup use.
float lookupEntries[2*256]; voTextureLookupPost *aLookupTable = new voTextureLookupPost( LUMINANCE8_ALPHA8_EXT, 256, LUMINANCE_ALPHA, FLOAT, lookupEntries); for(int j1=0;j1<256;j1++) lookupEntries[2*j1] = lookupEntries[2*j1+1] = 0.5*(float)j1/256.0; aLookupTable->load(); aLookupTable->enable(); |
![]() | Note: If you use a linear ramp for table lookups, you could replace the for() loop with the following method: |
aLookupTable->set(voTextureLookup::LINEAR,195,50); |
This construct also allows certain types of optimizations to be performed.
The Volumizer API provides a routine, getBestParameters(), that selects a suitable table type, and internal and external table formats given the following parameters:
Volumetric shape
Required rendering mode (MONOCHROME or COLOR)
Requested table length
Example 3-0 provides a sample implementation of getBestParameters().
voLookupTableType lookupType = POST_INTERPOLATION; // preferred type voInternalFormatType internalFormat; voExternalFormatType externalFormat; voLookupTable::getBestParameters(aVolume, renderingMode, lookupLength, lookupType, internalFormat, externalFormat); if (lookupType == PRE_INTERPOLATION) aLookupTable = new voLookupTablePre( internalFormat, lookupLength, externalFormat, _DATA_TYPE_FLOAT, lookupEntries); else aLookupTable = new voLookupTablePost( internalFormat, lookupLength, externalFormat, _DATA_TYPE_FLOAT, lookupEntries); |
The voError class handles errors and prints debugging information. voError can provide simple error checking and call error handling routines if used after each API call.
After each API call the application can test for any new error conditions with a call to getErrorNumber() and get the description of any such errors with a call to getErrorString(). For example, to test for errors after polygonizing a volume, your code segment would look similar to the following:
voGeometryActions::polygonize(...); if( voError::getErrorNumber() != voError::NO_ERROR ) fprintf(stderr,”voError %s\n”,voError::getErrorString()); |
The error conditions that can arise are listed below:
enum voErrorType{ NO_ERROR , BAD_VALUE, BAD_ENUM, OUT_OF_MEMORY, UNSUPPORTED }; |
Calls to getErrorNumber() and getErrorString() return the most recently set error code number and string, respectively, and then reset the current error number and string to NO_ERROR and NULL, respectively.
An alternative way of handing API errors is to set up an error handler with a call to setErrorHandler(), which allows the application to intercept all the errors and act on them immediately, as shown in the following code fragment:
voGeometryActions::polygonize(...); if( voError::getErrorNumber() != voError::NO_ERROR ) fprintf(stderr,"voError %s\n",voError::getErrorString()); voGeometryActions::draw(...); if( voError::getErrorNumber() != voError::NO_ERROR ) fprintf(stderr,"voError %s\n",voError::getErrorString()); |
This fragment has a similar effect to the following fragment:
void myExceptionHandler(void) { // inform the user AND reset the error flags and messages fprintf(stderr,"OpenGL Volumizer error %d: %s\n", voError::getErrorNumber(),voError::getErrorString()); // take other actions e.g., reallocate storage ... } ... voError::setErrorHandler(myExceptionHandler); voGeometryActions::polygonize(...); voGeometryActions::draw(...); ... |
The solution based on the error handler always reacts to the first error condition (even if it occurred internally within the API), it is less error prone, and less burdensome, which results in cleaner code. A pointer to the previously set handler (or NULL, if none was set) is returned to facilitate building of stacks of error handling functions.
Applications are required to clean up upon completion. This involves freeing all the objects used to describe the volumetric shape (tetra sets, brick set collections), but also all the voxel memory, transient geometry, and texture objects: