macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Developing Visualization Applications with Cocoa and VTK, Part 2
Pages: 1, 2, 3

Creating the Animoltion User Interface

Enough coding for the time being ... let's build some interface. Double-click the MainMenu.nib file in the Resources group to open it in Interface Builder.



We now need to make Interface Builder aware of the classes we have declared in Project Builder, so drag the following header files from Project Builder onto the MainMenu.nib window in Interface Builder: vtkCocoaGLView.h, VTKView.h, and MoleculeView.h.

Before we make use of any of these classes, let's set up our application's window. Click on the application window, which should be visible in Interface Builder. Select Show Info from the Tools menu, and make sure the Attributes item is selected in the Inspector's pop-up button. Fill in the attributes using the figure below as an example.

Screen shot.
How to set the Animoltion window attributes in Interface Builder.

Basically, you should name the window "Animoltion," and ensure that the Close checkbox is not selected. If you like, you can also select the Textured checkbox. This became available in Mac OS X 10.2, and gives the window that brushed-metal look. Apple recommends this look for single-window applications and applications that in some way behave like real-world devices. Since Animoltion is single-window, and behaves a bit like a video recorder, I think it qualifies. Lastly, if you want your window to be the same size as mine, select the Size pop-up item, and fill in a width of 480 and a height of 383.

With the main window prepared, we can begin filling it with controls. Let's start with the MoleculeView, which will contain our 3D scene. From the Cocoa Container Views tab of the Interface Builder palettes window, drag a CustomView onto the Animoltion window. Reposition and resize it so that it looks like the figure of the Animoltion application above.

Now select the new view, and again bring up the Inspector window by choosing Show Info from the Tools menu, if it is not already visible. Select the Custom Class item from the pop-up, and select the MoleculeView class. Now select the Size item from the Inspector's pop-up, and change the Auto-sizing box so that it has springs on the inside and straight lines on the outside, as shown in the figure below. This will mean our MoleculeView will resize if the Animoltion window is resized.

Screen shot.
How to set the auto-sizing attributes of the MoleculeView.

We still need a play button, and a slider to control our mysterious stiffness attribute. Drag a button from Palettes window onto the Animoltion window. Open the inspector for the button and set the attributes according to the figure below. The button should be a rounded bevel button with Toggle behavior. Enter Play for the title and Stop for the alternative title. Also, in the Size panel of the Inspector, set up auto-sizing so that the button is free to move relative to the top and sides of the Animoltion window, but has a fixed distance to the bottom of the window. You do this by ensuring that there are springs on the top and sides, but straight lines everywhere else.

Screen shot.
How to set the Play button attributes in Interface Builder.

Now drag an NSSlider from the Palettes window and position it accordingly. Open the Inspector and fill in a maximum value of 2.0, a minimum value of 0.5, and a current value of 1.0. The slider should have the Continuous attribute, and you can also select Small if you wish. Set the auto-sizing attributes of the slider in the same way as for the Play button.

Before we can say we are finished with Interface Builder we need to connect up our outlets and select our actions. Control-click and drag from the Play button to the MoleculeView, select target in the Inspector window, and select the action togglePlayState:.

Screen shot.
How to set the Play button's action in Interface Builder.

Do the same for the NSSlider, but connect it to the MoleculeView action updateStiffness:. Now control-click and drag from the MoleculeView to the Play button, and select the MoleculeView outlet _playStateButton and press the Connect button. We can now save our changes and return to Project Builder to finish off.

Implementing MoleculeView

We're on the home straight now. A lot of the implementation of MoleculeView relates to the physics of moving the atoms. This is a little technical, and not really important to our objectives here, so we will only quickly glance over these sections, and instead concentrate on more relevant aspects.

Add the following preprocessor directives to the top of MoleculeView.mm:

#import "MoleculeView.h"
#import "Atom.h"

#define id Id
#include "vtkProperty.h"
#include "vtkActor.h"
#include "vtkPolyData.h"
#undef id

static const float ANIMATION_TIME_STEP = 0.05;
static const float PROPAGATION_TIME_STEP = 0.20;
static const unsigned SPHERE_RESOLUTION = 32;

The included VTK classes will be discussed when we make use of them below. The ANIMATION_TIME_STEP constant is the time in seconds between frames in our animation. PROPAGATION_TIME_STEP is related to how the atoms are propagated in time. This is one of those technical aspects to which we will pay little attention. The SPHERE_RESOLUTION constant is the number of latitudinal and longitudinal lines used to draw the spheres in our view. The higher this number is, the smoother our spheres will look, and the more computational resources required to draw them.

The SPHERE_RESOLUTION constant is used in our designated constructor, initWithFrame:, which is the first method on our list of those to add.

@implementation MoleculeView

-(id)initWithFrame:(NSRect)frame {

    if ( self = [super initWithFrame:frame] ) {
        _sphereSource = vtkSphereSource::New();
            _sphereSource->SetThetaResolution(SPHERE_RESOLUTION); 
            _sphereSource->SetPhiResolution(SPHERE_RESOLUTION);
        _sphereMapper = vtkDataSetMapper::New();
            _sphereMapper->SetInput(_sphereSource->GetOutput());
        _playing = NO;
    }
    
    return self;
    
}

This constructor creates two VTK class instances. The vtkSphereSource is a data source object that generates a data set of points on the surface of a sphere. The factory method New() is called first, and then setter methods are called to set the resolution in the latitudinal and longitudinal directions. Note how these setters are indented relative to the New() method. You will see this style of initializing objects used often in VTK.

A vtkMapper in VTK is responsible for mapping data into graphics primitives, such as polygons. vtkDataSetMapper is a subclass of vtkMapper that can be used to map the data set produced by vtkSphereSource. The output of the vtkSphereSource is set to be the input of the vtkDataSetMapper, and later we will connect the output of the vtkDataSetMapper to be the input of another object.

This introduces an important VTK design aspect. VTK utilizes a pipeline architecture: Objects are arranged into pipelines, with the output of one object being assigned as the input of another. This is analogous to building pipelines of UNIX commands, such as

cat file.txt | grep "some string" > output_file.txt

The output of one command is "piped" into another command, which is then piped into another command or file, and so forth. In our example, the data produced by the vtkSphereSource will be fed into the vtkDataSetMapper, which in turn will process the data and output graphics primitives to another object, as we will see later.

The dealloc method calls the Delete() method on these two VTK objects. Recall that the Delete() method is analogous to the Cocoa release method. The destructor also invalidates the animation timer, which we are about to create.

-(void)dealloc {
    [_animationTimer invalidate];
    _sphereSource->Delete();
    _sphereMapper->Delete();
    [super dealloc];
}

We perform most of our initializations in the awakeFromNib method:

-(void)awakeFromNib {    

    _animationTimer = 
        [NSTimer scheduledTimerWithTimeInterval:ANIMATION_TIME_STEP 
             target:self 
             selector:@selector(displayNextFrame) 
             userInfo:nil 
             repeats:YES];
        
    _stiffnessFactor = 1.0;
            
    // Initialize atom positions, and create spheres to 
    // represent atoms
    _atoms[0] = MakeAtom( 0.0, 0.0, 0.0, 0.3, 0.0, 0.0, 0.5 );
    [self addSphereActorWithRadius:_atoms[0].radius red:0.7 
        green:0.0 blue:0.0 alpha:1.0];

    _atoms[1] = MakeAtom( 1.0, 0.0, 0.0, -0.3, 0.0, 0.2, 0.7 );
    [self addSphereActorWithRadius:_atoms[1].radius red:0.0 
        green:0.7 blue:0.0 alpha:1.0];

    _atoms[2] = MakeAtom( 0.0, 1.0, 0.0, 0.0, 0.0, -0.2, 0.5 );
    [self addSphereActorWithRadius:_atoms[2].radius red:0.0 
        green:0.0 blue:0.7 alpha:1.0];
    
    [self updateActors];
    [self setNeedsDisplay:YES];

}

First, a timer is created which fires repeatedly at regular intervals. When it fires it calls the displayNextFrame method, which renders the next frame in our animation. The stiffness factor gets set next, and then we initialize the three atoms in our molecule. The method addSphereWithRadius:red:green:blue:alpha: is also called three times. This method adds a spherical "actor" to our view. An actor in VTK is basically an object in a 3D scene. Each sphere will represent an atom and will be moved according to the changing state of its corresponding atom. The call to the updateActors method simply sets the positions of the spheres according to the current positions of the atoms, synchronizing model and view.

Our MoleculeView class has a couple of actions, which we have already connected up in Interface Builder, so you should have a good idea of what they do.

-(IBAction)togglePlayState:(id)sender {
    _playing = !_playing;
}


-(IBAction)updateStiffness:(id)sender {
    _stiffnessFactor = [sender floatValue];
}

The togglePlayState: action sets the _playing ivar, which determines whether the animation is playing or paused. In the updateStiffness: method, the sender, which is the stiffness slider, is queried for the current stiffness factor, and this is stored in _stiffnessFactor.

The addSphereActorWithRadius:red:green:blue:alpha: performs much of the initialization of VTK objects associated with the MoleculeView class. The following code is thus mostly written in C++.

-(void)addSphereActorWithRadius:(float)radius red:(float)r 
    green:(float)g blue:(float)b alpha:(float)a{

    // Setup actors for atoms
    vtkActor *sphereActor = vtkActor::New();
        sphereActor->SetMapper(_sphereMapper);
        sphereActor->GetProperty()->SetColor(r, g, b);
        sphereActor->GetProperty()->SetOpacity(a);
        sphereActor->GetProperty()->SetInterpolation( VTK_GOURAUD );
        sphereActor->SetScale(radius, radius, radius);    
    [self renderer]->AddActor(sphereActor);
    
    sphereActor->Delete();
    
}

This code adds a spherical actor to the renderer of our MoleculeView. The renderer is the object which renders the actors on the screen using OpenGL. But before that a vtkActor object is created, and its mapper is set to _sphereMapper, which we initialized in the initWithFrame: constructor above. Various properties of the actor are also set here: the vtkActor has a vtkProperty object--which we get using GetProperty()--that contains the attributes of the actor object.

Pages: 1, 2, 3

Next Pagearrow