圖像渲染的實現
先看用一個平面著色器渲染出的一個甜甜圈
代碼實現:
-
main
函數,程序入口。所以OpenGL
處理圖形、圖像都是鏈式形式,以及基于OpenGL
封裝的圖像處理框架也是鏈式編程
gltSetWorkingDirectory(argv[0]);
glutInit(&argc, argv);
// 初始化窗口
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
glutInitWindowSize(800, 600);
glutCreateWindow("ZB");
// 注冊函數
glutReshapeFunc(ChangeSize);
glutSpecialFunc(SpecialKeys);
glutDisplayFunc(RenderScene);
GLenum err = glewInit();
if (GLEW_OK != err) {
fprintf(stderr, "GLEW Error:%s\n", glewGetErrorString(err));
return 1;
}
// 主動觸發,準備工作
SetupRC();
// 一個無限執行的循環,負責一直處理窗口和操作系統的用戶輸入等操作
glutMainLoop();
return 0;
-
changeSize
通過glutReshapeFunc
注冊為重塑函數,當第一次創建窗口或屏幕大小發生改變時,會調用該函數調整窗口大小/視口大小
// 保證高度不能為0
if (h == 0) {
h = 1;
}
// 將視口設置為窗口尺寸
glViewport(0, 0, w, h);
// 創建投影矩陣,并將它載入投影矩陣堆棧中
viewFrustum.SetPerspective(35, float(w)/float(h), 1, 1000);
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
// 初始化渲染管線
transformPipeline.SetMatrixStacks(modelViweMatix, projectionMatrix);
-
SetupRC
設置需要渲染圖形相關頂點數據、顏色值等,手動在main
函數調用
// 1. 設置背景色
glClearColor(0.3, 0.3, 0.3, 1);
// 2. 初始化著色器管理器
shaderManager.InitializeStockShaders();
// 3. 將相機向后移動7個單元,肉眼到物體的距離
viewFrame.MoveForward(5.0);
// 4. 創建一個甜甜圈
/**
void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
參數1: GLTriangleBatch 容器幫助類
參數2: 外邊緣半徑
參數3: 內邊緣半徑
參數4、5: 主半徑和從半徑的細分單元數量
*/
gltMakeTorus(torusBatch, 1, 0.3, 88, 33);
// 5. 點的大小(方便點填充時,肉眼觀察)
glPointSize(4.0);
-
RenderScene
通過glutDisplayFunc
注冊為渲染函數。當屏幕發生變化或者開發者主動渲染會調用此函數,用來實現數據->渲染過程
// 1. 清除窗口和深度緩沖區
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 2. 把攝像機矩陣壓入模型矩陣中,壓棧 -- 存儲一個狀態
modelViweMatix.PushMatrix(viewFrame);
// 3. 設置繪圖顏色
GLfloat vRed[] = {1, 0, 0, 1};
// 4. 使用平面著色器
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
// 5. 繪制
torusBatch.Draw();
// 6. 出棧,繪制完成恢復 出棧 -- 恢復一個狀態
modelViweMatix.PopMatrix();
// 7. 強制執行緩存區
glutSwapBuffers();
到這里為止,編譯運行就能過出現上圖所示的效果圖。利用的是平面著色器。
相當的low。
下面在此基礎上進行酷炫的一波操作。
在main
函數中注冊了一個函數SpecialKeys
,顧名思義,特殊鍵位,這里控制的是上下左右鍵位
// 1. 判斷方向
if (key == GLUT_KEY_UP) {
// 2. 根據方向調整觀察者位置
// 參數1: 旋轉的弧度
// 參數2、3、4:表示繞哪個軸進行旋轉
viewFrame.RotateWorld(m3dDegToRad(-5), 1, 0, 0);
}
if (key == GLUT_KEY_DOWN) {
viewFrame.RotateWorld(m3dDegToRad(5), 1, 0, 0);
}
if (key == GLUT_KEY_LEFT) {
viewFrame.RotateWorld(m3dDegToRad(-5), 0, 1, 0);
}
if (key == GLUT_KEY_RIGHT) {
viewFrame.RotateWorld(m3dDegToRad(5), 0, 1, 0);
}
// 3. 重新刷新
glutPostRedisplay();
看實現效果
在來一波更真實的操作,我們使用默認光源著色器來實現
// 1. 清除窗口和深度緩沖區
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 2. 把攝像機矩陣壓入模型矩陣中
modelViweMatix.PushMatrix(viewFrame);
// 3. 設置繪圖顏色
GLfloat vRed[] = {1, 0, 0, 1};
// 4. 使用平面著色器
// shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
// 4.1 使用默認光源著色器
// 通過光源、陰影效果跟體現立體效果
// 參數1:GLT_SHADER_DEFAULT_LIGHT 默認光源著色器
// 參數2:模型視圖矩陣
// 參數3:投影矩陣
// 參數4:基本顏色值
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
// 5. 繪制
torusBatch.Draw();
// 6. 出棧,繪制完成恢復
modelViweMatix.PopMatrix();
// 7. 強制執行緩存區
glutSwapBuffers();
效果圖如下:
可以看出,我們的渲染出了問題。
問題分析
在使用默認光源著色器時,由于產生了光照,有光照的一面,按照原本的顏色顯示,而背光面,則是黑暗的,我們看不見的。其實很好理解,太陽光照地球,迎光面是白天,背光面是黑夜。
在繪制3D場景的時候,我們需要決定哪些部分是對觀察者可見的,或者哪些部分是對觀察者不可見的,對于不可見的部分,應該及早丟棄。例如在一個不透明的墻壁后,就不應該有渲染,這種情況叫做隱藏面消除
下面討論一下解決這個問題的方案。
解決問題的方案
油畫算法
先繪制場景中離觀察者較遠的物體,在繪制較近的物體,如下圖
繪制順序依次是紅、黃、灰,這樣的話按序渲染能過解決隱藏面消除的問題。
但是隨之而來的會有一些不好的問題出現
- 效率很低,重疊部分會進行多次繪制渲染,浪費資源
- 對于某些存在場景,無法區別遠近順序的,無法用該方法解決問題,如下圖
正背面剔除
首先需要確定一個問題,任何平面都有2個面,正面/背面,意味著你一個時刻只能看到一面。
一個立方體圖形,從任何一個方向去觀察,最多可以看到3個面,意味著其他看不到的面,我們不需要去繪制它,如果能以某種方式去丟棄這部分數據,OpenGL
在渲染的性能即可提高50%。
沒錯,OpenGL
能夠區別正面和背面,通過分析頂點數據的順序
正面/背面區分
- 正面:按照逆時針頂點鏈接順序的三角形面
- 背面:按照順時針頂點連接順序的三角形面
立方體中的正背面
分析:
- 左側三角形頂點順序為:1->2->3; 右側三角形的頂點順序為:1->2->3
- 當觀察者在右側時,則右邊的三角形方向為逆時針方向為正面,而左側的三角形為順時針則為反面
- 當觀察者在左側時,則左邊的三?形方向為逆時針?方向為正面,?右側的三角形為順時針則為背面
總結:
正面和背面是由三角形的頂點定義順序和觀察者方向共同決定的,隨著觀察者的角度方向的改變,正面背面也會跟著改變
相關代碼
// 開啟表面剔除(默認背面剔除)
void glEnable(GL_CULL_FACE);
// 關閉表面剔除(默認背面剔除)
void glDisable(GL_CULL_FACE);
// 用戶選擇剔除那個面(即可自定義剔除,默認為正面)
void glCullFace(GLenum mode);
mode參數為:GL_FRONT, GL_BACK, GL_FRONT_AND_BACK, 默認為GL_BACK
// 用戶也可以指定正面
void glFrontFace(GLenum mode);
mode參數為:GL_CW, GL_CCW, 默認為GL_CCW
// 剔除正面實現
glCullFace(GL_BACK);
glFrontFace(GL_CW);
或
glCullface(GL_FRONT);
具體代碼實現
// 1. 清除窗口和深度緩沖區
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 開啟正背面剔除
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
// 2. 把攝像機矩陣壓入模型矩陣中
modelViweMatix.PushMatrix(viewFrame);
// 3. 設置繪圖顏色
GLfloat vRed[] = {1, 0, 0, 1};
// 后面代碼和上面一樣,不再重復
實現效果如下圖:
可以看到,之前的問題已經解決了,可是又面臨了一個尷尬的問題,這個甜甜圈貌似有個很大的缺口,了解過圖形渲染的讀者肯定知道,這是深度問題,下面來了解一下。
深度
深度就是該像素點在3D世界中距離攝像機的距離,也就是Z值。
深度緩存區就是一塊內存區域,專門存儲著每個像素點(繪制在屏幕上的)深度值Z。Z越大,則距離屏幕越遠。
那么為什么需要深度緩沖區?
在不實用深度測試的時候,如果我們先繪制一個距離比較近的物體,在繪制距離遠的物體,則距離遠的位圖因為后繪制,會把距離近的物體覆蓋掉。有了深度緩沖區后,繪制物體的順序就不那么重要了。上面出現的大缺口,也就是這個問題造成的。
實際上,只要存在深度緩沖區,OpenGL
都會把像素的深度值寫入到緩沖區中,除非調用glDepthMask(GL_FALSE)
來禁止寫入。
深度測試
深度緩沖區和顏色緩存區是對應的。顏色緩存區存儲像素的顏色信息,而深度緩沖區存儲像素的深度信息。在決定是否繪制一個物體表面時,首先要將表面對應的像素的深度值與當前深度緩存區中的值進行比較,如果大雨深度緩存區的值,則丟棄這部分,否則利用這個像素對應的深度值和顏色值,分別更新深度緩存區和顏色緩存區。這個過程稱為深度測試。
相關代碼
// 開啟深度測試
glEnable(GL_DEPTH_TEST);
// 在繪制場景前,清除顏色緩存區和深度緩沖區
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_GEPTH_BUFFER_BIT);
清除深度緩沖區默認值為1.0,表示最大的深度值,深度值的范圍為(0,1)之間。值越小表示越靠近觀察者,反正表示距離觀察者越遠。
下面有關深度測試的判斷式
指定深度測試判斷模式
void glDepthFunc(GLEnum mode);
打開/阻斷 深度緩存區寫入
void glDepthMask(GKBool value);
value :GL_TURE
開啟寫入GL_FALSE
關閉寫入
最終的實現效果如下:
ZFighting閃爍問題
為什么會出現ZFighting閃爍問題
因為開啟深度測試后,OpenGL
就不會去繪制模型被遮擋的部分,這樣實現現實更加真實,但是由于深度緩存區精度的限制,對于深度相差無幾的情況下,OpenGL
就可能出現不能正確判斷兩者深度值,會導致深度測試的結果不可預測,現實出來的現象會交錯閃爍。
解決方式
- 第一步:啟用
Polygon Offset
方式解決
讓深度值之間產生間隔,可以理解為在執行深度測試前,將立方體的深度值做一些細微的增加,于是就能將重疊的2個圖形深度值之間有所區分。
// 啟用Polygon Offset方式
glEnable(GL_POLYGON_OFFSET_FILL);
參數列表:
GL_POLYGON_OFFSET_POINT 對應光柵化模式:GL_POINT
GL_POLYGON_OFFSET_LINE 對應光柵化模式:GL_LINE
GL_POLYGON_OFFSET_FILL 對應光柵化模式:GL_FILL
-
第二步:指定偏移量
- 通過
glPolygon Offset
來指定.glPolygon Offset
需要2個參數:factor
,units
. - 每個
Fragment
的深度值都會增加如下所示的偏移量:
Offset = ( m * factor ) + ( r * units);
m : 多邊形的深度的斜率的最大值,理解一個多邊形越是與近裁剪?平行,m就越接近于0.
r : 能產生于窗口坐標系的深度值中可分辨的差異最小值.r是由具體是由具體OpenGL
平臺指定的一個常量. - 一個?于0的
Offset
會把模型推到離你(攝像機)更遠的位置,相應的?個小于0的Offset
會把模型拉近 - 一般?言,只需要將-1.0 和 -1 這樣簡單賦值給
glPolygon Offset
基本可以滿?足需求.
- 通過
第三步:關閉
Polygon Offset
glDisable(GL_POLYGON_OFFSET_FILL);
OK,到此為止,我們完美的把這個甜甜圈給渲染出來了。上面遇到的一些問題也得已解決。