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

HelloCone with Fixed Function

We’re finally ready to upgrade the HelloArrow program into HelloCone. We’ll not go only from rendering in 2D to rendering in 3D; we’ll also support two new orientations for when the device is held face up or face down.

Even though the visual changes are significant, they’ll all occur within RenderingEngine1.cpp and RenderingEngine2.cpp. That’s the beauty of the layered, interface-based approach presented in the previous chapter. First we’ll deal exclusively with the ES 1.1 renderer, RenderingEngine1.cpp.

RenderingEngine Declaration

The implementations of HelloArrow and HelloCone diverge in several ways, as shown in Table 2-5.

Table 2-5. Differences between HelloArrow and HelloCone

Rotation state is an angle on the z-axis.Rotation state is a quaternion.
One draw call.Two draw calls: one for the disk, one for the cone.
Vectors are represented with small C arrays.Vectors are represented with objects like vec3.
Triangle data is small enough to be hardcoded within the program.Triangle data is generated at runtime.
Triangle data is stored in a C array.Triangle data is stored in an STL vector.

With Table 2-5 in mind, take a look at the top of RenderingEngine1.cpp, shown in Example 2-6 (note that this moves the definition of struct Vertex higher up in the file than it was before, so you’ll need to remove the old version of this struct from this file).


If you’d like to follow along in code as you read, make a copy of the HelloArrow project folder in Finder, and save it as HelloCone. Open the project in Xcode, and then select Rename from the Project menu. Change the project name to HelloCone, and click Rename. Next, visit the appendix, and add Vector.hpp, Matrix.hpp, and Quaternion.hpp to the project. RenderingEngine1.cpp will be almost completely different, so open it and remove all its content. Now you’re ready to make the changes shown in this section as you read along.

Example 2-6. RenderingEngine1 class declaration

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

static const float AnimationDuration = 0.25f;

using namespace std;

struct Vertex {
    vec3 Position;
    vec4 Color;

struct Animation {1
    Quaternion Start;
    Quaternion End;
    Quaternion Current;
    float Elapsed;
    float Duration;

class RenderingEngine1 : public IRenderingEngine {
    void Initialize(int width, int height);
    void Render() const;
    void UpdateAnimation(float timeStep);
    void OnRotate(DeviceOrientation newOrientation);
    vector<Vertex> m_cone;2
    vector<Vertex> m_disk;
    Animation m_animation;
    GLuint m_framebuffer;
    GLuint m_colorRenderbuffer;
    GLuint m_depthRenderbuffer;3

The Animation structure enables smooth 3D transitions. It includes quaternions for three orientations: the starting orientation, the current interpolated orientation, and the ending orientation. It also includes two time spans: Elapsed and Duration, both of which are in seconds. They’ll be used to compute a slerp fraction between 0 and 1.


The triangle data lives in two STL containers, m_cone and m_disk. The vector container is ideal because we know how big it needs to be ahead of time, and it guarantees contiguous storage. Contiguous storage of vertices is an absolute requirement for OpenGL.


Unlike HelloArrow, there are two renderbuffers here. HelloArrow was 2D and therefore only required a color renderbuffer. HelloCone requires an additional renderbuff for depth. We’ll learn more about the depth buffer in a future chapter; briefly, it’s a special image plane that stores a single Z value at each pixel.

OpenGL Initialization and Cone Tessellation

The construction methods are very similar to what we had in HelloArrow:

IRenderingEngine* CreateRenderer1()
    return new RenderingEngine1();

    // Create & bind the color buffer so that the caller can allocate its space.
    glGenRenderbuffersOES(1, &m_colorRenderbuffer);
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_colorRenderbuffer);

The Initialize method, shown in Example 2-7, is responsible for generating the vertex data and setting up the framebuffer. It starts off by defining some values for the cone’s radius, height, and geometric level of detail. The level of detail is represented by the number of vertical “slices” that constitute the cone. After generating all the vertices, it initializes OpenGL’s framebuffer object and transform state. It also enables depth testing since this a true 3D app. We’ll learn more about depth testing in Chapter 4.

Example 2-7. RenderingEngine initialization

void RenderingEngine1::Initialize(int width, int height)
    const float coneRadius = 0.5f;1
    const float coneHeight = 1.866f;
    const int coneSlices = 40;

      // Generate vertices for the disk.

      // Generate vertices for the body of the cone.

    // Create the depth buffer.
    glGenRenderbuffersOES(1, &m_depthRenderbuffer);2
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_depthRenderbuffer);
    // Create the framebuffer object; attach the depth and color buffers.
    glGenFramebuffersOES(1, &m_framebuffer);3
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_framebuffer);
    // Bind the color buffer for rendering.
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_colorRenderbuffer);4
    glViewport(0, 0, width, height);5
    glFrustumf(-1.6f, 1.6, -2.4, 2.4, 5, 10);
    glTranslatef(0, 0, -7);

Much of Example 2-7 is standard procedure when setting up an OpenGL context, and much of it will become clearer in future chapters. For now, here’s a brief summary:


Define some constants to use when generating the vertices for the disk and cone.


Generate an ID for the depth renderbuffer, bind it, and allocate storage for it. We’ll learn more about depth buffers later.


Generate an ID for the framebuffer object, bind it, and attach depth and color to it using glFramebufferRenderbufferOES.


Bind the color renderbuffer so that future rendering operations will affect it.


Set up the left, bottom, width, and height properties of the viewport.


Turn on depth testing since this is a 3D scene.


Set up the projection and model-view transforms.

Example 2-7 replaces the two pieces of vertex generation code with ellipses because they deserve an in-depth explanation. The problem of decomposing an object into triangles is called triangulation, but more commonly you’ll see the term tessellation, which actually refers to the broader problem of filling a surface with polygons. Tessellation can be a fun puzzle, as any M.C. Escher fan knows; we’ll learn more about it in later chapters.

For now let’s form the body of the cone with a triangle strip and the bottom cap with a triangle fan, as shown in Figure 2-16.

Tessellation in HelloCone

Figure 2-16. Tessellation in HelloCone

To form the shape of the cone’s body, we could use a fan rather than a strip, but this would look strange because the color at the fan’s center would be indeterminate. Even if we pick an arbitrary color for the center, an incorrect vertical gradient would result, as shown on the left in Figure 2-17.

Left: Cone with triangle fan. Right: Cone with triangle strip

Figure 2-17. Left: Cone with triangle fan. Right: Cone with triangle strip

Using a strip for the cone isn’t perfect either because every other triangle is degenerate (shown in gray in Figure 2-16). The only way to fix this would be resorting to GL_TRIANGLES, which requires twice as many elements in the vertex array. It turns out that OpenGL provides an indexing mechanism to help with situations like this, which we’ll learn about in the next chapter. For now we’ll use GL_TRIANGLE_STRIP and live with the degenerate triangles. The code for generating the cone vertices is shown in Example 2-8 and depicted visually in Figure 2-18 (this code goes after the comment // Generate vertices for the body of the cone in RenderingEngine1::Initialize). Two vertices are required for each slice (one for the apex, one for the rim), and an extra slice is required to close the loop (Figure 2-18). The total number of vertices is therefore (n+1)*2 where n is the number of slices. Computing the points along the rim is the classic graphics algorithm for drawing a circle and may look familiar if you remember your trigonometry.

Vertex order in HelloCone

Figure 2-18. Vertex order in HelloCone

Example 2-8. Generation of cone vertices

m_cone.resize((coneSlices + 1) * 2);

// Initialize the vertices of the triangle strip.
vector<Vertex>::iterator vertex = m_cone.begin();
const float dtheta = TwoPi / coneSlices;
for (float theta = 0; vertex != m_cone.end(); theta += dtheta) {
    // Grayscale gradient
    float brightness = abs(sin(theta));
    vec4 color(brightness, brightness, brightness, 1);
    // Apex vertex
    vertex->Position = vec3(0, 1, 0);
    vertex->Color = color;
    // Rim vertex
    vertex->Position.x = coneRadius * cos(theta);
    vertex->Position.y = 1 - coneHeight;
    vertex->Position.z = coneRadius * sin(theta);
    vertex->Color = color;

Note that we’re creating a grayscale gradient as a cheap way to simulate lighting:

float brightness = abs(sin(theta));
vec4 color(brightness, brightness, brightness, 1);

This is a bit of a hack because the color is fixed and does not change as you reorient the object, but it’s good enough for our purposes. This technique is sometimes called baked lighting, and we’ll learn more about it in Chapter 9. We’ll also learn how to achieve more realistic lighting in Chapter 4.

Example 2-9 generates vertex data for the disk (this code goes after the comment // Generate vertices for the disk in RenderingEngine1::Initialize). Since it uses a triangle fan, the total number of vertices is n+2: one extra vertex for the center, another for closing the loop.

Example 2-9. Generation of disk vertices

// Allocate space for the disk vertices.
m_disk.resize(coneSlices + 2);

// Initialize the center vertex of the triangle fan.
vector<Vertex>::iterator vertex = m_disk.begin();
vertex->Color = vec4(0.75, 0.75, 0.75, 1);
vertex->Position.x = 0;
vertex->Position.y = 1 - coneHeight;
vertex->Position.z = 0;

// Initialize the rim vertices of the triangle fan.
const float dtheta = TwoPi / coneSlices;
for (float theta = 0; vertex != m_disk.end(); theta += dtheta) {
    vertex->Color = vec4(0.75, 0.75, 0.75, 1);
    vertex->Position.x = coneRadius * cos(theta);
    vertex->Position.y = 1 - coneHeight;
    vertex->Position.z = coneRadius * sin(theta);

Smooth Rotation in Three Dimensions

To achieve smooth animation, UpdateAnimation calls Slerp on the rotation quaternion. When a device orientation change occurs, the OnRotate method starts a new animation sequence. Example 2-10 shows these methods.

Example 2-10. UpdateAnimation and OnRotate

void RenderingEngine1::UpdateAnimation(float timeStep)
    if (m_animation.Current == m_animation.End)

    m_animation.Elapsed += timeStep;
    if (m_animation.Elapsed >= AnimationDuration) {
        m_animation.Current = m_animation.End;
    } else {
        float mu = m_animation.Elapsed / AnimationDuration;
        m_animation.Current = m_animation.Start.Slerp(mu, m_animation.End);

void RenderingEngine1::OnRotate(DeviceOrientation orientation)
    vec3 direction;

    switch (orientation) {
        case DeviceOrientationUnknown:
        case DeviceOrientationPortrait:
            direction = vec3(0, 1, 0);
        case DeviceOrientationPortraitUpsideDown:
            direction = vec3(0, -1, 0);
        case DeviceOrientationFaceDown:       
            direction = vec3(0, 0, -1);
        case DeviceOrientationFaceUp:
            direction = vec3(0, 0, 1);
        case DeviceOrientationLandscapeLeft:
            direction = vec3(+1, 0, 0);
        case DeviceOrientationLandscapeRight:
            direction = vec3(-1, 0, 0);

    m_animation.Elapsed = 0;
    m_animation.Start = m_animation.Current = m_animation.End;
    m_animation.End = Quaternion::CreateFromVectors(vec3(0, 1, 0), direction);

Render Method

Last but not least, HelloCone needs a Render method, as shown in Example 2-11. It’s similar to the Render method in HelloArrow except it makes two draw calls, and the glClear command now has an extra flag for the depth buffer.

Example 2-11. RenderingEngine1::Render

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

    mat4 rotation(m_animation.Current.ToMatrix());

    // Draw the cone.
    glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &m_cone[0].Position.x);
    glColorPointer(4, GL_FLOAT, sizeof(Vertex),  &m_cone[0].Color.x);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, m_cone.size());

    // Draw the disk that caps off the base of the cone.
    glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &m_disk[0].Position.x);
    glColorPointer(4, GL_FLOAT, sizeof(Vertex), &m_disk[0].Color.x);
    glDrawArrays(GL_TRIANGLE_FAN, 0, m_disk.size());


Note the call to rotation.Pointer(). In our C++ vector library, vectors and matrices have a method called Pointer(), which exposes a pointer to the first innermost element. This is useful when passing them to OpenGL.


We could’ve made much of our OpenGL code more succinct by changing the vector library such that it provides implicit conversion operators in lieu of Pointer() methods. Personally, I think this would be error prone and would hide too much from the code reader. For similar reasons, STL’s string class requires you to call its c_str() when you want to get a char*.

Because you’ve implemented only the 1.1 renderer so far, you’ll also need to enable the ForceES1 switch at the top of GLView.mm. At this point, you can build and run your first truly 3D iPhone application! To see the two new orientations, try holding the iPhone over your head and at your waist. See Figure 2-19 for screenshots of all six device orientations.

Left to right: Portrait, UpsideDown, FaceUp, FaceDown, LandscapeRight, and LandscapeLeft

Figure 2-19. Left to right: Portrait, UpsideDown, FaceUp, FaceDown, LandscapeRight, and LandscapeLeft

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