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 Finale: SpringyStars

To show off point sprites in action, let’s wrap up the chapter with a mass-spring simulation that renders a network of little white stars, as shown in Figure 7-16. The “star net” droops down in the direction of gravity according to how you’re holding the iPhone. You can find the complete source for this example from this book’s web page.

The SpringyStars sample app

Figure 7-16. The SpringyStars sample app

Physics Diversion: Mass-Spring System

Before we dive into the code, let’s take a brief detour from graphics and review some physics. The easiest way to create a simple physics simulation to call an Update() method on every object in the simulation. The update method has an argument for the time step, which represents elapsed time since the previous call. (This isn’t much different from the UpdateAnimation() method presented way back in Chapter 1.) For our SpringyStars app, the pseudocode for the update method looks like this:

void Update(float dt)
    Acceleration = TotalForces / Mass;
    Velocity += Acceleration * dt;
    Position += Velocity * dt;

The previous code snippet should make sense if you remember your high-school physics. Perhaps a bit foggier in your memory is Hooke’s law, which we’ll need for modeling the spring forces between the star sprites; see Equation 7-1.

Equation 7-1. Hooke’s law of elasticity

Hooke’s law of elasticity

F is the restoring force of the spring, so called because it represents the effort to restore the spring’s length to its rest position. You can think of the k constant as being the stiffness of the spring. x is the displacement between the current end of the spring and its rest position.


To learn physics from a much more authoritative source, take a look at Physics for Game Developers (O’Reilly) by David Bourg.

Hooke’s law deals with only a single spring, but in our case we have a network of springs. Our simulation needs to maintain a list of “nodes” (rigid bodies), each of which is connected to a set of neighboring nodes. Here’s a code snippet that applies Hooke’s law to a node and its neighbor:

vec2 v = neighbor->position - this->position;
float length = v.Length();
vec2 direction = v.Normalized();
vec2 restoringForce = direction * StiffnessContant * (length - RestLength);

In the previous snippet, the restoringForce vector applies to the this node, while an equal and opposite force (that is, -restoringForce) applies to the neighbor node.

Taken alone, Hooke’s law can produce oscillations that last forever. To be more realistic, the simulation needs to include a damping force to subdue the spring’s effect. The damping force between two nodes is proportional to their relative velocity:

Hooke’s law of elasticity

In this case, kd is a damping constant. Much like the stiffness constant in Hooke’s law, I find that the best way to come up with a reasonable value is through experimentation. (More rigorous mass-spring systems include a global damping force, but this is good enough for our purposes.)

The C++ code snippet for computing damping force looks like this:

vec2 relativeVelocity = neighbor->velocity - this->m_velocity;
vec2 dampingForce = relativeVelocity * DampingConstant;

At this point, we’re ready to design a C++ class to represent a star sprite in a simple mass-spring system. See Example 7-19.

Example 7-19. SpringNode.hpp

#include <list>

class SpringNode;

typedef std::list<SpringNode*> NeighborList;

class SpringNode {
        m_position = vec2(0, 0);
        m_velocity = vec2(0, 0);
        m_mass = 1;
        m_pinned = false;
    void Pin()
        m_pinned = true;
    void SetPosition(const vec2& p)
        m_position = p;
    vec2 GetPosition() const
        return m_position;
    void AddNeighbor(SpringNode* node)
    void ResetForce(vec2 force)
        m_force = force;
    void ComputeForce()
        const float StiffnessContant = 3.0f;
        const float RestLength = 0.075f;
        const float DampingConstant = 2.0f;
        NeighborList::const_iterator n = m_neighbors.begin();
        for (; n != m_neighbors.end(); ++n) {

            // Compute the spring force:
            vec2 v = (*n)->m_position - m_position;
            float length = v.Length();
            vec2 direction = v.Normalized();
            vec2 restoringForce = direction * StiffnessContant 
              * (length - RestLength);

            // Compute the damping force:
            vec2 relativeVelocity = (*n)->m_velocity - m_velocity;
            vec2 dampingForce = relativeVelocity * DampingConstant;
            // Add the two forces to this node and subtract them 
            // from the neighbor:
            vec2 totalForce = restoringForce + dampingForce;
            m_force += totalForce;
            (*n)->m_force -= totalForce;
    void Update(float dt)
        if (m_pinned)
        vec2 acceleration = m_force / m_mass;
        m_velocity += acceleration * dt;
        m_position += m_velocity * dt;
    vec2 m_force;
    vec2 m_position;
    vec2 m_velocity;
    float m_mass;
    bool m_pinned;
    NeighborList m_neighbors;

Note the boolean field called m_pinned, which causes a node to be impervious to the forces acted upon it. We’ll use this to affix the four corners of the net to their starting positions. This prevents the net from falling off the screen.

Speaking of falling off the screen, note that there’s nothing obvious in Example 7-19 that takes gravity into account. That’s because the application can use the ResetForce() method to initialize the m_force field to the gravity vector and then call ComputeForce() in a separate pass to add in all the relevant spring forces. The simulation will perform three separate passes through the node list; see the pseudocode in Example 7-20. (Don’t combine these into a single loop.)

Example 7-20. Simulation pseudocode

void UpdateSimulation(float timeStep, vec2 gravityVector)
    for each node:

    for each node:

    for each node:

C++ Interfaces and GLView

To avoid code duplication between the ES 2.0 and ES 1.1 backends, let’s put the physics into the application engine and pass it a normalized 2D vector for the direction of gravity. As a result, the IRenderingEngine interface is very simple; see Example 7-21.

Example 7-21. Interfaces.hpp (SpringyStars)

#pragma once
#include "Vector.hpp"
#include <vector>

typedef std::vector<vec2> PositionList;

struct IApplicationEngine {
    virtual void Initialize(int width, int height) = 0;
    virtual void Render() const = 0;
    virtual void UpdateAnimation(float timeStep) = 0;
    virtual void SetGravityDirection(vec2 direction) = 0;
    virtual ~IApplicationEngine() {}

struct IRenderingEngine {
    virtual void Initialize() = 0;
    virtual void Render(const PositionList& positions) const = 0;
    virtual ~IRenderingEngine() {}

IApplicationEngine* CreateApplicationEngine(IRenderingEngine*);

namespace ES1 { IRenderingEngine* CreateRenderingEngine(); }
namespace ES2 { IRenderingEngine* CreateRenderingEngine(); }

The GLView class looks much like all the other samples in this book, except that it needs to pass in a gravity vector. See Example 7-22. For more information about setting up the accelerometer, flip back to the section Replacing Buttons with Orientation Sensors.

Example 7-22. Snippet of GLView.mm (SpringyStars)

- (void) accelerometer: (UIAccelerometer*) accelerometer
         didAccelerate: (UIAcceleration*) acceleration
    [m_filter addAcceleration:acceleration];
    vec2 direction(m_filter.x, m_filter.y);

ApplicationEngine Implementation

See Example 7-23 for the application engine implementation. Recall that the GLView class calls UpdateAnimation according to the refresh rate of the display. This provides enough time to perform several simulation iterations, each using a small time step. Performing several small iterations produces more accurate results than a single iteration with a large time step. In fact, an overly large time step can cause the simulation to go ballistic.


Updating the physics along with the rendering is a bit of a hack but good enough for our purposes. For a production-quality application, you might want to create a timer object in your GLView class just for physics.

Example 7-23. ApplicationEngine.cpp (SpringyStars)

#include "Interfaces.hpp"
#include "SpringNode.hpp"

using namespace std;

class ApplicationEngine : public IApplicationEngine {
    ApplicationEngine(IRenderingEngine* renderingEngine);
    void Initialize(int width, int height);
    void SetGravityDirection(vec2 direction);
    void Render() const;
    void UpdateAnimation(float dt);
    vec2 m_gravityDirection;1
    vector<SpringNode> m_springNodes;2
    PositionList m_positions;3
    IRenderingEngine* m_renderingEngine;
IApplicationEngine* CreateApplicationEngine
  (IRenderingEngine* renderingEngine)
    return new ApplicationEngine(renderingEngine);

    delete m_renderingEngine;

void ApplicationEngine::Initialize(int width, int height)

void ApplicationEngine::SetGravityDirection(vec2 direction)
    m_gravityDirection = direction;

void ApplicationEngine::Render() const

ApplicationEngine::ApplicationEngine(IRenderingEngine* renderingEngine) :
    m_gravityDirection(vec2(0, -1))
    const int NumColumns = 10;4
    const int NumRows = 14;
    const float SpreadFactor = 0.125f;
    m_springNodes.resize(NumColumns * NumRows);
    vector<SpringNode>::iterator node = m_springNodes.begin();
    for (int r = 0; r < NumRows; ++r) {
        for (int c = 0; c < NumColumns; ++c) {
            vec2 position;
            position.x = c - (NumColumns - 1) / 2.0f;5
            position.y = r - (NumRows - 1) / 2.0f;
            node->SetPosition(position * SpreadFactor);
            if (c > 0)
                node->AddNeighbor(&*node - 1);6
            if (r > 0)
                node->AddNeighbor(&*node - NumColumns);7
    m_springNodes[NumColumns - 1].Pin();
    m_springNodes[NumColumns * NumRows - 1].Pin();
    m_springNodes[NumColumns * (NumRows - 1)].Pin();

void ApplicationEngine::UpdateAnimation(float dt)
    const float GravityStrength = 0.01f;10
    const int SimulationIterations = 10;11
    vector<SpringNode>::iterator node;
    vec2 force = m_gravityDirection * GravityStrength;
    for (int i = 0; i < SimulationIterations; ++i) {12
        for (node = m_springNodes.begin(); 
             node != m_springNodes.end(); 

        for (node = m_springNodes.begin(); 
             node != m_springNodes.end(); 

        PositionList::iterator position = m_positions.begin();
        for (node = m_springNodes.begin(); 
             node != m_springNodes.end(); 
            *position++ = node->GetPosition();13

The m_gravityDirection field stores the normalized direction provided by the GLView layer.


The m_springNodes vector stores the rigid bodies in the simulation.


The m_positions vector provides a contiguous list of node positions to the rendering engine.


The NumColumns, NumPositions, and SpreadFactor constants determine the initial layout of the star sprites.


Center the grid of stars with respect to (0, 0).


Add a connection to the node to the left. The &* prefix converts an STL iterator into a vanilla pointer.


Add a connection to the above node.


Pin the four corners of the net so that they don’t move.


Call UpdateAnimation once at startup to initialize the position list.


The gravity direction vector is normalized, so it needs to be scaled by the GravityStrength constant before passing it in as a force vector.


As mentioned earlier, we make several passes through the simulation loop for increased precision.


The contents of this loop corresponds to pseudocode in Example 7-20.


Copy the node position into the vertex array that gets passed to the rendering engine.

OpenGL ES 1.1 Rendering Engine and Additive Blending

One difficulty you might come across with point sprites is the order-dependency problem imposed by some blending equations (flip back to the section Blending Caveats to review this issue). One way to avoid this is to use additive blending. Here’s how to set it up:

glBlendFunc(GL_SRC_ALPHA, GL_ONE);

This sets up the following equation:

OpenGL ES 1.1 Rendering Engine and Additive Blending

You can see how this differs from traditional blending because it can only make the framebuffer color brighter and brighter as more sprites are rendered. This produces an effect that may be desirable anyway; for example, if you’re rendering fireworks with a dense cloud of point sprites, additive blending helps vary the brightness and make the scene more interesting.

Recall that the IRenderingEngine interface has only two methods; Example 7-24 shows the ES 1.1 implementations of these. The remainder of the file is much the same as other samples in this book. For the full source, download the code from this book’s website.

Example 7-24. RenderingEngine.ES1.cpp (SpringyStars)

void RenderingEngine::Initialize()
    // Load up some textures:
    m_textures.Star = CreateTexture(Star);
    m_textures.Background = CreateTexture(_Background_pvrtc);
    // Extract width and height from the color buffer:
    ivec2 screenSize;
    // Create the on-screen FBO:
    glGenFramebuffersOES(1, &m_framebuffers.Screen);
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_framebuffers.Screen);
    // Set up various OpenGL state:
    glViewport(0, 0, screenSize.x, screenSize.y);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    // Set up the transforms:
    const float NearPlane = 5, FarPlane = 100;
    const float Scale = 0.0005;
    glFrustumf(-Scale * screenSize.x / 2, Scale * screenSize.x / 2,
               -Scale * screenSize.y / 2, Scale * screenSize.y / 2,
               NearPlane, FarPlane);
    vec3 eye(0, 0, 40);
    vec3 target(0, 0, 0);
    vec3 up(0, 1, 0);
    mat4 modelview = mat4::LookAt(eye, target, up);

void RenderingEngine::Render(const PositionList& positions) const
    // Render the background:
    int backgroundRectangle[] = { 0, 0, 480, 320 };
    glBindTexture(GL_TEXTURE_2D, m_textures.Background);
    glColor4f(0.75, 0.75, 0.75, 1);
    glDrawTexfOES(0, 0, 0, 320, 480);

    // Set the state for point rendering:

    // Set up the vertex array:
    glVertexPointer(2, GL_FLOAT, sizeof(vec2), &positions[0].x);

    // Render the point sprites:
    glBindTexture(GL_TEXTURE_2D, m_textures.Star);
    glColor4f(1, 1, 1, 1);
    glDrawArrays(GL_POINTS, 0, positions.size());
    // Restore the OpenGL state:

The only new OpenGL function in Example 7-24 is glPointSize. This sets the width (and height) of the point sprites. OpenGL uses the current model-view matrix to determine the distance of each point sprite from the camera and shrinks the size of distant point sprites. This effect can be abrogated like this:

float params[] = { 1, 0, 0 };
glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, params);

This seems rather obscure, but it has to do with how OpenGL computes the point size:

actualSize = desiredSize / sqrt(p[0] + p[1] * d + p[2] * d * d)

In the previous formula, desiredSize is what you pass to glPointSize, d is the distance from the camera, and p is the array of values passed to glPointParameterfv. (I’ve simplified this a bit by leaving out some clamping that can occur.) In my opinion, the API designers made this a bit too complex!


You can even vary the size of the point sprites on a per-vertex basis through the use of the OES_point_size_array extension, which is supported on all iPhone models.

OpenGL ES 2.0 handles point size quite differently from ES 1.1, which brings us to the next section.

Point Sprites with OpenGL ES 2.0

Before going over the C++ code for the ES 2.0 rendering engine, let’s take a look at the shaders. See Examples 7-25 and 7-26.

Example 7-25. Vertex shader (SpringyStars)

attribute vec4 Position;
attribute vec2 TextureCoord;

uniform mat4 Projection;
uniform mat4 Modelview;

varying vec2 TextureCoordOut;

void main(void)
    gl_Position = Projection * Modelview * Position;
    TextureCoordOut = TextureCoord;
    gl_PointSize = 15.0;

Example 7-26. Fragment shader (SpringyStars)

varying mediump vec2 TextureCoordOut;

uniform sampler2D Sampler;
uniform bool IsSprite;

void main(void)
    gl_FragColor = texture2D(Sampler, 
                             IsSprite ? gl_PointCoord : TextureCoordOut);

You’ve probably noticed that all built-in variables can be recognized by their gl_ prefix. There are two new built-in variables introduced in the previous listings: gl_PointSize (written to by the vertex shader) and gl_PointCoord (fed into the fragment shader).

OpenGL ES 2.0 requires you to set up the point size from the vertex shader rather than the application code, which gives you the option to compute it dynamically; if you want, you can evaluate the same distance formula that ES 1.1 does. Or, you can do something much simpler, like what we’re doing here.

The gl_PointCoord variable gives you the autocomputed texture coordinate that varies across the point sprite. That’s why ES 2.0 doesn’t require you to call glTexEnvi with GL_COORD_REPLACE; it’s implicit in your fragment shader.

Example 7-27. RenderingEngine.ES2.cpp (SpringyStars)

void RenderingEngine::Initialize()
    // Load up some textures:
    m_textures.Star = CreateTexture(Star);
    m_textures.Background = CreateTexture(_Background_pvrtc);
    // Extract width and height from the color buffer.
                                 GL_RENDERBUFFER_WIDTH, &m_screenSize.x);
                                 GL_RENDERBUFFER_HEIGHT, &m_screenSize.y);
    // Create the on-screen FBO.
    glGenFramebuffers(1, &m_framebuffers.Screen);
    glBindFramebuffer(GL_FRAMEBUFFER, m_framebuffers.Screen);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER, m_renderbuffers.Screen);
    // Create the GLSL program.
    GLuint program = BuildProgram(SimpleVertexShader, SimpleFragmentShader);
    // Extract the handles to attributes and uniforms.
    m_attributes.Position = glGetAttribLocation(program, "Position");
    m_attributes.TextureCoord = glGetAttribLocation(program, "TextureCoord");
    m_uniforms.Projection = glGetUniformLocation(program, "Projection");
    m_uniforms.Modelview = glGetUniformLocation(program, "Modelview");
    m_uniforms.Sampler = glGetUniformLocation(program, "Sampler");
    m_uniforms.IsSprite = glGetUniformLocation(program, "IsSprite");
    // Set up various GL state.
    glViewport(0, 0, m_screenSize.x, m_screenSize.y);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    // Set up the transforms.
    const float NearPlane = 5, FarPlane = 100;
    const float Scale = 0.0005;
    const float HalfWidth = Scale * m_screenSize.x / 2;
    const float HalfHeight = Scale * m_screenSize.y / 2;
    mat4 projection = mat4::Frustum(-HalfWidth, HalfWidth, 
                                    -HalfHeight, HalfHeight,
                                    NearPlane, FarPlane);
    glUniformMatrix4fv(m_uniforms.Projection, 1, 0, projection.Pointer());

    vec3 eye(0, 0, 40);
    vec3 target(0, 0, 0);
    vec3 up(0, 1, 0);
    mat4 modelview = mat4::LookAt(eye, target, up);
    glUniformMatrix4fv(m_uniforms.Modelview, 1, 0, modelview.Pointer());

void RenderingEngine::Render(const PositionList& positions) const

    glBindTexture(GL_TEXTURE_2D, m_textures.Star);
    glUniform1i(m_uniforms.IsSprite, GL_TRUE);
    glDrawArrays(GL_POINTS, 0, positions.size());

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