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( C++ | Java | .NET ) 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( C++ | Java | .NET ) and overriding the compose() method. To use the custom operator, the preDefCompositor field must be set to NONE.

The SoDataCompositor( C++ | Java | .NET ) 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( C++ | Java | .NET ) or SoVolumeTransform( C++ | Java | .NET ).

A number of rules apply to the use of SoDataCompositor( C++ | Java | .NET ):

  • The SoDataCompositor( C++ | Java | .NET ) node and SoDataSet( C++ | Java | .NET ) nodes must be children of an SoMultiDataSeparator( C++ | Java | .NET ) node (an ordinary SoSeparator will not work correctly).

  • The SoDataCompositor( C++ | Java | .NET ) node must be inserted before the SoDataSet( C++ | Java | .NET ) nodes in the scene graph.

  • No rendering primitives (e.g., SoOrthoSlice( C++ | Java | .NET ), SoVolumeRender( C++ | Java | .NET )) are allowed between the SoDataSet( C++ | Java | .NET ) nodes that are used for the composition.

  • An SoDataSet( C++ | Java | .NET ) node used for data compositing cannot be referenced twice in the scene graph. Another data set node pointing to the same file must be instantiated.

  • All nodes needed to realize the compositing must be under the same SoSeparator( C++ | Java | .NET ) node.

  • No other nodes must be under this SoSeparator( C++ | Java | .NET ) node.

  • It is not possible to mix SoDataSet( C++ | Java | .NET ) nodes used for compositing with SoDataSet( C++ | Java | .NET ) nodes used for normal rendering under the same SoSeparator( C++ | Java | .NET ).

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


Each SoDataSet( C++ | Java | .NET ) 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.


The following code shows an application defined custom compositor:.


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

// Implement custom compose method
void
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();
  inputBuffer[1]->map(inputBufferCpu1,SoBufferObject::READ_ONLY);
  unsigned char* inputPtr1 = inputBufferCpu1->map(SoBufferObject::READ_ONLY);

  SoRef<SoCpuBufferObject> outputBufferCpu = new SoCpuBufferObject();
  outputBuffer->map(outputBufferCpu1,SoBufferObject::SET);
  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
  outputBufferCpu1->unmap();
  outputBuffer->unmap(outputBufferCpu1);
  inputBufferCpu1->unmap();
  inputBuffer->unmap(inputBufferCpu1);
  inputBufferCpu2->unmap();
  inputBuffer->unmap(inputBufferCpu2);
  }

.NET
class MyCompositor : SoDataCompositor
{
    // Automatically set preDefCompositor field to NONE
    // (enabling use of our overridden compose function)
    public MyCompositor()
    {
      preDefCompositor.SetValue("NONE");
    }

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

}

Java
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();
        }
        ((ByteBuffer)output_buffer).put(b);
      }
  }

};
dataCompositor.dataType.setValue(SoDataSet.DataTypes.UNSIGNED_BYTE);
dataCompositor.preDefCompositor.setValue(SoDataCompositor.PreDefCompositors.NONE);