版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
英文原文地址:
http://www.onjava.com/pub/a/onjava/2005/06/01/kgpjava_part2.html
中文地址:
http://www.matrix.org.cn/resource/article/43/43676_JAVA_3D.html
关键词: Java 3D QuickTime
在这一系列的第一部分中,我描述了在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 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是这种思想的图示
图4:QuickTime动画的内部构造机理
每个轨迹控制着其自身数据,例如它包含的媒体类型和媒体本身.媒体容器(media container)有它自己的数据结构,包括它的持续时间和播放率(每秒播放抽样数).媒体是由一组抽样(或帧)组成,第一个抽样时间为0(与媒体时间有关).抽样是被变址的,第一个抽样在1位置(非0).
图5大致描述了QuickTime的轨迹和媒体结构
图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是效果显示图.从一段早期视频截得的错误像素使用值.
图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技术
--------------------------------------------------------------------------------------
Andrew Davison 是个教育者,研究员,及作家。以前曾在墨尔本大学的计算机科学部门工作,现居住在泰国,并在Prince of Songkla大学任教。
Java, java, J2SE, j2se, J2EE, j2ee, J2ME, j2me, ejb, ejb3, JBOSS, jboss, spring, hibernate, jdo, struts, webwork, ajax, AJAX, mysql, MySQL, Oracle, Weblogic, Websphere, scjp, scjd
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声
标签: