So far, we've been drawing quads and cubes, but if we want to draw more complex geometry, it is probably handier to model it on a 3D modeling tool rather than doing it by code. We can cover multiple chapters on this topic, but let's just look at a quick example of how can it be done and you can extend it to your needs.
We have used a Blender to model our example data. Blender is a free and open source 3D modeling toolset and can be downloaded for free at its website:
https://www.blender.org/.
For this example, we haven't modeled an extremely complex example; we've just used one of the primitives that Blender provides: Suzanne:
![](assets/0f8fb505-8aeb-46bd-b17e-fc7dcd36f3f4.png)
To simplify our importing tool, we'll select the object mesh under the Scene | Suzanne drop-down menu on the right and, when we press Ctrl + T, Blender will convert all faces into triangles. Otherwise, we'll have both triangles and quads on our exported file and it's not straightforward to implement the face importer from our Android application code:
![](assets/976e8d5b-34e3-4ddb-8ebf-9d791dc29f78.png)
Now, we'll export it as a Wavefront (.obj) file, which will create both a .obj and a .mtl file. The latter is the material information that, for the moment, we'll ignore. Let's put the exported file into our project in the assets folder.
Let's now create a very simple Wavefront file object parser ourselves. As we'll be dealing with files, loading, and parsing, we'll have to do it asynchronously:
public class WavefrontObjParser { public static void parse(Context context, String name, ParserListener listener) { WavefrontObjParserHelper helper = new WavefrontObjParserHelper(context, name, listener); helper.start(); } public interface ParserListener { void parsingSuccess(Scene scene); void parsingError(String message); } }
As you can see, there is no actual work done here. To do the real loading and parsing, we've created a helper class that will do it on a separate thread and call the listener either if it's successful or if there has been an error parsing the file:
class WavefrontObjParserHelper extends Thread { private String name; private WavefrontObjParser.ParserListener listener; private Context context; WavefrontObjParserHelper(Context context, String name,
WavefrontObjParser.ParserListener listener) { this.context = context; this.name = name; this.listener = listener; }
Then, when we call helper.start(), it'll create the actual thread, and execute the run() method on it:
public void run() { try { InputStream is = context.getAssets().open(name); BufferedReader br = new BufferedReader(new
InputStreamReader(is)); Scene scene = new Scene(); Object3D obj = null; String str; while ((str = br.readLine()) != null) { if (!str.startsWith("#")) { String[] line = str.split(""); if("o".equals(line[0])) { if (obj != null) obj.prepare(); obj = new Object3D(); scene.addObject(obj); } else if("v".equals(line[0])) { float x = Float.parseFloat(line[1]); float y = Float.parseFloat(line[2]); float z = Float.parseFloat(line[3]); obj.addCoordinate(x, y, z); } else if("f".equals(line[0])) { int a = getFaceIndex(line[1]); int b = getFaceIndex(line[2]); int c = getFaceIndex(line[3]); if (line.length == 4) { obj.addFace(a, b, c); } else { int d = getFaceIndex(line[4]); obj.addFace(a, b, c, d); } } else { // skip } } } if (obj != null) obj.prepare(); br.close(); if (listener != null) listener.parsingSuccess(scene); } catch(Exception e) { if (listener != null) listener.parsingError(e.getMessage()); e.printStackTrace(); } }
In the previous code, we first read the asset by opening the file with the name provided. To get the application assets, we need a context here:
InputStream is = context.getAssets().open(name); BufferedReader br = new BufferedReader(new InputStreamReader(is));
Then, we read the file line by line and we take different actions depending on the starting keyword, except if the line starts with #, which means that it's a comment. We're only taking into consideration the commands of a new object, vertex coordinates, and face index; we're ignoring any additional commands that there might be on the file, such as material used, or vertex and face normals.
As we can get face index information, such as f 330//278 336//278 338//278 332//278, we created a helper method to parse that information and only extract the face index. The number after the slashes is the face normal index. Refer to the official file format to understand the usage of the face index numbers in more detail:
private static int getFaceIndex(String face) { if(!face.contains("/")) { return Integer.parseInt(face) - 1; } else { return Integer.parseInt(face.split("/")[0]) - 1; } }
Also, as face indices start at 1, we have to subtract 1 to get it right.
To store all this data we're reading from the file, we've also created some data classes. The Object3D class will store all relevant information-vertices, face indexes, and the Scene class will store the whole 3D scene with all the Objects3D inside. For simplicity, we've kept these implementations as short as possible, but they can be made way more complex depending on our needs:
public class Scene { private ArrayList<Object3D> objects; public Scene() { objects = new ArrayList<>(); } public void addObject(Object3D obj) { objects.add(obj); } public ArrayList<Object3D> getObjects() { return objects; } public void render(int shaderProgram, String posAttributeName,
String colAttributeName) { GLES20.glEnable(GLES20.GL_DEPTH_TEST); for (int i = 0; i < objects.size(); i++) { objects.get(i).render(shaderProgram, posAttributeName,
colAttributeName); } GLES20.glDisable(GLES20.GL_DEPTH_TEST); } }
We can see that there is a render() method on the Scene class. We've moved the responsibility of rendering all its 3D objects to the Scene itself, and, applying the same principle, each object is also responsible for rendering itself:
public void prepare() { if (coordinateList.size() > 0 && coordinates == null) { coordinates = new float[coordinateList.size()]; for (int i = 0; i < coordinateList.size(); i++) { coordinates[i] = coordinateList.get(i); } } if (indexList.size() > 0 && indexes == null) { indexes = new short[indexList.size()]; for (int i = 0; i < indexList.size(); i++) { indexes[i] = indexList.get(i); } } colors = new float[(coordinates.length/3) * 4]; for (int i = 0; i < colors.length/4; i++) { float intensity = (float) (Math.random() * 0.5 + 0.4); colors[i * 4 ] = intensity; colors[i * 4 + 1] = intensity; colors[i * 4 + 2] = intensity; colors[i * 4 + 3] = 1.f; } ByteBuffer vbb = ByteBuffer.allocateDirect(coordinates.length *
(Float.SIZE / 8)); vbb.order(ByteOrder.nativeOrder()); vertexBuffer = vbb.asFloatBuffer(); vertexBuffer.put(coordinates); vertexBuffer.position(0); ByteBuffer ibb = ByteBuffer.allocateDirect(indexes.length *
(Short.SIZE / 8)); ibb.order(ByteOrder.nativeOrder()); indexBuffer = ibb.asShortBuffer(); indexBuffer.put(indexes); indexBuffer.position(0); ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length *
(Float.SIZE / 8)); cbb.order(ByteOrder.nativeOrder()); colorBuffer = cbb.asFloatBuffer(); colorBuffer.put(colors); colorBuffer.position(0); Log.i(TAG, "Loaded obj with " + coordinates.length + " vertices &"
+ (indexes.length/3) + " faces"); }
Once we've set all the data to the 3DObject, we can prepare it to render by calling its prepare() method. This method will create the vertex and index Buffer, and, as in this case we don't have any color information from the mesh on the data file, it'll generate a random color, or rather an intensity, for each vertex.
Creating the buffers here in the 3DObject itself allows us to render any kind of object. The Scene container doesn't know what kind of object or what kind of geometry is inside. We could easily extend this class with another type of 3DObject, as long as it handles its own rendering.
Finally, we've added a render() method to the 3DObject:
public void render(int shaderProgram, String posAttributeName, String colAttributeName) { int positionHandle = GLES20.glGetAttribLocation(shaderProgram,
posAttributeName); GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 3 * 4, vertexBuffer); int colorHandle = GLES20.glGetAttribLocation(shaderProgram,
colAttributeName); GLES20.glVertexAttribPointer(colorHandle, 4, GLES20.GL_FLOAT, false, 4 * 4, colorBuffer); GLES20.glEnableVertexAttribArray(colorHandle); GLES20.glEnableVertexAttribArray(positionHandle); GLES20.glDrawElements( GLES20.GL_TRIANGLES, indexes.length, GLES20.GL_UNSIGNED_SHORT, indexBuffer); GLES20.glDisableVertexAttribArray(positionHandle); GLES20.glDisableVertexAttribArray(colorHandle); }
This method is responsible for enabling and disabling the right arrays and rendering itself. We get the shader attributes from the method parameters. Ideally, each object could have its own shader, but we didn't want to add that much complexity in this example.
In our GLDrawer class, we've also added a helper method to calculate a perspective frustrum matrix. One of the most used calls in OpenGL was gluPerspective, and NeHe, the author of many awesome OpenGL tutorials, created a function to convert gluPerspective to a glFrustrum call:
// source: http://nehe.gamedev.net/article/replacement_for_gluperspective/21002/ private static void perspectiveFrustrum(float[] matrix, float fov, float aspect, float zNear, float zFar) { float fH = (float) (Math.tan( fov / 360.0 * Math.PI ) * zNear); float fW = fH * aspect; Matrix.frustumM(matrix, 0, -fW, fW, -fH, fH, zNear, zFar); }
As we don't need it anymore, we've removed all vertex and face index information from GLDrawer and simplified the onDrawFrame() method to now delegate the rendering of all objects to the Scene class, and, by default, to each individual 3DObject:
@Override public void onDrawFrame(GL10 unused) { angle = ((float) SystemClock.elapsedRealtime() - startTime) *
0.02f; GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
GLES20.GL_DEPTH_BUFFER_BIT); if (scene != null) { Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -4, 0f, 0f, 0f, 0f, 1.0f, 0.0f); Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0,
mViewMatrix, 0); Matrix.rotateM(mMVPMatrix, 0, angle, 0.8f, 2.f, 1.f); GLES20.glUseProgram(shaderProgram); int mMVPMatrixHandle = GLES20.glGetUniformLocation(shaderProgram, "uMVPMatrix"); GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false,
mMVPMatrix, 0); scene.render(shaderProgram, "vPosition", "aColor"); } }
Putting it all together, if we run this example, we'll get the following screen:
![](assets/88851ca6-f800-445a-9f74-e2e0c7edf0f9.png)
Check the Example26-GLDrawing on the GitHub repository for the full example source code.