
|
August 2005
Here's the second installment of the "3D programming for mobile devices using M3G (JSR 184)" tutorial from Mikael Baros, Senior Programmer at Redikod, and active member of Sony Ericsson Developer World's Mobile Java 3D discussion forum. Below you can download the source code and application package zip files for part two, as well as refresh your memory with part one of the tutorial. |
|||||||||||||||||
|
Part two: Light 3D theory and orientation: Part one: Quick jump into the world of Mobile Java 3D programming: Over to Mikael to guide you through part two. Introduction to Light 3D theory and orientation This is the second part of the JSR 184 (M3G) tutorial. Here I'll go through some very basic 3D theory, 3D math and then finish the lesson with a simple orientation demo. Here are also all the links from the last tutorial, just in case you get lost: First of all, and probably most importantly, the dedicated Mobile Java 3D web section on Sony Ericsson Developer World. Second, if you ever get stuck, go to the Sony Ericsson Mobile Java 3D forum. For everything else, use the Sony Ericsson World Developer web portal, there you will find the answers to your questions and more. The goal of this tutorial is to give you a good enough understanding of 3D math for you to be able to utilize JSR 184's translation and orientation methods. I'll go through 3D coordinate systems, translation in 3D space and orientation around a vector. Also, you'll be able to use this newfound knowledge at the end of the tutorial to rotate meshes in code and place them in 3D space. This will however be far from any kind of reference, hence the title "Light 3D theory and orientation". Later chapters of this tutorial series will contain more advanced topics. Since the code is meant for educational purposes it isn't optimal, nor does it cover all the errors you might encounter while programming in 3D. Mikael Baros, Senior Programmer at Redikod.
3D coordinate system
As you can see from the pictures above, in 3D the objects don't just have width and height (x, y) but also depth (z). Thus, a point on a three-dimensional object is always defined with three coordinates, the x, y and z. 3D objects consist of an array of points (vertrices) that define the faces of the model. A cube has eight corners and thus eight points (although a lot of them are shared). So coordinates for a cube could be (in x, y, z order):
Above we have the points of a cube with its center point in Origo (0, 0, 0). Also note how the back face is identical to the front face, apart from the z (depth) coordinate which is positive 1 for the front face and negative 1 for the back face. If you think about it, it's logical. Since the z-axis is also called depth it's only logical that the back face has a negative depth.
Orientation and Translation So this translation business is obviously pretty important. How is it done, you ask? It's rather simple, really. All you need to do to move a model, is to simply translate all its points. For instance, our cube in the example above was created around its own origo but we want the cube to be displayed at coordinates 10.0, 0.0, -10.0. That's 10 units to the right and 10 units deep. To do this we just simply go over our vertex array and just add 10.0 to the x coordinate of each point and deduce 10.0 from the z coordinate of each point. Simple! If you remember the first tutorial, we translated our camera to move around in the world. The camera can also be seen as a 3D object, so what we did there was just add or subtract from the camera's point coordinates to move it.
Rotation around an axis is best visualized by taking a cubic piece of cheese. (Yes, please go and get a small piece of cheese from the fridge, and bring three toothpicks as well.) This cheese will resemble our cube. Now, by piercing the cheese with a toothpick where the axis you wish to rotate around is, and spin the toothpick, you will get rotation around an axis. Rotation around an axis is always performed counter-clockwise for positive degrees and clockwise for negative degrees. Rotation of a 3D object is really pretty easy, as you can rotate an object around your three axes and that's it... or is it? First of all, you can rotate your object around any vector you want, not just the three axes. For instance, if you stabbed your piece of cheese diagonally from one corner to another, you have created a vector that isn't an axis, but still you can spin the toothpick and rotate the cheese. If you remember the first chapter, I mentioned the rotation method used in JSR 184. It looks something like this:
nameOfRotation(float degrees, float x, float y, float z)
The three floats, x, y and z, are the components of the vector you wish to rotate around. Now, to rotate around a single axis you just supply the x-axis vector. The x-axis vector is (quite logically) 1.0, 0.0, 0.0. That is, a positive one on the x-axis and zeros on the other axes. There are some problems with 3D rotation, though. Namely, the rotation is order-sensitive. Rotating an object around the x-axis, then the y-axis and then the z-axis is completely different from doing it in any other sequence. Also, if you have rotated an object around one axis, you also rotate its other axes. This creates another problem, where the x-axis actually moves if you rotate around the y-axis first, and thus a rotation around the x-axis won't give the desired effect. Usually a simple 3D game doesn't have to worry about this, as one usually doesn't rotate most models around all axes, and even if you do, you can usually store the degree of rotation on each axis and use the setRotation method instead. I won't go into depth about Orientation of an object and its axes here, instead that'll be saved for one of the advanced tutorials later on. Don't let this trouble your mind right now. Another thing that I'll mention about rotation is local rotation and world rotation. There's a difference if you rotate an object in its own local coordinate system or if you rotate it after it has been translated to world coordinates. The difference is that rotation in its local coordinate system occurs around its local coordinate axes, and in the world coordinate system, it will be rotated along the world's axes. The 3D Universe
/** Loads our world */
private void loadWorld() { try { // Loading the world is very simple. Note that I like to use a // res-folder that I keep all files in. If you normally just put your // resources in the project root, then load it from the root. Object3D[] buffer = Loader.load("/res/cube.m3g"); // Find the world node, best to do it the "safe" way for(int i = 0; i < buffer.length; i++) { if(buffer[i] instanceof World) { world = (World)buffer[i]; break; } } // Clean objects buffer = null; // Now find the cube so we can rotate it // We know the cube's ID is 1 cube = (Mesh)world.find(13); } catch(Exception e) { // ERROR! System.out.println("Loading error!"); reportException(e); } } The method is basically the same, apart from the last line cube = (Mesh)world.find(13);. This line might seem a little puzzling, so let's talk it through.
Normally objects in the JSR 184 scene graph have ID's that can tell them apart (although several objects may lack an ID). These IDs can be directly manipulated when exporting a model from your favorite 3D modeling software. When executing the find-method on an object, it will fetch the object with that ID if it exists and is linked to the object you executed the find method from. The easiest way to picture this is to see a HashMap that has the object ID as key and object as value. The find method is a method of the Object3D class, so almost every class in JSR 184 has this method.
Now, when I created the cube I exported it so that its ID would be 13. As I know this, I can easily extract the cube Mesh from the World and manipulate it (rotate, translate, scale, etc...). The Mesh class is a very versatile class that holds all the necessary information a model must have; the vertex buffer, the triangle index buffer, the texture coordinates, etc... It even holds advanced rendering mode operations in a class called Appearance. We'll talk more about this fantastic class later. For now, know that a Mesh represents a model and that it is a subclass of Transformable.
Transformable
The abstract class Transformable represents an object in 3D space that can be transformed in some way (scaled, rotated or translated). Many objects inherit from this class. A rule of thumb is that any object that can move in the 3D universe is a subclass of Transformable. If you want detailed information on the Transformable class, please refer to the JSR 184 API documentation. It has many useful methods which we will explore in detail in other parts of this tutorial. For now we will only use one method, the preRotate method. It is a method that simply rotates the object before any translations are applied and thus rotates it around its own local axes. (Remember what I said about that earlier in this tutorial.). So, the effect we accomplish by using preRotate is that we always rotate the cube correctly, no matter where it might be positioned in our world. The preRotate method works like any rotate method, taking four float arguments. The first is the rotation in degrees and the last three represent the vector we wish to rotate around. Let's look at our new moveCube method.
private void moveCube() {
// Check controls if(key[LEFT]) { cube.preRotate(5.0f, 0.0f, 0.0f, 1.0f); } else if(key[RIGHT]) { cube.preRotate(-5.0f, 0.0f, 0.0f, 1.0f); } else if(key[UP]) { cube.preRotate(5.0f, 1.0f, 0.0f, 0.0f); } else if(key[DOWN]) { cube.preRotate(-5.0f, 1.0f, 0.0f, 0.0f); } // If the user presses the FIRE key, let's quit
if(key[FIRE]) M3GMidlet.die(); } As you see, all we do is call the preRotate method and supply the degrees and appropriate axis to rotate. Now, left and right rotate around the z-axis (remember, the z-axis is a vector with all zeroes except the z coordinate, which is 1.0) and up and down rotate around the x-axis. We don't rotate around the y-axis in the demo, but it isn't at all hard to add into the code. See it as your assignment to also make the cube spin around the y-axis if another pair of keys is pressed.
We will call the moveCube method once in each game loop to update the cube's rotation, just as we did with the moveCamera method in the previous tutorial.
The rest of the code in this demo is exactly the same as the first demo, so I won't be talking more about it here. Check out the code listing at the bottom, or download the source code, to see for yourself.
Conclusion
So to wrap it all up, below are a couple of screenshots of the code in action:
Alright, so we have our rotating cube. I hope that you now have a greater understanding of 3D coordinate systems and 3D rotation. Please keep reading this series of tutorials for more advanced topics on JSR 184 3D application development. Below's the code listing of this example. You can also download the code as a separate ZIP file, containing all resources so you can play with it at home.
M3GMidlet
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Display; import javax.microedition.lcdui.Displayable; import javax.microedition.midlet.MIDlet; import javax.microedition.midlet.MIDletStateChangeException; public class M3GMidlet extends MIDlet implements CommandListener
{ // A variable that holds the unique display private Display display = null; // The canvas private M3GCanvas canvas = null; // The MIDlet itself private static MIDlet self = null; /** Called when the application starts, and when it is resumed.
* We ignore the resume here and allocate data for our canvas * in the startApp method. This is generally very bad practice. */ protected void startApp() throws MIDletStateChangeException { // Allocate display = Display.getDisplay(this); canvas = new M3GCanvas(30); // Add a quit command to the canvas // This command won't be seen, as we // are running in fullScreen mode // but it's always nice to have a quit command canvas.addCommand(new Command("Quit", Command.EXIT, 1)); // Set the listener to be the MIDlet canvas.setCommandListener(this); // Start canvas canvas.start(); display.setCurrent(canvas); // Set the self self = this; } /** Called when the game should pause, such as during a call */
protected void pauseApp() { } /** Called when the application should shut down */
protected void destroyApp(boolean unconditional) throws MIDletStateChangeException { // Method that shuts down the entire MIDlet notifyDestroyed(); } /** Listens to commands and processes */
public void commandAction(Command c, Displayable d) { // If we get an EXIT command we destroy the application if(c.getCommandType() == Command.EXIT) notifyDestroyed(); } /** Static method that quits our application * by using the static field 'self' */ public static void die() { self.notifyDestroyed(); } }
M3GCanvas import javax.microedition.lcdui.Graphics;
import javax.microedition.lcdui.game.GameCanvas; import javax.microedition.m3g.Camera; import javax.microedition.m3g.Graphics3D; import javax.microedition.m3g.Group; import javax.microedition.m3g.Loader; import javax.microedition.m3g.Mesh; import javax.microedition.m3g.Object3D; import javax.microedition.m3g.Transform; import javax.microedition.m3g.World; public class M3GCanvas
extends GameCanvas implements Runnable { // Thread-control boolean running = false; boolean done = true; // If the game should end public static boolean gameOver = false; // Rendering hints public static final int STRONG_RENDERING_HINTS = Graphics3D.ANTIALIAS | Graphics3D.TRUE_COLOR | Graphics3D.DITHER; public static final int WEAK_RENDERING_HINTS = 0; public static int RENDERING_HINTS = STRONG_RENDERING_HINTS; // Key array boolean[] key = new boolean[5]; // Key constants public static final int FIRE = 0; public static final int UP = FIRE + 1; public static final int DOWN = UP + 1; public static final int LEFT = DOWN + 1; public static final int RIGHT = LEFT + 1; // Global identity matrix Transform identity = new Transform(); // Global Graphics3D object Graphics3D g3d = null; // The global world object World world = null; // The global camera object Camera cam = null; // Camera rotation float camRot = 0.0f; double camSine = 0.0f; double camCosine = 0.0f; // Head bobbing float headDeg = 0.0f; // The model we will be controlling Mesh cube = null; /** Constructs the canvas */ public M3GCanvas(int fps) { // We don't want to capture keys normally super(true); // We want a fullscreen canvas setFullScreenMode(true); // Load our world loadWorld(); // Load our camera loadCamera(); } /** When fullscreen mode is set, some devices will call
* this method to notify us of the new width/height. * However, we don't really care about the width/height * in this tutorial so we just let it be */ public void sizeChanged(int newWidth, int newHeight) { } /** Loads our camera */ private void loadCamera() { // BAD! if(world == null) return; // Get the active camera from the world cam = world.getActiveCamera(); } /** Loads our world */ private void loadWorld() { try { // Loading the world is very simple. Note that I like to use a // res-folder that I keep all files in. If you normally just put your // resources in the project root, then load it from the root. Object3D[] buffer = Loader.load("/res/cube.m3g"); // Find the world node, best to do it the "safe" way for(int i = 0; i < buffer.length; i++) { if(buffer[i] instanceof World) { world = (World)buffer[i]; break; } } // Clean objects buffer = null; // Now find the cube so we can rotate it // We know the cube's ID is 1 cube = (Mesh)world.find(13); } catch(Exception e) { // ERROR! System.out.println("Loading error!"); reportException(e); } } /** Draws to screen
*/ private void draw(Graphics g) { // Envelop all in a try/catch block just in case try { // Move the camera around moveCube(); // Get the Graphics3D context g3d = Graphics3D.getInstance(); // First bind the graphics object. We use our pre-defined rendering hints. g3d.bindTarget(g, true, RENDERING_HINTS); // Now, just render the world. Simple as pie! g3d.render(world); } catch(Exception e) { reportException(e); } finally { // Always remember to release! g3d.releaseTarget(); } } /** * */ private void moveCube() { // Check controls if(key[LEFT]) { cube.preRotate(5.0f, 0.0f, 0.0f, 1.0f); } else if(key[RIGHT]) { cube.preRotate(-5.0f, 0.0f, 0.0f, 1.0f); } else if(key[UP]) { cube.preRotate(5.0f, 1.0f, 0.0f, 0.0f); } else if(key[DOWN]) { cube.preRotate(-5.0f, 1.0f, 0.0f, 0.0f); } // If the user presses the FIRE key, let's quit
if(key[FIRE]) M3GMidlet.die(); } /** Starts the canvas by firing up a thread
*/ public void start() { Thread myThread = new Thread(this); // Make sure we know we are running running = true; done = false; // Start myThread.start(); } /** Run, runs the whole thread. Also keeps track of FPS */ public void run() { while(running) { try { // Call the process method (computes keys) process(); // Draw everything draw(getGraphics()); flushGraphics(); // Sleep to prevent starvation try{ Thread.sleep(30); } catch(Exception e) {} } catch(Exception e) { reportException(e); } } // Notify completion done = true; } /** * @param e */ private void reportException(Exception e) { System.out.println(e.getMessage()); System.out.println(e); e.printStackTrace(); } /** Pauses the game
*/ public void pause() {} /** Stops the game */ public void stop() { running = false; } /** Processes keys */ protected void process() { int keys = getKeyStates(); if((keys & GameCanvas.FIRE_PRESSED) != 0) key[FIRE] = true; else key[FIRE] = false; if((keys & GameCanvas.UP_PRESSED) != 0) key[UP] = true; else key[UP] = false; if((keys & GameCanvas.DOWN_PRESSED) != 0) key[DOWN] = true; else key[DOWN] = false; if((keys & GameCanvas.LEFT_PRESSED) != 0) key[LEFT] = true; else key[LEFT] = false; if((keys & GameCanvas.RIGHT_PRESSED) != 0) key[RIGHT] = true; else key[RIGHT] = false; } /** Checks if thread is running */ public boolean isRunning() { return running; } /** checks if thread has finished its execution completely */ public boolean isDone() { return done; } } About Redikod Redikod from Malmo, Sweden, has been developing web and mobile games since 1997, and this small company is now one of the leaders in the Nordic games industry. Its disproportionate influence stems from strategic initiatives such as Nordic Game Potential, the yearly conference, and the Nordic booth at E3 2006. Redikod is commissioned to design the Nordic public support system and funding for games development, including mobile, that, pending final political decision this fall, will come into effect in 2006. But 3D and multiplayer mobile games development are their day job. To find out more, visit Redikod's website>> | |||||||||||||||||
Copyright © 2001 - 2009 Sony Ericsson Mobile Communications AB. All Rights Reserved.