第二部分:光的3d理论与定位
l 源代码(java类和资源)
http://developer.sonyericsson.com/getdocument.do?docid=74042
l 应用程序包(jar/jad)
http://developer.sonyericsson.com/getdocument.do?docid=74043
第一部分:“java手机3d编程世界”链接
l 指南第一部分链接
http://developer.sonyericsson.com/site/global/tipstrickscode/mobilejava3d/p_java3d_tutorial_part1_compliments_redikod.jsp
下面,我将引导你学习第二部分。
绪论
这是jsr 184(m3g)指南的第二部分,这里我将通过一些非常基础的3d理论、3d算术和一个简单的定位demo完成该课程。如果你不清楚,这里有最新的指南链接。
首先,或许是最主要的是在索尼爱立信开发者世界的移动java 3d网部分;其次,如果你曾经感到困惑,那就去索尼爱立信java手机3d论坛。在索尼爱立信开发网,你将找到你问题的答案以及其他信息。
该指南的目标是让你更好的理解3d算术,并能够运用jsr 184提供的平移和定位方法。我将讲解3d坐标系统,3d空间的平移和坐标向量的定位。同时,在指南的最后你将使用刚学到的知识在代码中实现在3d空间中旋转网眼(mesh)。这些是没有任何资料所提到的,因此这个标题“光的3d理论与定位”。该指南后面的章节将会包括更多高级主题。以教育为目的的最佳讲授方法不是讲解代码,它不能包括你可能遇到的所有错误当你编写3d程序的时候。
预备知识
在你开始读该指南前,你应该先读该指南的第一部分,并且要有基本的微积分和线性代数知识。数学知识不是必须的,但它有助于你的理解。
3d坐标系统
3d坐标系统有很多都是和2d坐标系统相同的,除了多增加了一个z轴,该轴通常也叫深度。
2d坐标系统 |
3d坐标系统 |
如你在上面的图片所看到的一样,3d对象不仅有宽和高,而且还有深度。因此,3d对象的一个点总是有三个坐标所确定,x、y和z。而3d对象的一个面是由一系列点组成的数组构成的。立方体有八个棱角,因此有八个点(虽然许多点都是重合的)。因此一个立方体坐标应为(坐标顺序为:x、y、z):
正面为:
-1.0, -1.0, 1.0
-1.0, 1.0, 1.0
1.0, -1.0, 1.0
1.0, 1.0, 1.0
背面为:
-1.0, -1.0, -1.0
-1.0, 1.0, -1.0
1.0, -1.0, -1.0
1.0, 1.0, -1.0
上述正方体的中心为坐标原点(0,0,0)。背面与正面有何不同之处呢?正面的z(深度)坐标值为1,而背面的z(深度)坐标值为-1。如果你仔细思考,它是合逻辑的。既然z轴被叫为深度,它最合适的逻辑是背面为负深度。
然而,即使立方体上的所有点都是必需的,我们仍然需要了解立方体的面。因为3d扫描光栅不知道如何去处理一个3d的点,并且他们可能被转换为象素点,因而造成在屏幕上画出8个象素点,但我们需要的是一个立方体。这是为什么要引入面的原因,一个对象有它的点的列表(就象上面的立方体一样),这些点是用来构成一个一个的面的。一个面是3d模型的一个表面,它是由3个或更多点所确定的。通常,一个模型是由很多三角面组成的,每个面由3个点确定的一个三角形。因此,在实际情况下,我们的立方体的每边(两个三角形构成一个四方形)需要两个三角形,这个立方体有6边,因此需要6*2=12个三角形,一个三角形由3个点确定,最终我们需要12*3=36个点来定义一个立方体。这鲜艳要比原始的8个点要多。然而,真的是这样的吗?你可能已经明白,正如下面的插图:在这12三角形中有很多点是公用的。

正如你所看到的,标记为“point 1”的点实际上是面a与面c共用点。因此我们不是真正的要存储36个点。实际上,最初的8个点存储在数组里已经足够了,然后让三角形拥有对顶点数组的索引。这种方法节约了36-8=28个点。在复杂模式这个数值将会更大。下面的插图是顶点数组和三角形索引的关系。

你可能没有注意到,显示的面是与我们的立方体面a相同的,并且在顶点数组里的8个点是我们的立方体的8个点。正如你所看见的,每个三角形没有用9个float变量来存储三个点来组成3角坐标,取而代之的是每个三角形仅仅用3个int变量来存储索引。这样大大的减少了内存的使用量,尤其是在你建立的模型中有300个或更多的三角形。
定位和平移
每个模型的旋转和平移从自己的坐标到全局坐标里。这是为了我们在3d世界显示他们,而不管我们在那里创建的。(例如,大多数模型的创建是在他自己的本地原点周围。如果我们没有相对于全局坐标的转换模式,我们总是要在全局坐标的原点重画所有事物。)
因此平移物体显得十分重要。你会问,这是如何实现的?其实很简单。如果你需要移动一个模型,简单的移动所有点即可。通过上面的实例,立方体是相对于原点而生成的,但是我们需要立方体显示坐标为10.0, 0.0, -10.0,那是向右和深度上各10单位,为了实现这个效果,我们只需简单的转变我们的顶点数组,并给每个点在x坐标上加10.0,在z坐标上减10.0,非常简单!
如果你还记得指南第一部分,我们周围world移动camera时。camera仍然能看见3d对象,因为我们只是增加或减少camera上点的坐标去移动该对象。
通常我们也想旋转我们的对象。旋转要比平移难理解一点,因为这个在自然界更复杂一点。让我们快速浏览一下立方体那张图片,现在我们做细微的修改如图:

如你所见,一个在3d空间的对象可以围绕三个坐标轴其中一个进行旋转。围绕y轴旋转称为偏移,围绕x和z轴旋转称为滚动。然而在该指南中,我都把他们称为旋转,不管是围绕哪个轴旋转,我这样做是为了让理解更简单些。
围绕一个坐标轴旋转最形象的就是拿一个干酪(是的,请从电冰箱里拿出一小块干酪,最好再带上三根牙签),这块干酪最好象我们的立方体。现在,在干酪上插入一跟牙签,位置是你想围绕着旋转的哪个轴,并且旋转这根牙签,你将看到该轴周围的旋转。围绕轴的旋转总是显现为正的度数为逆时针和负的度数为顺时针。
旋转一个3d物体真的如此简单,就象你能旋转一个物体围绕着你的三个轴,那是一样的…或不是?首先,你能旋转你的物体绕着任何一个你想要的向量,不仅仅是三个轴。举例来说,如果你从一个角到另外一对角的刺了你的干酪块,你建立的向量并不是轴,但是你仍然可以旋转牙签从而使干酪旋转。如果你记得第一章,我提到过jsr 184的旋转理论。它类似于:
nameofrotation(float degrees, float x, float y, float z)
x, y和z三个float是组成你想绕着旋转的向量,现在,围绕一个轴旋转仅需提供x轴向量。x轴向量是(十分合逻辑)1.0, 0.0, 0.0。那就是x轴为1.0,y和z轴为0.0。
3d旋转有存在一些问题。即,旋转是次序敏感的。一个物体绕x、y和z轴顺序旋转得到的结果是不同与其他任何顺序的。同样的,如果你已经绕某一个轴旋转一个对象,你也就旋转了它的其他轴。这样产生了另一个问题,当在绕x轴旋转之前已经绕y轴旋转,然后再绕x轴旋转后,我们得不到想要的效果。通常一个简单的3d游戏是不需要为这些事而担心的,通常不会把模型绕所有轴旋转的,即使你要这样做,你可以存储在每个轴上旋转的度,并且使用setrotation方法取而代之,我不会深究一个物体和轴关于深度定位问题,这将在以后的指南中提到,没有必要让这个问题困惑你。
我将提到的另一件事是局部旋转和全局旋转。你在局部坐标系统下旋转一个物体是不同于旋转该物体后转换到全局坐标。不同在于在局部坐标系统下发生的旋转是绕局部坐标轴的,而在全局坐标系统下, 它将绕着全局坐标旋转。
3d世界
在脑海里形成3d的概念要比2d难一些。我们所讲到的2d图形都是在屏幕上显示的一组象素点。然而,不是象画一张2d的png图片那样去描述3d模型和3d游戏世界,而是在3d坐标系统下用通过计算来画模型的每个点(顶点)。这些顶点被平移和旋转成你想要的模型,并最终进入图形管道,图中每个小象素区域都被定义成为一个点。对此有疑惑吗?我将不打算讲投影公式或者其他复杂的代数,因为我认为该图在最终出来之前你不会知道该模型的真实样子。
为了帮助你理解3d投影图,你可以看下面的图片。

一个屏幕上映射两个多变形
这是一张非常简单的投影图,不过已经足够让你脑海里有一个映像了,正如你看见的,从多变形顶点到眼睛的线是投影线。透视图实际上象一颗子弹从一点射入模型,然后直接进入观看者的眼睛。在这颗子弹沿着进入眼睛的路线时,击中一块铅板(屏幕)并留下一个凹痕(象素点)。而且,如果一颗子弹从多边形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(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; }
}


闽公网安备 35060202000074号