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

Adding Shaders to ModelViewer

OpenGL ES 2.0 does not automatically perform lighting math behind the scenes; instead, it relies on developers to provide it with shaders that perform whatever type of lighting they desire. Let’s come up with a vertex shader that mimics the math done by ES 1.1 when lighting is enabled.

To keep things simple, we’ll use the infinite light source model for diffuse (Feeding OpenGL with Normals) combined with the infinite viewer model for specular (Give It a Shine with Specular). We’ll also assume that the light is white. Example 4-14 shows the pseudocode.

Example 4-14. Basic lighting pseudocode

vec3 ComputeLighting(vec3 normal)
{
    N = NormalMatrix * normal
    L = Normalize(LightPosition)
    E = (0, 0, 1)
    H = Normalize(L + E)
    df = max(0, NL)
    sf = max(0, NH)
    sf = sf ^ Shininess
    return AmbientMaterial + DiffuseMaterial * df + SpecularMaterial * sf
}

Note the NormalMatrix variable in the pseudocode; it would be silly to recompute the inverse-transpose of the model-view at every vertex, so we’ll compute it up front in the application code then pass it in as the NormalMatrix uniform. In many cases, it happens to be equivalent to the model-view, but we’ll leave it to the application to decide how to compute it.

Let’s add a new file to the ModelViewer project called SimpleLighting.vert for the lighting algorithm. In Xcode, right-click the Shaders folder, and choose AddNew file. Select the Empty File template in the Other category. Name it SimpleLighting.vert, and add /Shaders after the project folder name in the location field. Deselect the checkbox in the Targets list, and click Finish.

Example 4-15 translates the pseudocode into GLSL. To make the shader usable in a variety of situations, we use uniforms to store light position, specular, and ambient properties. A vertex attribute is used to store the diffuse color; for many models, the diffuse color may vary on a per-vertex basis (although in our case it does not). This would allow us to use a single draw call to draw a multicolored model.

Warning

Remember, we’re leaving out the STRINGIFY macros in all shader listings from here on out, so take a look at Example 1-13 to see how to add that macro to this file.

Example 4-15. SimpleLighting.vert

attribute vec4 Position;
attribute vec3 Normal;
attribute vec3 DiffuseMaterial;

uniform mat4 Projection;
uniform mat4 Modelview;
uniform mat3 NormalMatrix;
uniform vec3 LightPosition;
uniform vec3 AmbientMaterial;
uniform vec3 SpecularMaterial;
uniform float Shininess;

varying vec4 DestinationColor;

void main(void)
{
    vec3 N = NormalMatrix * Normal;
    vec3 L = normalize(LightPosition);
    vec3 E = vec3(0, 0, 1);
    vec3 H = normalize(L + E);

    float df = max(0.0, dot(N, L));
    float sf = max(0.0, dot(N, H));
    sf = pow(sf, Shininess);

    vec3 color = AmbientMaterial + df * DiffuseMaterial + sf * SpecularMaterial;
    
    DestinationColor = vec4(color, 1);
    gl_Position = Projection * Modelview * Position;
}

Take a look at the pseudocode in Example 4-14; the vertex shader is an implementation of that. The main difference is that GLSL requires you to qualify many of the variables as being attributes, uniforms, or varyings. Also note that in its final code line, Example 4-15 performs the standard transformation of the vertex position, just as it did for the nonlit case.

Warning

GLSL is a bit different from many other languages in that it does not autopromote literals from integers to floats. For example, max(0, myFloat) generates a compile error, but max(0.0, myFloat) does not. On the other hand, constructors for vector-based types do perform conversion implicitly; it’s perfectly legal to write either vec2(0, 0) or vec3(0.0, 0.0).

New Rendering Engine

To create the ES 2.0 backend to ModelViewer, let’s start with the ES 1.1 variant and make the following changes, some of which should be familiar by now:

  1. Copy the contents of RenderingEngine.ES1.cpp into RenderingEngine.ES2.cpp.

  2. Remove the _OES and OES suffixes from the FBO code.

  3. Change the namespace from ES1 to ES2.

  4. Change the two #includes to point to the ES2 folder rather than the ES1 folder.

  5. Add the BuildShader and BuildProgram methods (see Example 1-18). You must change all instances of RenderingEngine2 to RenderingEngine because we are using namespaces to distinguish between the 1.1 and 2.0 renderers.

  6. Add declarations for BuildShader and BuildProgram to the class declaration as shown in Example 1-15.

  7. Add the #include for iostream as shown in Example 1-15.

Now that the busywork is out of the way, let’s add declarations for the uniform handles and attribute handles that are used to communicate with the vertex shader. Since the vertex shader is now much more complex than the simple pass-through program we’ve been using, let’s group the handles into simple substructures, as shown in Example 4-16. Add this code to RenderingEngine.ES2.cpp, within the namespace declaration, not above it. (The bold part of the listing shows the two lines you must add to the class declaration’s private: section.)

Example 4-16. ES2::RenderingEngine structures

#define STRINGIFY(A)  #A
#include "../Shaders/SimpleLighting.vert"
#include "../Shaders/Simple.frag"

struct UniformHandles {
    GLuint Modelview;
    GLuint Projection;
    GLuint NormalMatrix;
    GLuint LightPosition;
};
    
struct AttributeHandles {
    GLint Position;
    GLint Normal;
    GLint Ambient;
    GLint Diffuse;
    GLint Specular;
    GLint Shininess;
};

class RenderingEngine : public IRenderingEngine {
    // ...
    UniformHandles m_uniforms;
    AttributeHandles m_attributes;
};

Next we need to change the Initialize method so that it compiles the shaders, extracts the handles to all the uniforms and attributes, and sets up some default material colors. Replace everything from the comment // Set up various GL state to the end of the method with the contents of Example 4-17.

Example 4-17. ES2::RenderingEngine::Initialize()

...

// Create the GLSL program.
GLuint program = BuildProgram(SimpleVertexShader, SimpleFragmentShader);
glUseProgram(program);

// Extract the handles to attributes and uniforms.
m_attributes.Position = glGetAttribLocation(program, "Position");
m_attributes.Normal = glGetAttribLocation(program, "Normal");
m_attributes.Ambient = glGetAttribLocation(program, "AmbientMaterial");
m_attributes.Diffuse = glGetAttribLocation(program, "DiffuseMaterial");
m_attributes.Specular = glGetAttribLocation(program, "SpecularMaterial");
m_attributes.Shininess = glGetAttribLocation(program, "Shininess"); 
m_uniforms.Projection = glGetUniformLocation(program, "Projection");
m_uniforms.Modelview = glGetUniformLocation(program, "Modelview");
m_uniforms.NormalMatrix = glGetUniformLocation(program, "NormalMatrix");
m_uniforms.LightPosition = glGetUniformLocation(program, "LightPosition");

// Set up some default material parameters.
glVertexAttrib3f(m_attributes.Ambient, 0.04f, 0.04f, 0.04f);
glVertexAttrib3f(m_attributes.Specular, 0.5, 0.5, 0.5);
glVertexAttrib1f(m_attributes.Shininess, 50);

// Initialize various state.
glEnableVertexAttribArray(m_attributes.Position);
glEnableVertexAttribArray(m_attributes.Normal);
glEnable(GL_DEPTH_TEST);

// Set up transforms.
m_translation = mat4::Translate(0, 0, -7);

Next let’s replace the Render() method, shown in Example 4-18.

Example 4-18. ES2::RenderingEngine::Render()

void RenderingEngine::Render(const vector<Visual>& visuals) const
{
    glClearColor(0, 0.125f, 0.25f, 1);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    vector<Visual>::const_iterator visual = visuals.begin();
    for (int visualIndex = 0; 
         visual != visuals.end(); 
         ++visual, ++visualIndex) {

        // Set the viewport transform.
        ivec2 size = visual->ViewportSize;
        ivec2 lowerLeft = visual->LowerLeft;
        glViewport(lowerLeft.x, lowerLeft.y, size.x, size.y);
        
        // Set the light position.
        vec4 lightPosition(0.25, 0.25, 1, 0);
        glUniform3fv(m_uniforms.LightPosition, 1, lightPosition.Pointer());

        // Set the model-view transform.
        mat4 rotation = visual->Orientation.ToMatrix();
        mat4 modelview = rotation * m_translation;
        glUniformMatrix4fv(m_uniforms.Modelview, 1, 0, modelview.Pointer());
        
        // Set the normal matrix.
        // It's orthogonal, so its Inverse-Transpose is itself!
        mat3 normalMatrix = modelview.ToMat3();
        glUniformMatrix3fv(m_uniforms.NormalMatrix, 1, 
                           0, normalMatrix.Pointer());

        // Set the projection transform.
        float h = 4.0f * size.y / size.x;
        mat4 projectionMatrix = mat4::Frustum(-2, 2, -h / 2, h / 2, 5, 10);
        glUniformMatrix4fv(m_uniforms.Projection, 1, 
                           0, projectionMatrix.Pointer());
        
        // Set the diffuse color.
        vec3 color = visual->Color * 0.75f;
        glVertexAttrib4f(m_attributes.Diffuse, color.x, 
                         color.y, color.z, 1);
        
        // Draw the surface.
        int stride = 2 * sizeof(vec3);
        const GLvoid* offset = (const GLvoid*) sizeof(vec3);
        GLint position = m_attributes.Position;
        GLint normal = m_attributes.Normal;
        const Drawable& drawable = m_drawables[visualIndex];
        glBindBuffer(GL_ARRAY_BUFFER, drawable.VertexBuffer);
        glVertexAttribPointer(position, 3, GL_FLOAT, 
                              GL_FALSE, stride, 0);
        glVertexAttribPointer(normal, 3, GL_FLOAT, GL_FALSE, 
                              stride, offset);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, drawable.IndexBuffer);
        glDrawElements(GL_TRIANGLES, drawable.IndexCount, 
                       GL_UNSIGNED_SHORT, 0);
    }
}

That’s it for the ES 2.0 backend! Turn off the ForceES1 switch in GLView.mm, and you should see something very similar to the ES 1.1 screenshot shown in Figure 4-11.

Per-Pixel Lighting

When a model has coarse tessellation, performing the lighting calculations at the vertex level can result in the loss of specular highlights and other detail, as shown in Figure 4-13.

Tessellation and lighting (from left to right: infinite tessellation, vertex lighting, and pixel lighting)

Figure 4-13. Tessellation and lighting (from left to right: infinite tessellation, vertex lighting, and pixel lighting)

One technique to counteract this unattractive effect is per-pixel lighting; this is when most (or all) of the lighting algorithm takes place in the fragment shader.

Warning

Shifting work from the vertex shader to the pixel shader can often be detrimental to performance. I encourage you to experiment with performance before you commit to a specific technique.

The vertex shader becomes vastly simplified, as shown in Example 4-19. It simply passes the diffuse color and eye-space normal to the fragment shader.

Example 4-19. PixelLighting.vert

attribute vec4 Position;
attribute vec3 Normal;
attribute vec3 DiffuseMaterial;

uniform mat4 Projection;
uniform mat4 Modelview;
uniform mat3 NormalMatrix;

varying vec3 EyespaceNormal;
varying vec3 Diffuse;

void main(void)
{
    EyespaceNormal = NormalMatrix * Normal;
    Diffuse = DiffuseMaterial;
    gl_Position = Projection * Modelview * Position;
}

The fragment shader now performs the burden of the lighting math, as shown in Example 4-20. The main distinction it has from its per-vertex counterpart (Example 4-15) is the presence of precision specifiers throughout. We’re using lowp for colors, mediump for the varying normal, and highp for the internal math.

Example 4-20. PixelLighting.frag

varying mediump vec3 EyespaceNormal;
varying lowp vec3 Diffuse;

uniform highp vec3 LightPosition;
uniform highp vec3 AmbientMaterial;
uniform highp vec3 SpecularMaterial;
uniform highp float Shininess;

void main(void)
{
    highp vec3 N = normalize(EyespaceNormal);
    highp vec3 L = normalize(LightPosition);
    highp vec3 E = vec3(0, 0, 1);
    highp vec3 H = normalize(L + E);
    
    highp float df = max(0.0, dot(N, L));
    highp float sf = max(0.0, dot(N, H));
    sf = pow(sf, Shininess);

    lowp vec3 color = AmbientMaterial + df * Diffuse + sf * SpecularMaterial;

    gl_FragColor = vec4(color, 1);
}

Note

To try these, you can replace the contents of your existing .vert and .frag files. Just be sure not to delete the first line with STRINGIFY or the last line with the closing parenthesis and semicolon.

Shifting work from the vertex shader to the fragment shader was simple enough, but watch out: we’re dealing with the normal vector in a sloppy way. OpenGL performs linear interpolation on each component of each varying. This causes inaccurate results, as you might recall from the coverage of quaternions in Chapter 3. Pragmatically speaking, simply renormalizing the incoming vector is often good enough. We’ll cover a more rigorous way of dealing with normals when we present bump mapping in Bump Mapping and DOT3 Lighting.

Toon Shading

Mimicking the built-in lighting functionality in ES 1.1 gave us a fairly painless segue to the world of GLSL. We could continue mimicking more and more ES 1.1 features, but that would get tiresome. After all, we’re upgrading to ES 2.0 to enable new effects, right? Let’s leverage shaders to create a simple effect that would otherwise be difficult (if not impossible) to achieve with ES 1.1.

Toon shading (sometimes cel shading) achieves a cartoony effect by limiting gradients to two or three distinct colors, as shown in Figure 4-14.

Toon shading

Figure 4-14. Toon shading

Assuming you’re already using per-pixel lighting, achieving this is actually incredibly simple; just add the bold lines in Example 4-21.

Example 4-21. ToonShading.frag

varying mediump vec3 EyespaceNormal;
varying lowp vec3 Diffuse;

uniform highp vec3 LightPosition;
uniform highp vec3 AmbientMaterial;
uniform highp vec3 SpecularMaterial;
uniform highp float Shininess;

void main(void)
{
    highp vec3 N = normalize(EyespaceNormal);
    highp vec3 L = normalize(LightPosition);
    highp vec3 E = vec3(0, 0, 1);
    highp vec3 H = normalize(L + E);
    
    highp float df = max(0.0, dot(N, L));
    highp float sf = max(0.0, dot(N, H));
    sf = pow(sf, Shininess);

    if (df < 0.1) df = 0.0;
    else if (df < 0.3) df = 0.3;
    else if (df < 0.6) df = 0.6;
    else df = 1.0;
    
    sf = step(0.5, sf);

    lowp vec3 color = AmbientMaterial + df * Diffuse + sf * SpecularMaterial;

    gl_FragColor = vec4(color, 1);
}

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