圖像渲染的實現(xiàn)
先看用一個平面著色器渲染出的一個甜甜圈
代碼實現(xiàn):
-
main
函數(shù)跺讯,程序入口。所以OpenGL
處理圖形世剖、圖像都是鏈式形式间聊,以及基于OpenGL
封裝的圖像處理框架也是鏈式編程
gltSetWorkingDirectory(argv[0]);
glutInit(&argc, argv);
// 初始化窗口
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
glutInitWindowSize(800, 600);
glutCreateWindow("ZB");
// 注冊函數(shù)
glutReshapeFunc(ChangeSize);
glutSpecialFunc(SpecialKeys);
glutDisplayFunc(RenderScene);
GLenum err = glewInit();
if (GLEW_OK != err) {
fprintf(stderr, "GLEW Error:%s\n", glewGetErrorString(err));
return 1;
}
// 主動觸發(fā),準備工作
SetupRC();
// 一個無限執(zhí)行的循環(huán)壳贪,負責一直處理窗口和操作系統(tǒng)的用戶輸入等操作
glutMainLoop();
return 0;
-
changeSize
通過glutReshapeFunc
注冊為重塑函數(shù)财著,當?shù)谝淮蝿?chuàng)建窗口或屏幕大小發(fā)生改變時,會調(diào)用該函數(shù)調(diào)整窗口大小/視口大小
// 保證高度不能為0
if (h == 0) {
h = 1;
}
// 將視口設(shè)置為窗口尺寸
glViewport(0, 0, w, h);
// 創(chuàng)建投影矩陣撑碴,并將它載入投影矩陣堆棧中
viewFrustum.SetPerspective(35, float(w)/float(h), 1, 1000);
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
// 初始化渲染管線
transformPipeline.SetMatrixStacks(modelViweMatix, projectionMatrix);
-
SetupRC
設(shè)置需要渲染圖形相關(guān)頂點數(shù)據(jù)撑教、顏色值等,手動在main
函數(shù)調(diào)用
// 1. 設(shè)置背景色
glClearColor(0.3, 0.3, 0.3, 1);
// 2. 初始化著色器管理器
shaderManager.InitializeStockShaders();
// 3. 將相機向后移動7個單元醉拓,肉眼到物體的距離
viewFrame.MoveForward(5.0);
// 4. 創(chuàng)建一個甜甜圈
/**
void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
參數(shù)1: GLTriangleBatch 容器幫助類
參數(shù)2: 外邊緣半徑
參數(shù)3: 內(nèi)邊緣半徑
參數(shù)4伟姐、5: 主半徑和從半徑的細分單元數(shù)量
*/
gltMakeTorus(torusBatch, 1, 0.3, 88, 33);
// 5. 點的大小(方便點填充時亿卤,肉眼觀察)
glPointSize(4.0);
-
RenderScene
通過glutDisplayFunc
注冊為渲染函數(shù)愤兵。當屏幕發(fā)生變化或者開發(fā)者主動渲染會調(diào)用此函數(shù),用來實現(xiàn)數(shù)據(jù)->渲染過程
// 1. 清除窗口和深度緩沖區(qū)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 2. 把攝像機矩陣壓入模型矩陣中排吴,壓棧 -- 存儲一個狀態(tài)
modelViweMatix.PushMatrix(viewFrame);
// 3. 設(shè)置繪圖顏色
GLfloat vRed[] = {1, 0, 0, 1};
// 4. 使用平面著色器
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
// 5. 繪制
torusBatch.Draw();
// 6. 出棧秆乳,繪制完成恢復(fù) 出棧 -- 恢復(fù)一個狀態(tài)
modelViweMatix.PopMatrix();
// 7. 強制執(zhí)行緩存區(qū)
glutSwapBuffers();
到這里為止,編譯運行就能過出現(xiàn)上圖所示的效果圖。利用的是平面著色器屹堰。
相當?shù)膌ow肛冶。
下面在此基礎(chǔ)上進行酷炫的一波操作。
在main
函數(shù)中注冊了一個函數(shù)SpecialKeys
扯键,顧名思義睦袖,特殊鍵位,這里控制的是上下左右鍵位
// 1. 判斷方向
if (key == GLUT_KEY_UP) {
// 2. 根據(jù)方向調(diào)整觀察者位置
// 參數(shù)1: 旋轉(zhuǎn)的弧度
// 參數(shù)2荣刑、3馅笙、4:表示繞哪個軸進行旋轉(zhuǎn)
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();
看實現(xiàn)效果
在來一波更真實的操作,我們使用默認光源著色器來實現(xiàn)
// 1. 清除窗口和深度緩沖區(qū)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 2. 把攝像機矩陣壓入模型矩陣中
modelViweMatix.PushMatrix(viewFrame);
// 3. 設(shè)置繪圖顏色
GLfloat vRed[] = {1, 0, 0, 1};
// 4. 使用平面著色器
// shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
// 4.1 使用默認光源著色器
// 通過光源厉亏、陰影效果跟體現(xiàn)立體效果
// 參數(shù)1:GLT_SHADER_DEFAULT_LIGHT 默認光源著色器
// 參數(shù)2:模型視圖矩陣
// 參數(shù)3:投影矩陣
// 參數(shù)4:基本顏色值
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
// 5. 繪制
torusBatch.Draw();
// 6. 出棧董习,繪制完成恢復(fù)
modelViweMatix.PopMatrix();
// 7. 強制執(zhí)行緩存區(qū)
glutSwapBuffers();
效果圖如下:
可以看出,我們的渲染出了問題爱只。
問題分析
在使用默認光源著色器時阱飘,由于產(chǎn)生了光照,有光照的一面虱颗,按照原本的顏色顯示,而背光面蔗喂,則是黑暗的忘渔,我們看不見的。其實很好理解缰儿,太陽光照地球畦粮,迎光面是白天,背光面是黑夜乖阵。
在繪制3D場景的時候宣赔,我們需要決定哪些部分是對觀察者可見的,或者哪些部分是對觀察者不可見的瞪浸,對于不可見的部分儒将,應(yīng)該及早丟棄。例如在一個不透明的墻壁后对蒲,就不應(yīng)該有渲染钩蚊,這種情況叫做隱藏面消除
下面討論一下解決這個問題的方案。
解決問題的方案
油畫算法
先繪制場景中離觀察者較遠的物體蹈矮,在繪制較近的物體砰逻,如下圖
繪制順序依次是紅、黃泛鸟、灰蝠咆,這樣的話按序渲染能過解決隱藏面消除的問題。
但是隨之而來的會有一些不好的問題出現(xiàn)
- 效率很低,重疊部分會進行多次繪制渲染刚操,浪費資源
- 對于某些存在場景闸翅,無法區(qū)別遠近順序的,無法用該方法解決問題赡茸,如下圖
正背面剔除
首先需要確定一個問題缎脾,任何平面都有2個面,正面/背面占卧,意味著你一個時刻只能看到一面遗菠。
一個立方體圖形,從任何一個方向去觀察华蜒,最多可以看到3個面辙纬,意味著其他看不到的面,我們不需要去繪制它叭喜,如果能以某種方式去丟棄這部分數(shù)據(jù)贺拣,OpenGL
在渲染的性能即可提高50%。
沒錯捂蕴,OpenGL
能夠區(qū)別正面和背面譬涡,通過分析頂點數(shù)據(jù)的順序
正面/背面區(qū)分
- 正面:按照逆時針頂點鏈接順序的三角形面
- 背面:按照順時針頂點連接順序的三角形面
立方體中的正背面
分析:
- 左側(cè)三角形頂點順序為:1->2->3; 右側(cè)三角形的頂點順序為:1->2->3
- 當觀察者在右側(cè)時,則右邊的三角形方向為逆時針方向為正面啥辨,而左側(cè)的三角形為順時針則為反面
- 當觀察者在左側(cè)時,則左邊的三?形方向為逆時針?方向為正面,?右側(cè)的三角形為順時針則為背面
總結(jié):
正面和背面是由三角形的頂點定義順序和觀察者方向共同決定的涡匀,隨著觀察者的角度方向的改變,正面背面也會跟著改變
相關(guān)代碼
// 開啟表面剔除(默認背面剔除)
void glEnable(GL_CULL_FACE);
// 關(guān)閉表面剔除(默認背面剔除)
void glDisable(GL_CULL_FACE);
// 用戶選擇剔除那個面(即可自定義剔除溉知,默認為正面)
void glCullFace(GLenum mode);
mode參數(shù)為:GL_FRONT, GL_BACK, GL_FRONT_AND_BACK, 默認為GL_BACK
// 用戶也可以指定正面
void glFrontFace(GLenum mode);
mode參數(shù)為:GL_CW, GL_CCW, 默認為GL_CCW
// 剔除正面實現(xiàn)
glCullFace(GL_BACK);
glFrontFace(GL_CW);
或
glCullface(GL_FRONT);
具體代碼實現(xiàn)
// 1. 清除窗口和深度緩沖區(qū)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 開啟正背面剔除
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glCullFace(GL_BACK);
// 2. 把攝像機矩陣壓入模型矩陣中
modelViweMatix.PushMatrix(viewFrame);
// 3. 設(shè)置繪圖顏色
GLfloat vRed[] = {1, 0, 0, 1};
// 后面代碼和上面一樣陨瘩,不再重復(fù)
實現(xiàn)效果如下圖:
可以看到,之前的問題已經(jīng)解決了级乍,可是又面臨了一個尷尬的問題舌劳,這個甜甜圈貌似有個很大的缺口,了解過圖形渲染的讀者肯定知道玫荣,這是深度問題甚淡,下面來了解一下。
深度
深度就是該像素點在3D世界中距離攝像機的距離捅厂,也就是Z值材诽。
深度緩存區(qū)就是一塊內(nèi)存區(qū)域,專門存儲著每個像素點(繪制在屏幕上的)深度值Z恒傻。Z越大脸侥,則距離屏幕越遠。
那么為什么需要深度緩沖區(qū)盈厘?
在不實用深度測試的時候睁枕,如果我們先繪制一個距離比較近的物體,在繪制距離遠的物體,則距離遠的位圖因為后繪制外遇,會把距離近的物體覆蓋掉注簿。有了深度緩沖區(qū)后,繪制物體的順序就不那么重要了跳仿。上面出現(xiàn)的大缺口诡渴,也就是這個問題造成的。
實際上菲语,只要存在深度緩沖區(qū)妄辩,OpenGL
都會把像素的深度值寫入到緩沖區(qū)中,除非調(diào)用glDepthMask(GL_FALSE)
來禁止寫入山上。
深度測試
深度緩沖區(qū)和顏色緩存區(qū)是對應(yīng)的眼耀。顏色緩存區(qū)存儲像素的顏色信息,而深度緩沖區(qū)存儲像素的深度信息佩憾。在決定是否繪制一個物體表面時哮伟,首先要將表面對應(yīng)的像素的深度值與當前深度緩存區(qū)中的值進行比較,如果大雨深度緩存區(qū)的值妄帘,則丟棄這部分楞黄,否則利用這個像素對應(yīng)的深度值和顏色值,分別更新深度緩存區(qū)和顏色緩存區(qū)抡驼。這個過程稱為深度測試鬼廓。
相關(guān)代碼
// 開啟深度測試
glEnable(GL_DEPTH_TEST);
// 在繪制場景前,清除顏色緩存區(qū)和深度緩沖區(qū)
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_GEPTH_BUFFER_BIT);
清除深度緩沖區(qū)默認值為1.0婶恼,表示最大的深度值,深度值的范圍為(0,1)之間柏副。值越小表示越靠近觀察者勾邦,反正表示距離觀察者越遠。
下面有關(guān)深度測試的判斷式
指定深度測試判斷模式
void glDepthFunc(GLEnum mode);
打開/阻斷 深度緩存區(qū)寫入
void glDepthMask(GKBool value);
value :GL_TURE
開啟寫入GL_FALSE
關(guān)閉寫入
最終的實現(xiàn)效果如下:
ZFighting閃爍問題
為什么會出現(xiàn)ZFighting閃爍問題
因為開啟深度測試后割择,OpenGL
就不會去繪制模型被遮擋的部分眷篇,這樣實現(xiàn)現(xiàn)實更加真實,但是由于深度緩存區(qū)精度的限制荔泳,對于深度相差無幾的情況下蕉饼,OpenGL
就可能出現(xiàn)不能正確判斷兩者深度值,會導(dǎo)致深度測試的結(jié)果不可預(yù)測玛歌,現(xiàn)實出來的現(xiàn)象會交錯閃爍昧港。
解決方式
- 第一步:啟用
Polygon Offset
方式解決
讓深度值之間產(chǎn)生間隔,可以理解為在執(zhí)行深度測試前支子,將立方體的深度值做一些細微的增加创肥,于是就能將重疊的2個圖形深度值之間有所區(qū)分。
// 啟用Polygon Offset方式
glEnable(GL_POLYGON_OFFSET_FILL);
參數(shù)列表:
GL_POLYGON_OFFSET_POINT 對應(yīng)光柵化模式:GL_POINT
GL_POLYGON_OFFSET_LINE 對應(yīng)光柵化模式:GL_LINE
GL_POLYGON_OFFSET_FILL 對應(yīng)光柵化模式:GL_FILL
-
第二步:指定偏移量
- 通過
glPolygon Offset
來指定.glPolygon Offset
需要2個參數(shù):factor
,units
. - 每個
Fragment
的深度值都會增加如下所示的偏移量:
Offset = ( m * factor ) + ( r * units);
m : 多邊形的深度的斜率的最大值,理解一個多邊形越是與近裁剪?平行叹侄,m就越接近于0.
r : 能產(chǎn)生于窗口坐標系的深度值中可分辨的差異最小值.r是由具體是由具體OpenGL
平臺指定的一個常量. - 一個?于0的
Offset
會把模型推到離你(攝像機)更遠的位置巩搏,相應(yīng)的?個小于0的Offset
會把模型拉近 - 一般?言,只需要將-1.0 和 -1 這樣簡單賦值給
glPolygon Offset
基本可以滿?足需求.
- 通過
第三步:關(guān)閉
Polygon Offset
glDisable(GL_POLYGON_OFFSET_FILL);
OK趾代,到此為止贯底,我們完美的把這個甜甜圈給渲染出來了。上面遇到的一些問題也得已解決撒强。