利用Java3D技術播放動畫之QTJ技術
· 一個Java 3D Behavior類TimeBehavior是控制器,由它完成從動畫中的周期性幀檢索,然后在屏幕上畫出。
在這篇文章中,我將revisit該動畫組件,用 QuickTime for Java (QTJ)來重新實現之。QTJ在QuickTime API之上提供了一個面向對象的Java 層,使之有可能實現播放,編輯和創建QuickTime 動畫;捕獲音頻與視頻;執行2D和3D動畫。QuickTime在Mac 和Windows上平臺都可以使用。關于QTJ的安裝,文檔和舉例的細節信息請參見developer.apple.com/quicktime/qtjava。
由QTJ 來取代JMF 設計模式的結果對應用程序影響很小-只有動畫類JMFSnapper分離出來,由QuickTime for Java版本的QTSnapper所代替。
圖 1展示了QTJ 版本的Movie3D應用程序中的兩幅屏幕快照,其中右邊的那幅是屏幕從后面看上去的視圖。
![]() 圖 1.QTJ Movie3D應用程序中的兩幅視圖 |
如果快點回顧一下第一部分中的圖1,你會發現基于QTJ的應用程序和JMF 版本的實現沒有太明顯的區別。
但是,如果細致比較一下這兩處執行程序會發現有兩個變化:QTJ版本的動畫像素化pixelation 更明顯,且播放速度更慢些。像素化的出現是由于原始的動畫被從MPEG 格式翻譯成QuickTime的MOV格式所致,可以借助于一個更好的轉換工具來實現修補。速度問題更為根本性:它與QTSnapper的基本實現有關。
該文中主要涉及到:
·討論QTSnapper實現中的兩種主要方法。一種方法是把動畫的每一幀都著色到屏幕上去,另一種方法是基于當前時間選擇一幀。 后一種方法意味著,可以跳過一些幀,使得動畫顫動一點,但是卻使動畫播放速度加快。
·介紹幾種簡單的幀/秒(FPS)計算方法,我將用之來判斷不同實現方式的相對速度的不同,并用來檢測跳過的幀。
我不會再細致地介紹動畫屏幕和動畫更新行為,因為這些與第一部分中是一致的。
下面我將詳細介紹的是我應用在QTSnapper中的用于從動畫中提取幀的QTJ技術。
1. 程序實現中的兩幅輪廓圖
下面的圖2描述了該應用程序中的場景圖。
![]() 圖 2. Movie3D場景圖 |
該圖幾乎和第一部分中的一模一樣。
QuickTime動畫由QTSnapper類負責裝載。動畫屏幕由QTMovieScreen創建,它管理一個放置在跳棋盤地板上的Java 3D四邊形。每隔40毫秒,TimeBehavior 對象調用QTMovieScreen中的nextFrame()方法一次,該方法調用中QTSnapper 的getFrame()方法來取得動畫中的一幀,該幀最后被放置到由QTMovieScreen管理的四邊形上。
JMFSnapper和QTSnapper之間有一個重要的不同。JMFSnapper返回當前正播放動畫的當前幀,而QTSnapper依賴于一個遞增的索引值返回動畫中的當前幀。
例如,當getFrame()在JMFSnapper中被反復調用時,它可以檢索幀1,3,6,9,11,等等,具體依賴于調用的方法和動畫的播放速度。當在QTSnapper中調用getFrame()時,它將返回幀1,2,3,4,等等。
下面的圖3描述了該應用程序的UML類圖,其中僅顯示了類的公共方法。
![]() 圖 3. Movie3D類圖 |
除了動畫屏幕QTMovieScreen類和動畫QTSnapper類名字的區別外,這里的應用程序類繼承圖與第一部分中的沒有區別。事實上,只有Snapper類的內部實現變更了一些。
把應用程序從JMF Movie3D版本遷移到QTJ版本要求代替Snapper類。另外,還需要把動畫屏幕類中的兩行代碼改變一下,其中聲明了Snapper 類并被實例化:
| //全局變量定義 private QTSnapper snapper; //以前是JMFSnapper //在構造器中以fnm方式裝入動畫 snapper = new QTSnapper(fnm); |
這兩處變化是把JMFMovieScreen 改名為QTMovieScreen的唯一原因。
所有該示例的代碼以及本文的一個早期版本,都能在KGPJ website處找到。
2. 一部幀到幀Frame-By-Frame 的動畫
理解了QTSnapper的內部工作機理,有助于對QuickTime動畫的結構有一個基本了解。每個動畫可能由多個音頻和視頻軌道合成,而在時間上相重疊。下圖4展示了這種基本思想。
![]() 圖 4.一個QuickTime動畫的內部結構 |
每個軌道管理自己的數據,如它包含的媒體的類型和媒體本身。該媒體容器具有它自己的數據結構,包括持續時間和播放速率(也就是,每秒要顯示多少個樣本)。這里的媒體實際上是一系列的樣本(或者幀),第一個樣本從時間0開始(with respect to media time)。樣本被索引化了,如第一個樣本在位置1處(而不是0)。
簡化后的QuickTime軌道和媒體結構如下圖5所示。
![]() 圖 5. 一個QuickTime軌道和媒體的內部結構 |
要想更全面的了解,請參考QuickTime教程的動畫部分。
存取動畫的視頻媒體
QTSnapper1遵循了與QTSnapper相同的步驟來實現動畫視頻的存取。一旦視頻可用,好幾個媒體值作為全局變量存儲,以備后面getFrame()方法所用:
| // 全局變量 private Media vidMedia; private int numSamples; private int timeScale; //媒體的時間刻度 private int duration; //媒體的持續時間 //在構造器中,取得由視頻軌道使用的媒體 vidMedia = videoTrack.getMedia(); //存儲視頻細節以備后用 numSamples = vidMedia.getSampleCount(); timeScale = vidMedia.getTimeScale(); duration = vidMedia.getDuration(); |
獲取幀
getFrame()方法中的新的編碼部分在于如何計算用來存取一個特別樣本的索引值;而該方法中的其余部分,對于writeToBufferedImage()的調用,以及把當前時間寫到圖像上等等,都和QTSnapper中一樣。
| // 全局變量 private MediaSample mediaSample; private BufferedImage img, formatImg; private int prevSampNum; private int sampNum = 0; private int numCycles = 0; private int numSkips = 0; // 在getFrame()函數中, //以秒為單位取得從QTSnapper1開始以來的時間 double currTime =((double)(System.currentTimeMillis()-startTime))/1000.0; // 使用視頻的時間刻度 int videoCurrTime=((int)(currTime*timeScale)) % duration; try { // 備份前一個樣本號 prevSampNum = sampNum; //計算新的樣本號 sampNum = vidMedia.timeToSampleNum(videoCurrTime).sampleNum; //如果沒有樣本變化,則不生成一幅新的圖像 if (sampNum == prevSampNum) return formatImg; if (sampNum < prevSampNum) numCycles++; //動畫剛剛開始播放 // 記下跳過的幀號 int skipSize = sampNum - (prevSampNum+1); if (skipSize > 0) //跳過的幀 numSkips += skipSize; //取得從樣本號的時間開始的一個樣本 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的timeToSampleNum()方法映射到一個樣本索引號:
| sampNum=vidMedia.timeToSampleNum(videoCurrTime).sampleNum; |
前面使用的樣本號存儲在變量prevSampNum中,這就允許進行一些測試和計算。
如果"新的"樣本號與前一個相同,那么就沒有必要費勁地把這個樣本轉化成一個BufferedImage;getFrame()方法能夠返回存在的formatImg參照。
如果新的樣本號小于前一個相同,這意味著動畫已經循環播放了,動畫最開始的一幀將被播出。該循環通過增加變量numCycles的值來登記。
如果新的樣本號大于前一個幀號加1,那就記下跳過的樣本號。
收尾處理
stopMovie()打印出FPS值并關閉QuickTime會話,其實現方式同QTSnapper中的stopMovie()方法很相近。該函數還給出一些額外信息:
| long totalFrames =(numCycles * numSamples) + sampNum; //報告跳過幀的百分比數 double skipPerCent=(double)(numSkips*100)/totalFrames; System.out.println("Percentage frames skipped: "+ frameDf.format(skipPerCent) + "%"); //"表面上的"FPS (AFPS) double appFrameRate = ((double) totalFrames * 1000.0) / duration; System.out.println("AFPS: "+frameDf.format(appFrameRate));//1dp |
appFrameRate代表了"表面上的"幀速率,該值是從QTSnapper1的創建開始算起,直到迭代結束得到的總幀數。所謂"表面上的",其意指并非所有的樣本必須被著色到屏幕上。
應用程序能運行嗎?是否運行良好?
以QTSnapper1取代QTSnapper,原來慢速的動畫(如圖1所示)現在的播放加快了。在播放快結束時,程序報出的表面上的幀速率數字達到31 FPS,實際的幀速率大約在16 FPS,跳過的幀數將近達到總幀數的50%。驚人的是,這么巨大數目的幀丟失在屏幕上的影響看上去并不明顯。
對于另外一些小型動畫來說,速度的提升并不那么引人注目;跳過的幀數所占百分比大約在5-10%。
不幸的是,存在兩個問題:當跳躍幀時可能出現混雜的像素;對可以跳過的幀號缺乏有效的控制。
混雜的像素
這種效果如圖6所示,不準確的像素使用了來自于視頻中前一階段的值。任何時候當QTSnapper1跳過一個動畫幀時,下一幅被檢索到的幀將會包含一些混雜的像素。其效果可以在圖6中看到。這些不準確的像素使用了來自于視頻中前一階段的值。
![]() 圖 6. 部分混雜的圖像 |
借助于來自quicktime-java 郵件列表的人們(特別感謝George Birbilis和Dean Perry)的幫助,我找到了一個解決辦法。
該問題是,我原先所有的動畫樣本都使用了temporal compression壓縮算法,該壓縮算法主要是利用了相鄰視頻幀的相似性。如果兩個連續的幀有相同的背景,那么就沒有必要再次存儲背景了,而是僅存起這兩幀的不同處即可。
這項技術,幾乎為所有流行的視頻格式所應用,意味著從幀中提取的一幅圖像將依賴于該幀,也潛在地依賴于前面幾個幀。
Temporal decompression解壓算法在一個QuickTime DSequence對象中完成,該對象又為我自己的writeToBufferedImage()方法中所用。在Dsequence的構造器指定,在解壓過程中QuickTime應該使用脫屏圖像緩沖區進行操作。
幀中的圖像被寫向緩沖區,在此其又與早期的幀數據相結合。結果圖像被傳遞到轉換過程的下一步。
這種方式在QTSnapper1以順序方式(不進行幀跳過,例如:1,2,3,4)解壓樣本時運行良好,但是在有跳幀的情況下報出錯誤。例如,當QTSnapper1跳過第5、6幀,然后解壓第7幀,情況會怎樣呢?該幀被寫向一個QuickTime 圖像緩沖區,然后又與早期的幀數據相結合。不幸的是,第5、6幀中的數據丟失了,因此結果圖像是不正確的。
簡單地說,圖像中進來的混雜像素是由于動畫中使用了temporal compression壓縮算法的結果。一種選擇是使用spatial compression壓縮算法,這種算法獨立地壓縮每一幀數據,就象對單一圖像進行JPEG或者GIF壓縮的方式很相似。這種算法意味著,用于解壓一幀的所有信息均來自于該幀本身;沒有必要對前面的幀進行檢查。
QuickTime MOV動畫格式支持一種稱作Motion-JPEG (M-JPEG)的a spatial compression壓縮方式。我使用 QuickTime 6 Pro中的輸出工具,選取"M-JPEG A codec"編碼方式后,把圖1中的樣本保存成一種MOV文件。當Movie3D應用程序播放該動畫時,沒有混雜現象發生。
限制幀跳躍
關于QTSnapper1的另外一個問題是,getFrame()方法并不限制能夠跳過的幀數。在我的測試過程中,跳過幀數的上限是3時,其效果并不顯著。但是,如果getFrame()方法中使用一個較大的樣本(例如,較大的尺寸和分辨率)加以轉換,那么,代碼運行速度的明顯變慢將會由更多的幀跳越數所補償,只是動畫質量非常明顯地變差。
4. 盡量加快圖像的生成
上面在QTSnapper和QTSnapper1中使用的sample-to-BufferedImage轉換方法(writeToBufferedImage())來源于Chris W. Johnson的一個例程。是否還存在更快的從樣本中提取圖像的方法?
關于QTJ的標準參考書是《QuickTime for Java: A Developer’s Notebook》(作者Chris Adamson,O’Reilly,出版時間2005.1)。本書的第五章介紹了QuickDraw內容,其中包含一個例子ConvertToJavaImageBetter.java,該例說明了怎樣把一個樣本抓取成一個PICT圖像,然后把它轉化成一個Java Image對象。你也可以在quicktime-java郵件列表處發現這個例子。
其中的轉化方法并不直截易懂,這依賴于加在PICT對象前面的一個虛構的512字節的頭部,這樣該對象就可以被QuickTime 版本的ImageProducer當作一個PICT文件對待。
我借用了Adamson的代碼作為我的另一個稱作QTSnapper2的Snapper類的基本代碼組成。該類負責一個沒有幀跳躍的幀系列的著色,其工作方式同QTSnapper,但是使用了PICT-to-Image轉換方法。
對于一些小的動畫,QTSnapper2的性能與QTSnapper相差無幾,但是對于一些稍微大些的如圖1中的動畫,與QTSnapper的16 FPS幀速率相比其平均幀速率下降到大約9 FPS。也就是說,基于PICT的轉換技術慢于Johnson所用的技術。





