最近看紅寶書學習 OpenGL 一段時間了,寫了簡單的 demo 程序溫習一下知識。
主要是 使用 glScissor 多視圖顯示畫面和使用 glReadPixels 給畫面截屏,使用顯示列表(display list)加上一些簡單的光照。
程序運行后,按字母 P 鍵截屏,圖片存放在當前目錄,按字母 Q 鍵在單視圖與多視圖之間切換。
效果圖如下,代碼已上傳到 github 上,地址。
下面是創建顯示列表的函數。
GLuint displayList(void) { static GLfloat gold_ambient[4] = { 0.24725f, 0.1995f, 0.0745f, 1.0f }; static GLfloat gold_diffuse[4] = { 0.75164f, 0.60648f, 0.22648f, 1.0f }; static GLfloat gold_specular[4] = { 0.628281f, 0.555802f, 0.366065f, 1.0f }; static GLfloat gold_shininess = 41.2f; static GLfloat silver_ambient[4] = { 0.05f, 0.05f, 0.05f, 1.0f }; static GLfloat silver_diffuse[4] = { 0.4f, 0.4f, 0.4f, 1.0f }; static GLfloat silver_specular[4] = { 0.7f, 0.7f, 0.7f, 1.0f }; static GLfloat silver_shininess = 12.0f; GLuint list = glGenLists(1); glNewList(list, GL_COMPILE); glMaterialfv(GL_FRONT, GL_AMBIENT, gold_ambient); glMaterialfv(GL_FRONT, GL_DIFFUSE, gold_diffuse); glMaterialfv(GL_FRONT, GL_SPECULAR, gold_specular); glMaterialf(GL_FRONT, GL_SHININESS, gold_shininess); glMaterialfv(GL_BACK, GL_AMBIENT, silver_ambient); glMaterialfv(GL_BACK, GL_DIFFUSE, silver_diffuse); glMaterialfv(GL_BACK, GL_SPECULAR, silver_specular); glMaterialf(GL_BACK, GL_SHININESS, silver_shininess); glutWireTorus(0.3/*innerRadius*/, 0.5/*outerRadius*/, 64/*nsides*/, 128/*rings*/); glEndList(); return list; }
關於截圖的代碼有點多,請下載文件查看,不在這里粘貼了,這里簡單說一下步驟。
1. 分配足夠的內存 GLubyte *image=(GLubyte*)malloc(width*height*3*sizeof(GLubyte));
2. 調用 void glReadBuffer(GLenum mode); 函數聲明顏色緩沖區,單緩沖默認用 GL_FRONT,雙緩沖默認用 GL_BACK
3. 調用 void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid * data); 函數從 frame buffer 取數據,一般為 glReadPixels(0, 0, height, width, GL_RGB, GL_UNSIGNED_BYTE ,image); 讀取緩沖區RGB數據存入分配的 image 中,也可以根據需求調節參數配置。需要注意的一點是內存對齊問題,由於 bmp 文件數據按 4 字節(DWORD)對齊,設置 glPixelStorei(GL_PACK_ALIGNMENT,4);,雖然默認的也是 4 字節對齊,還是顯示寫一下比較好。jpg 文件沒有如此要求,所以設置 glPixelStorei(GL_PACK_ALIGNMENT,1);
4. 數據在手,剩下的就是調用 png, jpg, tiff 等圖像庫的 API 進行讀寫了。
之前接觸過 DirectX,由於剛剛接觸 OpenGL,把自己遇到一些問題總結一下。
Direct3D 和 OpenGL 是在 GPU 上渲染 3D 圖形的兩大技術。拿 DirectX 跟 OpenGL 比是錯誤的,DirectX 包括許多其他的功能(DirectInput,DirectSound 等),末尾的X是未知數,也是一系列功能的統稱。
Direct3D 局限於 Windows 平台, OpenGL 跨平台;
Direct3D 使用 左手坐標系, OpenGL 使用右手坐標系。
Direct3D 使用 row_major 矩陣,變換矩陣之間左乘, 向量看成行向量;
OpenGL 使用 column_major 矩陣,變換矩陣之間右乘, 向量看成列向量。
Direct3D 函數多使用弧度制, OpenGL 函數多使用角度制。
1. glColor* 函數使用
OpenGL 偏向於 C 語言,很多函數通過添加后綴定義了不同的版本,以便使用不同類型的參數,有數量{3,4},類型{b,s,i,f,d,ub,us,ui},表示該函數接受3/4個8位整數/16位整數/32位整數/32位浮點數/64位浮點數/8位無符號整數/16位無符號整數/32位無符號整數類型的參數,有的最后還有一個字母v,表示該函數所接受的參數是一個指向向量或數組的指針,而不是一系列的單獨參數。
當時自己很討厭這些后綴,心想着如果用 C++ 的函數重載,也不用那么麻煩啊,直到我的膝蓋中了一箭。
在用 Directx3D 時,經常用宏 D3DCOLOR_RGBA 來指定一種顏色,使用起來也很簡單。
// from d3d9types.h // maps unsigned 8 bits/channel to D3DCOLOR #define D3DCOLOR_ARGB(a,r,g,b) \ ((D3DCOLOR)((((a)&0xff)<<24)|(((r)&0xff)<<16)|(((g)&0xff)<<8)|((b)&0xff))) #define D3DCOLOR_RGBA(r,g,b,a) D3DCOLOR_ARGB(a,r,g,b) #define D3DCOLOR_XRGB(r,g,b) D3DCOLOR_ARGB(0xff,r,g,b)
有一天,我用上 OpenGL 的函數 glColor3i(255, 255, 255); 發現顯示的是黑色,但是換用 glColor3f(1.0, 1.0, 1.0); 就是白色,不明白錯誤所在。稍微查看了文檔 man glColor,明白了錯誤的所在。如果使用整數的話,應該使用 glColor3ub(255, 255, 255); 當前顏色值都是以浮點數存儲的,浮點值直接映射;無符號整形 [0,255] 線性映射到 [0.0,1.0],有符號整形 [-128,127] 線性映射到 [-1.0,1.0]。所以 glColor3i(255, 255, 255); 等價於 glColor3f(255.0/INT_MAX, 255.0/INT_MAX, 255.0/INT_MAX); 值只會在更新當前顏色的時候 clamp 到 [0.0,1.0] 區間。不過在插值或者寫入到顏色緩沖區前也會 clamp,多查看文檔學習的來源。所以,有時候看別人的代碼覺得似乎很簡單,但是輪到自己親自動手寫的時候不一定會那么寫,導致犯錯。
2. glGet* 函數使用
OpenGL 是一個狀態機,打開某些狀態調用函數 void glEnable(GLenum capability);,關閉某些狀態調用函數 void glDisable(GLenum capability); 查詢用 glGet* 族函數,我想知道我這台機器有關 OpenGL 的版本信息。
void glInfo() { printf("\n"); printf("Vendor : %s\n", glGetString(GL_VENDOR)); printf("Renderer : %s\n", glGetString(GL_RENDERER)); printf("Version : %s\n", glGetString(GL_VERSION)); printf("GLSL version : %s\n", glGetString(GL_SHADING_LANGUAGE_VERSION)); printf("Extensions : %s\n", glGetString(GL_EXTENSIONS)); printf("\n"); }
我插入上面 glInfo 函數的位置太靠前,函數沒有返回任何結果。經過多次試驗發現,glGet* 函數在 glutCreateWindow 函數后調用,否則輸出為空。OpenGL 提供了功能強大且又非常基本的渲染函數,但是 OpenGL 程序必須使用窗口系統的底層機制。int glutCreateWindow(char* string); 創建了一個支持 OpenGL 渲染環境的窗口,函數返回唯一的標識符。而且,在調用 glutMainLoop(); 函數前窗口是不會顯示的。如果在 glutCreateWindow 之前沒有調用 glutInit(&argc, argv);會給出錯誤提示 freeglut ERROR: Function <glutCreateWindow> called without first calling 'glutInit'.
需要注意的是,在程序對性能有所要求的情況下盡量少調用 glGet*/glIs* 查詢狀態,但調試的時候使用沒什么。因為 OpenGL 是一個狀態機,設置狀態來得快,但查詢某些狀態會很耗時,想想多個流水線在跑,CPU 等待 GPU 忙玩一批渲染命令后停下來,找到並返回正確的而且是當前最新的狀態給你。
3. GLenum 錯誤
有一次筆誤,將 glEnable(GL_DEPTH_TEST) 寫成 glEnable(GL_DEPTH),沒有出現預期的效果。OpenGL 的頭文件里,很多都是宏定義成的數值,寫錯了話,編譯不會出錯,運行也不會有錯,只能在調式的時候希望能快點找到錯誤。由於自己粗心,glMatrixMode 函數名字里面也有 matrix,導致自己寫程序時,順手就將 glMatrixMode(GL_MODELVIEW); 寫成了 glMatrixMode(GL_MODELVIEW_MATRIX); 很長一段時間,程序沒有輸出,我不知道錯在哪里。也然后調用了 glGetDoublev 打印當前矩陣棧信息
/* * @param mode Specifies which matrix stack is the target for subsequent matrix * operations. Three values are accepted: GL_MODELVIEW_MATRIX, * GL_PROJECTION_MATRIX, and GL_TEXTURE_MATRIX. The initial value is * GL_MODELVIEW_MATRIX. */ void printMatrix(GLenum mode=GL_MODELVIEW_MATRIX) { const int ORDER=4; GLdouble M[ORDER*ORDER]={0}; glGetDoublev(mode, (GLdouble*)&M); for(int i=0; i<ORDER; ++i) printf("%10.6lf %10.6lf %10.6lf %10.6lf\n", M[i], M[i+ORDER], M[i+ORDER*2], M[i+ORDER*3]); }
發現打印的都是0,程序也沒有崩潰。(當然,上面是已經改正后的函數。)后來利用 glGetError 函數才看到打印出錯的信息
OpenGL error 1280: invalid enumerant,枚舉值有問題,然后一點點排除,又對比別人的代碼,才找出問題所在。
void printIfError() { GLenum gl_error=glGetError(); if(gl_error!=GL_NO_ERROR) printf("OpenGL error %d: %s\n", gl_error, gluErrorString(gl_error)); }
還需要注意的是記住當前的矩陣棧模式,是 GL_MODELVIEW,還是 GL_PROJECTION,還是 GL_TEXTURE?以免在錯誤的矩陣棧上面做了變換,較好的做法是每次一大堆矩陣變換前調用一次 glMatrixMode,最后的最后再調用一次 glMatrixMode(GL_MODELVIEW); 回到默認的模式 GL_MODELVIEW。
還有很多其他的陷阱(pitfall),下面列出一些很好的網址鏈接。
http://www.opengl.org/wiki/Common_Mistakes
http://www.opengl.org/archives/resources/features/KilgardTechniques/oglpitfall/