MULTIMEDIA

iPhone 3D Programming : HelloCone with Fixed Function

10/10/2010 3:02:46 PM

1. RenderingEngine Declaration

The implementations of HelloArrow and HelloCone diverge in several ways, as shown in Table 1.

Table 1. Differences between HelloArrow and HelloCone
HelloArrowHelloCone
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.

STL: To Use or Not to Use?

I decided to use the C++ Standard Template Library (STL) in much of this book’s sample code. The STL simplifies many tasks by providing classes for commonly used data structures, such as resizeable arrays (std::vector) and doubly linked lists (std::list). Many developers would argue against using STL on a mobile platform like the iPhone when writing performance-critical code. It’s true that sloppy usage of STL can cause your application’s memory footprint to get out of hand, but nowadays, C++ compilers do a great job at optimizing STL code. Keep in mind that the iPhone SDK provides a rich set of Objective-C classes (e.g., NSDictionary) that are analogous to many of the STL classes, and they have similar costs in terms of memory footprint and performance.


With Table 1 in mind, take a look at the top of RenderingEngine1.cpp, shown in Example 1 (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).


Note: 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 1. 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 {
Quaternion Start;
Quaternion End;
Quaternion Current;
float Elapsed;
float Duration;
};

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


2. OpenGL Initialization and Cone Tessellation

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

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

RenderingEngine1::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, 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. RenderingEngine initialization
void RenderingEngine1::Initialize(int width, int height)
{
const float coneRadius = 0.5f;
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);
glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_depthRenderbuffer);
glRenderbufferStorageOES(GL_RENDERBUFFER_OES,
GL_DEPTH_COMPONENT16_OES,
width,
height);

// Create the framebuffer object; attach the depth and color buffers.
glGenFramebuffersOES(1, &m_framebuffer);
glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_framebuffer);
glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES,
GL_COLOR_ATTACHMENT0_OES,
GL_RENDERBUFFER_OES,
m_colorRenderbuffer);
glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES,
GL_DEPTH_ATTACHMENT_OES,
GL_RENDERBUFFER_OES,
m_depthRenderbuffer);

// Bind the color buffer for rendering.
glBindRenderbufferOES(GL_RENDERBUFFER_OES, m_colorRenderbuffer);

glViewport(0, 0, width, height);
glEnable(GL_DEPTH_TEST);

glMatrixMode(GL_PROJECTION);
glFrustumf(-1.6f, 1.6, -2.4, 2.4, 5, 10);

glMatrixMode(GL_MODELVIEW);
glTranslatef(0, 0, -7);
}


Much of Example 2 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:

Example 2 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 1.

Figure 1. 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.

Figure 2. 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 1). 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 3 and depicted visually in Figure 3 (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 3). 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.

Figure 3. Vertex order in HelloCone


Example 3. 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;
vertex++;

// Rim vertex
vertex->Position.x = coneRadius * cos(theta);
vertex->Position.y = 1 - coneHeight;
vertex->Position.z = coneRadius * sin(theta);
vertex->Color = color;
vertex++;
}



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.

Example 4 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 4. 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;
vertex++;

// 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);
vertex++;
}

3. 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 5 shows these methods.

Example 5. UpdateAnimation and OnRotate
void RenderingEngine1::UpdateAnimation(float timeStep)
{
if (m_animation.Current == m_animation.End)
return;

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);
break;

case DeviceOrientationPortraitUpsideDown:
direction = vec3(0, -1, 0);
break;

case DeviceOrientationFaceDown:
direction = vec3(0, 0, -1);
break;

case DeviceOrientationFaceUp:
direction = vec3(0, 0, 1);
break;

case DeviceOrientationLandscapeLeft:
direction = vec3(+1, 0, 0);
break;

case DeviceOrientationLandscapeRight:
direction = vec3(-1, 0, 0);
break;
}


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


4. Render Method

Last but not least, HelloCone needs a Render method, as shown in Example 6. 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 6. RenderingEngine1::Render
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);

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

// 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();
}


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.


Note: 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 4 for screenshots of all six device orientations.

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


Other  
  •  iPhone 3D Programming : Vector Beautification with C++
  •  jQuery 1.3 : An image carousel
  •  jQuery 1.3 : Headline rotator
  •  Silverlight : Print a Document
  •  Silverlight : Capture a Webcam
  •  Silverlight : Make Your Application Run out of the Browser
  •  Silverlight : Put Content into a 3D Perspective
  •  Silverlight : Response to Timer Events on the UI Thread
  •  Silverlight : Build a Download and Playback Progress Bar
  •  Silverlight : Play a Video
  •  C# 4.0 : Add a Static Constructor and Initialization
  •  C# 4.0 : Add a Constructor
  •  .NET Compact Framework : Font Selection
  •  .NET Compact Framework : Drawing Text
  •  Programming the Service Bus
  •  WCF Services : Generics
  •  WCF Services : Delegates and Data Contracts
  •  WCF Services : Enumerations
  •  WCF Services : Versioning
  •  WCF Services : Data Contract - Equivalence
  •  
    Top 10
    Microsoft XNA Game Studio 3.0 : Displaying Images - Using Resources in a Game (part 3) - Sprite Drawing with SpriteBatch
    The .NET Security Architecture
    Exploring Sample Virtualized SharePoint 2010 Architecture
    IIS 7.0 : Hosting ASP.NET Applications
    CSS for Mobile Browsers : Where to Insert the CSS
    Advanced ASP.NET : Output Caching
    Role-Based Security Explained
    Mobile Application Security : Windows Mobile Security - Development and Security Testing (part 1)
    iPhone Application Development : Customizing Interface Appearance
    Installing SharePoint 2010 Using PowerShell
    Most View
    Algorithms for Compiler Design: A HANDLE OF A RIGHT SENTENTIAL FORM
    Silverlight : Capture a Webcam
    Creating a Development Provisioning Profile on iPhone
    Programming with SQL Azure : Record Navigation in WCF Data Services
    Mobile Application Security : BlackBerry Security - Introduction to Platform
    Adobe InDesign CS5 : Importing Graphic Objects (part 1) - Understanding Adobe Bridge
    Windows Server 2008 R2 monitoring and troubleshooting : Data Collector Sets
    Transact-SQL in SQL Server 2008 : Insert over DML
    IIS 7.0 : Implementing Access Control - Authentication (part 1)
    Windows System Programming : File Pointers & Getting the File Size
    SharePoint 2010 : Workflow Modeling and Development Tools (part 1) - Microsoft Visio 2010 & SharePoint Designer 2010
    Web Security Testing : Changing Sessions to Evade Restrictions & Impersonating Another User
    Becoming an Excel Programmer : Start and Stop
    The best social game apps for iOS Device (Part 2) - SteamScope, Blockwick, Pinball Arcade
    SQL Server 2008 : Using ADO.NET Data Services
    .NET security : Isolated Storage Explained
    SharePoint 2010 : Beyond Built-In SharePoint PowerShell Cmdlets
    Server-Side Browser Detection and Content Delivery : Mobile Detection (part 1) - HTTP
    WCF Services : Data Contract - Equivalence
    Deploying a Native SharePoint 2010 Search Service Application