By Hamed “Kay” Khandan
Kobe University
The Visualisation Toolkit or VTK is the de facto standard API for writing visualisation programs. ParaView, a widely used visualisation app is written entirely using VTK.
In this tutorial, we are going to see how to use VTK to open and manipulate a scientific dataset programatically in C++ language. VTK has support for a number of scripting languages, including tcl and Python, but these will not be covered here.
Before reading this tutorial, it is highly recommended to read the ParaView Tutorial first.
Before being able to build VTK you need to have Qt installed. Qt is a cross-platform GUI framework (KDE is written using Qt). If you haven’t done so already, go to qt.io website and download the free distribution of Qt under LGPL license. Run the installer and follow on screen instructions to completion.
You also need cmake, which is the tool used for building VTK. You can get cmake at www.cmake.org.
When you have cmake and Qt installed, you may go ahead and build VTK. Download VTK source distribution from www.vtk.org, if you have not done so already. Lets assume the decompressed path is ~/Downloads/vtk. Next, decide upon a different directory where you want to put the binaries in. Lets assume it is going to be ~/Bin/vtk. Create the binary directory and set it as the current directory.
$mkdir ~/Bin/vtk
$cd ~/Bin/vtk
Now call cmake to generate the makefile.
$cmake ~/Downloads/vtk
Sometimes, you need to supply the Qt installation path to cmake:
$cmake -DQT_QT_EXECUTABLE:PATH=/path/to/qt ~/Downloads/vtk
If this step is done successfully, then you can go ahead and use make to compile VTK:
$make
If you are a regular user of C++ language, you will find the programming style under VTK rather unfamiliar.
The notation for identifier names is as follows. Class names start with lowercase “vtk”. For example vtkSphereSource is the name of a class. Name of class members start with upper case letters. For example, vtkSphereSource::SetResolution(int) is the name of a member function.
Pointers are replaced with a VTK-specific smart pointer implementation called vtkSmartPointer. Therefore, a pointer to vtkSphereSource would be denoted as vtkSmartPointer<vtkSphereSource>.
Objects are not created using the new keyword, and are not deleted explicitly. To create a new instance of a class, you need to follow the following syntax:
vtkSmartPointer<vtkSphereSource> sphereSource =
vtkSmartPointer<vtkSphereSource>::New();
Give your eyes a couple of minutes to adjust to this peculiar notation. The reason that VTK is designed like this is that, it is created based on factory design pattern. In many cases, the object created by calling the New() method is not exactly of the given class, but of a sub-class that is optimized for the current platform. This way, VTK provides access to platform-dependant code in a platform-independent manner. The New() method internally refers to a factory function that decides upon a suitable subclass to instantiate, and returns a new instance of that class.
All classes in VTK are subclasses of vtkObjectBase, and most classes are subclasses of vtkObject which is directly derived from vtkObjectBase.
Although VTK is a C++ framework, in a higher level, it follows a programming paradigm other than Object Oriented. This paradigm is called data-flow.
Unlike in OOP where process (function) belongs to data (object), in data-flow paradigm process and data are separated entities. Each process may have zero or more inputs and outputs. Processes can be connected to each other in directed graph formation by connecting the outputs of some to the inputs of some others. Data is passed through these links from one process to another as the program is being evaluated.
In VTK, all objects representing data are subclasses of vtkDataSet. Some of these subclasses are vtkImageData, vtkPolyData, vtkRectilinearGrid, etc.
There are three types of processes. The ones with zero inputs are called sources. vtkSphereSource named earlier is a source. It specifies the geometry and topology of a sphere and makes it available to other nodes. Other examples of source are all subclasses of the vtkReader class. These classes read data from files of various formats and make them available to the other ones.
Those nodes with both inputs and outputs are called filters. We will see many examples of filters later on.
Finally, those nodes with inputs but no outputs are called sinks. We will be using only one sink class in this tutorial, called vtkRenderer, which does exactly what its name implies. All subclasses of vtkWriter are also sinks.
Preparing the Source. Let’s start by creating and displaying a simple object. The first thing you need is a source. Let’s create a cone as our source.
vtkSmartPointer<vtkConeSource> coneSource
= vtkSmartPointer<vtkConeSource>::New();
coneSource->Update();
The Update() is necessary to get the values produced by our source node ready in its output(s).
A source object knows how to represent an object, but not necessarily in renderable polygonal format. So to make the source object renderable, we have to use a vtkPolyDataMapper, and connect it to our source.
vtkSmartPointer<vtkPolyDataMapper> coneMapper
= vtkSmartPointer<vtkPolyDataMapper>::New();
coneMapper->SetInputData(coneSource->GetOutput());
There is one more step needed before we get to rendering. We have to put our object as a prop in the 3D scene that is going to be rendered. All props in VTK are subclasses of vtkProp. In particular, those props that are renderable objects are called actors and are represented using vtkActor class.
vtkSmartPointer<vtkActor> cone = vtkSmartPointer<vtkActor>::New();
cone->SetMapper(coneMapper);
Rendering. Let’s proceed to make the sink of our data-flow pipeline, which is going to be a vtkRenderer. Then, we can add to it the actor that we just created.
vtkSmartPointer<vtkRenderer> renderer =
vtkSmartPointer<vtkRenderer>::New();
renderer->AddActor(cone);
Then, we need a window to display the rendering results in it.
vtkSmartPointer<vtkRenderWindow> renderWindow
= vtkSmartPointer<vtkRenderWindow>::New();
renderWindow->AddRenderer(renderer);
renderWindow->Render();
And, one last thing …
vtkSmartPointer<vtkRenderWindowInteractor> renderWindowInteractor =
vtkSmartPointer<vtkRenderWindowInteractor>::New();
renderWindowInteractor->SetRenderWindow(renderWindow);
renderWindowInteractor->Start();
The interactor we created above lets you to move your camera using your mouse (VTK always creates one camera and one light by default). But it does one more good for us. It runs an infinite event loop which prevents the program to exit immediately after the execution of previous commands completed. If you don’t put this interactor, when you execute your program you will see a window for a brief moment and then your program quits immediately.
The following is the complete code:
#include <vtkSmartPointer.h>
#include <vtkPolyDataMapper.h>
#include <vtkActor.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkConeSource.h>
#include <vtkProperty.h>
int main(int, char *argv[]) {
// Cone
vtkSmartPointer<vtkConeSource> coneSource
= vtkSmartPointer<vtkConeSource>::New();
coneSource->SetResolution(12);
coneSource->SetDirection(0, 1, 0);
coneSource->Update();
vtkSmartPointer<vtkPolyDataMapper> coneMapper
= vtkSmartPointer<vtkPolyDataMapper>::New();
coneMapper->SetInputData(coneSource->GetOutput());
vtkSmartPointer<vtkActor> cone = vtkSmartPointer<vtkActor>::New();
cone->SetMapper(coneMapper);
cone->GetProperty()->SetColor(0.4, 0.4, 1.0);
// Render
vtkSmartPointer<vtkRenderer> renderer =
vtkSmartPointer<vtkRenderer>::New();
renderer->AddActor(cone);
vtkSmartPointer<vtkRenderWindow> renderWindow
= vtkSmartPointer<vtkRenderWindow>::New();
renderWindow->AddRenderer(renderer);
renderWindow->Render();
vtkSmartPointer<vtkRenderWindowInteractor> renderWindowInteractor =
vtkSmartPointer<vtkRenderWindowInteractor>::New();
renderWindowInteractor->SetRenderWindow(renderWindow);
renderWindowInteractor->Start();
return 0;
}
The above code produces the following output.
Changing Properties. Some of properties belong to the source and some of the belong to the actor. For example, the direction of the cone is defined by the source. Add the following two lines to your code right before you call coneSource->Update();
coneSource->SetResolution(12);
coneSource->SetDirection(0, 1, 0);
The color attribute and everything that is related to shading belongs to the actor. Here is how you change the actor’s color:
cone->GetProperty()->SetColor(0.4, 0.4, 1.0);
A call to GetProperty will return pointer to a vtkProperty which then can be used to set various shading parameter like opacity, ambient and diffuse color, and so on.
Don’t forget to include vtkProperty.h in your new code. If compiled successfully, this program should produce the following:
Adding Filters. Technically speaking, mappers and actors are filters. But how about filters used for making the rendered subject more visually meaningful? Here is one example:
First include two additional headers:
#include <vtkElevationFilter.h>
#include <vtkLookupTable.h>
Then, before you create coneMapper, add the following lines:
vtkSmartPointer<vtkElevationFilter> coneElevation
= vtkSmartPointer<vtkElevationFilter>::New();
coneElevation->SetInputData(coneSource->GetOutput());
coneElevation->SetLowPoint(0, -1, 0);
coneElevation->SetHighPoint(0, 1, 0);
vtkSmartPointer<vtkLookupTable> colorTable
= vtkSmartPointer<vtkLookupTable>::New();
colorTable->SetHueRange(0.667, 0.0);
colorTable->SetSaturationRange(1, 1);
colorTable->SetValueRange(1, 1);
Next, remove this line,
coneMapper->SetInputData(coneSource->GetOutput());
And replace it with the following two:
coneMapper->SetLookupTable(colorTable);
coneMapper->SetInputConnection(coneElevation->GetOutputPort());
When you compile and run your code, you should get something like this:
The elevation filter calculates the height of each point, and the lookup table produces color values for each value. Finally, the mapper puts both together.
If you have not done so already, download the sample dataset from the following link and decompress it. You will end up with a directory called CFDData with some files and sub-directories in it.
https://www.dropbox.com/s/aloiiutmwads4fn/CFDData.zip?dl=0
In order to read the contents of this dataset, we will use subclasses of vtkReader as source nodes. The reader to be used depends on the file format. Our dataset contains a model of a car which saved in IQ-mod.stl file. To read a STL file we need to use, guess … , right, vtkSTLReader.
In the following code, we define a macro called VTK_CREATE that makes our lives much easier, eliminated hunger and disease in the world, and brings peace and prosperity to the universe. Even if it doesn’t do all of those things, at least it makes the code more readable.
#include <vtkSmartPointer.h>
#include <vtkPolyDataMapper.h>
#include <vtkActor.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkSTLReader.h>
#define VTK_CREATE(type, name) \
vtkSmartPointer<type> name = vtkSmartPointer<type>::New()
int main(int, char *argv[]) {
// Car
VTK_CREATE(vtkSTLReader, carReader);
carReader->SetFileName(“/path/to/data/CFDData/IQ-mod.stl”);
carReader->Update();
VTK_CREATE(vtkPolyDataMapper, carMapper);
carMapper->SetInputConnection(carReader->GetOutputPort());
VTK_CREATE(vtkActor, car);
car->SetMapper(carMapper);
// Render
VTK_CREATE(vtkRenderer, renderer);
renderer->AddActor(car);
VTK_CREATE(vtkRenderWindow, renderWindow);
renderWindow->AddRenderer(renderer);
renderWindow->Render();
VTK_CREATE(vtkRenderWindowInteractor, interactor);
interactor->SetRenderWindow(renderWindow);
interactor->Start();
return 0;
}
If you successfully compile and run the code, you will see the following result:
To process CFD data, you will need to use the following pattern:
VTK_CREATE(vtkXMLHierarchicalBoxDataReader, cfdReader);
cfdReader->SetFileName(“/path/to/data/CFDData/VTK/data-flow-0000005600.vthb”);
cfdReader->Update();
VTK_CREATE(vtkHierarchicalPolyDataMapper, cfdMapper);
cfdMapper->SetInputConnection(cfdReader->GetOutputPort());
~~ more will come soon ~~
This tutorial was made for SIGGRAPH Asia Open Data Visualisation contest.
Special thanks to Dr. Junya Ōnishi for producing the dataset used here.