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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Export Blender Models to OpenGL ES: Part 3/3
40 mins
- Getting Started
- Your New Model: The Star (Fox) of the Show
- The Starship Model Viewer
- Rendering With Depth
- Implementing Shaders
- Implementing the Vertex Shader
- Implementing the Fragment Shader
- The CPU-GPU Bridge
- Shader Data
- The Phong Reflection Model
- Adding a Decal Texture
- Adding a Second Model
- Switching Between Models
- Where to Go From Here?
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?
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:
- 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. - 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. - 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. - 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!
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