这是一张非常简单的投影图,不过已经足够让你脑海里有一个映像了,正如你看见的,从多变形顶点到眼睛的线是投影线。透视图实际上象一颗子弹从一点射入模型,然后直接进入观看者的眼睛。在这颗子弹沿着进入眼睛的路线时,击中一块铅板(屏幕)并留下一个凹痕(象素点)。而且,如果一颗子弹从多边形2穿过多变形1射入屏幕,但屏幕上不会显示,因为多边形2离屏幕更远些。当然,投影过程远比上述的复杂,不过这个简单的模型已经足够了。
编码
好,为了帮助你更好的理解上面一段理论知识,我们将利用新学的知识来写一些代码。我们将使用跟指南一相同的canvas,不过我们将添加一些方法,并且改变构造函数。我们不会去装载world,而是装载一个立方体并显示它。这是同样简单的过程,我们只需把指南一里的loader方法的文件名从map.m3g改为cube.m3g。在后面,我们将了解透视模式和模型更深层次的知识。
/** 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);
}
}
这个方法基本相同,不同在于最后一行cube = (Mesh)world.find(13);这行可能有点困惑,因此让我说明一下。
在JSR 184场景里的图形对象是有ID的,ID可以是它们分开(尽管有些对象没有ID)。这些ID能被直接操作在你想输出你中意的软件模型的时候。如果ID存在并与对象有关联,当你对该对象使用find方法时,将会得到这个对象的ID。对于图片最简单的方法可见HashMap,它有对象ID和对象的值。find方法是Object3D类的一个方法,因此在JSR 184的所有类都有这个方法。
现在,当我要把创建的立方体导出,所以他的ID应该为13。我知道了这个,我能很容易的从World中找到立方体的Mesh和操作它(旋转、移动、缩放、……)。Mesh类是一个非常有用的类,它能存储一个模式所需要的所有信息;顶点缓冲器(vertex buffer)、三角索引缓冲器(triangle index buffer)、纹理坐标(texture coordinates)等。它也能存储高级透视模式操作类Apearance。我们将在后面谈到这个奇特的类。到目前为止,知道Mesh的描述模式,并知道它是Transformable的一个子类。
Transformable类
抽象类Transformable描述了在3D空间物体能完成的一些变换(缩放、旋转、移动)。很多对象继承了这个类。规则允许Transformable的所有一个子类都能在3D世界移动。如果你想了解Transformable类的详细信息,请参考JSR 184 API文档。我们将在指南的其他部分发现很多有用的方法。现在,我们只使用preRotate方法。这个方法是在物体平移之前进行简单的旋转,因此它是绕着自己的轴旋转的(回想我在该指南的前面部分讲过的)。
因此,使用preRotate方法的效果总是恰当的旋转该立方体,不管它处于world的哪个位置。preRotate方法跟其他旋转方法一样,需要四个float参数。第一个参数是旋转的度数,后面三个参数是我们想绕着旋转的向量。让我们看看新的moveCube方法。
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();
}
正如你所看见的,我调用preRotate方法并传入度数和旋转轴。现在,左和右是绕着z轴旋转(记住,z轴上的值只有z坐标为1,其他为0),而上和下是绕着x轴旋转。在这个演示中,我们没有绕着y轴旋转,其实根本不需要编写代码,当你按下不同的两个键时,该立方体就绕着y轴旋转。
我们可以在更新立方体旋转的循环里调用moveCube方法,就像在之前指南里的moveCamera方法。
在该演示的其他代码是与演示一的代码相同,因此,在这里我就不重复了。看下面的代码清单或者下载源代码回去自己看。
结论
通过上面这些,下面是两张该程序运行时的屏幕截图:

好的,这样看来我们有自己的旋转立方体了。我希望你现在能理解3D坐标系统和3D旋转。在JSR 184 3D应用开发网,请继续读该系列的其他更高主题。下面是该例子的代码清单,你能下载代码的ZIP包,包括所有资源文件,因此你能在机器运行它。
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(Excep