Light Racer 3D - Days 1-2 - Learning OpenGL ES

I feel good about the game engine that I've developed while working on Light Racer. The only big thing that will be different about Light Racer 3D is that instead of drawing 2D sprites, each object will need to be rendered as 3D. To me, this sounds very simple. I plan on modeling everything in the game in 3D Studio Max, developing custom bits of rendering code then merging the 3D world renderer with the existing code base and tweaking to perfection. I'm confident that this will work, yet I'm pretty bad with 3DS, I've never developed anything in OpenGL, much less ES, and I have no existing tools to help me on my way. There has been a lot of reading and experimenting done, but at the end of the day, I have a working scene and a working OBJ file importer that maps textures correctly.

Here are some of the resources I've found most helpful in getting started with OpenGL ES:

The OpenGL ES 1.1 Reference Manual
This intro/tutorial on OpenGL Coordinate systems
This collection of tutorials of various OpenGL functions
The Google IO Presentation - Writing Real-Time Games in Android

Starting an OpenGL ES project from scratch can be seemingly impossible if it's your first time. Most likely, you will just end up with a black screen. I started with an example from the API demos. I believe I used the rotating cube. I started learning by just messing around with the cube. I'd add more cubes, make some bigger and some smaller, move some around, change colors, change rotations, change camera angles, etc. These are things you can do just to learn how the different transformations affect the scene.

Once I started to get the hang of that, I figured that I could create an Arena object and render it. I copied the cube to Arena and worked out the difference of Geometry. I pass in the width, height and depth on the constructor and build geometry to match. I also had to flip around the order of indices to make the triangles for the faces because I wanted the inside faces showing, not the outside. I removed the top and voila, I have a working arena!

A Note on Fixed Point conversions - 1.0 in floating point = 1 << 16 in fixed point. You can easily convert from float to fixed point by multiplying by 0x10000, since that is 16 bits. Try to do everything you can in fixed point. Floats will be slower!

Applying textures:

This is where I had the most problems. I found so many tutorials on OpenGL that would show how to put in texture coordinates for every vertex but OpenGL ES doesn't allow for point-by-point definition of primitives. We, instead, must define an array of vertices, an array of texture coordinates and an array of indices. If you're not sure how this works, think of it like this:

A vertex is a 3 dimensional point in space (x,y,z). 3 vertices can make a triangle.
OpenGL ES draws triangles using the indices. Each index points to a vertex. The idea is that you can re-use vertexes for multiple faces (triangles).
Texture coordinates are mapped 1-to-1 with vertices, NOT indices. The value of a texture coordinate is 0 to 1 and should always be a floating point number. The number represents a percentage of distance, starting in the upper left at 0,0 down to 1.0, 1.0 in the lower right, of the texture image to be applied.

The problem is that when applying textures, you basically need to define the same vertex more than once if two different faces connected to it are to be textured starting at two different locations on the texture. My arena texture is like this. I ended up defining 4 vertices and 6 indices for each side. This allowed me to map a specific square of texture for each face, using 4 texture coordinates. It worked very well this way.

Here is the texture I used and here are some screenshots of how things currently look.

Basic arena texture (128x128)Basic arena texture (128x128) Rectangle Racer in 3DSRectangle Racer in 3DS First arena w/rectangle racerFirst arena w/rectangle racer

The first image is the actual texture that is being applied to both the arena and the rectangle racer. The second image is a screenshot of the rectangle in 3DStudio. The arena is 440 in size, so I will make the racer 30x8x8 so that it will be the right size and I won't have to scale in the game. The third is a screenshot of how the "game" currently looks. The arena is rendered with the racer rectangle sitting in the middle. You can see how that texture applies.

Importing OBJ Files

If you're not lost in 3D land at this point, you may be asking yourself, "How did you get that 3D Studio rectangle into Android OpenGL ES?" Great question. I exported the .obj file and wrote a tool for my game that can import the obj, aggregate the geometry and output a file that looks like this:

public class ModelData3D {
public int[] vertices;
public float[] tex;
public short[] indices;
public int vertexCount;
public void print() {
System.out.println("vertices=" + Arrays.toString(vertices));
System.out.println("tex=" + Arrays.toString(tex));
System.out.println("indices=" + Arrays.toString(indices));
}
}

Once you have the model data in those arrays, you can easily create byte buffers, set your texture, transformations, etc and render it using GL10.glDrawElements.

Here's the obj importer I wrote. Depending on what you use to export and what options you have set, you may need to tweak it. It's written specifically for the settings I've chosen to use for my models. It may not work right off the bat for you.

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.StringTokenizer;
public class ObjImporter {
private static final String SPACE = " ";
private static final String SLASH = "/";
public static ModelData3D importObj(InputStream in) throws Exception {
ModelData3D data = new ModelData3D();
ArrayList<VertexF> verticies = new ArrayList<VertexF>();
ArrayList<UVCoord> uvs = new ArrayList<UVCoord>();
ArrayList<Face> faces = new ArrayList<Face>();
// 1) read in verticies,
// 2) read in uvs
// 3) create faces which are verticies and uvs expanded
// 4) unroll faces into ModelData3D using sequential indicies
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringTokenizer st;
String line = reader.readLine();
System.out.println("Loading obj data");
while (line != null) {
st = new StringTokenizer(line, SPACE);
if (st.countTokens() > 1) {
String lineType = st.nextToken(SPACE);
if (lineType.equals("v")) {
// vertex
VertexF vert = new VertexF();
vert.x = Float.valueOf(st.nextToken());
vert.y = Float.valueOf(st.nextToken());
vert.z = Float.valueOf(st.nextToken());
verticies.add(vert);
} else if (lineType.equals("vt")) {
// texture mapping
UVCoord uv = new UVCoord();
uv.u = Float.valueOf(st.nextToken());
uv.v = Float.valueOf(st.nextToken());
uvs.add(uv);
} else if (lineType.equals("f")) {
// face
Face face = new Face();
face.v1 = verticies.get(Integer.valueOf(st.nextToken(SLASH).trim()) - 1);
face.uv1 = uvs.get(Integer.valueOf(st.nextToken(SPACE).substring(1)) - 1);
face.v2 = verticies.get(Integer.valueOf(st.nextToken(SLASH).trim()) - 1);
face.uv2 = uvs.get(Integer.valueOf(st.nextToken(SPACE).substring(1)) - 1);
face.v3 = verticies.get(Integer.valueOf(st.nextToken(SLASH).trim()) - 1);
face.uv3 = uvs.get(Integer.valueOf(st.nextToken(SPACE).substring(1)) - 1);
faces.add(face);
}
}
line = reader.readLine();
}
//printFaces(faces);
int facesSize = faces.size();
System.out.println(facesSize + " polys");
data.vertexCount = facesSize * 3;
data.vertices = new int[facesSize * 3 * 3];
data.tex = new float[facesSize * 3 * 2];
data.indices = new short[facesSize * 3];
for (int i = 0; i < facesSize; i++) {
Face face = faces.get(i);
data.vertices[i * 9] = toFP(face.v1.x);
data.vertices[i * 9 + 1] = toFP(face.v1.y);
data.vertices[i * 9 + 2] = toFP(face.v1.z);
data.vertices[i * 9 + 3] = toFP(face.v2.x);
data.vertices[i * 9 + 4] = toFP(face.v2.y);
data.vertices[i * 9 + 5] = toFP(face.v2.z);
data.vertices[i * 9 + 6] = toFP(face.v3.x);
data.vertices[i * 9 + 7] = toFP(face.v3.y);
data.vertices[i * 9 + 8] = toFP(face.v3.z);
data.tex[i * 6] = face.uv1.u;
data.tex[i * 6 + 1] = face.uv1.v;
data.tex[i * 6 + 2] = face.uv2.u;
data.tex[i * 6 + 3] = face.uv2.v;
data.tex[i * 6 + 4] = face.uv3.u;
data.tex[i * 6 + 5] = face.uv3.v;
data.indices[i * 3] = (short) (i * 3);
data.indices[i * 3 + 1] = (short) (i * 3 + 1);
data.indices[i * 3 + 2] = (short) (i * 3 + 2);
}
reader.close();
return data;
}
private static int toFP(float f) {
// normally you'd << 16 but um, can't do that with a float so we multiply by 16 bits.
return (int)((double)f * 0x10000);
}
private static void printFaces(ArrayList faces) {
for (Face f : faces) {
System.out.println("Face uv1 " + f.uv1.u + " " + f.uv1.v);
System.out.println("Face uv2 " + f.uv2.u + " " + f.uv2.v);
System.out.println("Face uv3 " + f.uv3.u + " " + f.uv3.v);
System.out.println("Face v1 " + f.v1.x + " " + f.v1.y + " " + f.v1.z);
System.out.println("Face v2 " + f.v2.x + " " + f.v2.y + " " + f.v2.z);
System.out.println("Face v3 " + f.v3.x + " " + f.v3.y + " " + f.v3.z);
}
}
private static class VertexF {
public float x;
public float y;
public float z;
}
private static class UVCoord {
public float u;
public float v;
}
private static class Face {
public UVCoord uv1, uv2, uv3;
public VertexF v1, v2, v3;
}
public static void main(String[] args) {
try {
ModelData3D data = importObj(new FileInputStream("racer.obj"));
data.print();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}

For more help working with .obj files, check out Roy Rigg's OBJ File Format page

5 Comments

Post a comment here or discuss this and other topics in the forums

There is a small bug in the

There is a small bug in the code.

private static void printFaces(ArrayList faces) {
for (Face f : faces) {
...

should be:

private static void printFaces(ArrayList faces) {
for (Face f : faces) {
...

or

private static void printFaces(ArrayList faces) {
for (Object face : faces) {
Face f = (Face)face;
...

Question on filesystem

I was able to implement that snippet into my own code, but I'm having trouble with the FileInputStream. Where exactly did you place "racer.obj"? in the root of the sdcard? the resources folder?
I haven't been able to test it since I can't get the program to find the file i asked for :(

ps. I love this game :D

I actually used raw resources

I actually used raw resources to hold things like the geometry files but if I were to do it over again I'd use assets and use Android's built in input stream for assets (see context.getAssets().open("filename"))

Game Engine

Just curious, it seems like you are developing all the graphics by yourself. Have you ever consider the pros and cons of using a preexisting game engine for smartphone. I am not even sure if there is one out there, but I am just wondering what the pros and cons are.

Engine and graphics are two

Engine and graphics are two different things. Using an engine, you still must always create your own content, which includes graphics, levels, sound, text, etc..

I would have considered using an engine but there are currently none available for Android. I know of a few in the works but even with one, simple games that have specialized gameplay like this are sometimes easier to do without trying to modify a different engine. It all depends on how they are built, though. Engines are nice because they already set up the environment well so all you have to do is start plugging in content and game logic. When you don't have that, you have to develop all of that stuff by yourself. Check out the architectural diagram in a previous article to see what I'm talking about.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.