In Practice

We'll now go through all the necessary steps to get lighting to work with OpenGL ES. Along the way we'll create a few little helper classes that make working with light sources a bit easier. We'll put those in the com.badlogic.androidgames.framework.gl package.

Enabling and Disabling Lighting

As with all OpenGL ES states, we first have to enable the functionality in question. We do that with this:

gl.glEnable(GL10.GL_LIGHTING);

Once enabled, lighting will be applied to all objects we render. We'll have to specify the light sources and materials as well as the vertex normals to achieve meaningful results, of course. Once we are done with rendering all the objects that should be lit we can disable lighting again:

gl.glDisable(GL10.GL_LIGHTING);

Specifying Light Sources

OpenGL ES offers us four types of light sources: ambient lights, point lights, directional lights and spot lights. We'll take a look at how to define the first three. In order for spot lights to be effective and look good, we'd need to have a very high triangle count for each of our objects' models. That's prohibitive on most current mobile devices.

OpenGL ES lets us have eight light sources in a scene at most, plus a global ambient light. Each of the eight light sources has an identifier, from GL10.GL_LIGHT0 up to GL10.GL_LIGHT7. If we want to manipulate the properties of one of these light sources we do so by specifying the respective ID of that light source.

Light sources have to be enabled with this syntax: gl.glEnable(GL10.GL_LIGHT0);

OpenGL ES will then take the properties of that light source with ID zero and apply it to all rendered objects accordingly. If we want to disable a light source we can do it with a statement like this:

gl.glDisable(GL10.GL_LIGHT0);

Ambient light is a special case as it does not have an identifier. There is only one ambient light ever in an OpenGL ES scene. Let's have a look at that.

Ambient Light

Ambient light is a special type of light, as I explained already. It has no position or direction but only a color by which all objects in the scene will be uniformly lit. OpenGL ES lets us specify the global ambient light as follows:

float[] ambientColor = { 0.2f, 0.2f, 0.2f, 1.0f }; gl.glLightModelfv(GL10.GL_LIGHT_M0DEL_AMBIENT, color, 0);

The ambientColor array holds the RGBA values of the ambient light's color encoded as floats in the range 0 to 1. The glLightModelfv() method takes a constant as the first parameter specifying that we want to set the ambient light's color, the float array holding the color and an offset into the float array from which the method should start reading the RGBA values. Let's put this into a lovely little class. Listing 11-2 shows the code.

Listing 11-2. AmbientLight.java, a Simple Abstraction of OpenGL ES Global Ambient Light package com.badlogic.androidgames.framework.gl;

import javax.microedition.khronos.opengles.GL10;

public class AmbientLight {

public void setColor(float r, float g, float b, float a) {

color[l] = g color[2] = b color[i] = a public void enable(GL10 gl) {

gl.glLightModelfv(GL10.GL_LIGHT_MODEL_AMBIENT, color, 0);

All we do is store the ambient light's color in a float array and provide two methods: one to set the color and another to make OpenGL ES use the ambient light color we defined. By default we use a gray ambient light color.

Point Lights

Point lights have a position as well as an ambient, diffuse, and specular color/intensity (we leave out the emissive color/intensity). To specify the different colors we can do the following:

gl.glLightfv(GL10.GL_LIGHT3, GL10.GL_AMBIENT, ambientColor, 0); gl.glLightfv(GL10.GL_LIGHT3, GL10.GL_DIFFUSE, diffuseColor, 0); gl.glLightfv(GL10.GL_LIGHT3, GL10.GL_SPECULAR, specularColor, 0);

The first parameter is the light identifier; in this case we use the fourth light. The next parameter specifies the attribute of the light we want to modify. The third parameter is again a float array holding the RGBA values, and the final parameter is an offset into that array. Specifying the position is as easy:

gl.glLightfv(GL10.GL_LIGHT3, GL10.GL_P0SITI0N, position, 0);

We again specify the attribute we want to modify (in this case the position), along with a four-element array storing the x-, y- and z-coordinate of the light in our world. Note that the fourth element of the array must be set to 1 for a positional light source! Let's put this into a helper class. Listing 11-3 shows you the code.

Listing 11-3. PointLight.java, a Simple Abstraction of OpenGL ES Point Lights package com.badlogic.androidgames.framework.gl;

import javax.microedition.khronos.opengles.GL10;

public class PointLight {

float[] ambient = { 0.2f, 0.2f, 0.2f, 1.0f }; float[] diffuse = { 1.0f, 1.0f, 1.0f, 1.0f }; float[] specular = { 0.0f, 0.0f, 0.0f, 1.0f }; float[] position = { 0, 0, 0, 1 }; int lastLightId = 0;

public void setAmbient(float r, float g, float b, float a) {

ambient[0] = r; ambient[l] = g; ambient[2] = b; ambient[3] = a;

public void setDiffuse(float r, float g, float b, float a) {

diffuse[0] = r; diffuse[l] = g; diffuse^] = b; diffuse^] = a;

public void setSpecular(float r, float g, float b, float a) {

specular[0] = r; specular[l] = g; specular[2] = b; specular[3] = a;

public void setPosition(float x, float y, float z) {

public void enable(GL10 gl, int lightId) { gl.glEnable(lightld);

gl.glLightfv(lightId, GL10.GL_AMBIENT, ambient, 0); gl.glLightfv(lightId, GL10.GL_DIFFUSE, diffuse, 0); gl.glLightfv(lightId, GL10.GL_SPECULAR, specular, 0); gl.glLightfv(lightId, GL10.GL_POSITION, position, 0); lastLightId = lightId;

public void disable(GL10 gl) { gl.glDisable(lastLightId);

Our helper class stores the ambient, diffuse, and specular color components of the light as well as the position (with the fourth element set to 1). Additionally, we store the last light identifier used for this light so we can offer a disable() method that will turn off the light if necessary. For each light attribute we have a nice setter method. We also have an enable() method, which takes a GL10 instance and a light identifier (like GL10.GL_LIGHT6). It enables the light, sets its attributes, and stores the light identifier used. The disable() method just disables the light using the lastLightId member set in enable().

We use sensible defaults for the ambient, diffuse, and specular colors in the initializers of the member arrays. The light will be white and will not produce any specular highlights, because the specular color is black.

Directional Lights

A directional light is nearly identical to a point light. The only difference is that it has a direction instead of a position. The way the direction is expressed is a little confusing. Instead of using a direction vector, OpenGL ES expects us to define a point in the world. The direction is then calculated by taking the direction vector from the point to the origin of the world. The following snippet would produce a directional light that comes from the right side of the world:

gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_P0SITI0N, dirPos, 0); We can translate that to a direction vector: dir = -dirPos = {-1, 0, 0, 0}

The rest of the attributes, like the ambient or diffuse color, are identical to those of a point light. Listing 11-4 shows you the code of a little helper class for diffuse lights.

Listing 11-4. DirectionLight.java, a Simple Abstraction of OpenGL ES Directional Lights package com.badlogic.androidgames.framework.gl;

import javax.microedition.khronos.opengles.GL10;

public class DirectionalLight {

float[] ambient = { 0.2f, 0.2f, 0.2f, 1.0f }; float[] diffuse = { 1.0f, 1.0f, 1.0f, 1.0f }; float[] specular = { 0.0f, 0.0f, 0.0f, 1.0f }; float[] direction = { 0, 0, -1, 0 }; int lastLightId = 0;

public void setAmbient(float r, float g, float b, float a) {

ambient[0] = r; ambient[1] = g; ambient[2] = b; ambient[3] = a;

public void setDiffuse(float r, float g, float b, float a) {

diffuse[0] = r; diffuse[1] = g; diffuse^] = b; diffuse^] = a;

public void setSpecular(float r, float g, float b, float a) {

specular[0] = r; specular[1] = g; specular[2] = b; specular[3] = a;

public void setDirection(float x, float y, float z) {

direction[0] = -x; direction[1] = -y; direction[2] = -z;

public void enable(GL10 gl, int lightId) { gl.glEnable(lightId);

gl.glLightfv(lightId, GL10.GL_AMBIENT, ambient, 0); gl.glLightfv(lightId, GL10.GL_DIFFUSE, diffuse, 0); gl.glLightfv(lightId, GL10.GL_SPECULAR, specular, 0); gl.glLightfv(lightId, GL10.GL_POSITION, direction, 0); lastLightId = lightId;

public void disable(GL10 gl) { gl.glDisable(lastLightId);

Our helper class is nearly identical to the PointLight class. The only difference is that the direction array has its fourth element set to 1. We also have a setDirection() method instead of a setPosition() method. The setDirection() method allows us to specify a direction, like (-1, 0, 0) so that the light comes from the right side. Inside the method we just negate all the vector components so that we transform the direction to the format OpenGL ES expects from us.

Specifying Materials

A material is defined by[OK?] a couple of attributes. As with anything OpenGL ES, a material is a state and will be active until we change it again or the OpenGL ES context is lost. To set the currently active material attributes we can do the following:

gl.glMaterialfv(GL10.GL_FR0NT_AND_BACK, GL10.GL_AMBIENT, ambientColor, 0); gl.glMaterialfv(GL10.GL_FR0NT_AND_BACK, GL10.GL_DIFFUSE, diffuseColor, 0); gl.glMaterialfv(GL10.GL_FR0NT_AND_BACK, GL10.GL_SPECULAR, specularColor, 0);

As usual we have an ambient, a diffuse, and a specular RGBA color to specify. This is again done via four-element float arrays just as we did with light source attributes. Putting this together into a little helper class is again very easy. Listing 11-5 shows you the code.

Listing 11-5. Material.java, a Simple Abstraction of OpenGL ES Materials package com.badlogic.androidgames.framework.gl;

import javax.microedition.khronos.opengles.GL10;

public class Material {

float[] ambient = { 0.2f, 0.2f, 0.2f, 1.0f }; float[] diffuse = { 1.0f, 1.0f, 1.0f, 1.0f }; float[] specular = { 0.0f, 0.0f, 0.0f, 1.0f };

public void setAmbient(float r, float g, float b, float a) {

ambient[0] = r; ambient[1] = g; ambient[2] = b; ambient[3] = a;

public void setDiffuse(float r, float g, float b, float a) {

diffuse[0] = r; diffuse[1] = g; diffuse^] = b; diffuse[3] = a;

public void setSpecular(float r, float g, float b, float a) {

specular[0] = r; specular[1] = g; specular[2] = b; specular[3] = a;

public void enable(GL10 gl) {

gl.glMaterialfv(GL10. GL_FRONT_AND_BACK gl.glMaterialfv(GL10. GL_FRONT_AND_BACK gl.glMaterialfv(GL10. GL_FRONT_AND_BACK

No big surprises here, either. We just store the three components of the material and provide setters and an enable() method which sets the material.

OpenGL ES has one more trick up its sleeve when it comes to materials. Usually one wouldn't use glMaterialfv() but instead something called color material. This means that instead of the ambient and diffuse color specified via glMaterialfv() OpenGL ES will take the vertex color of our models as the ambient and diffuse material color. To enable this nice feature we just have to call it:

gl.glEnable(GL10.GL_C0L0R_MATERIAL);

I usually use this instead of a full-blown material class as shown earlier, because ambient and diffuse colors are often the same. Since I also don't use specular highlights in most of my demos and games, I can get away with just enabling color materials and not using any glMaterialfv() calls at all. Whether to use the Material class or color materials is totally up to you.

Specifying Normals

For lighting to work in OpenGL ES we have to specify vertex normals for each vertex of a model. A vertex normal must be a unit length vector pointing in the (average) facing direction of the surface(s) a vertex belongs to. Figure 11-5 illustrates vertex normals for our cube.

, GL10.GL_AMBIENT, ambient, 0); , GL10.GL_DIFFUSE, diffuse, 0); , GL10.GL_SPECULAR, specular, 0);

A vertex normal is just another vertex attribute, like position or color. In order to upload vertex normals, we have to modify our Vertices3 class one more time. To tell OpenGL ES where it can find the normals for each vertex we use the glNormalPointer() method, just like we used the glVertexPointer() or glColorPointer() methods previously. Listing 11-6 shows our final revised Vertices3 class.

Listing 11-6. Vertices3.java, the Final Version with Support for Normals package com.badlogic.androidgames.framework.gl;

import java.nio.ByteBuffer; import java.nio.Byte0rder; import java.nio.IntBuffer; import java.nio.ShortBuffer;

import javax.microedition.khronos.opengles.GL10;

import com.badlogic.androidgames.framework.impl.GLGraphics;

public class Vertices3 {

final GLGraphics glGraphics; final boolean hasColor; final boolean hasTexCoords; final boolean hasNormals; final int vertexSize; final IntBuffer vertices; final int[] tmpBuffer; final ShortBuffer indices;

Among the members, the only new addition is the hasNormals boolean, which keeps track of whether the vertices have normals or not.

public Vertices3(GLGraphics glGraphics, int maxVertices, int maxIndices, boolean hasColor, boolean hasTexCoords, boolean hasNormals) { this.glGraphics = glGraphics; this.hasColor = hasColor; this.hasTexCoords = hasTexCoords;

this.hasNormals = hasNormals;

this.vertexSize = (3 + (hasColor ? 4 : 0) + (hasTexCoords ? 2 : 0) + (hasNormals : 0)) * 4;

this.tmpBuffer = new int[maxVertices * vertexSize / 4];

ByteBuffer buffer = ByteBuffer.allocateDirect(maxVertices * vertexSize);

buffer.order(ByteOrder.nativeOrder());

vertices = buffer.asIntBuffer();

buffer = ByteBuffer.allocateDirect(maxIndices * Short.SIZE / 8); buffer.order(ByteOrder.nativeOrder()); indices = buffer.asShortBuffer();

indices = null;

In the constructor we now also take a hasNormals parameter. We have to modify the calculation of the vertexSize member as well, adding three floats per vertex if normals are available.

public void setVertices(float[] vertices, int offset, int length) { this.vertices.clear(); int len = offset + length; for (int i = offset, j = 0; i < len; i++, j++)

tmpBuffer[j] = Float. floatToRawIntBits(vertices[i]); this.vertices.put(tmpBuffer, 0, length); this.vertices.flip();

public void setIndices(short[] indices, int offset, int length) { this.indices.clear();

this.indices.put(indices, offset, length); this.indices.flip();

As you can see, the methods setVertices() and setIndices() stay the same.

public void bind() {

GL10 gl = glGraphics.getGL();

gl.glEnableClientState(GL10. GL_VERTEX_ARRAY); vertices.position(0);

gl.glVertexPointer(3, GL10.GL_FLOAT, vertexSize, vertices);

gl.glEnableClientState(GL10.GL_COLOR_ARRAY); vertices.position(3);

gl.glColorPointer(4, GL10.GL_FLOAT, vertexSize, vertices);

if (hasTexCoords) {

gl.glEnableClientState(GL10. GL_TEXTURE_COORD_ARRAY); vertices.position(hasColor ? 7 : 3);

gl.glTexCoordPointer(2, GL10.GL_FLOAT, vertexSize, vertices);

if (hasNormals) {

gl.glEnableClientState(GL10.GL_NORMAL_ARRAY); int offset = 3; if (hasColor)

offset += 4; if (hasTexCoords)

offset += 2; vertices.position(offset);

gl.glNormalPointer(GL10.GL_FLOAT, vertexSize, vertices);

In the bind() method just shown, we do the usual ByteBuffer tricks, this time incorporating normals via the glNormalPointer() method as well. To calculate the offset for the normal pointer we have to take into account whether colors and texture coordinates are given.

public void draw(int primitiveType, int offset, int numVertices) { GL10 gl = glGraphics.getGL();

indices.position(offset); gl.glDrawElements(primitiveType, numVertices, GL10.GL_UNSIGNED_SHORT, indices);

gl.glDrawArrays(primitiveType, offset, numVertices);

You can see that the draw() method is again unmodified; all the magic happens in the bind() method.

public void unbind() {

GL10 gl = glGraphics.getGL(); if (hasTexCoords)

gl.glDisableClientState(GL10. GL_TEXTURE_COORD_ARRAY);

if (hasColor)

gl.glDisableClientState(GL10. GL_COLOR_ARRAY);

if (hasNormals)

gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);

Finally, we also modify the unbind() method a little bit. We disable the normal pointer if normals have been, cleaning up the OpenGL ES state properly.

Using this modified Vertices3 version is as easy as before. Here's a small example:

float[] vertices = { -0.5f, -0.5f, 0, 0, 0, 1, 0.5f, -0.5f, 0, 0, 0, 1, 0.0f, 0.5f, 0, 0, 0, 1 }; Vertices3 vertices = new Vertices3(glGraphics, 3, 0, false, false, true); vertices.setVertices(vertices);

We create a float array to hold three vertices, each having a position (the first three floats on each line) and a normal (the last three floats on each line). In this case we have a triangle in the x/y plane with its normals pointing in the direction of the positive z-axis. All that's left is creating the Vertices3 instance and setting the vertices. Easy, right? Binding, drawing, and unbinding work exactly the same as with the old version. We can, of course, also add vertex colors and texture coordinates as previously.

Putting it All Together

Let's put all this together. We want to draw a scene with a global ambient light, a point light, and a directional light all illuminating a cube centered at the origin. For good measure we'll also throw in a call to gluLookAt() to position our camera in the world. Figure 11-6 shows the setup of our world.

Figure 11-6. Our first lit scene

As with all of our examples, we create a class called LightTest, which extends GLGame as usual. It returns a new LightScreen instance from its getStartScreen() method. The LightScreen class extends GLScreen and is shown in Listing 11-7.

Listing 11 -7. Excerpt from LightTest.java, Lighting with OpenGL ES

class LightScreen extends GLScreen { float angle; Vertices! cube; Texture texture; AmbientLight ambientLight; PointLight pointLight; DirectionalLight directionalLight; Material material;

We start off with a couple of members. The angle member stores the current rotation of the cube around the y-axis. The Vertices3 member stores the vertices of the cube model, which we are going to define in a bit. Additionally, we store an AmbientLight, PointLight and DirectionalLight instance as well as a Material.

public LightScreen(Game game) { super(game);

cube = createCube();

texture = new Texture(glGame, "crate.png"); ambientLight = new AmbientLight(); ambientLight.setColor(0, 0.2f, 0, 1); pointLight = new PointLight(); pointLight.setDiffuse(1, 0, 0, 1); pointLight.setPosition(3, 3, 0); directionalLight = new DirectionalLight(); directionalLight.setDiffuse(0, 0, 1, 1); directionalLight.setDirection(1, 0, 0); material = new Material();

Next is the constructor. Here we create the cube model's vertices and load the crate texture, just as we did in the previous chapter. We also instantiate all the lights and the material and set their attributes. The ambient light color is a light green, and the point light is red and sits at (3,3,0) in our world. The directional light has a blue diffuse color and comes from the left. For the material we use the default values (a little ambient, white for diffuse, and black for specular).

@0verride public void resume() { texture.reload();

In the resume() method we make sure that our texture is (re)loaded in case of a context loss.

private Vertices3 createCube() {

float[] vertices = { -0.5f, -0.5f, 0.5f, 0, 1, 0, 0, 1,

0.5f, 0.5f, 0.5f, 1, 1, 0, 1, 0, 0.5f, 0.5f, -0.5f, 1, 0, 0, 1, 0, -0.5f, 0.5f, -0.5f, 0, 0, 0, 1, 0,

-0.5f, -0.5f, -0.5f, 0, 1, 0, -1, 0, 0.5f, -0.5f, -0.5f, 1, 1, 0, -1, 0, 0.5f, -0.5f, 0.5f, 1, 0, 0, -1, 0, -0.5f, -0.5f, 0.5f, 0, 0, 0, -1, 0 }; short[] indices = { 0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4, 8, 9, 10, 10, 11, 8, 12, 13, 14, 14, 15, 12, 16, 17, 18, 18, 19, 16, 20, 21, 22, 22, 23, 20, 24, 25, 26, 26, 27, 24 }; Vertices3 cube = new Vertices3(glGraphics, vertices.length / 8, indices.length, false, true, true);

cube.setVertices(vertices, 0, vertices.length); cube.setIndices(indices, 0, indices.length); return cube;

The createCube() method is mostly the same as the one we used in previous examples. This time, however, we add normals to each vertex as shown in Figure 11-4. Apart from that nothing really changed.

^Override public void update(float deltaTime) { angle += deltaTime * 20;

In the update() method we simply increase the rotation angle of the cube. ^Override public void present(float deltaTime) { GL10 gl = glGraphics.getGL(); gl.glClearColor(0.2f, 0.2f, 0.2f, 1.0f);

gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); gl.glEnable(GL10. GL_DEPTH_TEST);

gl.glViewport(0, 0, glGraphics.getWidth(), glGraphics.getHeight());

gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity();

GLU.gluPerspective(gl, 67, glGraphics.getWidth()

/ (float) glGraphics.getHeight(), 0.1f, 10f); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity();

gl.glEnable(GL10.GL_LIGHTING);

ambientLight.enable(gl); pointLight.enable(gl, GL10.GL_LIGHT0); directionalLight.enable(gl, GL10.GL_LIGHT1); material.enable(gl);

gl.glEnable(GL10. GL_TEXTURE_2D);

texture.bind();

cube.draw(GL10.GL_TRIANGLES, 0, 6 * 2 * 3); cube.unbind();

pointLight.disable(gl); directionalLight.disable(gl);

gl.glDisable(GL10.GL_TEXTURE_2D); gl.glDisable(GL10. GL_DEPTH_TEST);

And here it gets interesting. The first couple of lines are our boilerplate code for clearing the color and depth buffer, enabling depth testing, and setting the viewport.

Next we set the projection matrix to a perspective projection matrix via gluPerspective() and also use gluLookAt() for the model view matrix so that we have a camera set up as in Figure 11-6.

Next we enable lighting itself. At this point no lights are defined yet, so we do that in the next couple of lines by calling the enable() methods of the lights as well as the material.

As usual we also enable texturing and bind our crate texture. Finally, we call glRotatef() to rotate our cube and then render its vertices with well-placed calls to the Vertices3 instance.

To round off the method, we disable the point and directional lights (remember, the ambient light is a global state) as well as texturing and depth testing. And that's all there is to lighting in OpenGL ES!

^Override public void dispose() { }

The rest of the class is just empty; we don't have to do anything special in case of a pause.

Figure 11-7 shows you the output of our example.

Figure 11-7. Our scene from Figure 11-6 rendered with OpenGL ES
0 0

Post a comment