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.
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 2/3
50 mins
- Getting Started
- A Fancy Blender Cube
- Diffuse Reflection and Specular Reflection
- Blender Materials
- The MTL File Format
- Exporting an MTL File From Blender
- Analyzing Your MTL File
- Building Your MTL to OpenGL ES Tool
- The Material Info
- The Material Data
- Pairing Your Materials to Your Geometry
- Writing the Header File (.h)
- Writing The Implementation File (.c)
- Enhancing Your Model Viewer iOS App
- GLKBaseEffect Materials
- Rendering by Parts
- Rendering Your Materials
- Where to Go From Here?
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?):
- 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 functionsubstr
and discarding the string prefix“newmtl ”
. The result is the material name, stored inmaterials
. - Here, try implementing the code to extract the RGB data of diffuse colors into the array
diffuses[][3]
, usingd
as a counter. - Here, try doing the same as above but for specular colors, using
speculars[][3]
ands
.
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
.
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:
- You’ve updated the method signature to account for the new
faces[][10]
array. You are also now passingmaterials
to check your list of material names andm
to loop through said list. -
mtl
is the index of a material inmaterials
. - 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. - Just as in
extractMTLdata()
, you extract the material name by using the functionsubstr
and discarding the string prefix"usemtl "
. - 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 tomaterials
inmtl
. - You then append
mtl
tofaces[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.
Now you’ve parsed your materials properly—nice one!