Developing Visualization Applications with Cocoa and VTKBack in June I read Michael Norton's MacDevCenter article 3-D Data Visualization on Mac OS X. I am a scientist by trade, but I hadn't made much use of visualization in my research. Michael's article opened my eyes to a whole new way of doing things and showed how easy it could be to turn your garden variety iBook into a powerful scientific ally.
For those of you not familiar with VTK, it is to visualization what Cocoa is to application development: VTK provides a high-level object-oriented framework which allows you to easily visualize 3D data sets without having to write any low-level OpenGL code. OpenGL is used to perform the eventual rendering, but the software developer writes code at a higher level of abstraction. Like Cocoa, VTK can literally save you having to write thousands of lines of source code.
In addition to being a scientist, I am also a Cocoa enthusiast, spending every spare minute of the day writing shareware for Mac OS X. So it wasn't long after reading Michael's article that I began wondering if it were possible to get the best of both worlds in one app: Cocoa's great Application building frameworks, and VTK's equally impressive visualization libraries. It turns out it is very possible to make visualization apps to-die-for with Cocoa and VTK. This article is about how to do just that.
|
|
I'll lead you through integrating VTK and Cocoa by way of example: In Part 1 we will learn the general steps required to setup a Cocoa-VTK project, and in Part 2 we will develop a simple Cocoa-VTK 3D animation application. Along the way we will learn how to compile VTK, configure Project Builder, overcome a few incompatibilities between VTK and Cocoa, and even get a quick intro to Objective-C++.
The best place to start in order to get a successful VTK build on Mac OS X is Michael Norton's original MacDevCenter article. Most of what we need to do here is covered there. But there are a few minor differences, so we will quickly move through the steps required to install VTK suitably for use in a Cocoa application.
Firstly, you will need the tool "CMake" to build VTK. If you haven't
installed this already, go to www.cmake.org and download the source code
file CMake1.4.5-src-unix.tar.gz. I have found this version to
be stable. Unless you enjoy the thrill of the unknown, I suggest you steer
clear of using CVS to get the source. I encountered problems building VTK
when I used a CVS derived CMake, and you don't want to waste time on CMake
when you could be wasting time on the really fun stuff.
Unpack the CMake source, and change directories:
tar xvzf CMake1.4.5-src-unix.tar.gz
cd CMake-1.4.5
Now configure, build, and install CMake
./configure
make
sudo make install
Note that you need to use "sudo" for the last step, so you will need
to be an administrator, and enter your password at the prompt. The last
thing you need to check is that your PATH environment variable includes
the /usr/local/bin directory where cmake gets installed. For
tcsh you need to put this in your ~/.tcshrc file:
setenv PATH $PATH:/usr/local/bin
and then source the file
source ~/.tcshrc
With CMake installed, you're ready to install VTK. Because Cocoa has not been supported that long in VTK, you will need a relatively new copy of the source code, and you will need CVS for this. In a terminal window, change to the directory where you would like to install VTK. I will assume this is the directory "Develop" in your home directory, in which case you should issue this command
cd ~/Develop
Now checkout the VTK source code with the following two commands:
cvs -d :pserver:anonymous@public.kitware.com:/cvsroot/VTK login
cvs -d :pserver:anonymous@public.kitware.com:/cvsroot/VTK checkout -D 09/20/2002 VTK
When asked for a password after the first command, enter
vtk. If you want the very latest sources, leave out the
-D 09/20/2002 in the second command. I had trouble compiling
more recent versions of VTK and found that for this date the code will at
least compile and run, even if it doesn't include cutting edge
developments. Now go and watch the wrestling for half an hour: checking
out the source can take a while.
In the ~/Develop directory, create a directory for
building VTK, which I will call VTKBuild and change to that
directory
mkdir VTKBuild
cd VTKBuild
Issue the following command:
cmake ../VTK
When this completes, you should find a file called
CMakeCache.txt in your VTKBuild directory. This is a
configuration file for the cmake system. We need to make a few changes to
it, so use your favorite editor to search the CMakeCache.txt file for the
following entries, and change them accordingly:
VTK_USE_CARBON:BOOL=OFF
VTK_USE_COCOA:BOOL=ON
Unlike in Michael Norton's original article, we're going to build
static libraries, so we are leaving the BUILD_SHARED_LIBS
option OFF. Static libraries make it easier to distribute your app once
you have written it because all binary code is included in the
executable. Your users do not need to install VTK in order to use your
app. (It is possible to embed shared libraries in an app too, but it is
much more involved, so we'll steer clear of it here.)
Run cmake again a couple of times from the VTKBuild directory.
cmake ../VTK; cmake ../VTK
Doing this is advised whenever you make changes to the
CMakeCache.txt file, as it can sometimes require several
iterations before the changes have completely propagated through the whole
system.
Now type ...
make
... and pray. Just kidding. If all goes well, you should end up with a handful of static libraries in the VTKBuild/bin directory, and you are ready to start on the Cocoa stage of the journey.
As I mentioned previously, in Part 2 we are going to build a small 3D animation program, so we will now start setting up a Project Builder project with that in mind. To begin with, open Project Builder, and choose New Project... from the File menu. Select Cocoa Application, enter the project name "Animoltion", select where you want to have the project folder, and click Finish. (To find out why it is called Animoltion, you will have to wait for our next riveting installment.)
VTK on Mac OS X is based on OpenGL, so we need to add the OpenGL framework--installed on every Mac OS X computer--to our new project. Choose "Add Frameworks..." from the "Project" menu, and browse to the framework bundle
/System/Library/Frameworks/OpenGL.framework
Click "Add", and accept the default options in the dialog that appears. It's not always easy to predict where the framework will appear in your "Files" tab of Project Builder, but it belongs in "Frameworks/Linked Frameworks", so drag it in there.
Now we need to add the VTK libraries to the project. Project Builder doesn't create a "Libraries" group, so we will make one ourselves. Select the root "Animoltion" group in the "Files" tab, and choose "New Group" from the "Project" menu. Enter "Libraries" and press return.
To add the libraries, we need to do something which might seem strange. Project Builder doesn't include an "Add Libraries..." command, so we will use the "Add Frameworks..." command from the "Project" menu. Select this menu item, and browse to the ~/Develop/VTKBuild/bin directory where the VTK libraries were compiled. Select the following libraries:
libvtkCommon.a
libvtkFiltering.a
libvtkGraphics.a
libvtkImaging.a
libvtkRendering.a
and click "Add" twice, again accepting the default options in the dialog that appears. If the libraries do not appear in your "Libraries" group, just drag them in there.
|
Related Reading Building Cocoa Applications: A Step by Step Guide |
We are only half way when it comes to importing VTK into our project; we still haven't got the header files. (This is a big advantage of Apple's frameworks: they include both the library binaries and header files in a single bundle.) Create a new group in the "Other Sources" group, and call it "VTK Headers". The problem we now face is that VTK's header files are distributed over multiple directories. This is where Jaguar's new improved finder can be a big help. (On pre-Jaguar versions of Mac OS X, you will probably need to use Sherlock instead.)
Make the Finder active, and choose "Find..." from the File menu. Choose
to search in "Specific Places" from the popup and add the
~/Develop/VTK and ~/Develop/VTKBuild
directories. Search for files whose name contains .h and
vtk. You will need to add a second search criteria by
pressing the "+" button in order to add vtk. Click the
"Search" button. When all the files have been found, select them all, and
drag them to the "VTK Headers" group in your Animoltion project. Click
"Add" in the sheet that appears.
By default, Project Builder sets up your project to use prebinding. Since we didn't compile VTK with any prebinding flags set, we need to turn this off in our project. Click the "Targets" tab, and then select the "Animoltion" target. Click "Linker Settings" under "Settings", and deselect the "Prebind" checkbox.
|
VTK is a cross-platform framework and a good thing too. But this means that things are not necessarily done in the way a Cocoa programmer might expect. So before writing code specific to our molecular viewer, we will need a few general classes to adapt VTK more to the Cocoa way of doing things.
In order to achieve portability, VTK is based on a system of abstract
factories. Usually a VTK programmer
would call the "New" method of the VTK class vtkRenderWindow,
in order to create a new window to render in. A concrete subclass of
vtkRenderWindow, in our case
vtkCocoaRenderWindow, is created by New and returned; but the
programmer doesn't need to know which subclass is used: she just interacts
with the window via the methods of the abstract
vtkRenderWindow class. In this way the code can be made
portable. If the program is compiled and run on a different architecture,
the New method will simply instantiate a different concrete subclass, and
since all interaction with the window occurs via the methods of the
abstract vtkRenderWindow class, the code will work without
modification. Elegant, isn't it?
This is excellent object-oriented design and highly admirable, but we
want to be a bit less admirable here. We want to be able to do things in
the Cocoa way, creating a custom view in Interface Builder, dragging it
into an NSWindow, connecting outlets and setting actions, the way we know
and love. To achieve this, we have to change the implementation of a class
called vtkCocoaWindow; thanks to the dynamism of Objective-C,
we can do this without subclassing, simply by using a category.
|
|
But first I probably should explain what vtkCocoaWindow
is. vtkCocoaWindow is an Objective-C subclass of
NSWindow. When the "New" method of the abstract
vtkRenderWindow class is called, an instance of
vtkCocoaRenderWindow gets created. The vtkCocoaRenderWindow,
which is a C++ object, needs to be able to draw with the Cocoa framework,
so it creates a Cocoa object for this purpose:
vtkCocoaWindow. The vtkCocoaWindow has a single
NSOpenGLView in it, of the subclass
vtkCocoaGLView, and this is where VTK renders using OpenGL.
Phew, that was a mouthful. If this is a bit overwhelming, check out the VTK Cocoa Classes figure, which tells the same thousand words a little more concisely. And if that doesn't help, have a quick lie down and then proceed with the rest of the article, completely ignoring what you just read. If you don't care how it works, just that it does, you don't need to understand this explanation.
vtkCocoaWindow and vtkCocoaGLView are
designed to work in close collaboration. If vtkCocoaWindow
changes size, it resizes its vtkCocoaGLView as well. This is
the way VTK expects things to be done. But what we would prefer is to have
a single NSView subclass, which we can drop in any window in Interface
Builder, and have everything work. So we basically want to keep
vtkCocoaGLView and get rid of
vtkCocoaWindow. Unfortunately, vtkCocoaWindow
does some important stuff, so we can't get rid of it completely. But we
can hide it in the background. We will change the methods of
vtkCocoaWindow that don't fit with our aim to use
vtkCocoaGLView as a standalone class by creating a category.
Due to the dynamism of Objective-C, it is possible to actually change the implementation of a class without subclassing, even without having access to the source code. We do have the source code in this case, but it would be nice not to have to change the official source of VTK, just extend it a bit. For this purpose, we will use a category.
Begin by selecting "New File..." from the File menu, and give it the
name "vtkCocoaWindowModifications". Move the header and implementation
files created to the Classes group. Now include the following code in
vtkCocoaWindowModifications.h
#import <AppKit/AppKit.h>
#import "vtkCocoaWindow.h"
@interface vtkCocoaWindow (CocoaWindowModifications)
- (void)setvtkCocoaGLView:(vtkCocoaGLView *)thevtkCocoaGLView;
- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)proposedFrameSize;
- (BOOL)windowShouldZoom:(NSWindow *)sender toFrame:(NSRect)newFrame;
@end
As you can see, this is a category of the vtkCocoaWindow
class called CocoaWindowModifications. Now add the
implementation for these methods to the
vtkCocoaWindowModifications.m file:
@implementation vtkCocoaWindow (CocoaWindowModifications)
// This has been overloaded to prevent the window making the vtkCocoaGLView its contentView.
- (void)setvtkCocoaGLView:(vtkCocoaGLView *)thevtkCocoaGLView {
myvtkCocoaGLView = thevtkCocoaGLView;
}
// This method, and the next one, have been changed to prevent them trying to
// to resize the render window.
- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)proposedFrameSize {
return proposedFrameSize;
}
- (BOOL)windowShouldZoom:(NSWindow *)sender toFrame:(NSRect)newFrame {
return YES;
}
@end
As indicated by the in-code comments, the implementation of the
setvtkCocoaGLView: has been modified so that it does not
attempt to set the vtkCocoaGLView as its content view. We don't want this
here because our vtkCocoaGLView should behave like any other
NSView. We want to be able to use the view in any NSWindow, or as the
subview of any other NSView, and not be restricted to using it only as the
content view of a vtkCocoaWindow.
The other two methods have been modified so that they no longer send
messages to resize and position the vtkCocoaRenderWindow with
which the vtkCocoaWindow is associated. Our objective is to
transform the vtkCocoaWindow from a view object, which
appears on the screen as a UI element, into a controller object, which is
invisible to the user and whose role is simply to mediate between the
vtkCocoaRenderWindow and vtkCocoaGLView
objects. Because it is no longer a view object, we don't care about its
size, and we don't want it to send geometry-related messages to the
vtkCocoaRenderWindow.
|
Ultimately we would like to have a single, simple NSView subclass for
use in all of our Cocoa/VTK apps. We should be able to instantiate this
class via Interface Builder or programmatically, and it should take care
of setting up any VTK state that it needs to render
itself. vtkCocoaGLView is a nice start, but it wasn't created
with this degree of responsibility in mind. The way VTK is setup by
default, vtkCocoaGLView is actually one of the last classes
instantiated; it doesn't create any other instances itself.
Since vtkCocoaGLView already does most of what we need,
we will just subclass it, and add the extra features we require to create
our all-powerful view class: VTKView. First, create files for
the VTKView class as described above. Before we begin to edit
the new files, we need to change the extension of the
VTKView.m file to .mm. Why?
VTKView will be written in Objective-C++, an Objective-C/C++
hybrid. Once you have changed the file extension, you are free to use C++
code and Objective-C code in the same file. When Project Builder sees the
.mm extension, it knows to engage the Objective-C++
compiler. It's that simple. There are a few restrictions to what you can
do with Objective-C++--probably the most significant is that you cannot
mix the inheritance hierarchies, for example, subclassing a C++ class with
an Objective-C class--but otherwise you are free to mix the two languages
to your heart's content, and that is what we are going to do here.
Back to the job at hand: add the following includes/imports to the
VTKView.h file:
#import <AppKit/AppKit.h>
#import "vtkCocoaGLView.h"
#import "vtkCocoaWindow.h"
#define id Id
#include "vtkRenderer.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#undef id
As you can see, when including VTK C++ headers, such as vtkRenderer.h,
it is necessary to wrap the includes in a
#define/#undef block. The reason for this is
that in Objective-C, "id" is a reserved keyword, and in C++ it is
not. "id" gets used regularly throughout the VTK source code as a variable
name, and this causes the Obj-C++ compiler problems. To avoid this, we
simply redefine "id" to "Id" whenever we include VTK C++ header files.
Add the following interface block to the VTKView.h file:
@interface VTKView : vtkCocoaGLView {
vtkCocoaWindow *_cocoaWindow;
vtkCocoaRenderWindow *_cocoaRenderWindow;
vtkRenderer *_renderer;
vtkCocoaRenderWindowInteractor *_interactor;
}
-(id)initWithFrame:(NSRect)frame;
-(void)dealloc;
// Access to VTK instances
-(vtkRenderer *)renderer;
-(vtkRenderWindow *)renderWindow;
-(vtkRenderWindowInteractor *)renderWindowInteractor;
-(void)removeAllActors;
@end
The VTKView is a subclass of vtkCocoaGLView
and has pointers to all of the other Cocoa specific classes in
VTK. VTKView will instantiate and coordinate these
objects. The interface includes the expected initialization and
deallocation methods, along with accessors for the various VTK objects
required to render a scene in the view. The "removeAllActors" method is a
convenience method which clears the view of "actors", which are basically
the entities that make up a VTK scene.
So what does the implementation of these methods look like? Well, here it is:
#define id Id
#include "vtkRenderer.h"
#include "vtkCocoaRenderWindow.h"
#include "vtkCocoaRenderWindowInteractor.h"
#include "vtkCommand.h"
#include "vtkCamera.h"
#undef id
#import "vtkCocoaWindow.h"
#import "vtkCocoaWindowModifications.h"
#import "VTKView.h"
@implementation VTKView
-(id)initWithFrame:(NSRect)frame {
if ( self = [super initWithFrame:frame] ) {
// Create instances of VTK classes. The vtkCocoaWindow used only passes messages,
// and is not displayed.
_cocoaWindow = [[vtkCocoaWindow alloc] initWithContentRect:frame
styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:YES];
[_cocoaWindow setvtkCocoaGLView:self];
_renderer = vtkRenderer::New();
_cocoaRenderWindow = vtkCocoaRenderWindow::New();
_cocoaRenderWindow->SetWindowId(_cocoaWindow);
_cocoaRenderWindow->AddRenderer(_renderer);
_interactor = vtkCocoaRenderWindowInteractor::New();
_interactor->SetRenderWindow(_cocoaRenderWindow);
[self setVTKRenderWindow:_cocoaRenderWindow];
[self setVTKRenderWindowInteractor:_interactor];
[_cocoaWindow setVTKRenderWindow:_cocoaRenderWindow];
[_cocoaWindow setVTKRenderWindowInteractor:_interactor];
_interactor->Initialize();
}
return self;
}
-(void)dealloc {
_renderer->Delete();
_cocoaRenderWindow->Delete();
_interactor->Delete();
[_cocoaWindow release];
[super dealloc];
}
-(vtkRenderer *)renderer {
return _renderer;
}
-(vtkRenderWindowInteractor *)renderWindowInteractor {
return [self getVTKRenderWindowInteractor];
}
-(vtkRenderWindow *)renderWindow {
return [self getVTKRenderWindow];
}
-(void)removeAllActors {
vtkRenderer *renderer = [self renderer];
if ( ! renderer ) return;
vtkActor *actor;
vtkActorCollection *coll = renderer->GetActors();
coll->RemoveAllItems();
}
@end
Much of this is C++ code related to constructing and destroying the
VTK objects. The initWithFrame: method is the designated
constructor and constructs the various VTK objects, connecting them
together in the correct manner. The VTK objects are generally constructed
using the Abstract Factory design pattern, which involves calling the
New method.
The dealloc method releases the Objective-C members, as
you would expect, but it also sends a Delete method to the VTK C++
objects. The Delete method is not exactly what you might expect: it is
actually analogous to the NSObject release method. VTK, like
Cocoa, uses a reference counting approach to memory management, so the
Delete method does not necessarily cause the messaged object to be
deallocated. The object will only really get deleted when the reference
count drops to zero.
The methods following dealloc are just simple
accessors. renderWindowInteractor and
renderWindow are renamed vtkCocoaGLView methods.
The removeAllActors method is not strictly necessary but
is convenient, so it has been included. It simply removes all
vtkActor objects in the view, providing a clean slate to
build a new scene in.
If you download the code for this article, you will find that several
other methods are included in the VTKView class. These methods all relate
to mouse events and override methods in vtkCocoaGLView. These
are actually bug fixes: You will undoubtedly find that if you don't
include this code, and try to interact with a VTK view by clicking or
dragging your mouse, it will not behave as described in the VTK
documentation. The correct behavior can be recovered by including the
mouse-event methods from the downloaded code.
That's it for part 1. You should now have a VTK build and Project Builder project setup to create that killer visualization app you have been dreaming about. If you can't wait for our next installment, when we will see how you make use of VTK within Cocoa, you can checkout the Kitware site, where VTK is hosted: http://www.kitware.com/. There you will find lots of support for VTK, including documentation and mailing lists. While there you can also purchase the "VTK Users's Guide", which is a very good overview of what you can do with the VTK and how you do it.
Drew McCormack works at the Free University in Amsterdam, and develops the Cocoa shareware Trade Strategist.
Read more Developing for Mac OS X columns.
Return to the Mac DevCenter.
Copyright © 2009 O'Reilly Media, Inc.