1.8.3. Data and Transfer Functions

VolumeViz does most of the work to make data (volumes and transfer functions) conveniently available to shader functions. Custom shaders can be used with a single volume or with multiple volumes. Custom shaders can be used with a single transfer function or with multiple transfer functions. Additional parameters can be sent to shaders using uniform parameters. In this subsection we will discuss what needs to be done in the application code. In the next subsection we will discuss how to access the data in the shader code.

Volumes (data sets)

When multiple volumes are used with a custom shader it is assumed that the shader will somehow combine the volumes to produce a single color/opacity value for each voxel. Some guidelines must be followed in order to use multiple volumes:

Some additional information:

Data Set Id

Each data set (SoVolumeData SoVolumeData SoVolumeData node) to be combined must have a unique id. The id is normally specified by setting the dataSetId field. The default value is 1. In order to support multiple data sets (volumes), shaders use this id when requesting a voxel value or other information from a data set. For a single data set, application shaders can use the predefined uniform parameter VVizDataSetId. VolumeViz automatically sets this parameter to the id of the primary volume. The primary volume is the first volume traversed under the SoMultiDataSeparator SoMultiDataSeparator SoMultiDataSeparator . For multiple data sets, the application must explicitly pass the data set id values as uniform parameters.

Only the data sets actually traversed need to have unique ids. For example the application can have multiple data sets with the same id under an SoSwitch SoSwitch SoSwitch node, as long as only one of them is traversed on any given traversal.

You should be aware that dataSetId also specifies the OpenGL texture unit in which the data textures for this data set will be stored on the GPU. The dataSetId is 1 by default because texture unit 0 is reserved for storing the transfer functions (color lookup tables) by default. The number of available texture units is limited and depends on the hardware. You can query this limit using the static method getMaxNumDataSets() in class SoDataSet SoDataSet SoDataSet .

The data set id can also be specified using an SoDataSetId SoDataSetId SoDataSetId node. If an SoDataSetId SoDataSetId SoDataSetId node is traversed before the SoVolumeData SoVolumeData SoVolumeData node, the id from the SoDataSetId SoDataSetId SoDataSetId node is used and the dataSetId field is ignored.

Example 1.4. Set dataSetIds for multi-volume rendering

// Data sets to be blended
SoVolumeData* pVolData1 = new SoVolumeData;
pVolData1->fileName  = filename1;
pVolData1->dataSetId = 1;

SoVolumeData* pVolData2 = new SoVolumeData;
pVolData2->fileName  = filename2;
pVolData2->dataSetId = 2;

. . .

SoMultiDataSeparator* pVolSep = new SoMultiDataSeparator;
pVolSep ->addChild( pVolData1 );
pVolSep ->addChild( pVolData2 );
// Data sets to be blended
SoVolumeData VolData1 = new SoVolumeData();
VolData1.fileName.Value  = filename1;
VolData1.dataSetId.Value = 1;

SoVolumeData VolData2 = new SoVolumeData();
VolData2.fileName.Value  = filename2;
VolData2.dataSetId.Value = 2;

. . .

SoMultiDataSeparator VolSep = new SoMultiDataSeparator();
VolSep.AddChild( VolData1 );
VolSep.AddChild( VolData2 );
// Data sets to be blended
SoVolumeData VolData1 = new SoVolumeData();
VolData1.fileName.setValue( filename1 );
VolData1.dataSetId.setValue( 1 );

SoVolumeData VolData2 = new SoVolumeData();
VolData2.fileName.setValue( filename2 );
VolData2.dataSetId.setValue( 2 );

. . .

SoMultiDataSeparator VolSep = new SoMultiDataSeparator();
VolSep.addChild( VolData1 );
VolSep.addChild( VolData2 );


Data Values

VolumeViz converts all data values, regardless of type, to 8 (default) or 12 bit unsigned integers in the GPU data textures. The 8 or 12 is controlled by the texturePrecision field on the SoVolumeData SoVolumeData SoVolumeData node. The scaling uses the min/max values given to the SoDataRange SoDataRange SoDataRange node, if it exists, else it uses the full range of the actual data type. For rendering purposes this is usually sufficient and maximizes the amount of data that can be loaded in the available memory on the GPU. (But note that the 12-bit option actually uses 16-bit textures to store the data on the GPU, so the memory requirement is actually double compared to using 8-bit data.) There are two issues to consider.

First, for 8-bit data (and for 12-bit data when texturePrecision is set to 12), the values stored on the GPU are the actual data values. However for larger data types, some range of actual data values is usually "aliased" onto each GPU data value. For example, all the data values in the range 32 to 47 might end up as 32 in the GPU data. In summary, the default data storage allows 256 distinct values on the GPU and 12-bit storage allows 4096 distinct values.

Second, GLSL always returns the voxel value from the GPU data texture as a floating point number in the range 0..1. This is true even for 8-bit data. So if your shader needs to know the actual data value, it needs to convert the 0..1 value either by knowing the actual data range or getting the actual data range through some uniform parameters.

Computing the value that the shader will see for a particular data value is straightforward, but remember to include the truncation that will occur when converting to unsigned integer in the data texture. The data min and max values are either the values specified to SoDataRange SoDataRange SoDataRange or the min and max of the volume’s data type (e.g. 0 and 65535 for the unsigned short data type):

Example 1.5. Calculate data value shader will see

double r = 256 / (double)(dataMax - dataMin);
double v = floor((dataValue - dataMin) * r);
float shaderValue = (float)(v / 256);  // Default 8-bit data texture
double r = 256 / (double)(dataMax - dataMin);
double v = Math.Floor((dataValue - dataMin) * r);
float shaderValue = (float)(v / 256);  // Default 8-bit data texture
double r = 256 / (double)(dataMax - dataMin);
double v = Math.floor((dataValue - dataMin) * r);
float shaderValue = (float)(v / 256);  // Default 8-bit data texture


RGBA data is a special case, but quite different. If you give VolumeViz a volume containing 32-bit RGBA values, it will store those 32-bit values on the GPU. Of course in this case there is no color map.

Data Range Id

When using multiple volumes, a single SoDataRange SoDataRange SoDataRange node can be used to specify a data range that applies to all volumes. However each volume may have its own separate data range. This is important because the data range specifies the range of values that will be scaled into the data values on the GPU. Seismic attribute volumes and medical volumes from different modalities generally have different data ranges. In this case, create an SoDataRange SoDataRange SoDataRange node for each volume and set the dataRangeId equal to the dataSetId of the corresponding SoVolumeData SoVolumeData SoVolumeData node.

Example 1.6. Set data range ids for multiple volumes

SoDataRange* pRange1 = new SoDataRange;
pRange1->dataRangeId = pVolData1->dataSetId.getValue();

SoDataRange* pRange2 = new SoDataRange;
pRange2->dataRangeId = pVolData2->dataSetId.getValue();
SoDataRange Range1 = new SoDataRange();
Range1.dataRangeId.Value = VolData1.dataSetId.Value;

SoDataRange Range2 = new SoDataRange();
Range2.dataRangeId.Value = VolData2.dataSetId.Value;
SoDataRange Range1 = new SoDataRange();
Range1.dataRangeId.setValue( VolData1.dataSetId.getValue() );

SoDataRange Range2 = new SoDataRange();
Range2.dataRangeId.setValue( VolData2.dataSetId.getValue() );


Transfer Function Id

Each transfer function (SoTransferFunction SoTransferFunction SoTransferFunction node) to be combined must have a unique id. The id is specified by setting the transferFunctionId field. The default value is 0. Shaders, for example VVizComputeFragmentColor(), use this id when requesting a color value from the VVizTransferFunction() method. VolumeViz will load all the transfer functions under the SoMultiDataSeparator SoMultiDataSeparator SoMultiDataSeparator , even if there is only one data set. So another way to switch between different color maps is to send an id as a uniform parameter and use that parameter to call VVizTransferFunction in VVizComputeFragmentColor().

Only the transfer functions actually traversed need to have unique ids. For example the application can have multiple transfer functions with the same id under an SoSwitch SoSwitch SoSwitch node, as long as only one of them is traversed on any given traversal.

Unlike data sets, all transfer functions are combined into a single data texture. So the transferFunctionId simply identifies the location of a particular color map in the transfer function texture, it does not affect the number of OpenGL texture units needed. By default the transfer function texture is stored in texture unit 0 (which is why data set ids normally start at 1). The texture unit for transfer functions can be changed through SoPreferences SoPreferences SoPreferences using the variable IVVR_TF_TEX_UNIT, but dataSetId can never be set to the texture unit number used to store the transfer functions. Application shaders should access the transfer functions using the VVizTransferFunction method. At this point voxel values are normalized to the range 0..1 (as discussed above under Data).

Generally the transfer function ids should start at zero and be consecutive values. This is not required, just recommended, because VolumeViz does not compact the range of transfer function ids and the transfer function texture is initialized to zero values. So if the application uses transfer function ids 1 and 5, but the shader does a color lookup using id 3, the resulting color and opacity will be zero. The order of the SoTransferFunction SoTransferFunction SoTransferFunction nodes in the scene graph does not matter. Transfer functions are stored in the texture in the order specified by their id.

Example 1.7. Set transfer function ids for multiple volumes

SoTransferFunction* pTF1 = new SoTransferFunction;
pTF1->predefColorMap     = SoTransferFunction::INTENSITY;
pTF1->transferFunctionId = 0;

SoTransferFunction* pTF2 = new SoTransferFunction;
pTF2->predefColorMap     = SoTransferFunction::BLUE_WHITE_RED;
pTF2->transferFunctionId = 1;
SoTransferFunction TF1 = new SoTransferFunction();
TF1.predefColorMap.Value     = SoTransferFunction.PredefColorMaps.INTENSITY;
TF1.transferFunctionId.Value = 0;

SoTransferFunction TF2 = new SoTransferFunction();
TF2.predefColorMap.Value     = SoTransferFunction.PredefColorMaps.BLUE_WHITE_RED;
TF2.transferFunctionId.Value = 1;
SoTransferFunction TF1 = new SoTransferFunction();
TF1.predefColorMap.setValue( SoTransferFunction.PredefColorMaps.INTENSITY );
TF1.transferFunctionId.setValue( 0 );

SoTransferFunction TF2 = new SoTransferFunction();
TF2.predefColorMap.setValue( SoTransferFunction.PredefColorMaps.BLUE_WHITE_RED );
TF2.transferFunctionId.setValue( 1 );


Uniform Parameters

Uniform parameters are named values that the application can set and the shader code can use, but not modify. GLSL calls them “uniform” variables because they cannot change during the rendering of a primitive, unlike shader stage outputs (formerly called “varying” variables in GLSL). The application can use these parameters to pass data set ids (as mentioned above), data set min and max values, blend factors or anything else useful in a shader. Use one of the subclasses of SoShaderParameter SoShaderParameter SoShaderParameter to create a uniform parameter. Then add the parameter object to the shader object (usually the SoFragmentShader SoFragmentShader SoFragmentShader object).

Example 1.8. Send data set ids to shader

// Send data set ids to shader
SoShaderParameter1i *pParam1 = new SoShaderParameter1i;
pParam1->name  = "data1";
pParam1->value = 1;
SoShaderParameter1i *pParam2 = new SoShaderParameter1i;
pParam2->name  = "data2";
pParam2->value = 2;

pFragmentShader->parameter.set1Value(0, pParam1);
pFragmentShader->parameter.set1Value(1, pParam2);

Or, using the convenience functions:

// Send data set ids to shader
pFragmentShader->addShaderParameter1i( "data1", 1 );
pFragmentShader->addShaderParameter1i( "data2", 2 );
// Send data set ids to shader
SoShaderParameter1i Param1 = new SoShaderParameter1i();
Param1.name.Value  = "data1";
Param1.value.Value = 1;
SoShaderParameter1i Param2 = new SoShaderParameter1i();
Param2.name.Value  = "data2";
Param2.value.Value = 2;

FragmentShader.parameter[0] = Param1;
FragmentShader.parameter[1] = Param2;
// Send data set ids to shader
SoShaderParameter1i Param1 = new SoShaderParameter1i();
Param1.name.setValue( "data1" );
Param1.value.setValue( 1 );
SoShaderParameter1i Param2 = new SoShaderParameter1i();
Param2.name.setValue( "data2" );
Param2.value.setValue( 2 );

FragmentShader.parameter.addShaderParameter( Param1 );
FragmentShader.parameter.addShaderParameter( Param2 );