1. RenderingEngine DeclarationThe implementations of HelloArrow and
HelloCone diverge in several ways, as shown in Table 1. Table 1. Differences between HelloArrow and HelloConeHelloArrow | 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. |
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 TessellationThe 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 initializationvoid 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.
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.
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.
Example 3. Generation of cone verticesm_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 DimensionsTo 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 OnRotatevoid 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 MethodLast 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::Rendervoid 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.
|