O'Reilly logo

iPhone 3D Programming by Philip Rideout

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

HelloArrow with Fixed Function

In the previous section, you learned your way around the development environment with Apple’s boilerplate OpenGL application, but to get a good understanding of the fundamentals, you need to start from scratch. This section of the book builds a simple application from the ground up using OpenGL ES 1.1. The 1.x track of OpenGL ES is sometimes called fixed-function to distinguish it from the OpenGL ES 2.0 track, which relies on shaders. We’ll learn how to modify the sample to use shaders later in the chapter.

Let’s come up with a variation of the classic Hello World in a way that fits well with the theme of this book. As you’ll learn later, most of what gets rendered in OpenGL can be reduced to triangles. We can use two overlapping triangles to draw a simple arrow shape, as shown in Figure 1-4. Any resemblance to the Star Trek logo is purely coincidental.

Arrow shape composed from two triangles

Figure 1-4. Arrow shape composed from two triangles

To add an interesting twist, the program will make the arrow stay upright when the user changes the orientation of his iPhone.

Layering Your 3D Application

If you love Objective-C, then by all means, use it everywhere you can. This book supports cross-platform code reuse, so we leverage Objective-C only when necessary. Figure 1-5 depicts a couple ways of organizing your application code such that the guts of the program are written in C++ (or vanilla C), while the iPhone-specific glue is written in Objective-C. The variation on the right separates the application engine (also known as game logic) from the rendering engine. Some of the more complex samples in this book take this approach.

Layered 3D iPhone applications

Figure 1-5. Layered 3D iPhone applications

The key to either approach depicted in Figure 1-6 is designing a robust interface to the rendering engine and ensuring that any platform can use it. The sample code in this book uses the name IRenderingEngine for this interface, but you can call it what you want.

A cross-platform OpenGL ES application

Figure 1-6. A cross-platform OpenGL ES application

The IRenderingEngine interface also allows you to build multiple rendering engines into your application, as shown in Figure 1-7. This facilitates the “Use ES 2.0 if supported, otherwise fall back” scenario mentioned in Choosing the Appropriate Version of OpenGL ES. We’ll take this approach for HelloArrow.

An iPhone application that supports ES 1.1 and 2.0

Figure 1-7. An iPhone application that supports ES 1.1 and 2.0

You’ll learn more about the pieces in Figure 1-7 as we walk through the code to HelloArrow. To summarize, you’ll be writing three classes:

RenderingEngine1 and RenderingEngine2 (portable C++)

These classes are where most of the work takes place; all calls to OpenGL ES are made from here. RenderingEngine1 uses ES 1.1, while RenderingEngine2 uses ES 2.0.

HelloArrowAppDelegate (Objective-C)

Small Objective-C class that derives from NSObject and adopts the UIApplicationDelegate protocol. (“Adopting a protocol” in Objective-C is somewhat analogous to “implementing an interface” in languages such as Java or C#.) This does not use OpenGL or EAGL; it simply initializes the GLView object and releases memory when the application closes.

GLView (Objective-C)

Derives from the standard UIView class and uses EAGL to instance a valid rendering surface for OpenGL.

Starting from Scratch

Launch Xcode and start with the simplest project template by going to FileNew Project and selecting Window-Based Application from the list of iPhone OS application templates. Name it HelloArrow.

Xcode comes bundled with an application called Interface Builder, which is Apple’s interactive designer for building interfaces with UIKit (and AppKit on Mac OS X). I don’t attempt to cover UIKit because most 3D applications do not make extensive use of it. For best performance, Apple advises against mixing UIKit with OpenGL.

Note

For simple 3D applications that aren’t too demanding, it probably won’t hurt you to add some UIKit controls to your OpenGL view. We cover this briefly in Mixing OpenGL ES and UIKit.

Linking in the OpenGL and Quartz Libraries

In the world of Apple programming, you can think of a framework as being similar to a library, but technically it’s a bundle of resources. A bundle is a special type of folder that acts like a single file, and it’s quite common in Mac OS X. For example, applications are usually deployed as bundles—open the action menu on nearly any icon in your Applications folder, and you’ll see an option for Show Package Contents, which allows you to get past the façade.

You need to add some framework references to your Xcode project. Pull down the action menu for the Frameworks folder. This can be done by selecting the folder and clicking the Action icon or by right-clicking or Control-clicking the folder. Next choose AddExisting Frameworks. Select OpenGLES.Framework, and click the Add button. You may see a dialog after this; if so, simply accept its defaults. Now, repeat this procedure with QuartzCore.Framework.

Note

Why do we need Quartz if we’re writing an OpenGL ES application? The answer is that Quartz owns the layer object that gets presented to the screen, even though we’re rendering with OpenGL. The layer object is an instance of CAEGLLayer, which is a subclass of CALayer; these classes are defined in the Quartz Core framework.

Subclassing UIView

The abstract UIView class controls a rectangular area of the screen, handles user events, and sometimes serves as a container for child views. Almost all standard controls such as buttons, sliders, and text fields are descendants of UIView. We tend to avoid using these controls in this book; for most of the sample code, the UI requirements are so modest that OpenGL itself can be used to render simple buttons and various widgets.

For our HelloArrow sample, we do need to define a single UIView subclass, since all rendering on the iPhone must take place within a view. Select the Classes folder in Xcode, click the Action icon in the toolbar, and select AddNew file. Under the Cocoa Touch Class category, select the Objective-C class template, and choose UIView in the Subclass of menu. In the next dialog, name it GLView.mm, and leave the box checked to ensure that the corresponding header gets generated. The .mm extension indicates that this file can support C++ in addition to Objective-C. Open GLView.h. You should see something like this:

#import <UIKit/UIKit.h>

@interface GLView : UIView {
}

@end

For C/C++ veterans, this syntax can be a little jarring—just wait until you see the syntax for methods! Fear not, it’s easy to become accustomed to.

#import is almost the same thing as #include but automatically ensures that the header file does not get expanded twice within the same source file. This is similar to the #pragma once feature found in many C/C++ compilers.

Keywords specific to Objective-C stand out because of the @ prefix. The @interface keyword marks the beginning of a class declaration; the @end keyword marks the end of a class declaration. A single source file may contain several class declarations and therefore can have several @interface blocks.

As you probably already guessed, the previous code snippet simply declares an empty class called GLView that derives from UIView. What’s less obvious is that data fields will go inside the curly braces, while method declarations will go between the ending curly brace and the @end, like this:

#import <UIKit/UIKit.h>

@interface GLView : UIView {
    // Protected fields go here...
}    

// Public methods go here...

@end

By default, data fields have protected accessibility, but you can make them private using the @private keyword. Let’s march onward and fill in the pieces shown in bold in Example 1-1. We’re also adding some new #imports for OpenGL-related stuff.

Example 1-1. GLView class declaration

#import <UIKit/UIKit.h>
#import <OpenGLES/EAGL.h>
#import <QuartzCore/QuartzCore.h>
#import <OpenGLES/ES1/gl.h>
#import <OpenGLES/ES1/glext.h>

@interface GLView : UIView {
    EAGLContext* m_context;
}

 - (void) drawView;

@end

The m_context field is a pointer to the EAGL object that manages our OpenGL context. EAGL is a small Apple-specific API that links the iPhone operating system with OpenGL.

Note

Every time you modify API state through an OpenGL function call, you do so within a context. For a given thread running on your system, only one context can be current at any time. With the iPhone, you’ll rarely need more than one context for your application. Because of the limited resources on mobile devices, I don’t recommend using multiple contexts.

If you have a C/C++ background, the drawView method declaration in Example 1-1 may look odd. It’s less jarring if you’re familiar with UML syntax, but UML uses - and + to denote private and public methods, respectively; with Objective-C, - and + denote instance methods and class methods. (Class methods in Objective-C are somewhat similar to C++ static methods, but in Objective-C, the class itself is a proper object.)

Take a look at the top of the GLView.mm file that Xcode generated. Everything between @implementation and @end is the definition of the GLView class. Xcode created three methods for you: initWithFrame, drawRect (which may be commented out), and dealloc. Note that these methods do not have declarations in the header file that Xcode generated. In this respect, an Objective-C method is similar to a plain old function in C; it needs a forward declaration only if gets called before it’s defined. I usually declare all methods in the header file anyway to be consistent with C++ class declarations.

Take a closer look at the first method in the file:

- (id) initWithFrame: (CGRect) frame
{
   if (self = [super initWithFrame:frame]) {
       // Initialize code...
   }
   return self;
}

This is an Objective-C initializer method, which is somewhat analogous to a C++ constructor. The return type and argument types are enclosed in parentheses, similar to C-style casting syntax. The conditional in the if statement accomplishes several things at once: it calls the base implementation of initWithFrame, assigns the object’s pointer to self, and checks the result for success.

In Objective-C parlance, you don’t call methods on objects; you send messages to objects. The square bracket syntax denotes a message. Rather than a comma-separated list of values, arguments are denoted with a whitespace-separated list of name-value pairs. The idea is that messages can vaguely resemble English sentences. For example, consider this statement, which adds an element to an NSMutableDictionary:

[myDictionary setValue: 30 forKey: @"age"];

If you read the argument list aloud, you get an English sentence! Well, sort of.

That’s enough of an Objective-C lesson for now. Let’s get back to the HelloArrow application. In GLView.mm, provide the implementation to the layerClass method by adding the following snippet after the @implementation line:

+ (Class) layerClass
{
    return [CAEAGLLayer class];
}

This simply overrides the default implementation of layerClass to return an OpenGL-friendly layer type. The class method is similar to the typeof operator found in other languages; it returns an object that represents the type itself, rather than an instance of the type.

Note

The + prefix means that this is an override of a class method rather than an instance method. This type of override is a feature of Objective-C rarely found in other languages.

Now, go back to initWithFrame, and replace the contents of the if block with some EAGL initialization code, as shown in Example 1-2.

Example 1-2. EAGL initialization in GLView.mm

- (id) initWithFrame: (CGRect) frame
{
    if (self = [super initWithFrame:frame]) {
        CAEAGLLayer* eaglLayer = (CAEAGLLayer*) super.layer; 1
        eaglLayer.opaque = YES; 2

        m_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1]; 3

        if (!m_context || ![EAGLContext setCurrentContext:m_context]) { 4
            [self release];
            return nil;5
        }

        // Initialize code...
    }
    return self;
}

Here’s what’s going on:

1

Retrieve the layer property from the base class (UIView), and downcast it from CALayer into a CAEAGLLayer. This is safe because of the override to the layerClass method.

2

Set the opaque property on the layer to indicate that you do not need Quartz to handle transparency. This is a performance benefit that Apple recommends in all OpenGL programs. Don’t worry, you can easily use OpenGL to handle alpha blending.

3

Create an EAGLContext object, and tell it which version of OpenGL you need, which is ES 1.1.

4

Tell the EAGLContext to make itself current, which means any subsequent OpenGL calls in this thread will be tied to it.

5

If context creation fails or if setCurrentContext fails, then poop out and return nil.

Next, continue filling in the initialization code with some OpenGL setup. Replace the OpenGL Initialization comment with Example 1-3.

Example 1-3. OpenGL initialization in GLView.mm

GLuint framebuffer, renderbuffer;
glGenFramebuffersOES(1, &framebuffer);
glGenRenderbuffersOES(1, &renderbuffer);

glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, renderbuffer);

[m_context
    renderbufferStorage:GL_RENDERBUFFER_OES
    fromDrawable: eaglLayer];

glFramebufferRenderbufferOES(
    GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_OES,
    GL_RENDERBUFFER_OES, renderbuffer);

glViewport(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame));

[self drawView];

Example 1-3 starts off by generating two OpenGL identifiers, one for a renderbuffer and one for a framebuffer. Briefly, a renderbuffer is a 2D surface filled with some type of data (in this case, color), and a framebuffer is a bundle of renderbuffers. You’ll learn more about framebuffer objects (FBOs) in later chapters.

Note

The use of FBOs is an advanced feature that is not part of the core OpenGL ES 1.1 API, but it is specified in an OpenGL extension that all iPhones support. In OpenGL ES 2.0, FBOs are included in the core API. It may seem odd to use this advanced feature in the simple HelloArrow program, but all OpenGL iPhone applications need to leverage FBOs to draw anything to the screen.

The renderbuffer and framebuffer are both of type GLuint, which is the type that OpenGL uses to represent various objects that it manages. You could just as easily use unsigned int in lieu of GLuint, but I recommend using the GL-prefixed types for objects that get passed to the API. If nothing else, the GL-prefixed types make it easier for humans to identify which pieces of your code interact with OpenGL.

After generating identifiers for the framebuffer and renderbuffer, Example 1-3 then binds these objects to the pipeline. When an object is bound, it can be modified or consumed by subsequent OpenGL operations. After binding the renderbuffer, storage is allocated by sending the renderbufferStorage message to the EAGLContext object.

Note

For an off-screen surface, you would use the OpenGL command glRenderbufferStorage to perform allocation, but in this case you’re associating the renderbuffer with an EAGL layer. You’ll learn more about off-screen surfaces later in this book.

Next, the glFramebufferRenderbufferOES command is used to attach the renderbuffer object to the framebuffer object.

After this, the glViewport command is issued. You can think of this as setting up a coordinate system. In Chapter 2 you’ll learn more precisely what’s going on here.

The final call in Example 1-3 is to the drawView method. Go ahead and create the drawView implementation:

- (void) drawView
{
    glClearColor(0.5f, 0.5f, 0.5f, 1);
    glClear(GL_COLOR_BUFFER_BIT);

    [m_context presentRenderbuffer:GL_RENDERBUFFER_OES];
}

This uses OpenGL’s “clear” mechanism to fill the buffer with a solid color. First the color is set to gray using four values (red, green, blue, alpha). Then, the clear operation is issued. Finally, the EAGLContext object is told to present the renderbuffer to the screen. Rather than drawing directly to the screen, most OpenGL programs render to a buffer that is then presented to the screen in an atomic operation, just like we’re doing here.

You can remove the drawRect stub that Xcode provided for you. The drawRect method is typically used for a “paint refresh” in more traditional UIKit-based applications; in 3D applications, you’ll want finer control over when rendering occurs.

At this point, you almost have a fully functioning OpenGL ES program, but there’s one more loose end to tie up. You need to clean up when the GLView object is destroyed. Replace the definition of dealloc with the following:

- (void) dealloc
{
    if ([EAGLContext currentContext] == m_context)
        [EAGLContext setCurrentContext:nil];

    [m_context release];  
    [super dealloc];
}

You can now build and run the program, but you won’t even see the gray background color just yet. This brings us to the next step: hooking up the application delegate.

Hooking Up the Application Delegate

The application delegate template (HelloArrowAppDelegate.h) that Xcode provided contains nothing more than an instance of UIWindow. Let’s add a pointer to an instance of the GLView class along with a couple method declarations (new/changed lines are shown in bold):

#import <UIKit/UIKit.h>
#import "GLView.h"

@interface HelloArrowAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow* m_window;
    GLView* m_view;
}

@property (nonatomic, retain) IBOutlet UIWindow *m_window;

@end

If you performed the instructions in Optional: Creating a Clean Slate, you won’t see the @property line, which is fine. Interface Builder leverages Objective-C’s property mechanism to establish connections between objects, but we’re not using Interface Builder or properties in this book. In brief, the @property keyword declares a property; the @synthesize keyword defines accessor methods.

Note that the Xcode template already had a window member, but I renamed it to m_window. This is in keeping with the coding conventions that we use throughout this book.

Note

I recommend using Xcode’s Refactor feature to rename this variable because it will also rename the corresponding property (if it exists). Simply right-click the window variable and choose Refactor. If you did not make the changes shown in Optional: Creating a Clean Slate, you must use Refactor so that the xib file knows the window is now represented by m_window.

Now open the corresponding HelloArrowAppDelegate.m file. Xcode already provided skeleton implementations for applicationDidFinishLaunching and dealloc as part of the Window-Based Application template that we selected to create our project.

Note

Since you need this file to handle both Objective-C and C++, you must rename the extension to .mm. Right-click the file to bring up the action menu, and then select Rename.

Flesh out the file as shown in Example 1-4.

Example 1-4. HelloArrowAppDelegate.mm

#import "HelloArrowAppDelegate.h"
#import <UIKit/UIKit.h>
#import "GLView.h"

@implementation HelloArrowAppDelegate

- (BOOL) application: (UIApplication*) application
         didFinishLaunchingWithOptions: (NSDictionary*) launchOptions    
{
    CGRect screenBounds = [[UIScreen mainScreen] bounds];

    m_window = [[UIWindow alloc] initWithFrame: screenBounds];
    m_view = [[GLView alloc] initWithFrame: screenBounds];

    [m_window addSubview: m_view];
    [m_window makeKeyAndVisible];
    return YES;
}

- (void) dealloc
{
    [m_view release];
    [m_window release];
    [super dealloc];
}

@end

Example 1-4 uses the alloc-init pattern to construct the window and view objects, passing in the bounding rectangle for the entire screen.

If you haven’t removed the Interface Builder bits as described in Optional: Creating a Clean Slate, you’ll need to make a couple changes to the previous code listing:

  • Add a new line after @implementation:

    @synthesize m_window;

    As mentioned previously, the @synthesize keyword defines a set of property accessors, and Interface Builder uses properties to hook things up.

  • Remove the line that constructs m_window. Interface Builder has a special way of constructing the window behind the scenes. (Leave in the calls to makeKeyAndVisible and release.)

Compile and build, and you should now see a solid gray screen. Hooray!

Setting Up the Icons and Launch Image

To set a custom launch icon for your application, create a 57×57 PNG file (72×72 for the iPad), and add it to your Xcode project in the Resources folder. If you refer to a PNG file that is not in the same location as your project folder, Xcode will copy it for you; be sure to check the box labeled “Copy items into destination group’s folder (if needed)” before you click Add. Then, open the HelloArrow-Info.plist file (also in the Resources folder), find the Icon file property, and enter the name of your PNG file.

The iPhone will automatically give your icon rounded corners and a shiny overlay. If you want to turn this feature off, find the HelloArrow-Info.plist file in your Xcode project, select the last row, click the + button, and choose Icon already includes gloss and bevel effects from the menu. Don’t do this unless you’re really sure of yourself; Apple wants users to have a consistent look in SpringBoard (the built-in program used to launch apps).

In addition to the 57×57 launch icon, Apple recommends that you also provide a 29×29 miniature icon for the Spotlight search and Settings screen. The procedure is similar except that the filename must be Icon-Small.png, and there’s no need to modify the .plist file.

For the splash screen, the procedure is similar to the small icon, except that the filename must be Default.png and there’s no need to modify the .plist file. The iPhone fills the entire screen with your image, so the ideal size is 320×480, unless you want to see an ugly stretchy effect. Apple’s guidelines say that this image isn’t a splash screen at all but a “launch image” whose purpose is to create a swift and seamless startup experience. Rather than showing a creative logo, Apple wants your launch image to mimic the starting screen of your running application. Of course, many applications ignore this rule!

Dealing with the Status Bar

Even though your application fills the renderbuffer with gray, the iPhone’s status bar still appears at the top of the screen. One way of dealing with this would be adding the following line to didFinishLaunchingWithOptions:

[application setStatusBarHidden: YES withAnimation: UIStatusBarAnimationNone];

The problem with this approach is that the status bar does not hide until after the splash screen animation. For HelloArrow, let’s remove the pesky status bar from the very beginning. Find the HelloArrowInfo.plist file in your Xcode project, and add a new property by selecting the last row, clicking the + button, choosing “Status bar is initially hidden” from the menu, and checking the box.

Of course, for some applications, you’ll want to keep the status bar visible—after all, the user might want to keep an eye on battery life and connectivity status! If your application has a black background, you can add a Status bar style property and select the black style. For nonblack backgrounds, the semitransparent style often works well.

Defining and Consuming the Rendering Engine Interface

At this point, you have a walking skeleton for HelloArrow, but you still don’t have the rendering layer depicted in Figure 1-7. Add a file to your Xcode project to define the C++ interface. Right-click the Classes folder, and choose AddNew file, select C and C++, and choose Header File. Call it IRenderingEngine.hpp. The .hpp extension signals that this is a pure C++ file; no Objective-C syntax is allowed.[2] Replace the contents of this file with Example 1-5.

Example 1-5. IRenderingEngine.hpp

// Physical orientation of a handheld device, equivalent to UIDeviceOrientation.
enum DeviceOrientation {
    DeviceOrientationUnknown,
    DeviceOrientationPortrait,
    DeviceOrientationPortraitUpsideDown,
    DeviceOrientationLandscapeLeft,
    DeviceOrientationLandscapeRight,
    DeviceOrientationFaceUp,
    DeviceOrientationFaceDown,
};

// Creates an instance of the renderer and sets up various OpenGL state.
struct IRenderingEngine* CreateRenderer1();

// Interface to the OpenGL ES renderer; consumed by GLView.
struct IRenderingEngine {
    virtual void Initialize(int width, int height) = 0;    
    virtual void Render() const = 0;
    virtual void UpdateAnimation(float timeStep) = 0;
    virtual void OnRotate(DeviceOrientation newOrientation) = 0;
    virtual ~IRenderingEngine() {}
};

It seems redundant to include an enumeration for device orientation when one already exists in an iPhone header (namely, UIDevice.h), but this makes the IRenderingEngine interface portable to other environments.

Since the view class consumes the rendering engine interface, you need to add an IRenderingEngine pointer to the GLView class declaration, along with some fields and methods to help with rotation and animation. Example 1-6 shows the complete class declaration. New fields and methods are shown in bold. Note that we removed the two OpenGL ES 1.1 #imports; these OpenGL calls are moving to the RenderingEngine1 class. The EAGL header is not part of the OpenGL standard, but it’s required to create the OpenGL ES context.

Example 1-6. GLView.h

#import "IRenderingEngine.hpp"
#import <OpenGLES/EAGL.h>
#import <QuartzCore/QuartzCore.h>

@interface GLView : UIView {
@private
    EAGLContext* m_context;
    IRenderingEngine* m_renderingEngine;
    float m_timestamp;
}

- (void) drawView: (CADisplayLink*) displayLink;
- (void) didRotate: (NSNotification*) notification;

@end

Example 1-7 is the full listing for the class implementation. Calls to the rendering engine are highlighted in bold. Note that GLView no longer contains any OpenGL calls; we’re delegating all OpenGL work to the rendering engine.

Example 1-7. GLView.mm

#import <OpenGLES/EAGLDrawable.h>
#import "GLView.h"
#import "mach/mach_time.h"
#import <OpenGLES/ES2/gl.h> // <-- for GL_RENDERBUFFER only

@implementation GLView

+ (Class) layerClass
{
    return [CAEAGLLayer class];
}

- (id) initWithFrame: (CGRect) frame
{
    if (self = [super initWithFrame:frame]) {
        CAEAGLLayer* eaglLayer = (CAEAGLLayer*) super.layer;
        eaglLayer.opaque = YES;

        m_context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES1];

        if (!m_context || ![EAGLContext setCurrentContext:m_context]) {
            [self release];
            return nil;
        }

        m_renderingEngine = CreateRenderer1();        

        [m_context
            renderbufferStorage:GL_RENDERBUFFER
            fromDrawable: eaglLayer];
        
        m_renderingEngine->Initialize(CGRectGetWidth(frame), CGRectGetHeight(frame));

        [self drawView: nil];
        m_timestamp = CACurrentMediaTime();

        CADisplayLink* displayLink;
        displayLink = [CADisplayLink displayLinkWithTarget:self
                                     selector:@selector(drawView:)];
        
        [displayLink addToRunLoop:[NSRunLoop currentRunLoop]
                     forMode:NSDefaultRunLoopMode];

        [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
        
        [[NSNotificationCenter defaultCenter]
            addObserver:self
            selector:@selector(didRotate:)
            name:UIDeviceOrientationDidChangeNotification
            object:nil];
    }
    return self;
}

- (void) didRotate: (NSNotification*) notification
{
    UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
    m_renderingEngine->OnRotate((DeviceOrientation) orientation);
    [self drawView: nil];
}

- (void) drawView: (CADisplayLink*) displayLink
{
    if (displayLink != nil) {
        float elapsedSeconds = displayLink.timestamp - m_timestamp;
        m_timestamp = displayLink.timestamp;
        m_renderingEngine->UpdateAnimation(elapsedSeconds);
    }

    m_renderingEngine->Render();
    [m_context presentRenderbuffer:GL_RENDERBUFFER];
}

@end

This completes the Objective-C portion of the project, but it won’t build yet because you still need to implement the rendering engine. There’s no need to dissect all the code in Example 1-7, but a brief summary follows:

  • The initWithFrame method calls the factory method to instantiate the C++ renderer. It also sets up two event handlers. One is for the “display link,” which fires every time the screen refreshes. The other event handler responds to orientation changes.

  • The didRotate event handler casts the iPhone-specific UIDeviceOrientation to our portable DeviceOrientation type and then passes it on to the rendering engine.

  • The drawView method, called in response to a display link event, computes the elapsed time since it was last called and passes that value into the renderer’s UpdateAnimation method. This allows the renderer to update any animations or physics that it might be controlling.

  • The drawView method also issues the Render command and presents the renderbuffer to the screen.

Note

At the time of writing, Apple recommends CADisplayLink for triggering OpenGL rendering. An alternative strategy is leveraging the NSTimer class. CADisplayLink became available with iPhone OS 3.1, so if you need to support older versions of the iPhone OS, take a look at NSTimer in the documentation.

Implementing the Rendering Engine

In this section, you’ll create an implementation class for the IRenderingEngine interface. Right-click the Classes folder, choose AddNew file, click the C and C++ category, and select the C++ File template. Call it RenderingEngine1.cpp, and deselect the “Also create RenderingEngine1.h” option, since you’ll declare the class directly within the .cpp file. Enter the class declaration and factory method shown in Example 1-8.

Example 1-8. RenderingEngine1 class and factory method

#include <OpenGLES/ES1/gl.h>
#include <OpenGLES/ES1/glext.h>
#include "IRenderingEngine.hpp"

class RenderingEngine1 : public IRenderingEngine {
public:
    RenderingEngine1();
    void Initialize(int width, int height);
    void Render() const;
    void UpdateAnimation(float timeStep) {}
    void OnRotate(DeviceOrientation newOrientation) {}
private:
    GLuint m_framebuffer;
    GLuint m_renderbuffer;
};

IRenderingEngine* CreateRenderer1()
{
    return new RenderingEngine1();
}

For now, UpdateAnimation and OnRotate are implemented with stubs; you’ll add support for the rotation feature after we get up and running.

Example 1-9 shows more of the code from RenderingEngine1.cpp with the OpenGL initialization code.

Example 1-9. Vertex data and RenderingEngine construction

struct Vertex {
    float Position[2];
    float Color[4];
};

// Define the positions and colors of two triangles.
const Vertex Vertices[] = {
    {{-0.5, -0.866}, {1, 1, 0.5f, 1}},
    {{0.5, -0.866},  {1, 1, 0.5f, 1}},
    {{0, 1},         {1, 1, 0.5f, 1}},
    {{-0.5, -0.866}, {0.5f, 0.5f, 0.5f}},
    {{0.5, -0.866},  {0.5f, 0.5f, 0.5f}},
    {{0, -0.4f},     {0.5f, 0.5f, 0.5f}},
};

RenderingEngine1::RenderingEngine1()
{
    glGenRenderbuffersOES(1, &m_renderbuffer);
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_renderbuffer);
}

void RenderingEngine1::Initialize(int width, int height)
{
    // Create the framebuffer object and attach the color buffer.
    glGenFramebuffersOES(1, &m_framebuffer);
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_framebuffer);
    glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES,
                                 GL_COLOR_ATTACHMENT0_OES,
                                 GL_RENDERBUFFER_OES,
                                 m_renderbuffer);
    
    glViewport(0, 0, width, height);

    glMatrixMode(GL_PROJECTION);

    // Initialize the projection matrix.
    const float maxX = 2;
    const float maxY = 3;
    glOrthof(-maxX, +maxX, -maxY, +maxY, -1, 1);

    glMatrixMode(GL_MODELVIEW);
}

Example 1-9 first defines a POD type (plain old data) that represents the structure of each vertex that makes up the triangles. As you’ll learn in the chapters to come, a vertex in OpenGL can be associated with a variety of attributes. HelloArrow requires only two attributes: a 2D position and an RGBA color.

In more complex OpenGL applications, the vertex data is usually read from an external file or generated on the fly. In this case, the geometry is so simple that the vertex data is defined within the code itself. Two triangles are specified using six vertices. The first triangle is yellow, the second gray (see Figure 1-4, shown earlier).

Next, Example 1-9 divides up some framebuffer initialization work between the constructor and the Initialize method. Between instancing the rendering engine and calling Initialize, the caller (GLView) is responsible for allocating the renderbuffer’s storage. Allocation of the renderbuffer isn’t done with the rendering engine because it requires Objective-C.

Last but not least, Initialize sets up the viewport transform and projection matrix. The projection matrix defines the 3D volume that contains the visible portion of the scene. This will be explained in detail in the next chapter.

To recap, here’s the startup sequence:

  1. Generate an identifier for the renderbuffer, and bind it to the pipeline.

  2. Allocate the renderbuffer’s storage by associating it with an EAGL layer. This has to be done in the Objective-C layer.

  3. Create a framebuffer object, and attach the renderbuffer to it.

  4. Set up the vertex transformation state with glViewport and glOrthof.

Example 1-10 contains the implementation of the Render method.

Example 1-10. Initial Render implementation

void RenderingEngine1::Render() const
{
    glClearColor(0.5f, 0.5f, 0.5f, 1);
    glClear(GL_COLOR_BUFFER_BIT);1

    glEnableClientState(GL_VERTEX_ARRAY);2
    glEnableClientState(GL_COLOR_ARRAY);
    
    glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &Vertices[0].Position[0]);3
    glColorPointer(4, GL_FLOAT, sizeof(Vertex), &Vertices[0].Color[0]);

    GLsizei vertexCount = sizeof(Vertices) / sizeof(Vertex);
    glDrawArrays(GL_TRIANGLES, 0, vertexCount);4
    
    glDisableClientState(GL_VERTEX_ARRAY);5
    glDisableClientState(GL_COLOR_ARRAY);
}

We’ll examine much of this in the next chapter, but briefly here’s what’s going on:

1

Clear the renderbuffer to gray.

2

Enable two vertex attributes (position and color).

3

Tell OpenGL how to fetch the data for the position and color attributes. We’ll examine these in detail later in the book; for now, see Figure 1-8.

4

Execute the draw command with glDrawArrays, specifying GL_TRIANGLES for the topology, 0 for the starting vertex, and vertexCount for the number of vertices. This function call marks the exact time that OpenGL fetches the data from the pointers specified in the preceding gl*Pointer calls; this is also when the triangles are actually rendered to the target surface.

5

Disable the two vertex attributes; they need to be enabled only during the preceding draw command. It’s bad form to leave attributes enabled because subsequent draw commands might want to use a completely different set of vertex attributes. In this case, we could get by without disabling them because the program is so simple, but it’s a good habit to follow.

Interleaved arrays

Figure 1-8. Interleaved arrays

Congratulations, you created a complete OpenGL program from scratch! Figure 1-9 shows the result.

HelloArrow!

Figure 1-9. HelloArrow!

Handling Device Orientation

Earlier in the chapter, I promised you would learn how to rotate the arrow in response to an orientation change. Since you already created the listener in the UIView class in Example 1-7, all that remains is handling it in the rendering engine.

First add a new floating-point field to the RenderingEngine class called m_currentAngle. This represents an angle in degrees, not radians. Note the changes to UpdateAnimation and OnRotate (they are no longer stubs and will be defined shortly).

class RenderingEngine1 : public IRenderingEngine {
public:
    RenderingEngine1();
    void Initialize(int width, int height);
    void Render() const;
    void UpdateAnimation(float timeStep);
    void OnRotate(DeviceOrientation newOrientation);
private:
    float m_currentAngle;
    GLuint m_framebuffer;
    GLuint m_renderbuffer;
};

Now let’s implement the OnRotate method as follows:

void RenderingEngine1::OnRotate(DeviceOrientation orientation)
{
    float angle = 0;
    
    switch (orientation) {
        case DeviceOrientationLandscapeLeft:
            angle = 270;
            break;

        case DeviceOrientationPortraitUpsideDown:
            angle = 180;
            break;

        case DeviceOrientationLandscapeRight:
            angle = 90;
            break;
    }
    
    m_currentAngle = angle;
}

Note that orientations such as Unknown, Portrait, FaceUp, and FaceDown are not included in the switch statement, so the angle defaults to zero in those cases.

Now you can rotate the arrow using a call to glRotatef in the Render method, as shown in Example 1-11. New code lines are shown in bold. This also adds some calls to glPushMatrix and glPopMatrix to prevent rotations from accumulating. You’ll learn more about these commands (including glRotatef) in the next chapter.

Example 1-11. Final Render implementation

void RenderingEngine1::Render() const
{
    glClearColor(0.5f, 0.5f, 0.5f, 1);
    glClear(GL_COLOR_BUFFER_BIT);

    glPushMatrix();
    glRotatef(m_currentAngle, 0, 0, 1);

    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);
    
    glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &Vertices[0].Position[0]);
    glColorPointer(4, GL_FLOAT, sizeof(Vertex), &Vertices[0].Color[0]);
    
    GLsizei vertexCount = sizeof(Vertices) / sizeof(Vertex);
    glDrawArrays(GL_TRIANGLES, 0, vertexCount);
    
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);

    glPopMatrix();
}

Animating the Rotation

You now have a HelloArrow program that rotates in response to an orientation change, but it’s lacking a bit of grace—most iPhone applications smoothly rotate the image, rather than suddenly jolting it by 90º.

It turns out that Apple provides infrastructure for smooth rotation via the UIViewController class, but this is not the recommended approach for OpenGL ES applications. There are several reasons for this:

  • For best performance, Apple recommends avoiding interaction between Core Animation and OpenGL ES.

  • Ideally, the renderbuffer stays the same size and aspect ratio for the lifetime of the application. This helps performance and simplifies code.

  • In graphically intense applications, developers need to have complete control over animations and rendering.

To achieve the animation effect, Example 1-12 adds a new floating-point field to the RenderingEngine class called m_desiredAngle. This represents the destination value of the current animation; if no animation is occurring, then m_currentAngle and m_desiredAngle are equal.

Example 1-12 also introduces a floating-point constant called RevolutionsPerSecond to represent angular velocity, and the private method RotationDirection, which I’ll explain later.

Example 1-12. Final RenderingEngine class declaration and constructor

#include <OpenGLES/ES1/gl.h>
#include <OpenGLES/ES1/glext.h>
#include "IRenderingEngine.hpp"

static const float RevolutionsPerSecond = 1;

class RenderingEngine1 : public IRenderingEngine {
public:
    RenderingEngine1();
    void Initialize(int width, int height);
    void Render() const;
    void UpdateAnimation(float timeStep);
    void OnRotate(DeviceOrientation newOrientation);
private:
    float RotationDirection() const;
    float m_desiredAngle;
    float m_currentAngle;
    GLuint m_framebuffer;
    GLuint m_renderbuffer;
};

...

void RenderingEngine1::Initialize(int width, int height)
{
    // Create the framebuffer object and attach the color buffer.
    glGenFramebuffersOES(1, &m_framebuffer);
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_framebuffer);
    glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES,
                                 GL_COLOR_ATTACHMENT0_OES,
                                 GL_RENDERBUFFER_OES,
                                 m_renderbuffer);

    glViewport(0, 0, width, height);

    glMatrixMode(GL_PROJECTION);

    // Initialize the projection matrix.
    const float maxX = 2;
    const float maxY = 3;
    glOrthof(-maxX, +maxX, -maxY, +maxY, -1, 1);

    glMatrixMode(GL_MODELVIEW);

    // Initialize the rotation animation state.
    OnRotate(DeviceOrientationPortrait);
    m_currentAngle = m_desiredAngle;
}

Now you can modify OnRotate so that it changes the desired angle rather than the current angle:

void RenderingEngine1::OnRotate(DeviceOrientation orientation)
{
    float angle = 0;
    
    switch (orientation) {
        ...
    }
    
    m_desiredAngle = angle;
}

Before implementing UpdateAnimation, think about how the application decides whether to rotate the arrow clockwise or counterclockwise. Simply checking whether the desired angle is greater than the current angle is incorrect; if the user changes his device from a 270º orientation to a 0º orientation, the angle should increase up to 360º.

This is where the RotationDirection method comes in. It returns –1, 0, or +1, depending on which direction the arrow needs to spin. Assume that m_currentAngle and m_desiredAngle are both normalized to values between 0 (inclusive) and 360 (exclusive).

float RenderingEngine1::RotationDirection() const
{
    float delta = m_desiredAngle - m_currentAngle;
    if (delta == 0)
        return 0;

    bool counterclockwise = ((delta > 0 && delta <= 180) || (delta < -180));
    return counterclockwise ? +1 : -1;
}

Now you’re ready to write the UpdateAnimation method, which takes a time step in seconds:

void RenderingEngine1::UpdateAnimation(float timeStep)
{
    float direction = RotationDirection();
    if (direction == 0)
        return;

    float degrees = timeStep * 360 * RevolutionsPerSecond;
    m_currentAngle += degrees * direction;

    // Ensure that the angle stays within [0, 360).
    if (m_currentAngle >= 360)
        m_currentAngle -= 360;
    else if (m_currentAngle < 0)
        m_currentAngle += 360;

    // If the rotation direction changed, then we overshot the desired angle.
    if (RotationDirection() != direction)
        m_currentAngle = m_desiredAngle;
}

This is fairly straightforward, but that last conditional might look curious. Since this method incrementally adjusts the angle with floating-point numbers, it could easily overshoot the destination, especially for large time steps. Those last two lines correct this by simply snapping the angle to the desired position. You’re not trying to emulate a shaky compass here, even though doing so might be a compelling iPhone application!

You now have a fully functional HelloArrow application. As with the other examples, you can find the complete code on this book’s website (see the preface for more information on code samples).



[2] Xcode doesn’t care whether you use hpp or h for headers; we use this convention purely for the benefit of human readers.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required