How To Export Blender Models to OpenGL ES: Part 3/3

In this third part of our Blender to OpenGL ES tutorial series, learn how to implement a simple shader to showcase your model! By Ricardo Rendon Cepeda.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

Implementing the Vertex Shader

OK, it’s time to beef up your vertex shader! Add the following lines to Phong.vsh, just above main():

// Attributes
attribute vec3 aPosition;
attribute vec3 aNormal;
 
// Uniforms
uniform mat4 uProjectionMatrix;
uniform mat4 uModelViewMatrix;
uniform mat3 uNormalMatrix;
 
// Varying
varying vec3 vNormal;

These are your shader variables and they’re separated into three main categories that determine the type and source of data they will receive from the main program:

  • Attributes typically change per vertex and are thus exclusive to the vertex shader. Your model’s positions and normals change per vertex, so you declare them as attributes.
  • Uniforms typically change per frame and can also be implemented by the fragment shader. Your scene’s projection and model-view matrices affect all vertices uniformly, so you declare them as uniforms.
  • Varying variables act as outputs from the vertex shader to the fragment shader—geometry first, coloring second.

Furthermore, vec variables refer to vectors with two, three or four components and mat variables refer to matrices with 2×2, 3×3 or 4×4 dimensions.

This tutorial will cover uNormalMatrix and vNormal later, but for now know that they’re both essential to your model’s normals and how they affect the lighting of your starship.

Finally, replace the contents of main() with the following:

vNormal = uNormalMatrix * aNormal;
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aPosition, 1.0);

You learned all about MVP matrices in Part 1 and this is how GLKBaseEffect implements them under the hood, along with GLKVertexAttribPosition (replaced by aPosition). See? Looking under the hood isn’t so scary. :]

Implementing the Fragment Shader

Let’s move on to your fragment shader. Add the following lines to Phong.fsh, just above main():

// Varying
varying highp vec3 vNormal;
 
// Uniforms
uniform highp vec3 uDiffuse;
uniform highp vec3 uSpecular;

Remember the scary calculations that showed how many more fragments than vertices there could be?

must_optimize

Every little bit of optimization helps avoid a nightmare filled with millions of fragments. This is why your fragment shader variables require a precision qualifier such as lowp, mediump or highp to determine the range and precision of your variable. You can read more about them in the docs, but your scene will be so simple that you’ll be totally fine just sticking to highp.

GLKEffectPropertyMaterial also sends its diffuse and specular color data to a shader, but you don’t get to see that. Here, you’re doing exactly the same and you get to define your own shading model! Let’s start with a simple one.

Replace the contents of main() with the following:

highp vec3 material = (0.5*uDiffuse) + (0.5*uSpecular);
gl_FragColor = vec4(material, 1.0);

With these two lines, you color your fragments 50% diffuse and 50% specular. :]

The CPU-GPU Bridge

Your shaders are all set, but they happen to live in the GPU, far away from your CPU-based app. In order to get all your model’s data from your app to your shaders, you need to build some sort of “bridge” to connect the two. It’s time to ditch GLSL and switch back to Objective-C!

Click File\New\File… and choose the iOS\Cocoa Touch\Objective-C class subclass template. Enter PhongShader for the class and Shader for the subclass. Make sure both checkboxes are unchecked, click Next and then click Create.

Open PhongShader.h and replace the existing file contents with the following:

#import "Shader.h"

@interface PhongShader : Shader

// Program Handle
@property (readwrite) GLint program;

// Attribute Handles
@property (readwrite) GLint aPosition;
@property (readwrite) GLint aNormal;

// Uniform Handles
@property (readwrite) GLint uProjectionMatrix;
@property (readwrite) GLint uModelViewMatrix;
@property (readwrite) GLint uNormalMatrix;
@property (readwrite) GLint uDiffuse;
@property (readwrite) GLint uSpecular;

@end

With this list of properties, you create a set of handles for your GLSL variables—notice the identical naming. The only new variable is program, which I’ll explain in a moment. All properties are type GLint because they are only indices to your shader variables—they’re not the actual data.

Next, open PhongShader.m and replace the existing file contents with the following:

#import "PhongShader.h"

// 1
// Shaders
#define STRINGIFY(A) #A
#include "Phong.vsh"
#include "Phong.fsh"

@implementation PhongShader

- (id)init
{
    if(self = [super init])
    {
        // 2
        // Program
        self.program = [self BuildProgram:PhongVSH with:PhongFSH];
        
        // 3
        // Attributes
        self.aPosition = glGetAttribLocation(self.program, "aPosition");
        self.aNormal = glGetAttribLocation(self.program, "aNormal");
        
        // 4
        // Uniforms
        self.uProjectionMatrix = glGetUniformLocation(self.program, "uProjectionMatrix");
        self.uModelViewMatrix = glGetUniformLocation(self.program, "uModelViewMatrix");
        self.uNormalMatrix = glGetUniformLocation(self.program, "uNormalMatrix");
        self.uDiffuse = glGetUniformLocation(self.program, "uDiffuse");
        self.uSpecular = glGetUniformLocation(self.program, "uSpecular");
    }
    
    return self;
}

@end

There’s a lot more going on here, so let’s walk through it step-by-step:

  1. There’s your STRINGIFY macro! This converts your shaders into strings so they can be processed by the GPU. You can learn more about stringification here.
  2. Since your shaders run on the GPU, they’re only readable at runtime with OpenGL ES. That means that the CPU needs to give the GPU special instructions to compile and link your shaders into a single program. The OpenGL ES 2.0 for iPhone Tutorial covers this process in detail, so give it a read for more information. The program creation functions from said tutorial have been encapsulated in your Shader class for an easy implementation here.
  3. You create your CPU-GPU handles by referencing your shader variable’s name and program. For calls to glGetAttribLocation(), the first parameter specifies the OpenGL ES program to be queried and the second parameter points to the name of the attribute within the program.
  4. Similar to the above, for calls to glGetUniformLocation(), the first parameter specifies the program to be queried and the second parameter points to the name of the uniform.

Good job—your bridge is looking pretty sturdy!

Shader Data

Time to send your shaders some meaningful data from your app.

Open MainViewController.m and import your new shader by adding the following line at the top of your file:

#import "PhongShader.h"

Just below, replace the line:

@property (strong, nonatomic) GLKBaseEffect* effect;

With:

@property (strong, nonatomic) PhongShader* phongShader;

Xcode won’t like that and will flag a ton of errors for you, but you’re here to say goodbye to GLKBaseEffect and say hello to PhongShader. Xcode just needs to be more patient.

Next, remove the function createEffect and add the following right below viewDidLoad:

- (void)loadShader
{
    self.phongShader = [[PhongShader alloc] init];
    glUseProgram(self.phongShader.program);
}

Consequently, remove this line from viewDidLoad:

[self createEffect];

And add this one instead:

[self loadShader];

There are just a couple more to go, but you can already see that you are replacing GLKBaseEffect line-by-line. This way, I hope you can appreciate its underlying shader-based functionality.

Locate the function setMatrices and replace the (non-consecutive) lines:

self.effect.transform.projectionMatrix = projectionMatrix;

self.effect.transform.modelviewMatrix = modelViewMatrix;

With this:

glUniformMatrix4fv(self.phongShader.uProjectionMatrix, 1, 0, projectionMatrix.m);

glUniformMatrix4fv(self.phongShader.uModelViewMatrix, 1, 0, modelViewMatrix.m);

These calls are slightly more complex, but really you’re just telling OpenGL ES the location of your shader’s uniform variable and the data for this variable.

Finally, turn your attention to glkView:drawInRect: and replace the following lines:

// Positions
glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, starshipPositions);
    
// Normals
glEnableVertexAttribArray(GLKVertexAttribNormal);
glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, 0, starshipNormals);

With these:

// Positions
glEnableVertexAttribArray(self.phongShader.aPosition);
glVertexAttribPointer(self.phongShader.aPosition, 3, GL_FLOAT, GL_FALSE, 0, starshipPositions);
    
// Normals
glEnableVertexAttribArray(self.phongShader.aNormal);
glVertexAttribPointer(self.phongShader.aNormal, 3, GL_FLOAT, GL_FALSE, 0, starshipNormals);

Everything remains the same except the attribute index.

One more to go! Within the for loop, replace the following lines:

self.effect.material.diffuseColor = GLKVector4Make(starshipDiffuses[i][0], starshipDiffuses[i][1], starshipDiffuses[i][2], 1.0f);
self.effect.material.specularColor = GLKVector4Make(starshipSpeculars[i][0], starshipSpeculars[i][1], starshipSpeculars[i][2], 1.0f);

With these:

glUniform3f(self.phongShader.uDiffuse, starshipDiffuses[i][0], starshipDiffuses[i][1], starshipDiffuses[i][2]);
glUniform3f(self.phongShader.uSpecular, starshipSpeculars[i][0], starshipSpeculars[i][1], starshipSpeculars[i][2]);

And remove this:

// Prepare effect
[self.effect prepareToDraw];

Drum roll, please… Build and run!

s_Run3

Your starship appears with its geometry and materials intact. You’ve pretty much replaced GLKBaseEffect with your own shader, note for note, and got a pretty good result by simply mixing your diffuse and specular colors.

The reason your starship looks flat is because you haven’t implemented any lighting… yet. So give yourself a quick Hi-5 and let’s keep on truckin’. :D

Ricardo Rendon Cepeda

Contributors

Ricardo Rendon Cepeda

Author

Over 300 content creators. Join our team.