OpenGL(六)之初入動畫製做

今天要講的是動畫製做——多是各位都很喜歡的。除了講授知識外,咱們還會讓昨天那個「太陽、地球和月亮」天體圖畫動起來。緩和一下枯燥的氣氛。


本次課程,咱們將進入激動人心的計算機動畫世界。

想必你們都知道電影和動畫的工做原理吧?是的,快速的把看似連續的畫面一幅幅的呈如今人們面前。一旦每秒鐘呈現的畫面超過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

相關文章
相關標籤/搜索