今天要講的是動畫製做——多是各位都很喜歡的。除了講授知識外,咱們還會讓昨天那個「太陽、地球和月亮」天體圖畫動起來。緩和一下枯燥的氣氛。
本次課程,咱們將進入激動人心的計算機動畫世界。
想必你們都知道電影和動畫的工做原理吧?是的,快速的把看似連續的畫面一幅幅的呈如今人們面前。一旦每秒鐘呈現的畫面超過24幅,人們就會錯覺得它是連續的。
咱們一般觀看的電視,每秒播放25或30幅畫面。但對於計算機來講,它能夠播放更多的畫面,以達到更平滑的效果。若是速度過慢,畫面不夠平滑。若是速度過快,則人眼未必就能反應得過來。對於一個正常人來講,每秒60~120幅圖畫是比較合適的。具體的數值因人而異。
假設某動畫一共有n幅畫面,則它的工做步驟就是:
顯示第1幅畫面,而後等待一小段時間,直到下一個1/24秒
顯示第2幅畫面,而後等待一小段時間,直到下一個1/24秒
……
顯示第n幅畫面,而後等待一小段時間,直到下一個1/24秒
結束
若是用C語言僞代碼來描述這一過程,就是:函數
1 for(i=0; i<n; ++i) 2 { 3 DrawScene(i); 4 Wait(); 5 }
一、雙緩衝技術
在 計算機上的動畫與實際的動畫有些不一樣:實際的動畫都是先畫好了,播放的時候直接拿出來顯示就行。計算機動畫則是畫一張,就拿出來一張,再畫下一張,再拿出 來。若是所須要繪製的圖形很簡單,那麼這樣也沒什麼問題。但一旦圖形比較複雜,繪製須要的時間較長,問題就會變得突出。
讓咱們把計算機想象成一個 畫圖比較快的人,假如他直接在屏幕上畫圖,而圖形比較複雜,則有可能在他只畫了某幅圖的一半的時候就被觀衆看到。然後面雖然他把畫補全了,但觀衆的眼睛卻 又沒有反應過來,還停留在原來那個殘缺的畫面上。也就是說,有時候觀衆看到完整的圖象,有時卻又只看到殘缺的圖象,這樣就形成了屏幕的閃爍。
如何 解決這一問題呢?咱們設想有兩塊畫板,畫圖的人在旁邊畫,畫好之後把他手裏的畫板與掛在屏幕上的畫板相交換。這樣以來,觀衆就不會看到殘缺的畫了。這一技 術被應用到計算機圖形中,稱爲雙緩衝技術。即:在存儲器(頗有多是顯存)中開闢兩塊區域,一塊做爲發送到顯示器的數據,一塊做爲繪畫的區域,在適當的時 候交換它們。因爲交換兩塊內存區域實際上只須要交換兩個指針,這一方法效率很是高,因此被普遍的採用。
注意:雖然絕大多數平臺都支持雙緩衝技術,但這一技術並非OpenGL標準中的內容。OpenGL爲了保證更好的可移植性,容許在實現時不使用雙緩衝技術。固然,咱們經常使用的PC都是支持雙緩衝技術的。
要啓動雙緩衝功能,最簡單的辦法就是使用GLUT工具包。咱們之前在main函數裏面寫:
glutInitDisplayMode(GLUT_RGB | GLUT_SINGLE);
其中GLUT_SINGLE表示單緩衝,若是改爲GLUT_DOUBLE就是雙緩衝了。
固然還有須要更改的地方——每次繪製完成時,咱們須要交換兩個緩衝區,把繪製好的信息用於屏幕顯示(不然不管怎麼繪製,仍是什麼都看不到)。若是使用GLUT工具包,也能夠很輕鬆的完成這一工做,只要在繪製完成時簡單的調用glutSwapBuffers函數就能夠了。
二、實現連續動畫
彷佛沒有任何疑問,咱們應該把繪製動畫的代碼寫成下面這個樣子:工具
1 for(i=0; i<n; ++i) 2 { 3 DrawScene(i); 4 glutSwapBuffers(); 5 Wait(); 6 }
但事實上,這樣作不太符合窗口系統的程序設計思路。還記得咱們的第一個OpenGL程序嗎?咱們在main函數裏寫:glutDisplayFunc(&myDisplay);
意 思是對系統說:若是你須要繪製窗口了,請調用myDisplay這個函數。爲何咱們不直接調用myDisplay,而要採用這種看似「捨近求遠」的作法 呢?緣由在於——咱們本身的程序沒法掌握究竟何時該繪製窗口。由於通常的窗口系統——拿咱們熟悉一點的來講——Windows和X窗口系統,都是支持 同時顯示多個窗口的。假如你的程序窗口碰巧被別的窗口遮住了,後來用戶又把原來遮住的窗口移開,這時你的窗口須要從新繪製。很不幸的,你沒法知道這一事件 發生的具體時間。所以這一切只好委託操做系統來辦了。
如今咱們再看上面那個循環。既然DrawScene均可以交給操做系統來代辦了,那讓整個循 環運行起來的工做是否也能夠交給操做系統呢?答案是確定的。咱們先前的思路是:繪製,而後等待一段時間;再繪製,再等待一段時間。但若是去掉等待的時間, 就變成了繪製,繪製,……,不停的繪製。——固然了,資源是公用的嘛,殺毒軟件總要工做吧?個人下載不能停下來吧?個人mp3播放還不能給耽擱了。總不能 由於咱們的動畫,讓其餘的工做都停下來。所以,咱們須要在CPU空閒的時間繪製。
這裏的「在CPU空閒的時間繪製」和咱們在第一課講的「在須要繪 制的時候繪製」有些共通,都是「在XX時間作XX事」,GLUT工具包也提供了一個比較相似的函數:glutIdleFunc,表示在CPU空閒的時間調 用某一函數。其實GLUT還提供了一些別的函數,例如「在鍵盤按下時作某事」等。
到如今,咱們已經能夠初步開始製做動畫了。好的,就拿上次那個「太陽、地球和月亮」的程序開刀,讓地球和月亮本身動起來。
Code:oop
1 #include <GL/glut.h> 2 3 // 太陽、地球和月亮 4 // 假設每月都是30天 5 // 一年12個月,共是360天 6 static int day = 200; // day的變化:從0到359 7 void myDisplay(void) 8 { 9 /**************************************************** 10 這裏的內容照搬上一課的,只由於使用了雙緩衝,補上最後這句 11 *****************************************************/ 12 glutSwapBuffers(); 13 } 14 15 void myIdle(void) 16 { 17 /* 新的函數,在空閒時調用,做用是把日期日後移動一天並從新繪製,達到動畫效果 */ 18 ++day; 19 if( day >= 360 ) 20 day = 0; 21 myDisplay(); 22 } 23 24 int main(int argc, char *argv[]) 25 { 26 glutInit(&argc, argv); 27 glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); // 修改了參數爲GLUT_DOUBLE 28 glutInitWindowPosition(100, 100); 29 glutInitWindowSize(400, 400); 30 glutCreateWindow("太陽,地球和月亮"); // 改了窗口標題 31 glutDisplayFunc(&myDisplay); 32 glutIdleFunc(&myIdle); // 新加入了這句 33 glutMainLoop(); 34 return 0; 35 }
三、關於垂直同步
代 碼是寫好了,但相信你們還有疑問。某些朋友可能在運行時發現,雖然CPU幾乎都用上了,但運動速度很快,根本看不清楚,另外一些朋友在運行時發現CPU使用 率很低,根本就沒有把空閒時間徹底利用起來。但對於上面那段代碼來講,這些現象都是合理的。這裏就牽涉到關於垂直同步的問題。
你們知道顯 示器的刷新率是比較有限的,通常爲60~120Hz,也就是一秒鐘刷新60~120次。但若是叫計算機繪製一個簡單的畫面,例如只有一個三角形,則一秒鐘 能夠繪製成千上萬次。所以,若是最大限度的利用計算機的處理能力,繪製不少幅畫面,但顯示器的刷新速度卻跟不上,這不只形成性能的浪費,還可能帶來一些負 面影響(例如,顯示器只刷新到一半時,須要繪製的內容卻變化了,因爲顯示器是逐行刷新的,因而顯示器上半部分和下半部分其實是來自兩幅畫面)。採用垂直 同步技術能夠解決這一問題。即,只有在顯示器刷新時,才把繪製好的圖象傳輸出去供顯示。這樣一來,計算機就沒必要去繪製大量的根本就用不到的圖象了。若是顯 示器的刷新率爲85Hz,則計算機一秒鐘只須要繪製85幅圖象就足夠,若是場景足夠簡單,就會形成比較多的CPU空閒。
幾乎全部的顯卡都支持「垂直同步」這一功能。
垂直同步也有它的問題。若是刷新頻率爲60Hz,則在繪製比較簡單的場景時,繪製一幅圖畫須要的時間很段,幀速能夠恆定在60FPS(即60幀/秒)。若是場景變得複雜,繪製一幅圖畫的時間超過了1/60秒,則幀速將急劇降低。
如 果繪製一幅圖畫的時間爲1/50,則在第一個1/60秒時,顯示器須要刷新了,但因爲新的圖畫沒有畫好,因此只能顯示原來的圖畫,等到下一個1/60秒時 才顯示新的圖畫。因而顯示一幅圖畫實際上用了1/30秒,幀速爲30FPS。(若是不採用垂直同步,則幀速應該是50FPS)
若是繪製一幅圖畫的時間更長,則降低的趨勢就是階梯狀的:60FPS,30FPS,20FPS,……(60/1,60/2,60/3,……)
如 果每一幅圖畫的複雜程度是不一致的,且繪製它們須要的時間都在1/60上下。則在1/60時間內畫完時,幀速爲60FPS,在1/60時間未完成時,幀速 爲30FPS,這就形成了幀速的跳動。這是很麻煩的事情,須要避免它——要麼想辦法簡化每一畫面的繪製時間,要麼都延遲一小段時間,以做到統一。
回過頭來看前面的問題。若是使用了大量的CPU並且速度很快沒法看清,則打開垂直同步能夠解決該問題。固然若是你認爲垂直同步有這樣那樣的缺點,也能夠關閉它。——至於如何打開和關閉,因操做系統而異了。具體步驟請本身搜索之。
固然,也有其它辦法能夠控制動畫的幀速,或者儘可能讓動畫的速度儘可能和幀速無關。不過這裏面不少內容都是與操做系統比較緊密的,何況它們跟OpenGL關係也不太大。這裏就不作介紹了。
四、計算幀速
不知道你們玩過3D Mark這個軟件沒有,它能夠運行各類場景,測出幀速,而且爲你的系統給出評分。這裏我也介紹一個計算幀速的方法。
根據定義,幀速就是一秒鐘內播放的畫面數目(FPS)。咱們能夠先測量繪製兩幅畫面之間時間t,而後求它的倒數便可。假如t=0.05s,則FPS的值就是1/0.05=20。
理論上是如此了,但是如何獲得這個時間呢?一般C語言的time函數精確度通常只到一秒,確定是不行了。clock函數也就到十毫秒左右,仍是有點不夠。由於FPS爲60和FPS爲100的時候,t的值都是十幾毫秒。
你知道如何測量一張紙的厚度嗎?一個粗略的辦法就是:用不少張紙疊在一塊兒測厚度,計算平均值就能夠了。咱們這裏也能夠這樣辦。測量繪製50幅畫面(包括垂直同步等因素的等待時間)須要的時間t',由t'=t*50很容易的獲得FPS=1/t=50/t'
下面這段代碼能夠統計該函數自身的調用頻率,(原理就像上面說的那樣),程序並不複雜,而且這並不屬於OpenGL的內容,因此我不打算詳細講述它。
性能
Code:
1 #include <time.h> 2 double CalFrequency() 3 { 4 static int count; 5 static double save; 6 static clock_t last, current; 7 double timegap; 8 9 ++count; 10 if( count <= 50 ) 11 return save; 12 count = 0; 13 last = current; 14 current = clock(); 15 timegap = (current-last)/(double)CLK_TCK; 16 save = 50.0/timegap; 17 return save; 18 }
最後,要把計算的幀速顯示出來,但咱們並無學習如何使用OpenGL把文字顯示到屏幕上。——但不要忘了,在咱們的圖形窗口背後,還有一個命令行窗口~使用printf函數就能夠輕易的輸出文字了。學習
1 #include <stdio.h> 2 3 double FPS = CalFrequency(); 4 printf("FPS = %f\n", FPS);
最後的一步,也被咱們解決了——雖然作法不太雅觀,不要緊,之後咱們還會改善它的。
時間過得過久,每次給的程序都只是一小段,一些朋友不免會出問題。
如今,我給出一個比較完整的程序,供你們參考。
動畫
Code:
1 #include <GL/glut.h> 2 #include <stdio.h> 3 #include <time.h> 4 5 // 太陽、地球和月亮 6 // 假設每月都是12天 7 // 一年12個月,共是360天 8 static int day = 200; // day的變化:從0到359 9 10 double CalFrequency() 11 { 12 static int count; 13 static double save; 14 static clock_t last, current; 15 double timegap; 16 17 ++count; 18 if( count <= 50 ) 19 return save; 20 count = 0; 21 last = current; 22 current = clock(); 23 timegap = (current-last)/(double)CLK_TCK; 24 save = 50.0/timegap; 25 return save; 26 } 27 28 void myDisplay(void) 29 { 30 double FPS = CalFrequency(); 31 printf("FPS = %f\n", FPS); 32 33 glEnable(GL_DEPTH_TEST); 34 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 35 36 glMatrixMode(GL_PROJECTION); 37 glLoadIdentity(); 38 gluPerspective(75, 1, 1, 400000000); 39 glMatrixMode(GL_MODELVIEW); 40 glLoadIdentity(); 41 gluLookAt(0, -200000000, 200000000, 0, 0, 0, 0, 0, 1); 42 43 // 繪製紅色的「太陽」 44 glColor3f(1.0f, 0.0f, 0.0f); 45 glutSolidSphere(69600000, 20, 20); 46 // 繪製藍色的「地球」 47 glColor3f(0.0f, 0.0f, 1.0f); 48 glRotatef(day/360.0*360.0, 0.0f, 0.0f, -1.0f); 49 glTranslatef(150000000, 0.0f, 0.0f); 50 glutSolidSphere(15945000, 20, 20); 51 // 繪製黃色的「月亮」 52 glColor3f(1.0f, 1.0f, 0.0f); 53 glRotatef(day/30.0*360.0 - day/360.0*360.0, 0.0f, 0.0f, -1.0f); 54 glTranslatef(38000000, 0.0f, 0.0f); 55 glutSolidSphere(4345000, 20, 20); 56 57 glFlush(); 58 glutSwapBuffers(); 59 } 60 61 void myIdle(void) 62 { 63 ++day; 64 if( day >= 360 ) 65 day = 0; 66 myDisplay(); 67 } 68 69 int main(int argc, char *argv[]) 70 { 71 glutInit(&argc, argv); 72 glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE); 73 glutInitWindowPosition(100, 100); 74 glutInitWindowSize(400, 400); 75 glutCreateWindow("太陽,地球和月亮"); 76 glutDisplayFunc(&myDisplay); 77 glutIdleFunc(&myIdle); 78 glutMainLoop(); 79 return 0; 80 }
小結:
OpenGL動畫和傳統意義上的動畫類似,都是把畫面一幅一幅的呈如今觀衆面前。一旦畫面變換的速度快了,觀衆就會認爲畫面是連續的。
雙緩衝技術是一種在計算機圖形中廣泛採用的技術,絕大多數OpenGL實現都支持雙緩衝技術。
一般都是利用CPU空閒的時候繪製動畫,但也能夠有其它的選擇。
介紹了垂直同步的相關知識。
介紹了一種簡單的計算幀速(FPS)的方法。
最後,咱們列出了一份完整的天體動畫程序清單。
===================== 第六課 完 =====================
=====================TO BE CONTINUED=====================spa