您现在的位置是:首页-> 米鼠技术 ->Java 3D的动画展示(Part1-使用JMF)

Java 3D的动画展示(Part1-使用JMF)

 

在Java 3D场景中插入动画片段使3D内容更加有趣充实.一段动画可以在更令人信服的背景下展示,例如飘动的云,繁忙的城市街道,或者是从窗向外看的效果.动画可以在屏幕效果和游戏效果之间任意转换.

这篇文章被分为两个部分,描写我怎样实现一个Java 3D动画屏幕效果.在这个部分,我将说明我怎样利用JMF(Java Media Framework),特别是在JMF Performance Pack for Windows v.2.1.1e情况下.我的另外两个工具是J2SE 5.0和Java 3D 1.3.2.我将讨论另外的使用Quicktime for Java的动画屏幕版本.

图1是应用JMF Movie3D在不同时间截取的两幅截屏,右边截屏是从屏幕后看的效果.

image

图1. Movie3D应用截屏

此应用程序中重点:
。JMF和Java 3D的集成.屏幕以任意尺寸成倍增加在一个应用程序.由于屏幕是Java3D的Shape3D类的一个子类,因此它可以很容易的统一到各种Java 3D场景中.
。程序执行使用Model-View-Controller设计模式.屏幕是一个视频元素,由JMFMovieScreen类描述.动画是一个由JMFSnapper类控制的模型部分.一个Java 3D Behavior类,TimeBehavior,控制动画定时定期更新.所有JMF编码都存放在JMFSnapper类,可以很方便的测试各种变化.这篇文章的第二部分JMFSnapper由QuickTime for Java版本中的QTSnapper取代.
。Java 3D 的使用将会使动画的播放速度毫无困难的上升到25帧/秒.
。使用JMF出现问题的讨论.问题是我首选解决方案将不会工作-JMF有可能变为一个巨大的API,但在其内部仍有一些程序没有及时运行.

1. 我坐在山上

事实上,我正坐在一个冰冷的办公室.我真正的意思是说这篇文章建立在大量Java 3D和JMF背景知识之上.

我将不会细致地解释Java 3D的基础知识,因为它们都可以在O'Reilly文章Killer Game Programming in Java(以下简称KGPJ)中找到.例如,图1场景效果图是其第15章中的轻微改良Checkers3D的版本实例.我再生了这些编码以生成底版,蓝天和灯光.

假如你不想买这本书,没关系,所有篇章的初稿和所有编码都可以在此书的站点查阅.

在此文章中,我将会解释我用来从动画中抽取帧的JMF技术.我将不会讨论流媒体或者编码转换.

2. 应用简述

动画由JMFSnapper类加载播放,并且不断的循环播放直到被停止.

JMFMovieScreen生成动画屏幕,并在底版上控制Java 3D四边形.

图2显示这些类的应用(该场景图说明场景中Java 3D节点怎样连接在一起)

image
图2:Movie3D场景图

图2种的很多细节可以被忽略,此图KGPJ15章中的得Checkers3D实例有很多相似之处. 只有特殊动画的节点是新的.

由于节点关系,JMFMovieScreen和TimeBehavior对象以三角形表示.JMFSnapper对象不属于这张图,但由JMFMovieScreen调用.

每40毫秒,TimeBehavior对象调用JMFMovieScreen类中的nextFrame()方法.接下来调用JMFSnapper中的getFrame()方法获取动画中当前播放的帧,由JMFMovieScreen控制成像.

TimeBehavior是Java 3D的Behavior类的子类,它是Java 3D应用的计时器.它与KGPJ18章中的3D sprites实例中的TimeBehavior类十分相似.

观察应用过程的另一种方式就是察看它的UML类图表,图3给出。类中的公共方法被显示.


image
图3:Movie3D类图表

Movie3D的子类JFrame,WrapMovie3D是JPanel的一个子类.图2展示了WrapMovie3D如何构建场景图,和将其译成应用的JPanel.他使用CheckerFloor 和ColouredTiles类构建底版.

JMFMovieScreen创建动画屏幕,将其加入场景中,通过创建一个JMFSnapper对象开始动画.TimeBehavior每40毫秒调用JMFMovieScreen中的nextFrame()方法. nextFrame()调用JMFSnapper中的getFrame()得到当前帧.

这个例子中的所有编码,此文章的早期版本可以在KGPJ网点查询.

3. 准备动画

动画,它的屏幕和更新屏幕的TimeBehavior对象,都是由WrapMovie3D中的addMovieScreen()方法创立.

// globals
private BranchGroup sceneBG;
private JMFMovieScreen ms;  // the movie screen
private TimeBehavior timer; // to update screen


private void addMovieScreen(String fnm)
{
  // put the movie in fnm onto a movie screen
  ms = new JMFMovieScreen(
          new Point3f(1.5f, 0, -1), 2.0f, fnm);
  sceneBG.addChild(ms);

  // set up the timer for animating the movie
  timer = new TimeBehavior(40, ms);  
    // update movie every 40ms (== 25 frames/sec)
  timer.setSchedulingBounds(bounds);
  sceneBG.addChild(timer);
}


两个Java 3D addChild()方法调用JMFMovieScreen和TimeBehavior节点间的连接.setSchedulingBounds()激活TimeBehavior节点.

4. 创建动画屏幕

JMFMovieScreen是Java 3D的Shape3D类的一个子类.所以必须仔细说明它的外形的几何形状和外观.

几何形状是指动画图像的四个边尺寸上成比例,它的最大尺寸(高 宽)必须向构造器仔细说明.这个四方形是垂直的,朝向Z轴正方向,可以在底版的任何位置被定位.

四方形外观是双面,允许从前或后观看动画.结构是用双线性插值,可以降低动画图像的像素化.

大多数的功能是从KGPJ24章中的FPS(first-person shooter)实例中的ImageCsSeries类拷贝而来. ImageCsSeries在一个区域中显示一系列的GIF图片. 为了简短起见,我仅描述了JMFMovieScreen与ImageCsSeries的不同特征.

高效显示图像

动画中的一个帧被转换结构扩大四倍;分为两个步骤:第一步 提供的BufferedImage传给Java 3D的ImageComponent2D对象,然后传给Java 3D Texture2D.

区域的图像更新非常快:每秒更新25帧,要求结构更新25次.因此结构有效率的更新非常的重要.这种高效率在利用BufferedImage和ImageComponent2D对象进行格式化的情况下是可能的.

JMFMovieScreen使用的ImageComponent2D对象以以下方式声明:
ImageComponent2D ic = new ImageComponent2D(
        ImageComponent2D.FORMAT_RGB,
        FORMAT_SIZE, FORMAT_SIZE, true, true);


构造器剩余两个需要说明的讨论点是它使用"by reference"和"Y-up"模式.这些模式降低了存储结构图像的内存大小,因为Java 3D避免将图像从应用空间拷贝到图形内存.

在Windows OS环境下,使用OpenGL作为Java 3D优先图像引擎,ImageComponent2D格式应是ImageComponent2D.FORMAT_RGB,BufferedImage格式应是BufferedImage.TYPE_3BYTE_BGR.BufferedImage格式在JMFSnapper中确定.

此项技术的更多细节可以在jsd.org中查询.

将纹理加进区域

通常在一个区域中确定一幅图像的方法是将图像的坐下角连接到区域的左下角,然后逆时针连接剩余的几个角.图4说明这种方法.

image
图4.图像与区域之间的标准连接

图像坐标区间在X Y轴的0 1之间,Y轴正方向.例如,图像左下点坐标为(0,0),右上点为(1,1).

当"Y-up"模式使用,图像坐标Y轴翻转,负方向.意味着(0,0)代表图像左上点,(1,1)指向右下.

当"Y-up"模式建立,图像坐标必须分配给区域中不同点以便获得图像的相同定位.图5显示了最新配置.


image
图5."Y-up"模式使用时,图像与区域之间的连接

连接区域点与图像定位的JMFMovieScreen编码是

TexCoord2f q = new TexCoord2f();

q.set(0.0f, 0.0f);    
plane.setTextureCoordinate(0, 3, q);  
  // (0,0) tex coord --> top left quad point (p3)

q.set(1.0f, 0.0f);  
plane.setTextureCoordinate(0, 2, q);  
       // (1,0) --> top right (p2)

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

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


PLANE对象指代区域.

更新图像

以上所讲,TimeBehavior是被设置用来被40毫秒调用JMFMovieScreen的nextFrame()方法.nextFrame()调用JMFSnapper对象中的getFrame()方法获得被看作BufferedImage对象的当前动画帧.指派给一个mageComponent2D对象,然后传给区域图像.nextFrame()是:

// globals
private Texture2D texture;   // used by the quad
private ImageComponent2D ic;

private JMFSnapper snapper;    
                    // to take snaps of the movie
private boolean isStopped = false;  
                    // is the movie stopped?


public void nextFrame()
{ if (isStopped)   // movie has been stopped
    return;

  BufferedImage im = snapper.getFrame();  
                          // get current frame
  if (im != null) {
    ic.set(im); //assign frame to ImageComponent2D
    texture.setImage(0,ic);  
                   // make it the shape's texture
  }
  else
    System.out.println("Null BufferedImage");
}


snapper,JMFSnapper对象,由JMFMovieScreen的构造器创建:
// load and play the movie
snapper = new JMFSnapper(movieFnm);

JMFSnapper的简单接口掩盖了播放动画和从动画中抽取帧的JMF编码的复杂.着这个系列的第二部分,JMFSnapper由使用QuickTime for Java的版本取代,对JMFMovieScreen只需要作出微小改动.

5. 管理动画

JMF提供了一种访问动画帧的高水平方法.以下的编码片断阐明了主要元素.我将省去错误检验和异常处理.

// create a movie player, in a 'realized' state
URL url = new URL("file:" + movieFnm);
Player p = Manager.createRealizedPlayer(url);

// create a frame positioner
FramePositioningControl fpc =
   (FramePositioningControl)
    p.getControl("javax.media.control.
                   FramePositioningControl");

// create a frame grabber
FrameGrabbingControl fg =
    (FrameGrabbingControl)
    p.getControl("javax.media.control.
                  FrameGrabbingControl");

// request that the player changes to a 'prefetched' state
p.prefetch();

// wait until the player is in that state...

// move to a particular frame, e.g. frame 100
fpc.seek(100);

// take a snap of the current frame
Buffer buf = fg.grabFrame();    

// get its video format details
VideoFormat vf = (VideoFormat) buf.getFormat();

// initialize BufferToImage with video format
BufferToImage bufferToImage =
                      new BufferToImage(vf);

// convert the buffer to an image
Image im = bufferToImage.createImage(buf);

// specify the format of desired BufferedImage
BufferedImage formatImg =
          new BufferedImage(
                  FORMAT_SIZE, FORMAT_SIZE,
                  BufferedImage.TYPE_3BYTE_BGR);

// convert the image to a BufferedImage
Graphics g = formatImg.getGraphics();
g.drawImage(im, 0, 0,
                FORMAT_SIZE, FORMAT_SIZE, null);
g.dispose();


一个媒体播放器从制作到完成需要六个步骤.播放器在构思过程中要清楚怎样运行数据,可以在要求时提供视觉上的组成和控制器.我要求两个控制器:FramePositioningControl和FrameGrabbingControl. FramePositioningControl提供了seek()和skip()方法,可以在动画中查检特殊帧.FrameGrabbingControl提供grabFrame()方法,可以在动画的视频轨迹中抽取当前帧.

为了使这些控制器工作,播放器必须由构思过程进入构建过程.播放器开始准备播放媒体,媒体数据被加载.

prefetch()的调用是异步的,意味着编码必须等待直到转换过程结束.标准JMF译码解决方案使用waitForState()方法,此方法将使执行过程暂停直到一个状态转换事件将其唤醒.

如果想寻找一个帧,可以使用seek()方法在轨迹中将这个帧定位,然后利用grabFrame()方法提取.提取的Buffer对象转变为JMFMovieScreen要求的 BufferedImage对象必须经过几个转变过程.注意:BufferedImage对象是TYPE_3BYTE_BGR格式.

Sun公司的JMF website有很多很有用的小例子,其中之一,Seek.java展示了如何使用FramePositioningControl方法做成动画.

三步审查

不幸的是,编码大体上是错误的,至少在JMF Performance Pack for Windows v.2.1.1e.我审查了几个编码重写以获得可工作的JMFSnapper版本.

1. 两个控制器,FramePositioningControl和FrameGrabbingControl,在JMF下的缺省播放器模块中是很难获得的."本地模块"播放器被要求:

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

这个播放器组成庞大,对Swing GUIs例如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(),检测返回的Buffer对象是否含有视频信息数据.它使用这些数据初始化一个BufferToImage对象,此对象被用来将每一个抓取帧转化为图像.

// globals
private FrameGrabbingControl fg; // frame grabber
private BufferToImage bufferToImage = null;
private int width, height;    // frame dimensions


private boolean hasBufferToImage()
{
  Buffer buf = fg.grabFrame();   // take a snap
  if (buf == null) {
    System.out.println("No grabbed frame");
    return false;
  }
    
  // there is a buffer, but check if it's empty
  VideoFormat vf = (VideoFormat) buf.getFormat();
  if (vf == null) {
    System.out.println("No video format");
    return false;
  }

  System.out.println("Video format: " + vf);
  // extract the image's dimensions
  width = vf.getSize().width;    
  height = vf.getSize().height;

  // initialize bufferToImage with video format
  bufferToImage = new BufferToImage(vf);
  return true;
}


这个编码方法的一个微小缺点是第一个视频帧(引起hasBufferToImage()返回true)在BufferToImage对象初始化后被丢弃.作为BufferedImage to JMFMovieScreen这个帧不能被使用.

抓图

JMFSnapper中的最重要的公共方法是getFrame(),此方法被周期性的调用以获得播放动画中的当前帧.

// global
private BufferedImage formatImg;  // frame image

synchronized public BufferedImage getFrame()
{
  // grab the current frame as a buffer object
  Buffer buf = fg.grabFrame();
  if (buf == null) {
    System.out.println("No grabbed buffer");
    return null;
  }
    
  // convert buffer to image
  Image im = bufferToImage.createImage(buf);
  if (im == null) {
    System.out.println("No grabbed image");
    return null;
  }

  // convert the image to a BufferedImage
  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;
}  // end of getFrame()


getFrame()和closeMovie()方法在JMFSnapper中是同步的.closeMovie()中止播放器,在任何时间都可能被调用.同步关键字确保当帧从动画中被抽取时播放器不会被关闭.

formatImg BufferedImage对象在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对象.胜于我曾描述的Buffer-to-BufferedImage转换步骤,DemoJMFJ3D以低水准构建BufferedImage,BufferedImage的像素化图像和Buffer原始数据时间的比特数组拷贝.

3D聊天室实例中的DemoJMFJ3D编码尽在Java Media APIs: Cross-Platform Imaging, Media and Visualization,A. Terrazas, J. Ostuni和M. Barlow
所著.这本书是对于JMF是本很好的入门书籍,其中也有很多关于Java 3D的有趣篇章.

Processor Codec插件

FrameAccess实例使用很多更先进的JMF元素,以Processor codec插件为中心.

Processor类是Player的一个延伸版本,它对于媒体数据处理有更强的能力.一个多媒体数字信号编解码器插件能够从一段轨迹中读取帧,以任意方式处理它们,然后将他们写回轨迹.Codec的process()方法中,提供其一个含有输入帧的Buffer对象,空Buffer对象输出.

FrameAccess附加一个Codec插件以访问动画的视频轨迹,使用输入帧Buffer对象传递给process()方法以产生一些关于视频的基本统计表.这个实例易于改进以便将Buffer对象转化为BufferedImage,任意使用我的方法或者DemoJMFJ3D的比特数组技术.

不幸的是,Processor类不能用来支持插件;结果,插件在JMF 1.0或2.0-based版本下不能工作.


热点文章
最新项目
相关文章 最新文章