Mac Research
Mac How To

Cocoa for Scientists (Part XIV): Beginning 3D Visualization

We have now covered the fundamental aspects of Cocoa development in this tutorial series. From here on, we will branch out in many diverse directions, with each tutorial being more stand-alone than the ones preceding this one. I want to begin this exploration by revamping a pair of tutorials (12) that I wrote for MacDevCenter several years ago. In this tutorial, and the next, you will learn how to write a simple visualization application for Mac OS X using the Visualization Toolkit (VTK) and Cocoa.

The Visualization Toolkit (VTK)

VTK is a 3D visualization library written in C++, with interfaces to scripting languages like Tcl and Python. To prepare for this tutorial I recently presented an introduction to installing VTK on Mac OS X. You will need a working copy of the VTK libraries on your system before you can follow this tutorial, so if you missed it the first time, I suggest you go back and follow the steps to installing VTK.

Setting up an Xcode project

Before we actually create an Xcode project, it is best to add the path to the VTK libraries and headers to the Xcode source tree preferences. Any directories declared there can be accessed throughout the rest of Xcode, including in the build settings, via a macro variable.

  1. Start up Xcode.
  2. Choose Xcode > Preferences, and click on the Source Trees tab.
  3. Click the + button under the table to add a new path. Enter ‘vtk-include’ for Setting Name, and for Display Name. In the Path, enter the full path to the directory containing the VTK header files. On my system, this is /Users/cormack/Develop/VTKBin/include/vtk-5.0
  4. Repeat this for a setting called ‘vtk-lib’, setting the path to the lib directory of your VTK installation. On my system, this is /Users/cormack/Develop/VTKBin/lib.

To create an Xcode project, start up Xcode, and choose File > New Project…. Then

  1. In the New Project panel, choose Cocoa Document-based Application in the Application group, and click on the Next button.
  2. Fill in the name ‘Animoltion’ for the project, and choose a directory for the project.
  3. Finally click the Finish button.

Now we need to add the VTK libraries to the project.

  1. Select the Animoltion project in the Groups & Files table on the left.
  2. Choose File > New Group.
  3. Enter the name ‘Libraries’ for the group.
  4. With the Libraries group selected, choose Project > Add to Project…
  5. Navigate to your VTK libraries, and select them all. Click Add.
  6. When presented with the configuration sheet, choose ‘Relative to vtk-lib’ in the Reference Type popup button. Leave the rest of the sheet unchanged, and click the Add button.

We won’t include the VTK header files directly in the project; instead we will simply add the VTK include directory to the header search path. To do that

  1. Double click the Animoltion project icon in the Groups & Files table to open the Project Info panel.
  2. Click the Build tab.
  3. Choose All Configurations from the Configuration popup button.
  4. Find the setting Header Search Paths in the table, and enter $(vtk-include) as the value. Xcode should take your variable and expand it into a path when you press enter if all goes well.
  5. Close the info panel.

VTK on Mac OS X is based on OpenGL, so we need to add the OpenGL framework to our new project.

  1. Open the Frameworks group in Groups & Files, and select Linked Frameworks.
  2. Choose Project > Add to Project…, and browse to /System/Library/Frameworks/OpenGL.framework.
  3. Select it, and click Add.
  4. In the panel, choose Default from the Reference Type popup button, and click the Add button.

Your Xcode project should now be ready to accept some source code.

Writing the BasicVTKView class

VTK already comes with a Cocoa view class — vtkCocoaGLView — that you can render your visualizations in, but we need to customize things a little, so a subclass is in order.

  1. Select the Classes group in Groups & Files, and choose File > New File…
  2. Choose Objective-C class from the Cocoa group in the New File panel, and click Next.
  3. Enter the file name BasicVTKView.mm, and click Finish.

You may be thinking that the .mm extension above is a typo, but actually it tells Xcode that the file in question is an Objective-C++ file. Objective-C++ is a hybrid of Objective-C and C++; you can basically mix the two to your heart’s content, with the restriction that you can’t combine the class inheritance trees of the two languages. A C++ object cannot subclass an Objective-C object, and vice versa.

Now enter the following code in the BasicVTKView.h file. (You will need to select the file in Groups & Files, and possibly drag the editor split view up to see the file content.)

#import <Cocoa/Cocoa.h>

#import "vtkCocoaGLView.h"

#define id Id
#import "vtkRenderer.h"
#undef id

@interface BasicVTKView : vtkCocoaGLView 
{
    vtkRenderer *renderer;
}

- (vtkRenderer *)getRenderer;
- (void)setRenderer:(vtkRenderer *)theRenderer;

@end

I should point out that most of the code I will be presenting is based on a recent rewrite of Animoltion by Marc Baaden. The BasicVTKView class is a subclass of vtkCocoaGLView which is in turn a subclass of the built-in Cocoa class NSOpenGLView. The class has a single instance variable, renderer, which is a C++ object with the class vtkRenderer. The methods declared are just accessors for the renderer.

You may have noticed a bit of witchcraft going on in the import block of BasicVTKView.h. id is defined to be Id, with a capital I, and after the VTK includes, is undefined again. The reason for this is that id is a reserved keyword in Objective-C, but not in C++. Unfortunately, in the VTK code, there are a few places where id is used as a variable, and this confuses the Objective-C compiler. So its a good idea to bracket all VTK imports with these #define/#undef directives in Objective-C++ files.

Now open BasicVTKView.mm, and add the following source code:

#import "BasicVTKView.h"

#define id Id
#import "vtkRenderer.h"
#import "vtkRenderWindow.h"
#import "vtkRenderWindowInteractor.h"
#import "vtkCocoaRenderWindowInteractor.h"
#import "vtkCocoaRenderWindow.h"
#undef id

@implementation BasicVTKView

-(id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        vtkRenderer *ren = vtkRenderer::New();
        vtkRenderWindow *renWin = vtkRenderWindow::New();
        vtkRenderWindowInteractor *renWinInt = vtkRenderWindowInteractor::New();

        renWin->SetWindowId([self window]);
        renWin->SetDisplayId(self);
        renWin->AddRenderer(ren);
        renWinInt->SetRenderWindow(renWin);

        [self setVTKRenderWindow:(vtkCocoaRenderWindow *)renWin];
        [self setRenderer:ren];

        if ( !renWinInt->GetInitialized() ) renWinInt->Initialize();
    }
    return self;
}

-(void)dealloc {
    vtkRenderer *ren = [self getRenderer];
    vtkRenderWindow *renWin = [self getVTKRenderWindow];
    vtkRenderWindowInteractor *renWinInt = [self getInteractor];

    if (ren) ren->Delete();
    if (renWin) renWin->Delete();
    if (renWinInt) renWinInt->Delete();

    [self setRenderer:NULL];
    [self setVTKRenderWindow:NULL];

    [super dealloc];
}

-(vtkRenderer*)getRenderer {
    return renderer;
}

-(void)setRenderer:(vtkRenderer*)theRenderer {
    renderer = theRenderer;
}


@end

The initializer method creates a vtkRenderer, a vtkRenderWindow, and a vtkRenderWindowInteractor.

-(id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        vtkRenderer *ren = vtkRenderer::New();
        vtkRenderWindow *renWin = vtkRenderWindow::New();
        vtkRenderWindowInteractor *renWinInt = vtkRenderWindowInteractor::New();

        renWin->SetWindowId([self window]);
        renWin->SetDisplayId(self);
        renWin->AddRenderer(ren);
        renWinInt->SetRenderWindow(renWin);

        [self setVTKRenderWindow:(vtkCocoaRenderWindow *)renWin];
        [self setRenderer:ren];

        if ( !renWinInt->GetInitialized() ) renWinInt->Initialize();
    }
    return self;
}

These VTK C++ classes are fairly self explanatory: The vtkRenderer is what renders the 3D scene in OpenGL; the vtkRenderWindow is simply the window in which the rendering takes place; and the vtkRenderWindowInteractor takes care of any interaction with the view, such as mouse clicks and keyboard presses.

VTK uses an Factory design pattern to create new objects. Instead of using the usual C++ new operator to create a new object, you call the class method New. The New method will select a subclass suitable for the environment that VTK is running in. For example, the call to vtkRenderWindow::New() will actually return an object of the class vtkCocoaRenderWindow in this case; vtkCocoaRenderWindow is a subclass of vtkRenderWindow for use with a Cocoa window. If you were to use VTK on Windows or with Carbon, you would get an object of a different concrete subclass. In this way, VTK abstracts the interface of a render window from its implementation, achieving cross-platform compatibility. This is nice object-oriented design.

The rest of the initWithFrame: method sets the relationships between the various VTK objects, the BasicVTKView object, and the Cocoa window containing the view. Finally, it checks if the vtkRenderWindowInteractor has been initialized already, and if not, initializes it, so that it can start receiving events.

The dealloc method breaks the whole stack of cards down again, as you would expect:

-(void)dealloc {
    vtkRenderer *ren = [self getRenderer];
    vtkRenderWindow *renWin = [self getVTKRenderWindow];
    vtkRenderWindowInteractor *renWinInt = [self getInteractor];

    if (ren) ren->Delete();
    if (renWin) renWin->Delete();
    if (renWinInt) renWinInt->Delete();

    [self setRenderer:NULL];
    [self setVTKRenderWindow:NULL];

    [super dealloc];
}

The Delete method may surprise you, because it is actually equivalent release in Cocoa. VTK uses reference counting to govern object lifetime, just like Cocoa, and Delete simply decrements the retain count of the object. When it reaches zero the object will get deleted.

Exit mobile version