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

Learn how to export blender models to OpenGL ES in this three part tutorial series! By Ricardo Rendon Cepeda.

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

The Model Data

Now that you know your model attribute sizes, it’s time to create the dedicated data arrays. Add the following lines to main():

// Model Data
float positions[model.positions][3];    // XYZ
float texels[model.texels][2];          // UV
float normals[model.normals][3];        // XYZ
int faces[model.faces][9];              // PTN PTN PTN

Each 2D array fits the exact number of attributes with the following data:

  • positions[][3]: three floats, one for each coordinate in the XYZ space.
  • texels[][2]: two floats, one for each coordinate in the UV space.
  • normals[][3]: three floats, for a vector in the XYZ space.
  • faces[][9]: nine integers, to describe the three vertices of a triangular face, where each vertex gets three indexes, one for its position (P), one for its texel (T) and one for its normal (N).

Please note that the above values defining your data model do not depend on the specific cube shape at all. These values all follow directly from the basic definition of a vertex, texel, normal and triangular face.

Your next goal is to parse the cube’s data from the OBJ file into these arrays. Add the following function definition above main():

void extractOBJdata(string fp, float positions[][3], float texels[][2], float normals[][3], int faces[][9])
{
    // Counters
    int p = 0;
    int t = 0;
    int n = 0;
    int f = 0;
    
    // Open OBJ file
    ifstream inOBJ;
    inOBJ.open(fp);
    if(!inOBJ.good())
    {
        cout << "ERROR OPENING OBJ FILE" << endl;
        exit(1);
    }
    
    // Read OBJ file
    while(!inOBJ.eof())
    {
        string line;
        getline(inOBJ, line);
        string type = line.substr(0,2);
        
        // Positions
        if(type.compare("v ") == 0)
        {
        }
        
        // Texels
        else if(type.compare("vt") == 0)
        {
        }
        
        // Normals
        else if(type.compare("vn") == 0)
        {
        }
        
        // Faces
        else if(type.compare("f ") == 0)
        {
        }
    }
    
    // Close OBJ file
    inOBJ.close();
}

This new function is very similar to getOBJinfo() in the previous section, so take a moment to notice the differences and similarities.

Both functions read the OBJ file and parse each line looking for a type of geometry element. But instead of simply counting the element types by incrementing members of the model object, extractOBJinfo extracts and stores the whole data set for each attribute. To do this, it needs to handle each type of geometry element differently.

Let’s start with positions[][3]. Add the following code to extractOBJdata() to make the if conditional for your positions look like this:

// Positions
if(type.compare("v ") == 0)
{
    // 1
    // Copy line for parsing
    char* l = new char[line.size()+1];
    memcpy(l, line.c_str(), line.size()+1);
            
    // 2
    // Extract tokens
    strtok(l, " ");
    for(int i=0; i<3; i++)
        positions[p][i] = atof(strtok(NULL, " "));
            
    // 3
    // Wrap up
    delete[] l;
    p++;
}

This is only a little bit of code, but it’s tricky:

  1. Before parsing the current OBJ line, it’s best to create a working copy (l) separate from the file being read. The +1 accounts for the end-of-line character. Keep in mind that you are allocating memory here.
  2. strtok(l, “ “) tells your program to create a token from l up to the first “ “ character. Your program ignores the first token (“v”), but stores the next three (x, y, z) as floats in positions[][3] (typecast by atof()). strtok(NULL, “ “) simply tells the program to parse the next token, continuing from the previous string.
  3. To wrap things up, you must deallocate your memory for l and increase the counter p for positions[][3].

It’s short but powerful! A similar process follows for texels[][2], normals[][3] and faces[][9]. Can you complete the code on your own?

Hint #1: Pay close attention to each array size to figure out the number of tokens it expects to receive.

Hint #2: After the initial token “f”, the data for each face is separated by either a “ “ or a “/” character.

[spoiler title="Parsing Attributes"]

// Texels
else if(type.compare("vt") == 0)
{
    char* l = new char[line.size()+1];
    memcpy(l, line.c_str(), line.size()+1);
            
    strtok(l, " ");
    for(int i=0; i<2; i++)
        texels[t][i] = atof(strtok(NULL, " "));
            
    delete[] l;
    t++;
}
        
// Normals
else if(type.compare("vn") == 0)
{
    char* l = new char[line.size()+1];
    memcpy(l, line.c_str(), line.size()+1);
            
    strtok(l, " ");
    for(int i=0; i<3; i++)
        normals[n][i] = atof(strtok(NULL, " "));
            
    delete[] l;
    n++;
}
        
// Faces
else if(type.compare("f ") == 0)
{
    char* l = new char[line.size()+1];
    memcpy(l, line.c_str(), line.size()+1);
            
    strtok(l, " ");
    for(int i=0; i<9; i++)
        faces[f][i] = atof(strtok(NULL, " /"));
            
    delete[] l;
    f++;
}

[/spoiler]

You should feel very proud of yourself if you figured that one out! If you didn’t, I don’t blame you, especially if you are new to C++.

Moving on, add the following line to main():

extractOBJdata(filepathOBJ, positions, texels, normals, faces);
cout << "Model Data" << endl;
cout << "P1: " << positions[0][0] << "x " << positions[0][1] << "y " << positions[0][2] << "z" << endl;
cout << "T1: " << texels[0][0] << "u " << texels[0][1] << "v " << endl;
cout << "N1: " << normals[0][0] << "x " << normals[0][1] << "y " << normals[0][2] << "z" << endl;
cout << "F1v1: " << faces[0][0] << "p " << faces[0][1] << "t " << faces[0][2] << "n" << endl;

Build and run! The console shows the first entry for each attribute of your cube model. Make sure the output matches your cube.obj file.

s_ModelData

Good job, you have successfully parsed your OBJ file!

Generating the Header File (.h)

OpenGL ES will read your Blender model as a collection of arrays. You could write all of these straight into a C header file, but this approach may cause trouble if you reference your model in more than one part of your app. So, you’re going to split the job into a header (.h) and an implementation (.c) file, with the header file containing the forward declarations for your arrays.

You already know how to read an existing file with C++, but now it’s time to create and write to a new file. Add the following function definition to main.cpp, above the definition of main():

// 1
void writeH(string fp, string name, Model model)
{
    // 2
    // Create H file
    ofstream outH;
    outH.open(fp);
    if(!outH.good())
    {
        cout << "ERROR CREATING H FILE" << endl;
        exit(1);
    }
    
    // 3
    // Write to H file
    outH << "// This is a .h file for the model: " << name << endl;
    outH << endl;
    
    // 4
    // Close H file
    outH.close();
}

This code snippet is very similar to the read implementation from before, but let’s go over each step to clarify the process:

  1. fp is the path of your new H file, with name being the name of your model and model containing its info.
  2. ofstream opens your H file for writing (output). If no file exists at fp, a new file is created for you.
  3. Much like cout, outH writes to your file in the same style.
  4. Close your H file and you’re good to go!

Now add the following line inside the body of main():

// Write H file
writeH(filepathH, nameOBJ, model);

Then build and run! Using Finder, check your project directory for the new H file (/Code/blender2opengles/product/cube.h), which should look something like this:

s_HeaderFile

Return to the function writeH() and add the following lines inside, just before you close the file:

// Write statistics
outH << "// Positions: " << model.positions << endl;
outH << "// Texels: " << model.texels << endl;
outH << "// Normals: " << model.normals << endl;
outH << "// Faces: " << model.faces << endl;
outH << "// Vertices: " << model.vertices << endl;
outH << endl;
    
// Write declarations
outH << "const int " << name << "Vertices;" << endl;
outH << "const float " << name << "Positions[" << model.vertices*3 << "];" << endl;
outH << "const float " << name << "Texels[" << model.vertices*2 << "];" << endl;
outH << "const float " << name << "Normals[" << model.vertices*3 << "];" << endl;
outH << endl;

The first set of statements simply adds useful statistics comments to your header file, for your reference. The second set declares your arrays. Remember that OpenGL ES needs to batch-process all 36 vertices for the cube (3 vertices * 12 faces) and that each attribute needs space for its own data—positions in XYZ, texels in UV and normals in XYZ.

Build and run! Open cube.h in Xcode and make sure it looks like this:

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

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

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

Looking good. ;]

Ricardo Rendon Cepeda

Contributors

Ricardo Rendon Cepeda

Author

Over 300 content creators. Join our team.