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

Chapter 3. Vertices and Touch Points

Second star to the right…and straight on ’til morning.

Peter Pan, J.M. Barrie

The iPhone has several input devices, including the accelerometer, microphone, and touchscreen, but your application will probably make the most use of the touchscreen. Since the screen has multitouch capabilities, your application can obtain a list of several touch points at any time. In a way, your 3D application is a “point processor”: it consumes points from the touchscreen and produces points (for example, triangle vertices) for OpenGL. So, I thought I’d use the same chapter to both introduce the touchscreen and cover some new ways of submitting vertices to OpenGL.

This chapter also covers some important best practices for vertex submission, such as the usage of vertex buffer objects. I would never argue with the great man who decreed that premature optimization is the root of all evil, but I want to hammer in good habits early on.

Toward the end of the chapter, you’ll learn how to generate some interesting geometry using parametric surfaces. This will form the basis for a fun demo app that you’ll gradually enhance over the course of the next several chapters.

Reading the Touchscreen

In this section, I’ll introduce the touchscreen API by walking through a modification of HelloCone that makes the cone point toward the user’s finger. You’ll need to change the name of the app from HelloCone to TouchCone, since the user now touches the cone instead of merely greeting it. To do this, make a copy of the project folder in Finder, and name the new folder TouchCone. Next, open the Xcode project (it will still have the old name), and select ProjectRename. Change the name to TouchCone, and click Rename.

Apple’s multitouch API is actually much richer than what we need to expose through our IRenderingEngine interface. For example, Apple’s API supports the concept of cancellation, which is useful to robustly handle situations such as an interruption from a phone call. For our purposes, a simplified interface to the rendering engine is sufficient. In fact, we don’t even need to accept multiple touches simultaneously; the touch handler methods can simply take a single coordinate.

For starters, let’s add three methods to IRenderingEngine for “finger up” (the end of a touch), “finger down” (the beginning of a touch), and “finger move.” Coordinates are passed to these methods using the ivec2 type from the vector library we added in RenderingEngine Declaration. Example 3-1 shows the modifications to IRenderingEngine.hpp (new lines are in bold).

Example 3-1. IRenderingEngine interface for TouchCone

#include "Vector.hpp"

...

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 void OnFingerUp(ivec2 location) = 0;
    virtual void OnFingerDown(ivec2 location) = 0;
    virtual void OnFingerMove(ivec2 oldLocation, ivec2 newLocation) = 0;
    virtual ~IRenderingEngine() {}
};

The iPhone notifies your view of touch events by calling methods on your UIView class, which you can then override. The three methods we’re interested in overriding are touchesBegan, touchedEnded, and touchesMoved. Open GLView.mm, and implement these methods by simply passing on the coordinates to the rendering engine:

- (void) touchesBegan: (NSSet*) touches withEvent: (UIEvent*) event
{
    UITouch* touch = [touches anyObject];
    CGPoint location  = [touch locationInView: self];
    m_renderingEngine->OnFingerDown(ivec2(location.x, location.y));
}

- (void) touchesEnded: (NSSet*) touches withEvent: (UIEvent*) event
{
    UITouch* touch = [touches anyObject];
    CGPoint location  = [touch locationInView: self];
    m_renderingEngine->OnFingerUp(ivec2(location.x, location.y));
}

- (void) touchesMoved: (NSSet*) touches withEvent: (UIEvent*) event
{
    UITouch* touch = [touches anyObject];
    CGPoint previous  = [touch previousLocationInView: self];
    CGPoint current = [touch locationInView: self];
    m_renderingEngine->OnFingerMove(ivec2(previous.x, previous.y),
                                    ivec2(current.x, current.y));
}

The RenderingEngine1 implementation (Example 3-2) is similar to HelloCone, but the OnRotate and UpdateAnimation methods become empty. Example 3-2 also notifies the user that the cone is active by using glScalef to enlarge the geometry while the user is touching the screen. New and changed lines in the class declaration are shown in bold. Note that we’re removing the Animation structure.

Example 3-2. RenderingEngine1.cpp in TouchCone

class RenderingEngine1 : public IRenderingEngine {
public:
    RenderingEngine1();
    void Initialize(int width, int height);
    void Render() const;
    void UpdateAnimation(float timeStep) {}
    void OnRotate(DeviceOrientation newOrientation) {}
    void OnFingerUp(ivec2 location);
    void OnFingerDown(ivec2 location);
    void OnFingerMove(ivec2 oldLocation, ivec2 newLocation);
private:
    vector<Vertex> m_cone;
    vector<Vertex> m_disk;
    GLfloat m_rotationAngle;
    GLfloat m_scale;
    ivec2 m_pivotPoint;
    GLuint m_framebuffer;
    GLuint m_colorRenderbuffer;
    GLuint m_depthRenderbuffer;
};

RenderingEngine1::RenderingEngine1() : m_rotationAngle(0), m_scale(1)
{
    glGenRenderbuffersOES(1, &m_colorRenderbuffer);
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_colorRenderbuffer);
}

void RenderingEngine1::Initialize(int width, int height)
{
    m_pivotPoint = ivec2(width / 2, height / 2);
    
    ...
}

void RenderingEngine1::Render() const
{
    glClearColor(0.5f, 0.5f, 0.5f, 1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glPushMatrix();
    
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_COLOR_ARRAY);

    glRotatef(m_rotationAngle, 0, 0, 1); // Replaces call to rotation()
    glScalef(m_scale, m_scale, m_scale); // Replaces call to glMultMatrixf()
    
    // 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());
    
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);

    glPopMatrix();
}

void RenderingEngine1::OnFingerUp(ivec2 location)
{
    m_scale = 1.0f;
}

void RenderingEngine1::OnFingerDown(ivec2 location)
{
    m_scale = 1.5f;
    OnFingerMove(location, location);
}

void RenderingEngine1::OnFingerMove(ivec2 previous, ivec2 location)
{
    vec2 direction = vec2(location - m_pivotPoint).Normalized();

    // Flip the y-axis because pixel coords increase toward the bottom.
    direction.y = -direction.y;

    m_rotationAngle = std::acos(direction.y) * 180.0f / 3.14159f;
    if (direction.x > 0)
        m_rotationAngle = -m_rotationAngle;
}

The only bit of code in Example 3-2 that might need some extra explanation is the OnFingerMove method; it uses some trigonometric trickery to compute the angle of rotation. The best way to explain this is with a diagram, as shown in Figure 3-1. Recall from high-school trig that the cosine is “adjacent over hypotenuse.” We normalized the direction vector, so we know the hypotenuse length is exactly one. Since cos(θ)=y, then acos(y)=θ. If the direction vector points toward the right of the screen, then the rotation angle should be reversed, as illustrated on the right. This is because rotation angles are counterclockwise in our coordinate system.

Trigonometry in OnFingerMove

Figure 3-1. Trigonometry in OnFingerMove

Note that OnFingerMove flips the y-axis. The pixel-space coordinates that come from UIView have the origin at the upper-left corner of the screen, with +Y pointing downward, while OpenGL (and mathematicians) prefer to have the origin at the center, with +Y pointing upward.

That’s it! The 1.1 ES version of the Touch Cone app is now functionally complete. If you want to compile and run at this point, don’t forget to turn on the ForceES1 switch at the top of GLView.mm.

Let’s move on to the ES 2.0 renderer. Open RenderingEngine2.cpp, and make the changes shown in bold in Example 3-3. Most of these changes are carried over from our ES 1.1 changes, with some minor differences in the Render method.

Example 3-3. RenderingEngine2.cpp in TouchCone

class RenderingEngine2 : public IRenderingEngine {
public:
    RenderingEngine2();
    void Initialize(int width, int height);
    void Render() const;
    void UpdateAnimation(float timeStep) {}
    void OnRotate(DeviceOrientation newOrientation) {}
    void OnFingerUp(ivec2 location);
    void OnFingerDown(ivec2 location);
    void OnFingerMove(ivec2 oldLocation, ivec2 newLocation);
private:

    ...

    GLfloat m_rotationAngle;
    GLfloat m_scale;
    ivec2 m_pivotPoint;
};

RenderingEngine2::RenderingEngine2() : m_rotationAngle(0), m_scale(1)
{
    glGenRenderbuffersOES(1, &m_colorRenderbuffer);
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_colorRenderbuffer);
}

void RenderingEngine2::Initialize(int width, int height)
{
    m_pivotPoint = ivec2(width / 2, height / 2);
    
    ...
}

void RenderingEngine2::Render() const
{
    GLuint positionSlot = glGetAttribLocation(m_simpleProgram, 
                                              "Position");
    GLuint colorSlot = glGetAttribLocation(m_simpleProgram, 
                                           "SourceColor");

    glClearColor(0.5f, 0.5f, 0.5f, 1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    glEnableVertexAttribArray(positionSlot);
    glEnableVertexAttribArray(colorSlot);
    
    mat4 rotation = mat4::Rotate(m_rotationAngle);
    mat4 scale = mat4::Scale(m_scale);
    mat4 translation = mat4::Translate(0, 0, -7);

    // Set the model-view matrix.
    GLint modelviewUniform = glGetUniformLocation(m_simpleProgram, 
                                                  "Modelview");
    mat4 modelviewMatrix = scale * rotation * translation;
    glUniformMatrix4fv(modelviewUniform, 1, 0, modelviewMatrix.Pointer());
    
    // Draw the cone.
    {
      GLsizei stride = sizeof(Vertex);
      const GLvoid* pCoords = &m_cone[0].Position.x;
      const GLvoid* pColors = &m_cone[0].Color.x;
      glVertexAttribPointer(positionSlot, 3, GL_FLOAT, 
                            GL_FALSE, stride, pCoords);
      glVertexAttribPointer(colorSlot, 4, GL_FLOAT, 
                            GL_FALSE, stride, pColors);
      glDrawArrays(GL_TRIANGLE_STRIP, 0, m_cone.size());
    }
    
    // Draw the disk that caps off the base of the cone.
    {
      GLsizei stride = sizeof(Vertex);
      const GLvoid* pCoords = &m_disk[0].Position.x;
      const GLvoid* pColors = &m_disk[0].Color.x;
      glVertexAttribPointer(positionSlot, 3, GL_FLOAT, 
                            GL_FALSE, stride, pCoords);
      glVertexAttribPointer(colorSlot, 4, GL_FLOAT, 
                            GL_FALSE, stride, pColors);
      glDrawArrays(GL_TRIANGLE_FAN, 0, m_disk.size());
    }
    
    glDisableVertexAttribArray(positionSlot);
    glDisableVertexAttribArray(colorSlot);
}

// See Example 3-2 for OnFingerUp, OnFingerDown, and OnFingerMove.
...

You can now turn off the ForceES1 switch in GLView.mm and build and run TouchCone on any Apple device. In the following sections, we’ll continue making improvements to the app, focusing on how to efficiently describe the cone geometry.

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