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 4 of 5 of this article. Click here to view the first page.

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 a 0 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. :]

s_Run5

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. :]

s_Run6

Ricardo Rendon Cepeda

Contributors

Ricardo Rendon Cepeda

Author

Over 300 content creators. Join our team.