Specifying Triangles

Next up we have to figure out how we can tell OpenGL ES about the triangles we want it to render. First let's define what a triangle is made of:

■ A triangle is made of three points.

■ Each point is called a vertex.

■ A vertex has a position in 3D space.

■ A position in 3D space is given as three floats, specifying the x-, y-, and z-coordinates.

■ A vertex can have additional attributes, such as a color or texture coordinates (which we'll talk about later). These can be represented as floats as well.

OpenGL ES expects to send our triangle definitions in the form of arrays. However, given that OpenGL ES is actually a C API, we can't just use standard Java arrays. Instead we have to use Java NIO buffers, which are just memory blocks of consecutive bytes.

A Small NIO Buffer Digression

To be totally exact, we need to use direct NIO buffers. This means that the memory is not allocated in the virtual machine's heap memory, but in native heap memory. To construct such a direct NIO buffer, we can use the following code snippet:

ByteBuffer buffer = ByteBuffer.allocateDirect(NUMBER_OF_BYTES); buffer.order(ByteOrder.nativeOrder());

This will allocate a ByteBuffer that can hold NUMBER_OF_BYTES bytes in total, and make sure that the byte order is equal to the byte order used by the underlying CPU. A NIO buffer has three attributes:

■ Capacity: The number of elements the buffer can hold in total

■ Position: The current position to which the next element would be written to or read from

■ Limit: The index of the last element that has been defined plus one

The capacity of a buffer is actually its size. In the case of a ByteBuffer, it is given in bytes. The position and limit attributes can be thought of as defining a segment within the buffer starting at position and ending at limit (exclusive).

Since we want to specify our vertices as floats, it would be nice not to have to cope with bytes. Luckily we can convert the ByteBuffer instance to a FloatBuffer instance which allows us just that: working with floats.

FloatBuffer floatBuffer = buffer.asFloatBuffer();

Capacity, position, and limit are given in floats in the case of a FloatBuffer. Our usage pattern of these buffers will be pretty limited — it goes like this:

float[] vertices = { ... definitions of vertex positions etc ...;

floatBuffer.clear();

floatBuffer.put(vertices);

floatBuffer.flip();

We first define our data in a standard Java float array. Before we put that float array into the buffer, we tell the buffer to clear itself via the clear() method. This doesn't actually erase any data, but sets the position to zero and the limit to the capacity. Next we use the FloatBuffer.put(float[] array) method to copy the content of the complete array to the buffer, beginning at the buffer's current position. After the copying, the position of the buffer will be increased by the length of the array. Next, the call to the put() method then appends the additional data to the data of the last array we copied to the buffer. The final call to FloatBuffer.flip() just swaps the position and limit.

For this example, let's assume that our vertices array is five floats in size and that our FloatBuffer has enough capacity to store those five floats. After the call to FloatBuffer.put(),the position of the buffer will be 5 (indices 0 to 4 are taken up by the five floats from our array). The limit will still be equal to whatever the capacity of the buffer is. After the call to FloatBuffer.flip(), the position will be set to 0 and the limit will be set to 5. Any party interested in reading the data from the buffer will then know that it should read the floats from index 0 to 4 (remember that the limit is exclusive). And that's exactly what OpenGL ES needs to know as well. Note, however, that it will happily ignore the limit. Usually we have to tell it the number of elements to read in addition to passing the buffer to it. There's no error checking done, so watch out.

Sometimes it is useful to set the position of the buffer manually after we have filled it. This can be done via a call to the following method:

FloatBuffer.position(int position)

This will come in handy later on, when we temporarily set the position of a filled buffer to something other than zero for OpenGL ES to start reading at a specific position.

Sending Vertices to OpenGL ES

So how do we define the positions of the three vertices of our first triangle? Easy— assuming our coordinate system is (0,0,1) to (320,480,-1), as we defined it in the preceding code snippet, we can do the following:

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(3 * 2 * 4); byteBuffer.order(ByteOrder.nativeOrder()); FloatBuffer vertices = byteBuffer.asFloatBuffer(); vertices.put(new float[] { 0.0f, 0.0f,

vertices.flip();

The first three lines should be familiar already. The only interesting part is how many bytes we allocate. We have three vertices, each composed of a position given as x- and y-coordinates. Each coordinate is a float, and thus takes up 4 bytes. That's three vertices times two coordinates times four bytes, for a total of 24 bytes for our triangle.

NOTE: We can specify vertices with x- and y-coordinates only, and OpenGL ES will automatically set the z-coordinate to zero for us.

Next we put a float array holding our vertex positions into the buffer. Our triangle starts at the bottom-left corner (0,0), goes to the right edge of the view frustum/screen (319,0), and then goes to the middle of the top edge of the view frustum/screen. Being the good NIO buffer users we are, we also call theflip() method on our buffer. Thus, the position will be 0 and the limit will be 6 (remember, FloatBuffer limits and positions are given in floats, not bytes).

Once we have our NIO buffer ready, we can tell OpenGL ES to draw it with its current state (e.g., viewport and projection matrix). This can be done with the following snippet:

gl.glEnableClientState(GL10. GL_VERTEX_ARRAY); gl.glVertexPointer( 2, GL10.GL_FLOAT, 0, vertices); gl.glDrawArrays(GL10.GL_TRIANGLES, 0, 3);

The call to glEnableClientState() is a bit of a relic. It tells OpenGL ES that the vertices we are going to draw have a position. This is a bit silly for two reasons:

■ The constant is called GL10.GL_VERTEX_ARRAY, which is a bit confusing. It would make more sense if it were called GL10.GL_P0SITI0N_ARRAY.

■ There's no way to draw anything that has no position, so the call to this method is a little bit superfluous. We do it anyway, though, to make OpenGL ES happy.

In the call toglVertexPointer() we tell OpenGL ES where it can find the vertex positions, and give it some additional information. The first parameter tells OpenGL ES that each vertex position is composed of two coordinates, x and y. If we would have specified x, y, and z, we would have passed 3 to the method. The second parameter tells OpenGL ES the data type we used to store each coordinate. In this case it's GL10.GL_FL0AT, indicating that we used floats encoded as 4 bytes each. The third parameter, stride, tells OpenGL how far apart each of our vertex positions are from each other in bytes. In the preceding case, stride is zero, as the positions are tightly packed (vertex 1 (x,y), vertex 2(x,y), etc.). The final parameter is our FloatBuffer, for which there are two things to remember:

■ The FloatBuffer represents a memory block in the native heap, and thus has a starting address.

■ The position of the FloatBuffer is an offset from that starting address.

OpenGL ES will take the buffer's starting address and add the buffer's positions to arrive at the float in the buffer that it will start reading the vertices from when we tell it to draw the contents of the buffer. The vertex pointer (which again should be called the position pointer) is a state of OpenGL ES. As long as we don't change it (and the context isn't lost), OpenGL ES will remember and use it for all subsequent calls that need vertex positions.

Finally there's the call to glDrawArrays(). It will draw our triangle. The first parameter specifies what type of primitive we are going to draw. In this case we say that we want to render a list of triangles, which is specified via GL10.GL_TRIANGLES. The next parameter is an offset relative to the first vertex the vertex pointer points to. The offset is measured in vertices, not bytes or floats. If we'd have specified more than one triangle, we could use this offset to render only a subset of our triangle list. The final argument tells OpenGL ES how many vertices it should use for rendering. In our case that's three vertices. Note that we always have to specify a multiple of 3 if we draw GL10.GL_TRIANGLES. Each triangle is composed of three vertices, so that makes sense. For other primitive types the rules are a little different.

Once we issue the glVertexPointer() command, OpenGL ES will transfer the vertex positions to the GPU and store them there for all subsequent rendering commands. Each time we tell OpenGL ES to render vertices, it takes their positions from the data we last specified via glVertexPointer().

Each of our vertices might have more attributes than just its position. One other attribute might be a vertex's color. We usually refer to those attributes as vertex attributes.

You might wonder how OpenGL ES knows what color our triangle should have, as we have only specified positions. It turns out that OpenGL ES has sensible defaults for any vertex attribute that we don't specify. Most of these defaults can be set directly. For example, if we want to set a default color for all vertices that we draw, we can use the following method:

GL10.glColor4f(float r, float g, float b, float a)

This method will set the default color to be used for all vertices for which we didn't specify a color. The color is given as RGBA values in the range 0.0 to 1.0, as was the case for the clear color earlier. The default color OpenGL ES starts with is (1,1,1,1)—that is, fully opaque white.

And that is all the code we need to render a triangle with a custom parallel projection with OpenGL ES. That's a mere 16 lines of code for clearing the screen, setting the viewport and projection matrix, creating an NIO buffer that we store our vertex positions in, and drawing the triangle. Now compare that to the six pages it took me to explain this to you. I could have of course left out the details and used coarser language. The problem is that OpenGL ES is a pretty complex beast at times, and to avoid getting an empty screen, it's best to learn what it is all about rather than just copying and pasting code.

0 0

Post a comment