网站首页

家园论坛

老版论坛

家园博客

业界新闻

技术文档

下载中心

速查中心

图片中心

硬件资讯
上一篇:Java 的JDBC 数据库连接池实现方法 下一篇:在Struts和Hibernate之间搭起桥梁
Java 3D的动画展示

来源:http://www.matrix.org.cn 作者:Andrew Davison 添加日期:2006-3-5 18:47:08 点击次数:
在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中确定.

此项技术的更多细节可以在j3d.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版本下不能工作.

在使用Sun公司的JMF实例前寻找jmf-interest mailing list是一个不错的注意,因为大多数的程序在各种版本的JMF下都存在问题.

 

在这一系列的第一部分中,我描述了在JMF(JAVA媒体帧工作器  下同)的帮助下,怎样将一部电影片断插入JAVA 3D场景中。这个执行过程使用Model-View-Controller设计模式。

。动画屏幕是由JMFMovieScreen类表示的视觉元素
。动画模型部分由JMFSnapper类控制
。java 3D行为类,TimeBehavior,是动画中引起帧周期性恢复的控制类

在这篇文章中,我将使用QTJ(QuickTime for Java  下同)再次解析动画成份。QTJ提供一个覆盖QuickTime API的依赖对象的java,使之可以展示,编辑和创建QuickTime动画;捕获视频 音频;展示2D和3D动画。QuickTime可用于Mac和Windows系统。关于QTJ的安装.文档和实例细节可在developer.apple.com/quicktime/qtjava查询。

由设计模式作出的推论,只在动画类JMFSnapper被QTSnapper取代时,QTJ取代JMF在应用中有微小的作用。

图1:两幅QTJ情况下的3D动画截屏,右边图片采取显示屏背面视角


从图1大致看出,QTJ-based 和JMF成像效果没有明显区别。

然而,通过更仔细得比较可以看出有两个变化:QTJ动画有轻微的被像素化,播放的更慢。像素化(像素化是对内部像素对于观看者易见的数字图像的显示。当一些用于普通的计算机显示的低分辨率图像被投射到一个大的显示器上,每一个像素都会变得单独可见,这种不常发生的现象就叫做像素化。)是由于原始动画从MPEG转换为QuickTime's MOV格式引起的,它可由更好的转换方法矫正。速度问题更基础:它与QTSnapper的潜在执行有关。

这篇文章的重点为?
。执行QTSnapper的两种主要方法的讨论.一种方法是将动画里的每一帧都提出来显示在屏幕上。另一种方法依靠当前的时间提出帧.第二种方法意味着可能将会遗漏部分帧,画面颤抖,但遗漏 可以使播放更快
。一些简单的FPS(frame-per-second)度量器的介绍.我将用它们这两种方法的相对速度,探测遗漏帧数

1.. 此山非彼山
与第一部分一样,编码将利用两个大型的API,在这里没有时间介绍利用的细节了。我将再次使用java 3D,但API媒体将由JMF转变为QTJ。

在我的O'Reilly book, Killer Game Programming in Java (KGPJ)中有大量关于java 3D的信息,还有图1的原码。

我将不会解释动画屏幕和动画更新行为,因为它们与第一部分相同。

在QTJ技术中我将会使用QTSnapper从动画中提取帧

2.应用的两种看法

image
图2:应用流程图
  
此图表与第一篇文章中的几乎相同

QuickTime动画由QTSnapper类加载,动画屏幕由QTMovieScreen创建.每40毫秒,TimeBehavior对象调用QTMovieScreen中的nextFrame()方法.然后调用QTSnapper中的getFrame()方法获取动画中的一个帧.依次循环.

JMFSnapper与QTSnapper之间有一个很重要的不同.JMFSnapper返回一个帧,这个帧是动画播放时的当前帧.而QTSnapper返回动画中的帧根据递增的索引.
例如.当getFrame()方法在JMFSnapper被反复调用时,也许会重新得到帧1,3,6,9等等,它是由方法何时被调用与动画播放速度决定的.当getFrame()方法在QTSnapper被调用,它将会返回帧1,2,3,4等等.


image
图3:UML类的应用图表,仅列出公共方法.


此图表除了动画屏幕名和动画类名(QTMovieScreen 和 QTSnapper)外,与第一篇文章的相同.事实上,只有Snapper类的内部执行被改变.

JMF Movie3D应用程序和QTJ-based版本之间的改动需要Snapper被重写.

 // global variable
private QTSnapper snapper;    // was JMFSnapper

// in the constructor, load the movie in fnm
snapper = new QTSnapper(fnm);



这两处改动是由于须将JMFMovieScreen重命名为QTMovieScreen.

这个例子中的所有代码,和文章的早期版本,可以在KGPJ website查询到.

3.一帧一帧的动画
理解QTSnapper的内在工作机理,可以帮助我们对QuickTime动画构造有一个大致的认识.每一幅动画可以理解为视频轨迹和音频轨迹在相同时间上的重叠.图4是这种思想的图示

image
图4:QuickTime动画的内部构造机理

每个轨迹控制着其自身数据,例如它包含的媒体类型和媒体本身.媒体容器(media container)有它自己的数据结构,包括它的持续时间和播放率(每秒播放抽样数).媒体是由一组抽样(或帧)组成,第一个抽样时间为0(与媒体时间有关).抽样是被变址的,第一个抽样在1位置(非0).
图5大致描述了QuickTime的轨迹和媒体结构

image
图5:QuickTime轨迹和媒体的内在构造机理

想要得到更多信息,请查询QuickTime指南的movie section

打开动画视频媒体

QTSnapper构造器打开动画:

// globals
private boolean isSessionOpen = false;
private OpenMovieFile movieFile;
private Movie movie;


// in the constructor,
// start a QuickTime session
QTSession.open();
isSessionOpen = true;

// open the movie
movieFile =
   OpenMovieFile.asRead( new QTFile(fnm) );
movie = Movie.fromFile(movieFile);



在QuickTime使用之前调用QTSession.open()方法将其初始化.在终止时相应的调用QTSession.close()方法.

轨迹定位和媒体访问

// more globals
private Track videoTrack;
private Media vidMedia;


// in the constructor,
// extract the video track from the movie
videoTrack =
    movie.getIndTrackType(1,
            StdQTConstants.videoMediaType,
            StdQTConstants.movieTrackMediaType);
if (videoTrack == null) {
  System.out.println("Sorry, not a video");
  System.exit(0);
}

// get the media used by the video track
vidMedia = videoTrack.getMedia();



一旦媒体打开,从中提取各种信息

// more globals
private MediaSample mediaSample;
private int numSamples;  // number of samples
private int sampIdx;     // current sample index
private int width;       // frame width
private int height;      // frame height


// in the constructor
numSamples = vidMedia.getSampleCount();

sampIdx = 1;   // get first sample in the track
mediaSample = vidMedia.getSample(0,
  vidMedia.sampleNumToMediaTime(sampIdx).time,1);

// store width and height of image in the sample
ImageDescription imgDesc =
      ImageDescription) mediaSample.description;
width = imgDesc.getWidth();
height = imgDesc.getHeight();



sampIdx作为计数器将在抽样中被重复调用(抽样于位置1开始).

动画图像的宽度和高度由第一个抽样获得,接下来所有的抽样都是同样的尺寸.

测算 FPS

由QTSnapper返回的 帧数/秒 将在稍后被用作类的不同使用方法的比较参数.构造器中必要的参数已被初始化.

// frame rate globals
private long startTime;
private long numFramesMade;

// initialize them in the constructor
startTime = System.currentTimeMillis();
numFramesMade = 0;



结束

将应用程序结束时,QTSnapper类中的stopMovie()方法将被调用.它报告FPS,关闭QuickTime.

// globals
private DecimalFormat frameDf =
    new DecimalFormat("0.#"); // 1 dp


synchronized public void stopMovie()
{
  if (isSessionOpen) {
    // report frame rate
    long duration =
        System.currentTimeMillis() - startTime;
    double frameRate =
       ((double) numFramesMade*1000.0)/duration;
    System.out.println("FPS: " +
                  frameDf.format(frameRate));

    QTSession.close();  // close down QuickTime
    isSessionOpen = false;
  }
}



由于stopMovie()和getFrame()是同步的,所以从动画中提取帧和QuickTime关闭在时间上是不可能同时进行.

缓存帧

getFrame()返回一次抽样样品,称作BufferedImage对象.被选择的帧利用索引指数存贮在sampIdx.

// globals
private BufferedImage img, formatImg;


synchronized public BufferedImage getFrame()
{
  if (!isSessionOpen)
    return null;
  if (sampIdx > numSamples)  
    // start back with the first sample
    sampIdx = 1;

  try {
    /* Get the sample starting at the
       specified index time */
    TimeInfo ti =
       vidMedia.sampleNumToMediaTime(sampIdx);
    mediaSample=vidMedia.getSample(0,ti.time,1);
    sampIdx++;

    writeToBufferedImage(mediaSample, img);

    // resize img, writing it to formatImg
    Graphics g = formatImg.getGraphics();
    g.drawImage(img, 0, 0,
         FORMAT_SIZE, FORMAT_SIZE, null);

    // Overlay current time on image
    g.setColor(Color.RED);
    g.setFont(
      new Font("Helvetica", Font.BOLD, 12));
    g.drawString(timeNow(), 5, 14);
    g.dispose();

    numFramesMade++;  // count frame
  }
  catch (Exception e) {
    System.out.println(e);
    formatImg = null;
  }

  return formatImg;
} // end of getFrame()



从QTJ的媒体类中调用getSample()方法可以容易的获得抽样.不幸的是,将抽样转化为BufferedImage仍然是个棘手的问题.

丰富的细节和注释,可以在编码中研究.从抽样种萃取一个“原始”图像,然后将其减压写成一个QuickTime版本的Graphics对象.Graphics对象中的无压缩数据被拷贝成为另一个“原始”图像,然后成为一个像素数组.最后,这个数组被写入空BufferedImage的DataBuffer.

程序能工作吗?能顺利工作吗?

是的,Movie3D显示动画,但是比较大的动画播放的比较慢.这是由于getFrame()方法在帧补给上的缓慢,它可以通过FPS数量进行量化.

对于图1的动画,在Windows 98系统FPS之大概在15-17帧/秒.然而,TimeBehavior对象要求每40毫秒更新,转化为帧数大概在25FPS.

getFrame()方法之所以慢是由于抽样转化为BufferedImage的时间消耗.由于当前调用的getFrame()方法在转化帧时停顿,更多的请求将被延迟直到当前转化完成.

我将考虑两种解决这一问题的方法:允许getFrame()方法在处理请求时遗漏帧,和在getFrame()方法中使用不同的转化方法.我将轮流考虑这两种方法,以帧遗漏开始.

4. 遗漏帧的动画

新的Snapper类,QTSnapper1,仍然返回一个帧当getFrame()方法被调用时.与QTSnapper的不同在于它提供的类相应于当前动画的执行时间.

例如,getFrame()也许重新获得帧1,2,5,8,14等等,依赖于方法的调用时间.因此,动画以一个很好的速度播放,但是由于帧的遗漏可能导致画面颤抖.

对比的看,QTSnapper将会返回所有的帧数(1,2,3,4等等),但是由于调用getFrame()方法的延迟可能会导致动画播放缓慢.然而,画面将不会出现颤抖,由于没有帧被遗漏.

QTSnapper1种的关键部分是对于动画的“当前执行时间”理念.我的方法是当getFrame()方法被调用估计QTSnapper的当前执行时间,将它转变为动画执行时间,然后作为样本给定值.

QTSnapper1有与QTSnapper相同的公共方法,所以它只需作出微小的改变就可用于QTMovieScreen.只有在动画播放时差别才变得明显,在以很好的速度播放时会发出喳喳声.详细的测量,以图1的“明显”帧率比较,31FPS对比QTSnapper的16FPS.

打开动画视频媒体

QTSnapper1访问动画视频的过程与QTSnapper相同.一旦视频可被利用,个别的媒体值将被储存并稍后被getFrame()方法调用:

// globals
private Media vidMedia;
private int numSamples;
private int timeScale;   // media's time scale
private int duration;    // duration of the media


// in the constructor,
// get the media used by the video track
vidMedia = videoTrack.getMedia();

// store media details for later
numSamples = vidMedia.getSampleCount();
timeScale = vidMedia.getTimeScale();
duration = vidMedia.getDuration();



获取一帧

getFrame()方法中的新要素是它怎样计算被用于访问详悉抽样的给定值.方法的其他部分,writeToBufferedImage()的调用和当前图像的编码与QTSnapper相同.

 // globals
private MediaSample mediaSample;
private BufferedImage img, formatImg;

private int prevSampNum;
private int sampNum = 0;
private int numCycles = 0;
private int numSkips = 0;


// inside getFrame(),
// get the time in secs since start of QTSnapper1
double currTime =
  ((double)(System.currentTimeMillis() -
            startTime))/1000.0;

// use the video's time scale
int videoCurrTime =
     ((int)(currTime*timeScale)) % duration;

try {
  // backup the previous sample number
  prevSampNum = sampNum;

  // calculate the new sample number
  sampNum = vidMedia.timeToSampleNum(
                     videoCurrTime).sampleNum;

  // if no sample change, then don't generate
  // a new image
  if (sampNum == prevSampNum)
    return formatImg;

  if (sampNum < prevSampNum)    
    numCycles++;   // movie has just started over

  // record the number of frames skipped
  int skipSize = sampNum - (prevSampNum+1);
  if (skipSize > 0)  // skipped frame(s)
    numSkips += skipSize;

  // get a single sample starting at the
  // sample number's time
  TimeInfo ti =
      vidMedia.sampleNumToMediaTime(sampNum);
  mediaSample = vidMedia.getSample(0,ti.time,1);



getFrame()在很短的时间内计算当前时间,从QTSnapper1开始时测量:

double currTime = 
  ((double)(System.currentTimeMillis() -
            startTime))/1000.0;



每一个QuickTime的媒体片断都有它自己的时间刻度,ts,例如一个个体的时间是1/ts 秒.恒定的时间刻度必须由currTime方法增加以获得当前动画时间.

int videoCurrTime = 
    ((int)(currTime*timeScale)) % duration;



以媒体持续时间为模校正刻度时间,允许动画在当前时间已超过动画结束时间重复.
调用Media's timeToSampleNum()方法将抽样序列数以刻度时间显示:

sampNum = vidMedia.timeToSampleNum(
                     videoCurrTime).sampleNum;



上次抽样序列号存储在prevSampNum,以便允许实现大量的检测和计算.

如果新的抽样序列号与上次取样的序列号相同,就不需要检查将抽样转化为BufferedImage的过程;getFrame()可以返回现有的formatImg接口.
如果新的抽样序列号小于上次取样的序列号,这就意味着动画开始循环,动画起始帧将被显示.这就是被注册的numCycles增加.
如果新的抽样序列号大于上次取样的序列号+1,意味着被遗漏的帧序列被记录上.

结束

stopMovie()打印出FPS,关闭QuickTime进程,与QTSnapper类中的stopMovie()方法相同.同时它也报告附加信息:

long totalFrames = 
   (numCycles * numSamples) + sampNum;

// report percentage of skipped frames
double skipPerCent =
    (double)(numSkips * 100) / totalFrames;
System.out.println("Percentage frames skipped: "+
              frameDf.format(skipPerCent) + "%");

// 'apparent' FPS (AFPS)
double appFrameRate =
      ((double) totalFrames * 1000.0) / duration;
System.out.println("AFPS: " +
      frameDf.format(appFrameRate));  // 1 dp



appFrameRate方法描述“显性”帧频,就是从QTSnapper1开始使用时的抽样总量.感觉上的“显性” 是因为不是所有的抽样都有被显示出来的必要.

程序能工作吗?能顺利工作吗?

QTSnapper被QTSnapper1取代后,缓慢的动画(图1所示)将会播放的更快.结束时间时,据报告的明显帧频是31FPS,实际上的帧频大概在16FPS,遗漏频大概占总数的50%.惊奇的是,大量遗漏频的并没有显示在屏幕上.

对与另外一些比较小的动画,速度的增加几乎是察觉不到的;遗漏频率大概在5%-10%.

不幸的是,仍然还有两个问题:帧遗漏产生的杂乱像素和对遗漏帧的数量的控制.

杂乱像素

无论何时,只要QTSnapper1遗漏一个动画帧,下一个帧将会包含一些杂乱像素.图6是效果显示图.从一段早期视频截得的错误像素使用值.

image
图6 杂乱图像截屏

问题是我所有的视频事例都是使用temporal compression,它是一种利用连续视频帧之间类似处的一种压缩方法.假如两个连续视频帧有相同的背景,就不用再次存储背景.只有两个帧之间的不同才会被存储.

这项技术,被用在几乎所有的流行视频格式,意味着从一个帧中抽取图像依赖这个帧和此前的几个帧.

暂时解压由在writeToBufferedImage()方法中的QuickTime DSequence对象处理.DSequence构造器详细说明了QuickTime在解压过程中应该使用的一种屏幕外图像缓冲器.

帧图像被写入缓冲器,在那里与早期的帧数据结合.结合后图像被传给转化的下一过程.

QTSnapper1顺次解压时工作的很好,没有遗漏帧,但是如果产生遗漏会导致错误.例如,当QTSnapper1遗漏帧5和6,然后解压帧7时会发生什莫?帧被写入QuickTime图像缓冲器,与此前帧的数据结合.然而,帧5和6的数据丢失,所以结合后的图像会有错误.

简单的讲,图像中的杂乱像素是由动画的暂时压缩引起的.一个可选择的办法是使用空间压缩技术,既独立的压缩每个帧.这就意味着解压时帧里所有的信息都会从帧本身被释放出来,不需要检查早期的帧.

QuickTime MOV支持名为Motion-JPEG(M-JPEG)的空间压缩方案.我使用QuickTime 6 Pro 中的工具将图1以M-JPEG A编码形式存储为MOV文件.当这个动画用Movie3D播放时,没有发生画面颤抖.

限制帧遗漏

QTSnapper1的另一个问题时getFrame()没有对可能遗漏帧的数量进行限制.在我的测试中,遗漏帧的数量被限制在3个以下.然而,如果getFrame()用于一个很大抽样的转化,那末它的缓慢增加将会导致更多帧的遗漏.动画质量将会发生明显的恶化.

5. 试着使图像更快

在QTSnapper和QTSnapper1中使用的sample-to-BufferedImage转换方法(writeToBufferedImage())是由Chris W. Johnson在实例中获得的.有没有一种更快的从抽样中随取图像的方法呢?

QTJ方面的权威书籍:QuickTime for Java: A Developer's Notebook,作者:Chris Adamson, O'Reilly,2005.1月出版.卷五,covering QuickDraw,中的ConvertToJavaImageBetter.java实例,展示了怎样获得一个PICT图像的抽样并将其转化为Java图像对象.这个例子也可在quicktime-java mailing list中找到.

我使用Adamson码作为另一个Snapper类的编码基础,称之为QTSnapper2.它可以无遗漏的返回帧,与QTSnapper的方法一样,但是使用PICT-to-Image转化.

在一部小动画中,QTSnapper2与QTSnapper的表现没有区别,但对于比较大的动画例如例1,它的平局帧频大概为9FPS,对比与QTSnapper的16FPS.换句话说,PICT-based转化慢于Johnson技术

 
设为首页 | 加入收藏 | 业务办理 | 友情链接 | 论坛版面 | 浙ICP备07502118号 |