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?
Adding a Decal Texture
Textures and materials aren’t mutually exclusive, so let’s bring textures back into your app (but don’t call it a comeback). You’ll be implementing a decal texture, which is a sticker-like effect transferred onto a surface that retains its own characteristics, like vinyl car graphics. Take a look at starship_decal.png in your /Resources/starship/ folder to give yourself an idea of what you’ll be adding to your model. You’ll start the implementation in your shaders.
Open Phong.vsh and add the following variables to your shader:
attribute vec2 aTexel;
varying vec2 vTexel;
Then, add the following line to main()
:
vTexel = aTexel;
With these three lines, you’re simply passing your aTexel
attribute to your fragment shader via vTexel
. Remember that fragment shaders can’t access attributes, so the varying type solves your problem here.
Next, open Phong.fsh and add vTexel
to your list of variables:
varying highp vec2 vTexel;
Also add the following to your list of uniforms:
uniform sampler2D uTexture;
sampler2D
is a special GLSL variable exclusively used for 2D texture access. It’s attached to a predetermined texture—more on this later.
Next, delete the statement assigning a value to gl_FragColor
, your current output, and instead add the following lines at the bottom of main()
:
// Decal
highp vec4 decal = texture2D(uTexture, vTexel);
// Surface
highp vec3 surface;
if(decal.a > 0.0)
surface = decal.rgb;
else
surface = Ip;
gl_FragColor = vec4(surface, 1.0);
texture2d()
extracts the RGBA color value of a texture (uTexture
) at a certain texel point (vTexel
). Since a decal works like a sticker, your shader will make a decision based on the alpha value of decal
: if there is a graphic, stick it onto your model’s surface; if there isn’t a graphic, let the calculated Phong illumination define your model’s surface. Notice that your scene affects neither your decal’s color nor its intensity.
With your shaders set, let’s move onto their bridge. Open PhongShader.h and add the following line to your list of attribute handles:
@property (readwrite) GLint aTexel;
Similarly, add the following to your list of uniform handles:
@property (readwrite) GLint uTexture;
Next, open PhongShader.m and add the following lines to init
in their respective attribute and uniform sections.
self.aTexel = glGetAttribLocation(self.program, "aTexel");
self.uTexture = glGetUniformLocation(self.program, "uTexture");
There, your updated bridge is all set! Let’s move onto the actual texture now.
Open MainViewController.m and add the following function, just below viewDidLoad
:
- (void)loadTexture
{
NSDictionary* options = @{GLKTextureLoaderOriginBottomLeft: @YES};
NSError* error;
NSString* path = [[NSBundle mainBundle] pathForResource:@"starship_decal.png" ofType:nil];
GLKTextureInfo* texture = [GLKTextureLoader textureWithContentsOfFile:path options:options error:&error];
if(texture == nil)
NSLog(@"Error loading file: %@", [error localizedDescription]);
glBindTexture(GL_TEXTURE_2D, texture.name);
glUniform1i(self.phongShader.uTexture, 0);
}
You’ve already seen the majority of this texture-loading process in Part 1, so I’ll only discuss the new gl
-prefixed functions:
-
glBindTexture()
specifies the target to which the texture is bound. In this case, your decal is a single 2D texture. -
glUniform1i()
sends the texture to your shader. You send a0
because you only have one active texture in your program, at the first position of 0.
Call your new function by adding the following to viewDidLoad
, at the very end:
// Load texture
[self loadTexture];
Finally, send your starship’s texel data to your shader by adding the following lines to glkView:drawInRect:
, just after the setup for your other attributes (positions and normals) and before the loop that renders the materials:
// Texels
glEnableVertexAttribArray(self.phongShader.aTexel);
glVertexAttribPointer(self.phongShader.aTexel, 2, GL_FLOAT, GL_FALSE, 0, starshipTexels);
Build and run! Your starship sports a cool decal finish—a proud testament of your achievements. :]
Adding a Second Model
While your starship looks amazing, it would be a shame not to see your cube from Part 1 and Part 2 rendered with your new shaders. It would be too easy to just replace every instance of starship with cube in your source code, so instead you’ll implement a new structure extendable for multiple models.
Open MainViewController.m and add the following line to the top of your file:
#import "cube.h"
Then, add the following lines just below:
typedef enum Models
{
M_CUBE,
M_STARSHIP,
}
Models;
This is an enumerator to easily reference your models by name. Create a variable for said enumerator by adding the following line to your @interface
variables:
Models _model;
Then initialize it to your cube model by adding the following to viewDidLoad
:
_model = M_CUBE;
The next steps in this section are essentially a light refactor of your code to create a variable-dependent rendering scenario based on the value of _model
. First up is your texture loader.
In your function loadTexture
, replace the line:
NSString* path = [[NSBundle mainBundle] pathForResource:@"starship_decal.png" ofType:nil];
With the following:
NSString* path;
switch(_model)
{
case M_CUBE:
path = [[NSBundle mainBundle] pathForResource:@"cube_decal.png" ofType:nil];
break;
case M_STARSHIP:
path = [[NSBundle mainBundle] pathForResource:@"starship_decal.png" ofType:nil];
break;
}
This simple switch
statement makes sure your program loads the correct texture for each model.
Now you’ll refactor your app’s rendering loop. Add the following functions at the end of MainViewController.m, before the @end
statement:
- (void)renderCube
{
glVertexAttribPointer(self.phongShader.aPosition, 3, GL_FLOAT, GL_FALSE, 0, cubePositions);
glVertexAttribPointer(self.phongShader.aTexel, 2, GL_FLOAT, GL_FALSE, 0, cubeTexels);
glVertexAttribPointer(self.phongShader.aNormal, 3, GL_FLOAT, GL_FALSE, 0, cubeNormals);
for(int i=0; i<cubeMaterials; i++)
{
glUniform3f(self.phongShader.uDiffuse, cubeDiffuses[i][0], cubeDiffuses[i][1], cubeDiffuses[i][2]);
glUniform3f(self.phongShader.uSpecular, cubeSpeculars[i][0], cubeSpeculars[i][1], cubeSpeculars[i][2]);
glDrawArrays(GL_TRIANGLES, cubeFirsts[i], cubeCounts[i]);
}
}
- (void)renderStarship
{
glVertexAttribPointer(self.phongShader.aPosition, 3, GL_FLOAT, GL_FALSE, 0, starshipPositions);
glVertexAttribPointer(self.phongShader.aTexel, 2, GL_FLOAT, GL_FALSE, 0, starshipTexels);
glVertexAttribPointer(self.phongShader.aNormal, 3, GL_FLOAT, GL_FALSE, 0, starshipNormals);
for(int i=0; i<starshipMaterials; i++)
{
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]);
glDrawArrays(GL_TRIANGLES, starshipFirsts[i], starshipCounts[i]);
}
}
These functions are virtually identical to one another, each responsible for rendering a model. You’re lifting the rendering process directly from glkView:drawInRect
, which is about to be a lot lighter. Replace said function with the new approach below:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Set matrices
[self setMatrices];
glEnableVertexAttribArray(self.phongShader.aPosition);
glEnableVertexAttribArray(self.phongShader.aTexel);
glEnableVertexAttribArray(self.phongShader.aNormal);
// Render model
switch(_model)
{
case M_CUBE:
[self renderCube];
break;
case M_STARSHIP:
[self renderStarship];
break;
}
}
That looks a lot nicer, doesn’t it? The rendering loop can get quite complicated in a large graphics scene, so allowing simple calls to functions like renderCube
or renderStarship
makes life much easier.
Build and run... Whoops, your cube is way too close! It’s actually twice the size of your starship model, so you’ll have to scale it down.
Locate the function setMatrices
and look for the line:
glUniformMatrix4fv(self.phongShader.uModelViewMatrix, 1, 0, modelViewMatrix.m);
Immediately above it, after you translate and rotate your model-view matrix, add the following lines:
switch(_model)
{
case M_CUBE:
modelViewMatrix = GLKMatrix4Scale(modelViewMatrix, 0.5f, 0.5f, 0.5f);
break;
case M_STARSHIP:
modelViewMatrix = GLKMatrix4Scale(modelViewMatrix, 1.0f, 1.0f, 1.0f);
break;
}
Build and run! Your cube is now 50% of its former size and it looks great. :]