1.9.3. Combining volumes

In this section we discuss the case where multiple data volumes are combined to render a single volume on the screen.

One or more data sets stored on disk can be composed together with a user-defined operator when loading from disk to main memory. The corresponding blocks of data (LDM tiles) are composed before the result is stored into main memory. Multiple data sets are present on disk but only the result of the composition is stored in main memory. The same result could be achieved by reading the input volumes and writing out a new composited volume. Using VolumeViz data composition has two advantages. First, we don’t need to keep the composited volume around on disk and second, we (potentially) don’t need to composite every voxel in the whole volume because LDM will only load tiles that are needed for rendering.

The SoDataCompositor SoDataCompositor SoDataCompositor node allows you to combine multiple data sets in memory instead of having to store the combined data sets on disk. For example, it can be used to visualize the result of the difference between two data sets. Some useful default operators are provided (add, subtract, and multiply) through the preDefCompositor field. Custom composition operators may be defined by deriving a subclass from SoDataCompositor SoDataCompositor SoDataCompositor and overriding the compose() method. To use the custom operator, the preDefCompositor field must be set to NONE.

The SoDataCompositor SoDataCompositor SoDataCompositor node cannot be used for unary operations (the number of data sets used with a data compositor must be greater than one). Unary operations can be performed using SoLDMDataTransform SoLDMDataTransform SoLDMDataTransform or SoVolumeTransform SoVolumeTransform SoVolumeTransform .

A number of rules apply to the use of SoDataCompositor SoDataCompositor SoDataCompositor :

For example, to realize the difference of two data sets, only the SoDataCompositor SoDataCompositor SoDataCompositor node, the SoDataSet SoDataSet SoDataSet nodes, and the rendering primitive node must be inserted under the SoMultiDataSeparator SoMultiDataSeparator SoMultiDataSeparator node created to handle the composition.

Scene graph for CPU composition of two data sets

Figure 1.82. Scene graph for CPU composition of two data sets

Each SoDataSet SoDataSet SoDataSet following the compositor must have the same dimensions. However, the data set nodes can have different voxel data types (bytes per voxel). The final voxel data type is specified by the data compositor node through the dataType field. If the rgbaMode field is set to TRUE, then dataType and numSigBits are ignored, and the output data is generated as UNSIGNED_INT32 with 32 significant bits.

CPU composition of two data sets using the multiply operator

Figure 1.83. CPU composition of two data sets using the multiply operator

The following code shows an application defined custom compositor:.

class MyCompositor : public SoDataCompositor
  void compose(int numVolumeData,
               const SbVec3i32& tileDimension,
               int* vdid,
               SoBufferObject** inputBuffer,
               SoBufferObject* outputBuffer );

// Implement custom compose method
myCompositor::compose( int numVolumeData,
                       const SbVec3i32& tileDimension,
                       int* vdid,
                       void** inputBuffer,
                       void* outputBuffer )
  // Get pointers to input and output buffers
  // Note: Assume there are exactly two input volumes of type unsigned byte
  SoRef<SoCpuBufferObject> inputBufferCpu0 = new SoCpuBufferObject();
  inputBuffer[0]->map(inputBufferCpu0, SoBufferObject::READ_ONLY);
  unsigned char* inputPtr0 = inputBufferCpu0->map(SoBufferObject::READ_ONLY);

  SoRef<SoCpuBufferObject> inputBufferCpu1 = new SoCpuBufferObject();
  unsigned char* inputPtr1 = inputBufferCpu1->map(SoBufferObject::READ_ONLY);

  SoRef<SoCpuBufferObject> outputBufferCpu = new SoCpuBufferObject();
  unsigned char* outputPtr = outputBufferCpu1->map(SoBufferObject::SET);

  // Compute output volume similar to SoDataCompositor::MULTIPLY
  int numVoxels = tileDimension[0] * tileDimension[1] * tileDimension[2];
  for (int i = 0; i < numVoxels; ++i) {
    *outputPtr++ = *inputPtr0++ * *inputPtr1++;

  // Release mapping on input and output buffers
class MyCompositor : SoDataCompositor
    // Automatically set preDefCompositor field to NONE
    // (enabling use of our overridden compose function)
    public MyCompositor()

    public override void Compose(int numDataSet, SbVec3i32 tileDimension, int[] vdid, OIV.Inventor.Devices.SoBufferObject[] inputBuffer, OIV.Inventor.Devices.SoBufferObject outputBuffer)
        SoCpuBufferObject bufIn1 = new SoCpuBufferObject();
        SoCpuBufferObject bufIn2 = new SoCpuBufferObject();
        SoCpuBufferObject bufOut = new SoCpuBufferObject();

        ulong numVoxels = (ulong)(tileDimension[0] * tileDimension[1] * tileDimension[2]);
        inputBuffer[0].Map(bufIn1, SoBufferObject.AccessModes.READ_ONLY, 0, numVoxels);
        inputBuffer[1].Map(bufIn2, SoBufferObject.AccessModes.READ_ONLY, 0, numVoxels);
        outputBuffer.Map(bufOut, SoBufferObject.AccessModes.SET, 0, numVoxels);

        SbNativeArray<byte> byteBuf1 = bufIn1.Map(SoBufferObject.AccessModes.READ_ONLY);
        SbNativeArray<byte> byteBuf2 = bufIn2.Map(SoBufferObject.AccessModes.READ_ONLY);
        SbNativeArray<byte> byteOut = bufOut.Map(SoBufferObject.AccessModes.SET);

        for (int i = 0; i < (int)numVoxels; ++i)
            byteOut[i] = (byte)(byteBuf1[i] * byteBuf2[i]);

        bufOut.Unmap(); outputBuffer.Unmap(bufOut);
        bufIn1.Unmap(); inputBuffer[0].Unmap(bufIn1);
        bufIn2.Unmap(); inputBuffer[1].Unmap(bufIn2);

SoDataCompositor dataCompositor = new SoDataCompositor() {
  public void compose(SbVec3i32 tile_dimension,
                      int[] volume_ids,
                      Buffer[] input_buffer,
                      int[] data_types,
                      Buffer output_buffer) 
      // compose by multiplying input buffers
      byte b;
      while (output_buffer.hasRemaining()) {
        b = 0x01;
        for (int j = 0; j < input_buffer.length; j++) {
          b *= ((ByteBuffer)input_buffer[j]).get();