An Euler Camera Example

We now want to use the EulerCamera class in a little program. We want to be able to rotate it up and down and left and right based on swiping the touchscreen with a finger.

We also want it to move forward when a button is pressed. Our world should be populated by a couple of crates. Figure 11-10 shows you the initial setup of our scene.

Figure 11-10. A simple scene with 25 crates, a point light, and an Euler camera in its initial position and orientation

The camera will be located at (0,1,3). We also have a white point light at (3,3,-3). The crates are positioned in a grid from -4 to 4 on the x-axis and 0 to -8 on the z-axis, with a 2-unit distance between the centers.

How will we rotate the camera via swipes? We want the camera to rotate around the y-axis when we swipe horizontally. That is equivalent to turning our head left and right. We also want the camera to rotate around the x-axis when we swipe vertically. That's equivalent to tilting our head up and down. We also want to be able to combine these two swipe motions. The most straightforward way to achieve this is to check for whether a finger is on the screen, and if so measure the difference on each axis to the last known position of that finger on the screen. We can then derive a change in rotation on both axes by using the difference in x for the y-axis rotation and the difference in y for the x-axis rotation.

We also want the camera to be able to move forward by pressing an on-screen button. That's simple; we just need to call EulerCamera.getDirection() and multiply its result[OK?] by the speed we want the camera to move with and the delta time, so that we once again perform time-based movement. The only thing that we need to do is draw the button (I decided to draw a 64! 64 button in the bottom-left corner of the screen) and check whether it is currently touched by a finger.

To simplify our implementation we'll only allow the user to either swipe-rotate or move. We could use the multitouch facilities for this but that would complicate our implementation quite a bit.

With this plan of attack let us look at EulerCameraScreen, a GLScreen implementation contained in a GLGame implementation called EulerCameraTest (just the usual test structure). Listing 11-10 shows the code.

Figure 11-10. A simple scene with 25 crates, a point light, and an Euler camera in its initial position and orientation

Listing 11-10. Excerpt from EulerCameraTest.java, the EulerCameraScreen class EulerCameraScreen extends GLScreen { Texture crateTexture; Vertices3 cube; PointLight light; EulerCamera camera; Texture buttonTexture; SpriteBatcher batcher; Camera2D guiCamera; TextureRegion buttonRegion; Vector2 touchPos; float lastX = -1; float lastY = -1;

We start off with a couple of members. The first two store the texture for the crate as well as the vertices of the texture cube. We'll generate the vertices with the createCube() method from the last example.

The next member is a PointLight, which we are already familiar with, as well as an instance of our new EulerCamera class.

Next up are a couple of members we need to render the button. We use a separate 64! 64 image called button.png for that button. To render it we also need a SpriteBatcher as well as a Camera2D instance and a TextureRegion. This means that we are going to combine 3D and 2D rendering in this example! The last three members are used to keep track of the current touchPos in the UI coordinate system (which is fixed to 480! 320) as well as store the last known touch positions. We'll use the value -1 for lastX and lastY to indicate that no valid last touch position is known yet.

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

crateTexture = new Texture(glGame, "crate.png", true); cube = createCube(); light = new PointLight(); light.setPosition(3, 3, -3);

camera = new EulerCamera(67, glGraphics.getWidth() / (float)glGraphics.getHeight(), 1, 100);

buttonTexture = new Texture(glGame, "button.png"); batcher = new SpriteBatcher(glGraphics, 1); guiCamera = new Camera2D(glGraphics, 480, 320); buttonRegion = new TextureRegion(buttonTexture, 0, 0, 64, 64); touchPos = new Vector2();

In the constructor we load the crate texture and create the cube vertices as we did in the last example. We also create a PointLight and set its position to (3,3,-3). The EulerCamera is created with the standard parameters, a 67-degree field of view, the aspect ratio of the current screen resolution, a near clipping plane distance of 1, and a far clipping plane distance of 100. Finally we set the camera position to (0,1,3) as shown in Figure 11-10.

In the rest of the constructor we just load the button texture and create a SpriteBatcher, a Camera2D, and TextureRegion instance needed for rendering the button. Finally we create a Vector2 instance so that we can transform real touch coordinates to the coordinate system of the Camera2D we use for UI rendering, just as we did in Super Jumper in Chapter 9.

private Vertices3 createCube() { // same as in previous example

^Override public void resume() { crateTexture.reload();

The createCube() and resume() methods are exactly the same as in the previous example, so I don't repeat all the code here.

^Override public void update(float deltaTime) { game.getInput().getTouchEvents(); float x = game.getInput().getTouchX(0); float y = game.getInput().getTouchY(0); guiCamera.touchToWorld(touchPos.set(x, y));

if(game.getInput().isTouchDown(0)) {

if(touchPos.x < 64 && touchPos.y < 64) {

Vector3 direction = camera.getDirection(); camera.getPosition().add(direction.mul(deltaTime)); } else {

camera.rotate((x - lastX) / 10, (y - lastY) / 10); lastX = x; lastY = y;

The update() method is where all the swipe rotation and movement happens, based on touch events. The first thing we do is empty the touch event buffer via a call to Input.getTouchEvents(). Next we fetch the current touch coordinates for the first finger on the screen. Note that if no finger is currently touching the screen, the methods we invoke will return the last known position of the finger with index zero. We also transform the real touch coordinates to the coordinate system of our 2D UI so that we can easily check whether the button in the bottom left corner is pressed.

Equipped with all these values, we then check whether a finger is actually touching the screen. If so, we first check whether it is touching the button, which spans the coordinates (0,0) to (64,64) in the 2D UI system. If that is the case, we fetch the current direction of the camera and add it to its position, multiplied by the current delta time. Since the direction vector is a unit-length vector, this means that the camera will move one unit per second.

If the button is not touched, we interpret the touch as a swipe gesture. For this to work we need to have a valid last known touch coordinate. The first time the user puts his finger down the lastX and lastY members will have a value of -1, indicating that we can't create a difference between the last and current touch coordinates, since we only have a single data point. So we just store the current touch coordinates and return from the update() method. If we recorded touch coordinates the last time update() was invoked, we simply take the difference on the x- and y-axes between the current and the last touch coordinates. We directly translate these into increments of the rotation angles. To make the rotation a little slower we divide the differences by 10. The only thing left is calling the EulerCamera.rotate() method, which will adjust the rotation angles accordingly.

Finally, if no finger is currently touching the screen we set the lastX and lastY members to -1 to indicate that we have to await the first touch event before we can do any swipe gesture processing.

^Override public void present(float deltaTime) {

GL10 gl = glGraphics.getGL();

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

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

camera.setMatrices(gl);

gl.glEnable(GL10. GL_DEPTH_TEST);

gl.glEnable(GL10. GL_TEXTURE_2D);

gl.glEnable(GL10.GL_LIGHTING);

crateTexture.bind();

light.enable(gl, GL10.GL_LIGHT0);

for(int z = 0; z >= -8; z-=2) { for(int x = -4; x <=4; x+=2 ) { gl.glPushMatrix(); gl.glTranslatef(x, 0, z); cube.draw(GL10.GL_TRIANGLES, 0, 6 * 2 * 3); gl.glPopMatrix();

cube.unbind();

gl.glDisable(GL10.GL_LIGHTING);

gl.glDisable(GL10. GL_DEPTH_TEST);

gl.glEnable(GL10.GL_BLEND);

gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);

gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);

guiCamera.setViewportAndMatrices(); batcher.beginBatch(buttonTexture); batcher.drawSprite(32, 32, 64, 64, buttonRegion); batcher.endBatch();

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

The present() method is surprisingly simple, thanks to the work we put into all those little helper classes. We start off with the usual things like clearing the screen and setting the viewport. Next we tell the EulerCamera to set the projection and model-view matrix. From this point on we can render anything that should be 3D on screen. Before we do that, we enable depth testing, texturing, and lighting. Next we bind the crate texture and the cube vertices and also enable the point light. Note that we bind the texture and cube vertices only once, since we are going to reuse them for all the crates we render. That's the same trick we used in our BobTest in Chapter 8 to speed up rendering by reducing state changes.

The next piece of code just draws the 25 cubes in the grid formation via a simple nested for loop. Since we have to multiply the model-view matrix with a translation matrix to put the cube vertices at a specific position, we must also use glPushMatrix() and glPopMatrix() so that we don't destroy the camera matrix that's also stored in the model-view matrix.

Once we are done with rendering our cubes, we unbind the cube vertices and disable lighting and depth testing. This is crucial since we are now going to render the 2D UI overlay with the button. Since the button is actually circular, we also enable blending to make the edges of the texture transparent.

Rendering the button works the same as when we rendered the UI elements in Super Jumper. We tell the Camera2D to set the viewport and matrices (we wouldn't really need to set the viewport here again; feel free to "optimize" this method) and tell the SpriteBatcher that we are going to render a sprite. We render the complete button texture at (32,32) in our 480! 320 coordinate system that we set up via the guiCamera.

Finally, we just disable the last few states we enabled previously, blending and texturing. ^Override public void pause() { }

^Override public void dispose() { }

The rest of the class is again just some stub methods for pause() and dispose(). Figure 11-11 shows the output of this little program.

Figure 11-11. A simple example of first-person-shooter controls, without multitouch for simplicity

Pretty nice, right? It also doesn't take a lot of code, either, thanks to the wonderful job our helper classes do for us. Now, adding multi-touch support would be awesome of course. Here's a hint: instead of using polling, as in the example just seen, use the actual touch events. On a "touch down" event, check whether the button was hit. If so, mark the pointer ID associated with it as not being able to produce swipe gestures until a corresponding "touch up" event is signaled. Touch events from all other pointer IDs can be interpreted as swipe gestures!

+1 -1

Post a comment