Open Inventor Release 2024.2.0
 
Loading...
Searching...
No Matches
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:

VolumeViz will load data for all the active data sets under the SoMultiDataSeparator node (active meaning that the SoVolumeData node is traversed with the current SoSwitch settings). Loading is synchronized, so the same tiles, at the resolution levels, are loaded for each volume. All the related nodes (range, transfer function, etc) should also be under the SoMultiDataSeparator node.

  • All the volumes must have exactly the same dimensions (number of voxels in X, Y and Z) and tile size.
  • Each volume must have a unique data set id.

See the discussion of dataSetId below. Some additional information:

For example the total memory required is the sum of all the volumes.

  • Each volume may have its own data range, set using SoDataRange.

See the discussion of dataRangeId below.

  • The base material, set with SoMaterial, is associated with the (or with each) rendering node.
  • The rendering effects, rendering mode, etc, set with SoVolumeRenderingQuality, are associated with the (or with each) rendering node.
  • Multiple transfer functions (color maps) may be loaded. The shader will choose which one(s) to use.

See the discussion of transferFunctionId below.

Data Set Id

Each data set (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. 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 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.

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

Example : Set dataSetIds for multi-volume rendering

C++ :

// 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 );

C# :

// 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 );

Java :

// 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 node. The scaling uses the min/max values given to the 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 or the min and max of the volume’s data type (e.g. 0 and 65535 for the unsigned short data type):

Example : Calculate data value shader will see

C++ :

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

C# :

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

Java :

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 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 node for each volume and set the dataRangeId equal to the dataSetId of the corresponding SoVolumeData node.

Example : Set data range ids for multiple volumes

C++ :

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

C# :

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

Java :

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 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, 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 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 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 nodes in the scene graph does not matter. Transfer functions are stored in the texture in the order specified by their id.

Example : Set transfer function ids for multiple volumes

C++ :

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

C# :

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;

Java :

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 to create a uniform parameter. Then add the parameter object to the shader object (usually the SoFragmentShader object).

Example : Send data set ids to shader

C++ :

// 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 );

C++ : Or, using the convenience functions:

C++ :

// Send data set ids to shader
pFragmentShader->addShaderParameter1i( "data1", 1 );
pFragmentShader->addShaderParameter1i( "data2", 2 );

C# :

// 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;

Java :

// 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 );