计算机毕业设计99基于java3D的模型动作引擎
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
基于java3D的模型动作引擎
学院:学生姓名:孙宏宇指导教师:
[中文摘要]
本工程着重对既有三维模型的复用,和对模型动画的制作。
现在业界很少有基于java3D的动画引擎,本工程也是对java3d在动画方面的创新。
工程围绕对3D Max文件格式的解析,动画帧的设计,动画文件的存储,动画的驱动,以及用户界面的设计做了详尽的说明,并提供了可运行的范例代码。
在工程的尾声部分,着重说明了代码的重构过程和调试方式。
使代码更加精简,易读。
[英文摘要]
This project, to already having the replying and using of a threedimensional model emphatically, and the making of the model animation. Seldom there is a engine of animation based on java3D in the industry now, this project is innovation in animation to java3d too. The project centres on the analysis of 3D Max file layout, the design of the animation frame, the memory of the animation file correctly, drive of animation, and design , user of interface make exhaustive explanation, have offered the example code that can be operated. In some of coda of the project , have stated constructing the course and debugging the way again of the code emphatically. Make the code simplified further, legible.
[关键词]
Java3D、GUI、Swing、GUI、重构
1、概述
1.1需求分析
在3D愈来愈普及的情况下,了解并掌握相关知识已经成为从业者的当务之急。
1.2开发工具选择
目前开发三维图形有几种工具:windows上的direct x ,支持多平台的openGL。
论资源,openGL和direct x都有很多的可参考编程实例可以参考。
其中direct x由于有比较快的更新速度,集成程度比openGL高。
OpenGL由于是硬件接口,比directx的硬件映射模式速度略高。
据我观察,direct x的普及率比openGL略高。
可以说两者不相伯仲,可以任选其一。
我选择用java开发,因为有一个系统的3D程序设计结构有利于以后向其他语言转变。
而java的系统化正是他的优点。
1.3可行性分析
现在用java开发3d游戏有两种途径:
/1:java3D. 这里面还分基于openGL和基于directx两种。
/2:jogl。
也就是java+openGL的缩写
java3d的结构继承了java的严谨。
条理性趋近于完美。
其结构如下:
VirtualUniverse
|
|
Locale
|
/ \
BG BG
| |
TG TG
| \
Shape ViewPlatform====View====Canvas3D====Screen3D
| \ / \
Ap Ge Viewer Enviorment
BG: BranchGroup 分支节点
TG: TransformGroup 变换节点,可以用变换矩阵控制
Ap:Appearance 物体外观数据
Ge: Geometry 物体形状数据
效率问题:java3D最大的问题就在于效率,实现同样的东西,FPS远不及用VC+D3D实现的高。
这就是jogl产生的原因。
缺陷:
动态加载就很成问题。
1.4工程定位及概念设计
我选择制作模型动画引擎,并选择最著名的模型制作软件3D Max制作出来的模型(*.3ds)作为我的引擎导入模型格式。
这不仅因为3ds模型逼真,而且这种模型在网上有很多共享资源可以使用。
2、基于java的模型动作引擎的选定和系统总体设计
有人说种纯oo的语言:java,c#在程序设计上强制oo,不仅仅带来了条理性和可靠性,在可重用方面更是其他语言不能比拟的。
2.1工程的划分
主模块/驱动模块:AnimationEditor.java
功能:一切模型和视角动作的驱动(用主循环实现),程序入口(main方法和setup 方法),GUI部分及响应(Swing类库和Listener的使用)。
IO模块:ModelLoader.java,FrameIO.java,LocalDAO.java
功能:负责模型的导入(3ds格式的解析,形成java3D格式模型), 动画的导入和导出(采用对象导入导出技术,对动作组进行反射压栈),本地数据库访问。
对象模块:Aframe.java,MoveMatrix.java, ModelStruc.java
功能:帧结构,动画结构,模型结构
功能模块:FrameAccesser.java, FramePlayer.java, ModelCutter.java, 功能:帧结构访问代理类,动画播放类(插值器),模型切分类(反射机制实现)辅助模块:Axis.java, Land.java, srcNameCheck.java
功能:坐标轴,三维地面参考,源路径检测。
控制模块:GameControl.java, PickHighLightBehavior.java
功能:编辑器视图控制,被选择部分模型高亮显示。
资源模块:*.3ds, *.ani
功能:模型资源,动画资源
2.2结构流程图
2.3数据存储方式选择
由于采用3d max的*.3ds作为导入资源,所以用平面文件存储数据比用数据库更具有灵活性和可扩展性。
用户可以方便的通过复制粘贴,或者指定路径来编辑指定模型的动画。
每个模型的动画被存为单独的文件,便于传播和拷贝。
2.4设计环境
采用IDE: Eclipse
环境:Windows XP professional edition, java sdk 1.4.1.2, java3d-1_3_1-windows-i586-opengl-sdk
3、数据结构分析与设计
3.1导入数据结构分析(属于IO模块)
3ds文件数据是由块(chunks)组成的。
块描述了紧接着的数据的信息,和数据的组成,以及数据块的ID和下一个块的位置。
如果有不明数据块,就忽略他。
下一个块的指针在这个不明块的开始就有说明。
3ds文件的二进制信息使用非常特殊的方式写的,也就是汇编方式,与正常的高位和地位正好相反。
比如:4A 5C, 那么5c是高位, 4A是地位。
如果一个长整数为:4A 5C 3B 8F,那么5C4A是低位,8F3B是高位。
对于数据块,被定义为:
start end size name
0 1 2 Chunk ID
2 5 4 Pointer to next chunk relative to the place where
Chunk ID is, in other words the length of the chunk 块有一个用ID表明的层次关系。
3DS文件的主块ID是4D4DH。
这个块永远是文件的第一个块。
下面的表就是块树的层次关系,不同的块ID就表明了他们的属性和位置。
每一个块都被起了一个名字,这样是为了更便于将其放在源代码中。
MAIN3DS (0x4D4D)
|
+--EDIT3DS (0x3D3D)
| |
| +--EDIT_MATERIAL (0xAFFF)
| | |
. //详见3D max文档
.
|
+--KEYF3DS (0xB000)
|
+--KEYF_UNKNWN01 (0xB00A)
+--............. (0x7001) ( viewport, same as editor )
3.2模型结构拆分(属于功能模块)
在导入3ds模型的时候形成了以整体模型为根节点的BranchGroup,其模型的树状结构如下:
TG 整体模型
|
BG
| \ \
BG BG .....
| \
TG TG
| \
Shape Shape...... 这些是模型最小分割
| \
Geo Ap
在java树中,我们可以用父节点枚举子节点。
利用反射原理,将整个的模型分割成最小的块。
下面的ModelCutter类用来分割模型。
public class ModelCutter
3.3帧结构设计(属于对象模块)
对于模型的每一个最小细分,一个帧应该记录这个最小细分的三维位置向量,三维旋转量,和三维比例。
除了这些,帧还应该有自己在整个动画里面的序号,指向前一个帧和后一个帧的指针。
作为辅助,应该加上toString函数,这样随时可以获取当前帧的信息。
最后提供了一个方便的构造函数,可以便捷的将输入帧作为上一帧。
public class AFrame implements Serializable
3.4动画结构*.ani的设计(IO模块)
有两个问题在这里需要讨论:
第一个是保存什么的讨论,在这里有两个选择,保存每个帧,或者保存关键帧。
前
者保存文件比较大,读取以后占用内存也比较大,但是动画播放速度快一点。
后者保存文件非常小,占用内存相对小,动画播放的时候需要动态插值,速度相对下降。
在这里我选择了只保存关键帧,因为这样动画文件很小,以后可以便于和3ds文件捆绑。
第二个问题是保存方式。
手工保存所有的帧的数据没有任何难度,但这个方法很费时间,无法利用Java Serialization Api的复用能力。
这个API可以进行大量基础工作。
我使用Java Serialization API来保存存储在hash table中的frame组。
Java Serialization API的核心功能是把对象变成字节流,把字节流变成对象,分别称为序列化(Serialization)和反序列化(Deserialization)。
运行时非null的引用字段要序列化时,要求引用所指的对象也实现Serializable接口。
这个规则递归采用。
另一方面,任何类声明引用其他对象时要经过认真分析确定这个类能否声明可序列化。
类声明任何引用字段都要一一检查引用的对象是否可以序列化。
在这个工程里面,帧结构是可序列化的:public class AFrame implements Serializable然后利用java的serialization API写出 *.ani动画文件的io类如下:public class FrameIO {
public static void saveObj(Object obj,String filename){// 存动画文件// 省略
}
public static Object loadObj(String filename){//读取动画文件
// 省略
}
}
4、GUI设计与反馈控制
插入图片:
4.1Swing组件概念
Swing的优点:
1、Swing有一套丰富的、更方便的用户界面元素。
2、Swing几乎不依赖于平台,因而不容易出现与平台有关的错误。
3、Swing给用户的感觉是:在各平台上的运行都是一致的。
4.2选择Swing还是AWT
Swing是由100%纯Java实现的,Swing组件是用Java实现的轻量级( light-weight)组件,没有本地代码,不依赖操作系统的支持,这是它与AWT组件的最大区别。
由于AWT 组件通过与具体平台相关的对等类(Peer)实现,因此Swing比AWT组件具有更强的实用性。
Swing在不同的平台上表现一致,并且有能力提供本地窗口系统不支持的其它特性。
在Swing中,可以指定程序中GUI的look and feel,真正做到GUI与平台无关,但在AWT 中则不行。
4.3从Swing看工程的MVC设计模式
MVC设计模式把一个软件组件区分为三个不同的部分,model,view,controller。
Model是代表组件状态和低级行为的部分,它管理着自己的状态并且处理所有对状态的操作,model自己本身并不知道使用自己的view和controller是谁,系统维护着它和view 之间的关系,当model发生了改变系统还负责通知相应的view。
View代表了管理model所含有的数据的一个视觉上的呈现。
一个Model可以有一个以上的
View,但是Swing中却很少有这样的情况。
Controller管理着model和用户之间的交互的控制。
它提供了一些方法去处理当model的状态发生了变化时的情况。
4.4图形界面编码与分析
从整个窗体界面的布局可以看出来中间部分是java3D可绘制区,右面是控制版面,下面是提示版面。
整体上使用了BoxLayout的布局。
public void setupGUI(){
.
.
.//由于代码过多,此处省略.
//add status bar
}
4.5界面反馈与控制
在swing中,有两种基本的响应组件消息的方式。
第一个方式,也就是我用的方式,就是给组件添加监听器:
item.addActionListener(this);
被添加的类需要继承Listener接口。
第二个方式,是使用回调函数。
其余的方式并不常用,在这里不说了。
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
//
//if is playing animation,nothing done
if(player.isPlaying())return;//这里判断是否在播放动画。
设定播放动画的
时候不能编辑动画
Update();//更新信息,保持动画信息为最新的
String name = ((Component)e.getSource()).getName();//这个追踪事件的源,并取得源的名字
String value = e.getActionCommand();//这里取得源的动作命令,这个命令是在创建组件的时候手工加上去的。
System.out.println("action: name = " + name + " value = " + value);//调试信息
//panel View
if(name=="View"){
panelView.add("Center",createTree());
panelView.repaint() ;
return;
}
.//代码过多,此处省略.
.
}
currentTrans[0].setTransform(FrameAccesser.readFrame(frames[tgIndex]));//将更新完的动作让模型显示出来
showFrameData(frames[tgIndex]);//将更新完的信息传入信息版面
}
5、辅助模块的设计和功用
5.1高亮显示被选模型部分
这个类的作用是让用户知道当前选择的模型最小细分是哪个。
在这里,我最终选用了线框模式作为被选指示。
其原理是:
首先要使被选模型允许动态改变外表渲染。
这样才能在被编译以后的3d模型上动态的改变外观。
这个在ModelCutter里面有一个辅助功能,可以设置指定序号的部分的动态改变能力。
其次,要有一个碰撞检测的过程,就是在三维空间中,由鼠标点,垂直于屏幕的一条直线和模型的碰撞。
碰撞检测算法
a、实体包围盒法
b、静态干涉和动态干涉结合法(单步检测法)
JAVA3D提供的碰撞检测功能的实现。
JAVA3D提供了两个类WakeupOnCollisionEntry及WakeupOnColisionExit,这两个类在构造的时候要求提供被检测的范围(类Bounds的子类)或三维对象(类Node的子类)。
当场景图中的任何对象与被检测的对象(范围)发生碰撞时WakeupOnCollisionEntry就会被唤醒(Wakeup),当碰撞解除时WakeupOnColisionExit就会被唤醒。
Java3D检测碰撞的方法有两个:USE_BOUNDS和USE_GEOMETRY,也就是使用包围盒或几何体进行碰撞检测,经过实验发现,前者的检测效果非常粗略,几乎是错误的结果,所以选择用几何体进行检测。
实际编程中,可以把唤醒条件加入Behavior类中,就可以在Behavior类的processStinulus过程中处理碰撞发生后或解除后要作的控制。
public class PickHighlightBehavior extends PickMouseBehavior {
Appearance savedAppearance = null; //选择前的外观
Shape3D oldShape = null; //选择前的图形
Appearance highlightAppearance; //选择后的外观
TransformGroup[] tg; //传递的对象组
.//代码过多,此处省略.
.
// Set up the polygon attributes
PolygonAttributes pa = new PolygonAttributes();
pa.setPolygonMode(PolygonAttributes.POLYGON_LINE);
pa.setCullFace(PolygonAttributes.CULL_NONE);
highlightAppearance.setPolygonAttributes(pa);
//pickCanvas.setMode(PickTool.BOUNDS); //设定碰撞边界
pickCanvas.setMode(PickTool.GEOMETRY); //设定为几何碰撞,这样比较精确,但是运算量大
}
public void updateScene(int xpos, int ypos) {
// 省略
//System.out.println(shape) ; savedAppearance =
shape.getAppearance();//保存原有外观 //TransformGroup
tg=(TransformGroup)shape.getParent() ;
oldShape = shape;
shape.setAppearance(highlightAppearance);//改变外观为选择状态
}
}
}
5.2三维地面和三维坐标系的建立
a、三维坐标系
三维坐标系事实上就一个3D模型,当然,我们也可以用*.3ds导入。
但是作为一个编辑器基本组件,速度是很重要的。
用纯的代码写模型,需要首先在纸上计算好。
这样才能准确的画出来。
我们的类只需要继承Shape3D就具有了基本的模型特征。
public class Axis extends Shape3D{
}
b、三维地面
三位地面就是一个充满交叉直线的图形。
画的方法很简单,不用一个一个坐标的写,我们可以设置地增量,用循环来实现。
需要注意的是,坐标不要超出整体的3D空间的范围。
Shape3D createLand()
5.3帧播放器
帧播放器仅仅放在辅助模块里面是由原因的。
因为动画的播放需要驱动循环的驱动,所以这个FramePlayer其实并不能播放帧,其作用仅仅是在调用的时候自动将当前帧更新到下一个帧,并判断是否还有下一个帧。
也就是说,这个类仅仅是一个驱动帧的工具。
public class FramePlayer {
private boolean playing=false;//这个变量用于判断是否正在播放动画
private AFrame[] frames;//动画
private TransformGroup[] trans;//模型指针
private int[] framePos;//模型动画位置
// 省略
public void start(){//开始播放,并自动退回到第一个帧
rollbackFrame();
playing=true;
}
.
.//代码过多,此处省略.
.
}
}
5.4视角控制
视角控制是必需的。
因为动画编辑者可以需要改变观察点的角度,或者靠近,远离模型以更好的观察模型动画的设计情况。
这里我们保持视角始终是与水平面呈45度角,也就是常用的3D视角。
用几个矩阵相乘实现。
另一方面,视角转了360度,回到原点。
6、驱动模块/主模块与模块集成
public class AnimationEditor implements ActionListener
主模块是整个工程的核心模块,也是其他模块的交叉点。
这个模块几乎是集成了其他所有模块的功能。
由于代码过多,功能也是面面俱到。
让我们从入口函数来看。
6.1程序入口
程序入口非常简单。
先设置了世界的比例,这里定为0.005F,然后初始化主模块。
public static void main( String[] args )
{
try
{
float scale = 0.005f;
AnimationEditor show = new AnimationEditor( scale );
show.frame.setSize(1024,768 );//这里改变窗体大小
show.frame.addWindowListener( new killAdapter() );//加入监听器 GameControl gc=new GameControl(direct) ;//初始化控制模块 show.canvas.addKeyListener(gc);//给3D画布添加监听器 show.frame.show();//显示窗体
show.mainLoop() ;//进入驱动循环
}
catch ( Throwable e )
{
System.err.println( "** static main " + e.getMessage() );//错误控制
}
}
6.2主模块构造
主模块的构造函数基本表明了这个工程的工作步骤。
首先将传进来的比例大小传给环境变量。
然后调用setup进行其余各个模块的加载。
public AnimationEditor( float scale )
{
scalingFactor = scale;
setup();
}
private void setup(){
setupGUI();//装载GUI
openFile();//打开资源文件
//
// Create the basic structure of a Universe.
//
createUniverse(); //创建三维空间
//
// Now create some content for the scene and attach it to
// the scene graph.
//
createScene();//创建三维空间树图
locale.addBranchGraph( sceneRoot );//加载三维空间的根节点
}
6.3驱动循环的工作
驱动循环是动画的原动力,他驱动着动画从一个帧进入下一个帧。
private void mainLoop(){
int i=0;
while(true){
//
//add codes to be active
//
//1, part selecting test 最小分割选择测试,碰撞检测
if(targetChanged()){//如果选择目标改变了
Update();//更新
showFrameData(frames[tgIndex]);//更新动作
}
//2, view point changed test 测试视角是否改变了。
i=changeView(i);
//3,play the animation 播放动画
player.play() ;
try{
Thread.sleep(20); //让出处理器给别的程序
}catch(InterruptedException ex){
}
}//end while
}
以上这三个工作繁琐,但代码却清晰明了,这是因为大规模的工作都让各个模块分摊了。
我们这里仅仅说明了很小一部分代码。
更多的代码,并不是特别关系到工程构架的,都并没有说明。
更多代码请看源程序。
7、调试运行与代码重构
7.1调试
基本的Java字节码 (也就是使用System.out.println())
使用注释
附加在一个正在运行的程序上
远程调试
需求调试(Debugging on demand)
优化代码的调试
共同的错误类型
编辑或句法错误是你最先和最容易遇到的错误。
它们通常是键入错误引起的。
逻辑错误不同于运行时错误,因为没有任何异常被抛出,但是输出不是期望的东西。
这些错误的范围从缓冲区溢出到内存泄漏。
运行时错误在程序执行时发生并且通常产生一个Java异常。
线程错误是最难重复和跟踪的。
Java debugging APIs
Sun已经定义了调试的结构,它们称之为JBUG。
7.2代码重构
a、简化设计
能够简单地被理解.这依赖于你代码的可理解性.只有可理解,下面的简单性才能达到. 能够简单地被修改和扩展.OO系统往往通过增量的方法改变或增加系统行为,所以也就是要被简单地重用,这要求我们没有重复代码.
b、设计模式
c、Refactoring的目标
设计模式不是纯粹理论上的体系结构或者分析方法,它是一种可实际操作的东西。
必须提出三个实际的应用例子才能够得上设计模式的条件。
8、代码保护
由于java的线性流存储结构,导致java的class文件很容易被反编译。
目前有许多Java反编译工具可以把(*.class)文件反编译出(*.java)文件。
可以使用Java 混淆器。
9、结束语
通过引擎设计,让我知道了java3D在实际应用中的局限性,----即对动态加载支持的不完善性,以及动画帧数较低,和Swing结合不是很完善等等。
这些局限性一方面是由于java3D还处于起步阶段,另一方面是由于3D这样一个对系统配置要求苛刻的技术,用中间层---即jvm虚拟机来实现还为时过早。
参考文献
[1]、J2SE API DOC
[2]、Java3D 1.3.1 Doc
[3]、 技术论坛
[4]、JavaRanch论坛,
[5]、中国游戏制作联盟技术版论坛
[6]、Sun技术文档,
[7]、《OpenGL三维图形系统》第一版,和平鸽工作室,重庆大学出版社,2003年
[8]、《游戏核心算法编程内幕》第一版,西班牙,Daniel Sanchez-Crespo Dalmau,邱仲潘译,中国环境科学出版社,北京希望電子出版社,2004年
[9]、《Java游戏编程》第一版,美,David Brackeen,邱仲潘译,科学出版社,2004年
[10]、《Java3D编程实践》第一版,都志辉,清华大学出版社,2002年
[11]、《Sun java SL275教程》内部教材。