利用Java 3D技术播放动画之JMF技术

Java 3D场景中动画片段的播放为更丰富更生动的3D内容带来了进一步发展的机会。动画可以显示更逼真的背景,例如运动的云彩,繁忙的城市街道,或者窗外的场景等等。动画可以使得帮助信息更生动,也可以用于游戏设计中的场景过渡。

  本文共分为两部分,描述了我是怎样实现一个Java 3D动画屏幕的。本部分将解释怎样利用Java媒体框架(JMF)开发本程序的,特别对JMF性能包Windows版本2.1.1e进行了具体的描述。我所用的另外一些工具包括J2SE 5.0和Java 3D 1.3.2。在第二部分中,我将讨论利用 Quicktime  for Java技术实现该动画程序的的另外一个版本。

  图1展示了JMF Movie3D 应用 程序中的两幅屏幕快照,它们是在不同的时刻拍摄的:其中右边的那幅是屏幕从后面看上去的视图。


图 1.Movie3D应用程序中的两幅视图

  该应用程序主要包括:

  ·JMF与Java 3D的整合。一个应用程序中可以有多个任意大小的屏幕。既然一个屏幕是Java 3D的Shape3D类的子集,那么它可以被轻易地整合进不同的场景中。

  ·程序的执行过程采用了"模型-视图-控制器"设计模式。其中,屏幕是视图元素,由JMFMovieScreen类描述。动画是模型部分,由JMFSnapper类所管理。一个Java 3D Behavior类TimeBehavior是控制器,由它来控制动画的周期性更新。所有的JMF代码都位于JMFSnapper类中,这使得 测试 任何变化都更为容易些。在这篇文章的第二部分中,实际上是用了一个称为QTSnapper的QuickTime for Java版本代替了JMFSnapper。

  ·充分利用了Java 3D性能技巧来加速着色过程;结果,动画可以以每秒25帧的速度播放而无任何困难。

  ·最后讨论一下我采用JMF开发时遇到的问题,由于这些问题,我更喜欢的解决方案不能正常工作-JMF的确具有成为一个伟大API的潜力,但在这一伟大的背后还有许多低劣的编码部分有待进一步改进。

  1. 我坐在一座高山上

  实际上,我根本没有坐在什么高山上,而是坐在没有自动调温器的寒冷的办公室的一把椅子上。我真正想说的是,这篇文章基于大量的Java 3D和JMF的背景知识。

  在本文中,我将解释我用来从影片中提取动画帧的JMF技术,而不去讨论诸如流媒体,捕获或者代码转换等技术。

  2. 程序实现中的两幅轮廓图

  动画由JMFSnapper类负责装载和播放,且动画播放循环进行,直到发命令让其停止。

  动画屏幕由JMFMovieScreen创建,它负责管理一个放在跳棋盘地板上的Java 3D四边形。

  想直观观察一下这些类吗?请看下图中的应用程序场景图(该场景图展示了在一个场景中的Java 3D结点是怎样被链接到一起的)。


图 2. Movie3D场景图

  图2的许多细节被忽略掉了,但是这幅图与KGPJ中第15章的示例Checkers3D有着惊人的相似。只有动画相关的结点是新东西。
由于JMFMovieScreen和TimeBehavior对象都是场景图中的结点,所以它们都以三角形方式显示。JMFSnapper对象不是图中的一部分,但是被JMFMovieScreen所调用。

  每隔40毫秒,TimeBehavior对象调用一次JMFMovieScreen的nextFrame()方法,而nextFrame()又依次调用JMFSnapper的 getFrame ()方法来检索正播放动画中的当前帧,该帧然后被放置到由JMFMovieScreen管理的四边形上。

  TimeBehavior是Java 3D的Behavior类的子类化,是一个定时器的Java 3D实现方式。它与KGPJ中第18章的3D精灵示例中的TimeBehavior类很相似。

  洞察该应用程序实现的另一方法是看一下图3中描述的UML类图,其中仅显示了类的公共方法。 


图 3. Movie3D类图

  Movie3D子类化了Jframe,而WrapMovie3D是Jpanel的子类化结果。WrapMovie3D创建了图2中的场景图,并在应用程序的JPanel上对其进行着色。它使用CheckerFloor类和ColouredTiles类来构造出跳棋盘地板效果。

  JMFMovieScreen完成动画屏幕的创建,并把它添加到场景中去,然后通过创建一个JMFSnapper对象来启动动画的播放。TimeBehavior每40毫秒调用JMFMovieScreen中的nextFrame()方法一次,而nextFrame()又调用JMFSnapper中的getFrame()方法来检索当前帧。

  所有该示例的代码以及本文的一个早期版本,都能在KGPJ website处找到。

  3.观看动画播放

  动画,动画屏幕以及用来更新屏幕的TimeBehavior对象都是由WrapMovie3D中的addMovieScreen()方法建立起来的:

//全局变量
private BranchGroup sceneBG;
private JMFMovieScreen ms; //动画屏幕
private TimeBehavior timer; //更新屏幕 
private void addMovieScreen(String fnm)
{
 //把fnm形式的动画投放到动画屏幕上
 ms = new JMFMovieScreen(new Point3f(1.5f, 0, -1), 2.0f, fnm);
 sceneBG.addChild(ms);

 //为animating动画建立计时器对象
 timer = new TimeBehavior(40, ms);
 //更新动画每40ms一次(25帧/秒)
 timer.setSchedulingBounds(bounds);
 sceneBG.addChild(timer);
}

  这里的两个Java 3D方法addChild()调用把JMFMovieScreen结点和TimeBehavior结点链接到场景图中。setSchedulingBounds()方法用于激活TimeBehavior结点(也就是说,使该结点开始计时)。

  4.生成动画屏幕

  JMFMovieScreen是Java 3D的Shape3D类的子类化,所以一定要为其指定一个几何形状和外观。

  几何形状是一个四边形,每边与动画的图像尺寸成比例,但是用该四边形宽度与高度中的较大值用作类JMFMovieScreen的构造器的参数。该四边形是竖直的,朝向正Z轴方向,可被放于地板的任意位置。

  该四边形是两面的,这样动画可以从屏幕的前后两个方向观看。纹理是使用双线性插补算法进行平滑处理,所以当从近处观看动画时,像素化现象大大减弱。

  这里的大部分功能是从KGPJ一书第24章的第一人称射击程例中的ImageCsSeries类中复制过来的。ImageCsSeries类用于在一个四边形上显示一系列的GIF图像。为节省篇幅,我仅描述一下JMFMovieScreen区别于ImageCsSeries的几个特点。

   实现图像的高效着色

  从动画中提取的帧被转换成纹理后放于四边形之上;其实现方式分为两步:第一,给定的BufferedImage被传递给一个Java 3D ImageComponent2D对象,然后又传递给一个Java 3D Texture2D。

  四边形的纹理更新是很迅速的:每秒可更新25帧,纹理需要变化25次。因此,实现高效地纹理化是十分重要的。这有可能通过把某些特定的格式指定给BufferedImage和ImageComponent2D对象来实现。

  ImageComponent2D对象由JMFMovieScreen所使用,其声明如下:

ImageComponent2D ic = new ImageComponent2D(
 ImageComponent2D.FORMAT_RGB,
 FORMAT_SIZE, FORMAT_SIZE, true, true);

  构造器中的最后两个参数指出,它使用的是"by reference"和"Y-up"方式。由于Java 3D会避免把图像从应用程序空间复制到图像 内存 中去,所以这里指出的方式将减少存储纹理图像的内存需求。

  在Windows操作系统环境下,Java 3D用OpenGL作基本的着色引擎,ImageComponent2D格式应该是ImageComponent2D.FORMAT_RGB(如上所述),BufferedImage格式应该是BufferedImage.TYPE_3BYTE_BGR。在JMFSnapper中BufferedImage格式是固定的。

 把纹理链接到四边形上

  把纹理图像平铺到一个四边形上的常用方法是,把纹理的左下角链接到四边形的左下角,并以反时针方向指定其它链接。这种方法显示在图4中。


图 4.纹理与四边形间的标准链接

  纹理坐标的范围是0-1,沿着x轴和y轴,且y轴方向向上。例如,纹理左下角使用坐标(0,0),则右上角为(1,1)。

  当使用"Y-up"方式时,y轴的纹理坐标是颠倒的,即指向下方。这就是说,坐标(0,0)对应纹理的左上角,而(1,1)对应纹理的右下角。

  在使用"Y-up"方式情况下,纹理坐标一定要赋给四边形的不同点以取得图像的相同的方向。这种新的配置显示在图5中。


图 5. 当使用"Y-up"方式时的纹理与四边形的链接
 
  在JMFMovieScreen中,实现把四边形顶点与纹理坐标相链接的代码如下:

TexCoord2f q = new TexCoord2f();

q.set(0.0f, 0.0f); 
plane.setTextureCoordinate(0, 3, q); 
//纹理坐标(0,0)-->四边形左上点(p3)
q.set(1.0f, 0.0f); 
plane.setTextureCoordinate(0, 2, q); 
// (1,0) -->右上(p2)

q.set(1.0f, 1.0f); 
plane.setTextureCoordinate(0, 1, q); 
// (1,1) -->右下(p1)

q.set(0.0f, 1.0f); 
plane.setTextureCoordinate(0, 0, q); 
// (0,1) -->左下(p0)

  这里的平面对象代表了四边形。

   更新图像

  前面已经提到,一个TimeBehavior对象被建立以每隔40毫秒调用一次JMFMovieScreen的nextFrame()方法。而nextFrame()又调用JMFSnapper对象的 getFrame ()方法来以一个BufferedImage对象方式检索当前动画帧。该BufferedImage对象被指派给ImageComponent2D对象,然后用于四边形的材质。nextFrame()的代码如下所示:

//全局变量
private Texture2D texture; //由四边形使用
private ImageComponent2D ic;

private JMFSnapper snapper; 
//快照该动画
private boolean isStopped = false; 
//动画停止了吗?

public void nextFrame()

 if (isStopped) //动画已经停止
  return;

 BufferedImage im = snapper.getFrame(); 
 //获取当前帧
 if (im != null) {
  ic.set(im); //把该帧指派给ImageComponent2D
  texture.setImage(0,ic); 
  //使成为该形状的材质
 }
 else
  System.out.println("Null BufferedImage");
}

  JMFSnapper对象snapper是在JMFMovieScreen的构造器中创建的:

//装载并播放动画
snapper = new JMFSnapper(movieFnm);

  JMFSnapper中的简单接口隐藏了用于播放动画和从动画中提取帧的JMF代码的复杂性。在本文的第二部分里,JMFSnapper类为一个使用 QuickTime  for Java的版本所取代,且JMFMovieScreen类也作了最少的修改。

  5.管理动画

  JMF为存取特定的动画帧提供了一种高级存取方式。下面的代码片断显示了该高级方式的主要组成,我略去了其中有关错误检测及异常处理的部分。

//在realized状态下,创建一个动画播放器
URL url = new URL("file:" + movieFnm);
Player p = Manager.createRealizedPlayer(url);

//生成一个帧放置器
FramePositioningControl fpc = (FramePositioningControl) p.getControl("javax.media.control. FramePositioningControl");

//创建一个帧抓取器
FrameGrabbingControl fg = (FrameGrabbingControl) p.getControl("javax.media.control.FrameGrabbingControl");

//要求改变到一个prefetched 态
p.prefetch();

//一直等待,直到播放器处于那种状态...

//移动到具体的某帧,例如第100帧
fpc.seek(100);

//取得当前帧的一个快照
Buffer buf = fg.grabFrame(); 

//取得它的视频格式细节
VideoFormat vf = (VideoFormat) buf.getFormat();

//用视频格式初始化BufferToImage
BufferToImage bufferToImage =new BufferToImage(vf);

//把缓冲区数据转化成一幅图像
Image im = bufferToImage.createImage(buf);

//指定想得到的BufferedImage的格式
BufferedImage formatImg = 
new BufferedImage(
FORMAT_SIZE, FORMAT_SIZE, 
BufferedImage.TYPE_3BYTE_BGR);

//把该图像转化成一个BufferedImage
Graphics g = formatImg.getGraphics();
g.drawImage(im, 0, 0, 
FORMAT_SIZE, FORMAT_SIZE, null);
g.dispose();

  一个媒体播放器从创建到开始播放共经历6种状态。处于realized态的播放器知道如何对其数据进行着色,所以在要求时可以提供可视化组件和控件。我用了两个控件:FramePositioningControl 和FrameGrabbingControl。FramePositioningControl提供seek()和skip()等方法,用于在一个动画中移动以查找一个特别的帧。FrameGrabbingControl提供了方法grabFrame(),它可以从动画的视频轨道中抓取当前帧。
为使这些控件工作,播放器必须实现从realized 态转入prefetched 态。这可以使播放器为进行媒体播放作好准备,并使媒体数据装入。

  对于prefetch()的调用是异步的,这意味着我的代码必须包含一个等待周期,直到完成一个变换状态为止。标准的JMF编码方案是实现一个waitForState()方法,它可以停止代码的执行,直到一个状态改变事件唤醒它。

  要抓取的帧可以用seek()方法在轨道中定位,然后调用grabFrame()方法实现帧的抓取。编码中必须经历多个转换步骤来把抓取的缓冲对象转换成JMFMovieScreen要求的BufferedImage对象。注意,BufferedImage对象使用了TYPE_3BYTE_BGR格式,这种格式对于该程序中的Java 3D部分通过引用方式使用纹理是必需的。Sun的JMF站点 包括了一些有用的小例子,其中Seek.java一例说明了如何使用FramePositioningControl来遍历一个动画。

 分三个步骤完成的技术攻关

  不幸的是,上面概述编码方法是失败的,至少对于Windows 版的JMF性能包版本2.1.1e是这样。经过几番修改,我最后得到一个可以正常工作的版本JMFSnapper。

  攻关1 上述的两个控件FramePositioningControl与FrameGrabbingControl,在JMF缺省的播放器模块中是难以得到(Solaris和Win32性能包均支持两种不同的MPEG播放器)的,要求用"native modular(本地组件)"播放器才行,其选取方式如下:

Manager.setHint(Manager.PLUGIN_PLAYER, new Boolean(true));

  该播放器是一个重量级的组件,其与轻量级的Swing GUI如JFrame 和JPanel交互性较差。不过,我不需要显示播放器的界面。使用本地组件播放器的一个更为严重的后果是,需要较长的时间装入媒体和出现一些不确定的播放结果(如播放快慢不一致且漏掉一些帧)。

  攻关2 经过一番考虑,我定出最好的加速播放器的方式是让其承担较少量的工作。我从MPEG文件中提取出了音频轨道部分,并确保该文件以相对简单的MPEG-1格式存储。任何一些视频编辑工具都可以胜任这些工作。我使用了两个自由软件工具: MPEG Properties和 FlasKMPEG。前者用于提供动画格式信息,后者是一个不错的编辑器。

  经提取加工后的动画播放速度快捷,帧速率稳定并且没有漏帧的情况发生。 

  然而,FramePositioningControl控件类并不可靠。在我的WinXP机器上,seek()方法几乎总是失败,skip()方法大约有百分之八十情况下工作正常。

  攻关3 我只好对FramePositioningControl忍痛割爱。我运用的帧抓取算法依赖于调用FrameGrabbingControl的grabFrame()方法--当播放器播放动画时以常规的间隔时段调用该方法。

  到此,我已有了可靠的从仅含视频的MPEG-1文件中抓取帧的代码。对于既有视频也有音频轨道的文件该代码也运行良好;但不足是,播放器启动很慢而且不确定的播放导致帧抓取的不确定性。

  我在JMFSnapper 的开始加上了一些"等待"代码来处理既有视频也有音频的动画。JMFSnapper对象等待播放器的启动(即进入启动状态),并等待第一个动画帧可用。

   等待第一帧

  JMFSnapper类的构造器调用了方法waitForBufferToImage(),该方法反复地调用方法hasBufferToImage()直到它检测到第一个视频帧为止。

  hasBufferToImage()调用了FrameGrabbingControl的方法grabFrame(),并检查是否返回的缓冲对象中包含视频格式数据。它使用该数据来初始化一个BufferToImag对象--该对象随后用于把每一个抓到帧转换成一幅图像。

// 全局变量
private FrameGrabbingControl fg; //帧抓取器
private BufferToImage bufferToImage = null;
private int width, height; //帧尺寸
private boolean hasBufferToImage()
{
 Buffer buf = fg.grabFrame(); //快照
 if (buf == null) {
  System.out.println("No grabbed frame");
  return false;
 }
 //存在一个缓冲区,但要检查其是否为空
 VideoFormat vf = (VideoFormat) buf.getFormat();
 if (vf == null) {
  System.out.println("No video format");
  return false;
 }
 System.out.println("Video format: " + vf);
 //提取图像的大小
 width = vf.getSize().width; 
 height = vf.getSize().height;

 // 用视频格式初始化bufferToImage
 bufferToImage = new BufferToImage(vf);
 return true;
}

  这种编码方法的一个小缺点是,第一个视频帧(使得方法hasBufferToImage()返回true)在对象初始化后被放弃。该帧没有被转化为BufferedImage 并为JMFMovieScreen所用。

   快照

  JMFSnapper类中最主要的公共方法是 getFrame (),它被周期性调用以取得正播放的动画的当前帧。

// 全局变量
private BufferedImage formatImg; // 帧图像

synchronized public BufferedImage getFrame()
{
 //以缓冲对象形式抓取当前帧 
 Buffer buf = fg.grabFrame();
 if (buf == null) {
  System.out.println("No grabbed buffer");
  return null;
 }

 //把缓冲区数据转换成图像
 Image im = bufferToImage.createImage(buf);
 if (im == null) {
  System.out.println("No grabbed image");
  return null;
 }

 //把该图像转换成一个缓冲图像
 Graphics g = formatImg.getGraphics();
 g.drawImage(im, 0, 0,FORMAT_SIZE, FORMAT_SIZE, null); 

 // Overlay current time on top of the image
 g.setColor(Color.RED);
 g.setFont(new Font("Helvetica",Font.BOLD,12));
 g.drawString(timeNow(), 5, 14);
 g.dispose();
 return formatImg;
} //结束getFrame()

  方法getFrame()和closeMovie()在JMFSnapper中都被同步处理。closeMovie()用于终止播放器,可以在任何时候调用。同步的目的是,为了确保提取一帧时,播放器不能被关闭。

  缓冲图像对象formatImg在JMFSnapper的构造器中被初始化:

formatImg = new BufferedImage( FORMAT_SIZE, FORMAT_SIZE, BufferedImage.TYPE_3BYTE_BGR);

  6.抓取帧的另外一些方法

   Sun 的JMF示例站点提供了从动画中抓取帧的另外两种方法。

   VideoRenderer接口

  DemoJMFJ3D 示例结合了Java 3D和JMF两种技术,它显示了怎样把一个视频包装到一个圆柱体上。

  例中的Java 3D部分其实与我前面讨论的技术相同-一个使用BufferedImage.TYPE_3BYTE_BGR格式的BufferedImage被传送给一个ImageComponent2D对象,然后该图像变成了圆柱体的纹理。该图像还可以使用BufferedImage.TYPE_4BYTE_ABGR格式,该格式在Solaris系统上使用以支持纹理的引用。

  例中的JMF部分与我采用的技术有很大不同。一个JMF的VideoRenderer接口的实现被附着到动画视频轨道的TrackControl对象上。一旦TrackControl对象被启动,对于在视频中出现的每个帧VideoRenderer接口的process()方法被自动调用。process()方法的输入参数是缓冲区对象(也就是被抓取的帧)。不是采用我描述的Buffer-to-BufferedImage转换技术,DemoJMFJ3D是通过在缓冲区的原始数据和BufferedImage的像素映射之间执行一种低级的字节数组复制技术来构造实现的BufferedImage。

   处理机Codec(多媒体数字信息编解码器)插件

  FrameAccess示例中利用了一些高级的JMF成分,主要围绕着一个处理机Codec插件。

  Processor类是Player的一个扩展版本,在处理媒体数据方面它提供了更多的能力。一个codec插件(其实是一个JMF接口Codec的执行)能够从轨道中读取帧,并对之进行任意的处理然后把它们写回到轨道中。特别地,每当在轨道中遇到一帧时,Codec的process()方法即被调用。该方法被提供给一个存有输入帧的缓冲对象和一个空缓冲对象用作输出。

  FrameAccess例中把一个Codec插件附加到动画的视频轨道上,并使用传递到函数process()的输入帧缓冲对象来生成一些基本的有关该视频的统计数据。你可以轻易地修改这个例子以把缓冲对象转化成一个缓冲图像,为此,你既可以使用我介绍的方法也可以利用DemoJMFJ3D中的字节数组技术。

  遗憾的是,要实现插件支持,Processor类不是必需的。结果,插件在JMF 1.0和基于2.0版本的一些JMF中并不工作。

  在使用Sun的JMF示例前先搜一下JMF-兴趣邮件列表是个不错的注意,因为其中许多的程序都存在JMF版本不同带来的一些问题。下文将介绍该动画程序的另外一个版本-其中使用了 Quicktime  for Java技术。


转载链接:http://dev.yesky.com/296/2041296_3.shtml

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值