在这一系列的第一部分中,我描述了在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.应用的两种看法 图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等等.
图3:UML类的应用图表,仅列出公共方法. 此图表除了动画屏幕名和动画类名(QTMovieScreen 和 QTSnapper)外,与第一篇文章的相同.事实上,只有Snapper类的内部执行被改变.
JMF Movie3D应用程序和QTJ-based版本之间的改动需要Snapper被重写.
// global variableprivate QTSnapper snapper; // was JMFSnapper// in the constructor, load the movie in fnmsnapper = new QTSnapper(fnm);
这两处改动是由于须将JMFMovieScreen重命名为QTMovieScreen.
这个例子中的所有代码,和文章的早期版本,可以在KGPJ website查询到.
3.一帧一帧的动画 理解QTSnapper的内在工作机理,可以帮助我们对QuickTime动画构造有一个大致的认识.每一幅动画可以理解为视频轨迹和音频轨迹在相同时间上的重叠.图4是这种思想的图示
图4:QuickTime动画的内部构造机理 每个轨迹控制着其自身数据,例如它包含的媒体类型和媒体本身.媒体容器(media container)有它自己的数据结构,包括它的持续时间和播放率(每秒播放抽样数).媒体是由一组抽样(或帧)组成,第一个抽样时间为0(与媒体时间有关).抽样是被变址的,第一个抽样在1位置(非0).
图5大致描述了QuickTime的轨迹和媒体结构
图5:QuickTime轨迹和媒体的内在构造机理 想要得到更多信息,请查询QuickTime指南的movie section
打开动画视频媒体
QTSnapper构造器打开动画:
// globalsprivate boolean isSessionOpen = false;private OpenMovieFile movieFile;private Movie movie;// in the constructor,// start a QuickTime sessionQTSession.open();isSessionOpen = true;// open the moviemovieFile = OpenMovieFile.asRead( new QTFile(fnm) );movie = Movie.fromFile(movieFile);
在QuickTime使用之前调用QTSession.open()方法将其初始化.在终止时相应的调用QTSession.close()方法.
轨迹定位和媒体访问
// more globalsprivate Track videoTrack;private Media vidMedia;// in the constructor, // extract the video track from the movievideoTrack = 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 trackvidMedia = videoTrack.getMedia();
一旦媒体打开,从中提取各种信息
// more globalsprivate MediaSample mediaSample;private int numSamples; // number of samplesprivate int sampIdx; // current sample indexprivate int width; // frame widthprivate int height; // frame height// in the constructornumSamples = vidMedia.getSampleCount();sampIdx = 1; // get first sample in the trackmediaSample = vidMedia.getSample(0, vidMedia.sampleNumToMediaTime(sampIdx).time,1);// store width and height of image in the sampleImageDescription imgDesc = ImageDescription) mediaSample.description;width = imgDesc.getWidth();height = imgDesc.getHeight();
sampIdx作为计数器将在抽样中被重复调用(抽样于位置1开始).
动画图像的宽度和高度由第一个抽样获得,接下来所有的抽样都是同样的尺寸.
测算 FPS
由QTSnapper返回的 帧数/秒 将在稍后被用作类的不同使用方法的比较参数.构造器中必要的参数已被初始化.
// frame rate globalsprivate long startTime;private long numFramesMade;// initialize them in the constructorstartTime = System.currentTimeMillis(); numFramesMade = 0;
结束
将应用程序结束时,QTSnapper类中的stopMovie()方法将被调用.它报告FPS,关闭QuickTime.
// globalsprivate DecimalFormat frameDf = new DecimalFormat("0.#"); // 1 dpsynchronized 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.
// globalsprivate 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 t