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.
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 1/3
50 mins
- Getting Started
- A Simple Blender Cube
- The OBJ File Format
- Exporting an OBJ From Blender
- Analyzing Your OBJ File
- Building an OBJ to OpenGL ES Command Line Tool
- Project Setup
- Project Directory
- Input/Output, Files and Strings
- Command Line Arguments
- The Model Info
- The Model Data
- Generating the Header File (.h)
- Generating the Implementation File (.c)
- Building the Model Viewer iOS App
- Project Setup
- Adding a GLKit View Controller
- Using Your Storyboard
- Drawing a Gray Screen
- Creating a GLKBaseEffect
- Rendering Your Model: Geometry
- Rendering Your Model: Texture
- Rendering Your Model: Light
- Rendering Your Model: Animation
- Where to Go From Here?
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:
- 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. -
strtok(l, “ “)
tells your program to create a token froml
up to the first“ “
character. Your program ignores the first token (“v”
), but stores the next three (x, y, z) as floats inpositions[][3]
(typecast byatof()
).strtok(NULL, “ “)
simply tells the program to parse the next token, continuing from the previous string. - To wrap things up, you must deallocate your memory for
l
and increase the counterp
forpositions[][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.
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:
-
fp
is the path of your new H file, withname
being the name of your model andmodel
containing its info. -
ofstream
opens your H file for writing (output). If no file exists atfp
, a new file is created for you. - Much like
cout
,outH
writes to your file in the same style. - 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:
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. ;]