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

The Material Data

With your materials counted, you’ll now use this information to create arrays for their names, diffuse colors and specular colors. Add the following lines to main(), just after your geometry arrays and before your call to extractOBJdata:

string* materials = new string[model.materials];    // Name
float diffuses[model.materials][3];                 // RGB
float speculars[model.materials][3];                // RGB

materials stores a string for each material name, as spelled out in your MTL file and referenced in your OBJ file. diffuses[][3] and speculars[][3] both store three floats, one for each color channel in an RGB representation.

Now you’ll parse the material data from the MTL file into these arrays. Add the following function above main():

void extractMTLdata(string fp, string* materials, float diffuses[][3], float speculars[][3])
{
    // Counters
    int m = 0;
    int d = 0;
    int s = 0;
    
    // Open MTL file
    ifstream inMTL;
    inMTL.open(fp);
    if(!inMTL.good())
    {
        cout << "ERROR OPENING MTL FILE" << endl;
        exit(1);
    }
    
    // Read MTL file
    while(!inMTL.eof())
    {
        string line;
        getline(inMTL, line);
        string type = line.substr(0,2);
        
        // Names
        if(type.compare("ne") == 0)
        {
            // 1
            // Extract token
            string l = "newmtl ";
            materials[m] = line.substr(l.size());
            m++;
        }
        
        // 2
        // Diffuses
        else if(type.compare("Kd") == 0)
        {
            // Implementation challenge!
        }
        
        // 3
        // Speculars
        else if(type.compare("Ks") == 0)
        {
            // Implementation challenge!
        }
    }
    
    // Close MTL file
    inMTL.close();
}

Once again, this should be very familiar, but this time I’ll only introduce the new code and let you implement the rest! (Challenge accepted?):

  1. As you know, the code identifies a material with the token newmtl. It then extracts the next token within the current line (after the white space) by using the function substr and discarding the string prefix “newmtl ”. The result is the material name, stored in materials.
  2. Here, try implementing the code to extract the RGB data of diffuse colors into the array diffuses[][3], using d as a counter.
  3. Here, try doing the same as above but for specular colors, using speculars[][3] and s.

Give it a shot! If you’re stuck or want to verify your implementation, check the solution below.

Hint: The code for this challenge is virtually identical to the parsing for positions[][3] in the function extractOBJdata() from Part 1.

[spoiler title="Parsing Materials"]

// Diffuses
else if(type.compare("Kd") == 0)
{
    // Copy line for parsing
    char* l = new char[line.size()+1];
    memcpy(l, line.c_str(), line.size()+1);
            
    // Extract tokens
    strtok(l, " ");
    for(int i=0; i<3; i++)
        diffuses[d][i] = atof(strtok(NULL, " "));
            
    // Wrap up
    delete[] l;
    d++;
}
        
// Speculars
else if(type.compare("Ks") == 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++)
        speculars[s][i] = atof(strtok(NULL, " "));
            
    delete[] l;
    s++;
}

[/spoiler]

Three cheers for completing the challenge! Or copying the code—that works, too. :]

To see your results, add the following lines to main(), right after the call to extractOBJdata():

extractMTLdata(filepathMTL, materials, diffuses, speculars);
cout << "Name1: " << materials[0] << endl;
cout << "Kd1: " << diffuses[0][0] << "r " << diffuses[0][1] << "g " << diffuses[0][2] << "b " << endl;
cout << "Ks1: " << speculars[0][0] << "r " << speculars[0][1] << "g " << speculars[0][2] << "b " << endl;

Build and run! The console now shows the data for the first material in your MTL file, MaterialDiffuseM.

s_MaterialData

Good job—you’ve successfully parsed your MTL file!

Pairing Your Materials to Your Geometry

You’re not quite done with the materials yet. You have their data but still don’t know which faces they’re attached to. For this, you’ll have to refer back to your OBJ file.

First, inside main(), increase your faces[model.faces][9] array by 1 to store 10 integers. Your new line should look like this:

int faces[model.faces][10];                         // PTN PTN PTN M

This change causes Xcode to flag a new error for you at the call to extractOBJdata(), because faces[][10] doesn’t match the function parameter. For now, remove this function call completely; you’ll add it again shortly.

This new storage unit will be a reference to the material appended to each face, just as the OBJ file sorts faces by material. extractOBJdata() requires a major facelift, so replace your current function with the following:

// 1
void extractOBJdata(string fp, float positions[][3], float texels[][2], float normals[][3], int faces[][10], string* materials, int m)
{
    // Counters
    int p = 0;
    int t = 0;
    int n = 0;
    int f = 0;
    
    // 2
    // Index
    int mtl = 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);
        
        // 3
        // Material
        if(type.compare("us") == 0)
        {
            // 4
            // Extract token
            string l = "usemtl ";
            string material = line.substr(l.size());
            
            for(int i=0; i<m; i++)
            {
                // 5
                if(material.compare(materials[i]) == 0)
                    mtl = i;
            }
        }
        
        // Positions
        if(type.compare("v ") == 0)
        {
            // Copy line for parsing
            char* l = new char[line.size()+1];
            memcpy(l, line.c_str(), line.size()+1);
            
            // Extract tokens
            strtok(l, " ");
            for(int i=0; i<3; i++)
                positions[p][i] = atof(strtok(NULL, " "));
            
            // Wrap up
            delete[] l;
            p++;
        }
        
        // 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, " /"));
            
            // 6
            // Append material
            faces[f][9] = mtl;
            
            delete[] l;
            f++;
        }
    }
    
    // Close OBJ file
    inOBJ.close();
}

That’s a big block of code (sorry!), but you’ve seen and implemented most of it before so it shouldn’t be too scary. Let’s go over the changes:

  1. You’ve updated the method signature to account for the new faces[][10] array. You are also now passing materials to check your list of material names and m to loop through said list.
  2. mtl is the index of a material in materials.
  3. As you know, the token usemtl references a material. In an OBJ file, a comparison to the two-character token “us” is sufficient to identify this reference.
  4. Just as in extractMTLdata(), you extract the material name by using the function substr and discarding the string prefix "usemtl ".
  5. You then compare this material name to each material in materials, as parsed from the MTL file. Unfortunately, the materials/faces in the OBJ file are not organized in the same order of appearance as in the MTL file—hence this approach. You store the matched material as an index to materials in mtl.
  6. You then append mtl to faces[f][9].

See? It’s not too bad after all.

Run your function and print out your results by adding the following lines to main(), after your call to extractMTLdata():

extractOBJdata(filepathOBJ, positions, texels, normals, faces, materials, model.materials);
    
cout << "Material References" << endl;
for(int i=0; i<model.faces; i++)
{
    int m = faces[i][9];
    cout << "F" << i << "m: " << materials[m] << endl;
}

Build and run! The console will tell you which material each face uses. Make sure the output matches your cube.obj file.

s_MaterialReferences

Now you’ve parsed your materials properly—nice one!

Ricardo Rendon Cepeda

Contributors

Ricardo Rendon Cepeda

Author

Over 300 content creators. Join our team.