The World Render Class

Let's recall what we have to render in 3D:

■ Our ship, using the ship model and texture and applying lighting.

■ The invaders, using the invader model and texture, again with lighting.

■ Any shots on the playfield, based on the shot model, this time without texturing but with lighting.

■ The shield blocks, based on the shield block model, again without texturing but with lighting and transparency (see Figure 12-3).

■ Explosions instead of the ship or invader model in case the ship or an invader is exploding. The explosion is not lit, of course.

We know how to code the first four items on this list. But what about the explosions?

It turns out that we can abuse the SpriteBatcher. Based on the state time of the exploding ship or invader, we can fetch a TextureRegion from the Animation instance holding the explosion animation (see Assets class). The SpriteBatcher can only render textured rectangles in the x/y plane, so we have to find a way to move such a rectangle to an arbitrary position in space (where the exploding ship or invader is). We can easily achieve this by using glTranslatef() on the model-view matrix before rendering the rectangle via the SpriteBatcher!

The rendering setup for the other objects is pretty straightforward. We have a directional light coming from the top right and an ambient light to light all objects a little bit no matter their orientation. The camera is located a little bit above and behind the ship and will look at a point a little bit ahead of the ship. We'll use our LookAtCamera for this. To let the camera follow the ship we just need to keep the x-coordinate of its position and look-at point in sync with the ship's x-coordinate.

For some extra eye-candy we'll rotate the invaders around the y-axis. We'll also rotate the ship around the z-axis based on its current velocity so that it appears to be leaning to the side it moves toward.

Let's put this into code! Listing 12-12 shows you the final class of Droid Invaders. Listing 12-12. WorldRenderer.java, the World Renderer package com.badlogic.androidgames.droidinvaders;

import java.util.List;

import javax.microedition.khronos.opengles.GL10;

import com.badlogic.androidgames.framework.gl.AmbientLight; import com.badlogic.androidgames.framework.gl.Animation; import com.badlogic.androidgames.framework.gl.DirectionalLight; import com.badlogic.androidgames.framework.gl.LookAtCamera; import com.badlogic.androidgames.framework.gl.SpriteBatcher; import com.badlogic.androidgames.framework.gl.TextureRegion; import com.badlogic.androidgames.framework.impl.GLGraphics; import com.badlogic.androidgames.framework.math.Vector3;

public class WorldRenderer { GLGraphics glGraphics; LookAtCamera camera; AmbientLight ambientLight; DirectionalLight directionalLight; SpriteBatcher batcher; float invaderAngle = 0;

The WorldRenderer keeps track of a GLGraphics instance from which we'll fetch the GL10 instance. We also have a LookAtCamera, an AmbientLight, and a DirectionLight and a SpriteBatcher. Finally, we have a member to keep track of the current rotation angle we'll use for all invaders.

public WorldRenderer(GLGraphics glGraphics) { this.glGraphics = glGraphics;

camera = new LookAtCamera(67, glGraphics.getWidth()

/ (float) glGraphics.getHeight(), 0.1f, 100); camera.getPosition().set(0, 6, 2); camera.getLookAt().set(0, 0, -4); ambientLight = new AmbientLight(); ambientLight.setColor(0.2f, 0.2f, 0.2f, 1.0f); directionalLight = new DirectionalLight(); directionalLight.setDirection(-1, -0.5f, 0); batcher = new SpriteBatcher(glGraphics, 10);

In the constructor we set up all members as usual. The camera has a field of view of 67 degrees, a near clipping plane distance of 0.1 units, and a far clipping plane distance of 100 units. The view frustum will thus easily contain all of our game world. We position it above and behind the ship and let it look at (0,0,-4). The ambient light is just a faint gray, and the directional light is white and comes from the top-right side. Finally, we instantiate the SpriteBatcher so that we can render the explosion rectangles.

public void render(World world, float deltaTime) { GL10 gl = glGraphics.getGL(); camera.getPosition().x = world.ship.position.x; camera.getLookAt().x = world.ship.position.x; camera.setMatrices(gl);

gl.glEnable(GL10. GL_DEPTH_TEST); gl.glEnable(GL10. GL_TEXTURE_2D); gl.glEnable(GL10.GL_LIGHTING); gl.glEnable(GL10. GL_COLOR_MATERIAL); ambientLight.enable(gl); directionalLight.enable(gl, GL10.GL_LIGHT0);

renderShip(gl, world.ship); renderInvaders(gl, world.invaders, deltaTime);

gl.glDisable(GL10.GL_TEXTURE_2D);

renderShields(gl, world.shields); renderShots(gl, world.shots);

gl.glDisable(GL10. GL_COLOR_MATERIAL); gl.glDisable(GL10.GL_LIGHTING); gl.glDisable(GL10. GL_DEPTH_TEST);

In the render() method we start off by setting the camera's x-coordinate to the ship's x-coordinate. We of course also set the x-coordinate of the camera's look-at point accordingly. This way, the camera will follow the ship. Once the position and look-at point are updated we can set the projection and model-view matrix via a call to LookAtCamera.setMatrices().

Next we set up all the states we need for rendering. We'll need depth-testing, texturing, lighting, and the color material functionality so that we don't have to specify a material for the objects via glMaterial(). The next two statements activate the ambient and directional light. With these calls we have everything set up so that we can start rendering our objects.

The first thing we render is the ship, via a call to renderShip(). Next we render the invaders with a call to renderInvaders().

Since the shield blocks and shots don't need texturing we simply disable that to save some computations. Once texturing is turned off, we render the shots and shields via calls to renderShots() and renderShields().

Finally we disable the other states we set so that we return a clean OpenGL ES state to whoever called us.

private void renderShip(GL10 gl, Ship ship) { if (ship.state == Ship.SHIP_EXPLODING) { gl.glDisable(GL10.GL_LIGHTING); renderExplosion(gl, ship.position, ship.stateTime); gl.glEnable(GL10.GL_LIGHTING); } else {

Assets.shipTexture.bind(); Assets.shipModel.bind(); gl.glPushMatrix();

gl.glTranslatef(ship.position.x, ship.position.y, ship.position.z); gl.glRotatef(ship.velocity.x / Ship.SHIP_VELOCITY * 90, 0, 0, -1); Assets.shipModel.draw(GL10.GL_TRIANGLES, 0, Assets.shipModel.getNumVertices()); gl.glPopMatrix(); Assets.shipModel.unbind();

The renderShip() method starts off by checking the state of the ship. If it is exploding we disable lighting, call renderExplosion() to render an explosion at the position of the ship, and enable lighting again.

If the ship is alive we bind its texture and model, push the model-view matrix, move it to its position and rotate it around the z-axis based on its velocity, and draw its model. Finally, we pop the model-view matrix again (leaving only the camera's view) and unbind the ship model's vertices.

private void renderInvaders(GL10 gl, List<Invader> invaders, float deltaTime) { invaderAngle += 45 * deltaTime;

Assets.invaderTexture.bind(); Assets.invaderModel.bind(); int len = invaders.size(); for (int i = 0; i < len; i++) {

Invader invader = invaders.get(i); if (invader.state == Invader.INVADER_DEAD) { gl.glDisable(GL10.GL_LIGHTING); Assets.invaderModel.unbind();

renderExplosion(gl, invader.position, invader.stateTime); Assets.invaderTexture.bind(); Assets.invaderModel.bind(); gl.glEnable(GL10.GL_LIGHTING); } else {

gl.glPushMatrix();

gl.glTranslatef(invader.position.x, invader.position.y, invader.position.z); gl.glRotatef(invaderAngle, 0, 1, 0); Assets.invaderModel .draw(GL10.GL_TRIANGLES, 0, Assets.invaderModel.getNumVertices()); gl.glPopMatrix();

Assets.invaderModel.unbind();

The renderInvaders() method is pretty much the same as the renderShip() method. The only difference is that we loop through the list of invaders and bind the texture and mesh before we do so. This reduces the number of binds considerably and speeds up the rendering a little. For each invader we then check its state again and render either an explosion or the normal invader model. Since we bind the model and texture outside the for loop, we have to unbind and rebind them before we can render an explosion instead of an invader.

private void renderShields(GL10 gl, List<Shield> shields) { gl.glEnable(GL10.GL_BLEND);

gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); gl.glColor4f(0, 0, 1, 0.4f); Assets.shieldModel.bind(); int len = shields.size(); for (int i = 0; i < len; i++) { Shield shield = shields.get(i); gl.glPushMatrix();

gl.glTranslatef(shield.position.x, shield.position.y, shield.position.z); Assets.shieldModel.draw(GL10.GL_TRIANGLES, 0, Assets.shieldModel.getNumVertices()); gl.glPopMatrix();

Assets.shieldModel.unbind(); gl.glColor4f(1, 1, 1, 1f); gl.glDisable(GL10.GL_BLEND);

The renderShields() method renders, you guessed it, the shield blocks. We apply the same principle as in the case of rendering the invaders. We only bind the model once. Since we have no texture we don't need to bind one. However, we need to enable blending. Another thing we do is set the global vertex color to blue, with the alpha component set to 0.4. This will make the shield blocks a little transparent.

private void renderShots(GL10 gl, List<Shot> shots) { gl.glColor4f(1, 1, 0, 1); Assets.shotModel.bind(); int len = shots.size(); for (int i = 0; i < len; i++) { Shot shot = shots.get(i); gl.glPushMatrix();

gl.glTranslatef(shot.position.x, shot.position.y, shot.position.z); Assets.shotModel.draw(GL10.GL_TRIANGLES, 0, Assets.shotModel.getNumVertices()); gl.glPopMatrix();

Assets.shotModel.unbind(); gl.glColor4f(1, 1, 1, 1);

Rendering the shots in renderShots() is the same as rendering the shields, except that we don't use blending and use a different vertex color (yellow).

private void renderExplosion(GL10 gl, Vector3 position, float stateTime) { TextureRegion frame = Assets.explosionAnim.getKeyFrame(stateTime, Animation.ANIMATION_NONLOOPING);

gl.glEnable(GL10.GL_BLEND); gl.glPushMatrix();

gl.glTranslatef(position.x, position.y, position.z);

batcher.beginBatch(Assets.explosionTexture);

batcher.endBatch();

gl.glPopMatrix();

gl.glDisable(GL10.GL_BLEND);

Finally we have the mysterious renderExplosion() method. We get the position at which we want to render the explosion as well as the state time of the object that is exploding. The latter is used to fetch the correct TextureRegion from the explosion Animation, just as we did for Bob in Super Jumper.

The first thing we do is fetch the explosion animation frame based on the state time. Next we enable blending, since the explosion has transparent pixels we don't want to render. We push the current model-view matrix and call glTranslatef() so that anything we render after that call will be positioned at the given location. Then we tell the SpriteBatcher that we are about to render a rectangle using the explosion texture.

The next call is where the magic happens. We tell the SpriteBatcher to render a rectangle at (0,0,0) (the z-coordinate is not given but implicitly zero, remember?), with a width and height of 2 units. Because we used glTranslatef(), that rectangle will not be centered around the origin but instead be centered around the position we specified to glTranslatef(), which is exactly the position of the ship or invader that exploded. Finally we pop the model-view matrix and disable blending again.

That's it. Twelve classes forming a full 3D game parroting the classic Space Invaders. Try it out. When you come back we'll have a look at the performance characteristics.

0 0

Post a comment