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

In this second part of our Blender to OpenGL ES tutorial series, learn how to export and render your model’s materials! By Ricardo Rendon Cepeda.

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

Writing the Header File (.h)

Your new header file will be very similar to the one you wrote in Part 1 and the new material data will follow the same style. Locate the function writeH() and add the following line at the end of your statistics set:

outH << "// Materials: " << model.materials << endl;

This line simply adds useful statistics comments to your header file, for your reference.

Next, add the following lines at the end of your declarations set:

// 1
outH << "const int " << name << "Materials;" << endl;
outH << "const int " << name << "Firsts[" << model.materials << "];" << endl;
outH << "const int " << name << "Counts[" << model.materials << "];" << endl;
outH << endl;
    
// 2
outH << "const float " << name << "Diffuses[" << model.materials << "]" << "[" << 3 << "];" << endl;
outH << "const float " << name << "Speculars[" << model.materials << "]" << "[" << 3 << "];" << endl;
outH << endl;

Let’s analyze these snippets separately:

  1. In Part 1 of this tutorial series, OpenGL ES batch-processed all 36 vertices of your cube with glDrawArrays(). If you look at the docs and expand this function, you’ll notice the full set of parameters is glDrawArrays(GLenum mode, GLint first, GLsizei count). Now that you’re adding materials to your cube, you’ll need to render your cube by stages, according to faces grouped by materials. first will be the starting vertex of each face group and count will be the number of vertices to draw for each material.
  2. This second set of statements should look a lot more familiar. Here you declare the arrays to store the actual material data, with each array tailor-made to fit the exact number of materials for the model, with three RGB channels for diffuse and specular colors.

Turn back to main() and uncomment the following line:

writeH(filepathH, nameOBJ, model);

Build and run! Using Finder, locate your new H file (/Code/blender2opengles/product/cube.h) and open it in Xcode. It should look like this:

// This is a .h file for the model: cube

// Positions: 8
// Texels: 14
// Normals: 6
// Faces: 12
// Vertices: 36
// Materials: 6

const int cubeVertices;
const float cubePositions[108];
const float cubeTexels[72];
const float cubeNormals[108];

const int cubeMaterials;
const int cubeFirsts[6];
const int cubeCounts[6];

const float cubeDiffuses[6][3];
const float cubeSpeculars[6][3];

Your header file is all set—way to fly through that one!

Writing The Implementation File (.c)

First, let’s do some housekeeping. Uncomment the following lines in main():

writeCvertices(filepathC, nameOBJ, model);
writeCpositions(filepathC, nameOBJ, model, faces, positions);
writeCtexels(filepathC, nameOBJ, model, faces, texels);
writeCnormals(filepathC, nameOBJ, model, faces, normals);

Xcode flags a few errors here because there’s a mismatch in the function parameters. Find the functions writeCpositions(), writeCtexels() and writeCnormals() and change their respective function signatures to:

void writeCpositions(string fp, string name, Model model, int faces[][10], float positions[][3])

void writeCtexels(string fp, string name, Model model, int faces[][10], float texels[][2])

void writeCnormals(string fp, string name, Model model, int faces[][10], float normals[][3])

Xcode is happy with these changes because they account for the new faces[][10] array.

Now your generated H file includes declarations of an array of cubeFirsts and an array of cubeCounts for glDrawArrays(). But there are no corresponding definitions in the generated C file yet. Let’s fix that.

Add the following lines to main(), before you call the functions to write your C file:

int firsts[model.materials];	// Starting vertex
int counts[model.materials];	// Number of vertices

Then, add counts[] as a parameter to the function call to writeCPositions(), like so:

writeCpositions(filepathC, nameOBJ, model, faces, positions, counts);

And change the function signature to:

void writeCpositions(string fp, string name, Model model, int faces[][10], float positions[][3], int counts[])

You’re going to get through this function first before you focus on the rest of the implementation file, since it’s slightly more complicated. The file handling remains the same, but the order in which you write the vertex positions is changing, so you’ll make all modifications to the for loop of writeCPositions(), shown below:

for(int i=0; i<model.faces; i++)
{
    int vA = faces[i][0] - 1;
    int vB = faces[i][3] - 1;
    int vC = faces[i][6] - 1;
        
    outC << positions[vA][0] << ", " << positions[vA][1] << ", " << positions[vA][2] << ", " << endl;
    outC << positions[vB][0] << ", " << positions[vB][1] << ", " << positions[vB][2] << ", " << endl;
    outC << positions[vC][0] << ", " << positions[vC][1] << ", " << positions[vC][2] << ", " << endl;
}

Replace said for loop with the following:

// 1
for(int j=0; j<model.materials; j++)
{
    counts[j] = 0;
        
    for(int i=0; i<model.faces; i++)
    {
        // 2
        if(faces[i][9] == j)
        {
            int vA = faces[i][0] - 1;
            int vB = faces[i][3] - 1;
            int vC = faces[i][6] - 1;
                
            outC << positions[vA][0] << ", " << positions[vA][1] << ", " << positions[vA][2] << ", " << endl;
            outC << positions[vB][0] << ", " << positions[vB][1] << ", " << positions[vB][2] << ", " << endl;
            outC << positions[vC][0] << ", " << positions[vC][1] << ", " << positions[vC][2] << ", " << endl;
                
            // 3
            counts[j] += 3;

	    // 4
            cout << "usemtl " << faces[i][9]+1 << endl;
        }
    }
}

In this instance, it’s a lot easier to show you the changes than to explain what to do step-by-step, but it’s pretty straightforward once you analyze it:

  1. The original loop is nested inside another for loop, since model faces are grouped according to their material for easy rendering.
  2. If there is a match between the face material reference faces[i][9] and the current material index j, then you write the current face to the material file.
  3. When this happens, the vertex count, counts[], of the material index j increases by 3 vertices (triangular face).
  4. This is a temporary log to show your algorithm in action. It’s difficult to appreciate the changes in your C file at the moment, and I don’t want to over-comment it either.

Build and run. There’s nothing too exciting just yet, but the console shows the materials written in order, each used in two faces as expected. You may remove the cout statement.

Challenge time! This one should be easy, since you’ll modify the functions writeCtexels() and writeCnormals() as above, but without having to worry about counts[].

Hint: There’s no need to change the function signature—just the for loop inside each.

[spoiler title="The Small Refactor"]
For writeCtexels():

// Texels
for(int j=0; j<model.materials; j++)
{
    for(int i=0; i<model.faces; i++)
    {
        if(faces[i][9] == j)
        {
            int vtA = faces[i][1] - 1;
            int vtB = faces[i][4] - 1;
            int vtC = faces[i][7] - 1;
                
            outC << texels[vtA][0] << ", " << texels[vtA][1] << ", " << endl;
            outC << texels[vtB][0] << ", " << texels[vtB][1] << ", " << endl;
            outC << texels[vtC][0] << ", " << texels[vtC][1] << ", " << endl;
        }
    }
}

For writeCnormals():

// Normals
for(int j=0; j<model.materials; j++)
{
    for(int i=0; i<model.faces; i++)
    {
        if(faces[i][9] == j)
        {
            int vnA = faces[i][2] - 1;
            int vnB = faces[i][5] - 1;
            int vnC = faces[i][8] - 1;
                
            outC << normals[vnA][0] << ", " << normals[vnA][1] << ", " << normals[vnA][2] << ", " << endl;
            outC << normals[vnB][0] << ", " << normals[vnB][1] << ", " << normals[vnB][2] << ", " << endl;
            outC << normals[vnC][0] << ", " << normals[vnC][1] << ", " << normals[vnC][2] << ", " << endl;
        }
    }
}

[/spoiler]

And just like that, you’ve restructured your OBJ data!

In Part 1, I mentioned the benefits of modules and then you implemented separate functions for each attribute array. You’ll be doing the same in this part, too, starting with the model’s firsts[] and counts[].

Add the following function to main.cpp, just above main():

void writeCmaterials(string fp, string name, Model model, int firsts[], int counts[])
{
    // Append C file
    ofstream outC;
    outC.open(fp, ios::app);
    
    // Materials
    outC << "const int " << name << "Materials = " << model.materials << ";" << endl;
    outC << endl;
    
    // Firsts
    outC << "const int " << name << "Firsts[" << model.materials << "] = " << endl;
    outC << "{" << endl;
    for(int i=0; i<model.materials; i++)
    {
        // 1
        if(i == 0)
            firsts[i] = 0;
        else
            firsts[i] = firsts[i-1]+counts[i-1];
        
        // 2
        outC << firsts[i] << "," << endl;
    }
    outC << "};" << endl;
    outC << endl;
    
    // Counts
    outC << "const int " << name << "Counts[" << model.materials << "] = " << endl;
    outC << "{" << endl;
    for(int i=0; i<model.materials; i++)
    {
        // 3
        outC << counts[i] << "," << endl;
    }
    outC << "};" << endl;
    outC << endl;
    
    // Close C file
    outC.close();
}

The majority of this function should be standard file I/O for you by now, so let’s just go over the new lines pertaining to firsts[] and counts[]:

  1. You’ve created an array of firsts but you haven’t stored any data in it. Luckily, you can do this as you write out your C file. You’re about to render the first group of faces. If you’re writing data for the first material, then the starting vertex is 0. For all additional materials, the starting vertex is equal to the first vertex of the previous material plus the number of vertices drawn.
  2. So, once you’ve determined the result you simply write it to your C file.
  3. You filled in the data for counts[] earlier, so another simple write suffices here.

Next, add the following line to the end of main():

writeCmaterials(filepathC, nameOBJ, model, firsts, counts);

Build and run! Using Finder, locate your new C file (/Code/blender2opengles/product/cube.c) and open it in Xcode. The new code at the end of the file should look like this:

const int cubeMaterials = 6;

const int cubeFirsts[6] = 
{
0,
6,
12,
18,
24,
30,
};

const int cubeCounts[6] = 
{
6,
6,
6,
6,
6,
6,
};

Your hard work has paid off! And there’s more good news—you’re almost done. It’s time to implement your model’s diffuse and specular colors.

Add the following function above main():

void writeCdiffuses(string fp, string name, Model model, float diffuses[][3])
{
    // Append C file
    ofstream outC;
    outC.open(fp, ios::app);
    
    // Diffuses
    outC << "const float " << name << "Diffuses[" << model.materials << "][3] = " << endl;
    outC << "{" << endl;
    for(int i=0; i<model.materials; i++)
    {
        outC << diffuses[i][0] << ", " << diffuses[i][1] << ", " << diffuses[i][2] << ", " << endl;
    }
    outC << "};" << endl;
    outC << endl;
    
    // Close C file
    outC.close();
}

You should know exactly what’s going on here—you’re simply writing out the RGB diffuse color data of each material. This makes your next challenge very easy... do the same for your specular colors!

Hint: Nope, no hint this time. That would make it far too easy. :]

[spoiler title="Time to Shine"]

void writeCspeculars(string fp, string name, Model model, float speculars[][3])
{
    // Append C file
    ofstream outC;
    outC.open(fp, ios::app);
    
    // Speculars
    outC << "const float " << name << "Speculars[" << model.materials << "][3] = " << endl;
    outC << "{" << endl;
    for(int i=0; i<model.materials; i++)
    {
        outC << speculars[i][0] << ", " << speculars[i][1] << ", " << speculars[i][2] << ", " << endl;
    }
    outC << "};" << endl;
    outC << endl;
    
    // Close C file
    outC.close();
}

[/spoiler]

Finally, add the following lines at the end of main():

writeCdiffuses(filepathC, nameOBJ, model, diffuses);
writeCspeculars(filepathC, nameOBJ, model, speculars);

Build and run! Open cube.c in Xcode and check out your finalized implementation file, which should look like this.

Congratulations, your blender2opengles tool is complete! Your fancy blender model is all set and your materials are ready to shine. Unless they are diffuse, that is…

happy-ba-dum-tss

Copy your files cube.h and cube.c into your /Resources/ folder and take a well-deserved break before you hit the app.

Ricardo Rendon Cepeda

Contributors

Ricardo Rendon Cepeda

Author

Over 300 content creators. Join our team.