Note
這是對(duì)MIT Foundation of 3D Computer Graphics第6章的翻譯,本章講解了如何使用現(xiàn)代OpenGL渲染管線方式利用矢量、線性變換等知識(shí)實(shí)現(xiàn)簡(jiǎn)單的3D繪制。本書(shū)內(nèi)容仍在不斷的學(xué)習(xí)中,因此本文內(nèi)容會(huì)不斷的改進(jìn)。若有任何建議,請(qǐng)不吝賜教ninetymiles@icloud.com
注:文章中相關(guān)內(nèi)容歸原作者所有,翻譯內(nèi)容僅供學(xué)習(xí)參考。
另:Github項(xiàng)目CGLearning中擁有相關(guān)翻譯的完整資料、內(nèi)容整理、課程項(xiàng)目實(shí)現(xiàn)。
已經(jīng)完成的章節(jié)
- 第一章
- 第二章
- 第三章
- 第四章
- 第五章
- 第六章
- 第七章
- 第八章
- 第九章
- 第十章
- 第十一章
- 第十二章
- 第十三章
- 第十四章
- 第十五章
- 第十六章
- 第十七章
- 第十八章
- 第十九章
- 第二十章
- 第二十一章
- 第二十二章
- 第二十三章
- 附錄B-仿射函數(shù)基礎(chǔ)
Hello World 3D
我們終于要開(kāi)始講述,在前面章節(jié)中所學(xué)的幀(坐標(biāo)系)和變換的概念是如何在交互式3D圖像環(huán)境中實(shí)現(xiàn)的。閱讀本章之前,你應(yīng)該已經(jīng)瀏覽了附錄,那里我們講述如何設(shè)置基本的OpenGL程序。
6.1 坐標(biāo)和矩陣(Coordinates and Matrices)
我們從使用Cvec2,Cvec3,Cvec4數(shù)據(jù)類(lèi)型表達(dá)坐標(biāo)矢量開(kāi)始,坐標(biāo)矢量的表達(dá)是很有用的。我們還需要實(shí)現(xiàn)兩個(gè)相同尺寸Cvec類(lèi)型(u+v)的加法,以及與一個(gè)實(shí)數(shù)標(biāo)量r的乘法(r*v)。在Cvec4的情形中,我們稱(chēng)元素項(xiàng)為x,y,z,w。目前,w元素項(xiàng)對(duì)于點(diǎn)將總是為1,對(duì)于矢量總是為0。
接著,我們需要Matrix4數(shù)據(jù)類(lèi)型表達(dá)仿射矩陣。我們需要支持右乘一個(gè)Cvec4,(M * v),兩個(gè)矩陣的乘法,(M * N),反轉(zhuǎn)操作inv(M),和移項(xiàng)操作transpose(M)。
要生成有效的變換矩陣,我們利用下列操作
Matrix4 identity();
Matrix4 makeXRotation(double ang);
Matrix4 makeYRotation(double ang);
Matrix4 makeZRotation(double ang);
Matrix4 makeScale(double sx, double sy, double sz);
Matrix4 makeTranslation(double tx, double ty, double tz);
(C++中,從默認(rèn)構(gòu)造器中返回同一矩陣是有效的。)
要實(shí)現(xiàn)小節(jié)3.5和5.2.1的思路,我們需要操作tranFact(M),其返回Matrix4只是表達(dá)M的平移因子,就如在方程(3.1)中,同時(shí)還有linFact(M),其返回Matrix4類(lèi)型只是表達(dá)M的線性因子。也就是說(shuō),M = transFact(M) * linFact(M)。
要實(shí)現(xiàn)小節(jié)3.6的思路,我們需要normalMatrix(M)操作,其只是的線性因子的反轉(zhuǎn)調(diào)換移項(xiàng)(存儲(chǔ)在
Matrix4的左上角)。
要實(shí)現(xiàn)小節(jié)5.2.1的思路,我們需要函數(shù)doQtoOwrtA(Q,O,A),“關(guān)聯(lián)于A對(duì)O實(shí)施Q變換”,其只是返回。我們還需要函數(shù)
makeMixedFrame(O,E),其將和
都分解并且返回
。
6.2 繪制形狀(Drawing a Shape)
首先,要實(shí)現(xiàn)3D繪制,我們需要在OpenGL中設(shè)置更多的狀態(tài)變量
static void InitGLState(){
glClearColor(128./255., 200./255., 255./255., 0.);
glClearDepth(0.0);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_GREATER);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
}
這些調(diào)用的詳細(xì)含義在這本書(shū)中隨后被講述。在glClearColor調(diào)用中,我們不僅設(shè)置默認(rèn)的清理后的圖像色彩,而且也設(shè)置默認(rèn)的清理后的“z-緩存”值。我們還需要啟用深度,或者稱(chēng)為z-緩存并且告知OpenGL“更大的z值”意味著"離眼睛更近"。z-緩存在第11章中被詳細(xì)討論。為了效率起見(jiàn),我們還要告知OpenGL剔除任何背向眼睛的面(也就是說(shuō)不繪制)。當(dāng)圖像中的頂點(diǎn)看起來(lái)以順時(shí)針?lè)较蚺帕袝r(shí),這個(gè)面就是背向面。背向面剔除在小節(jié)12.2中被詳細(xì)討論。
現(xiàn)在返回主題。我們使用全局變量Matrix4 objRbt表達(dá)剛體矩陣,其將物體的正交標(biāo)準(zhǔn)幀關(guān)聯(lián)到世界幀,就如在表達(dá)式中一樣。我們使用全局變量
Matrix4 eyeRbt表達(dá)剛體矩陣,其關(guān)聯(lián)物體的正交標(biāo)準(zhǔn)幀到世界幀,就如在表達(dá)式
中一樣。
讓我們觀察下面繪制兩個(gè)三角形和一個(gè)立方體的代碼碎片。
地面是由兩個(gè)三角形構(gòu)成的一個(gè)正方形。立方體由六個(gè)正方形構(gòu)成,也就是說(shuō),12個(gè)三角形。針對(duì)每個(gè)頂點(diǎn),我們存儲(chǔ)其位置和法線矢量的3D物體坐標(biāo)。所有這種數(shù)據(jù)相似于附錄A中所完成的情形。
GLfloat floorVerts[18] = {
-floor_size, floor_y, -floor_size,
floor_size, floor_y, floor_size,
floor_size, floor_y, -floor_size,
-floor_size, floor_y, -floor_size,
-floor_size, floor_y, floor_size,
floor_size, floor_y, floor_size
};
GLfloat floorNorms[18] = { 0,1,0, 0,1,0, 0,1,0, 0,1,0, 0,1,0, 0,1,0 };
GLfloat cubeVerts[36 * 3]= {
-0.5, -0.5, -0.5,
-0.5, -0.5, +0.5,
+0.5, -0.5, +0.5,
// 33 more vertices not shown
};
// Normals of a cube.
GLfloat cubeNorms[36 * 3] = {
+0.0, -1.0, +0.0,
+0.0, -1.0, +0.0,
+0.0, -1.0, +0.0,
// 33 more vertices not shown
};
我們現(xiàn)在初始化頂點(diǎn)緩存對(duì)象(VBOs),其為頂點(diǎn)數(shù)據(jù)集合的句柄(handles),諸如頂點(diǎn)位置和法線。
static GLuint floorVertBO, floorNormBO, cubeVertBO, cubeNormBO;
static void initVBOs(void){
glGenBuffers(1,&floorVertBO);
glBindBuffer(GL_ARRAY_BUFFER,floorVertBO);
glBufferData( GL_ARRAY_BUFFER, 18 * sizeof(GLfloat), floorVerts, GL_STATIC_DRAW);
glGenBuffers(1,&floorNormBO); glBindBuffer(GL_ARRAY_BUFFER,floorNormBO);
glBufferData( GL_ARRAY_BUFFER, 18 * sizeof(GLfloat), floorNorms, GL_STATIC_DRAW);
glGenBuffers(1,&cubeVertBO);
glBindBuffer(GL_ARRAY_BUFFER,cubeVertBO);
glBufferData( GL_ARRAY_BUFFER, 36 * 3 * sizeof(GLfloat), cubeVerts, GL_STATIC_DRAW);
glGenBuffers(1,&cubeNormBO); glBindBuffer(GL_ARRAY_BUFFER,cubeNormBO);
glBufferData( GL_ARRAY_BUFFER, 36 * 3 * sizeof(GLfloat), cubeNorms, GL_STATIC_DRAW);
}
我們使用其位置和法線VBOs繪制物體如下
void drawObj(GLuint vertbo, GLuint normbo, int numverts){
glBindBuffer(GL_ARRAY_BUFFER,vertbo);
safe_glVertexAttribPointer(h_aVertex); safe_glEnableVertexAttribArray(h_aVertex);
glBindBuffer(GL_ARRAY_BUFFER,normbo);
safe_glVertexAttribPointer(h_aNormal); safe_glEnableVertexAttribArray(h_aNormal);
glDrawArrays(GL_TRIANGLES,0,numverts);
safe_glDisableVertexAttribArray(h_aVertex); safe_glDisableVertexAttribArray(h_aNormal);
}
我們現(xiàn)在可以觀察我們的顯示函數(shù)display。
static void display(){
safe_glUseProgram(h_program_);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Matrix4 projmat = makeProjection(frust_fovy, frust_ar, frust_near,frust_far);
sendProjectionMatrix(projmat);
Matrix4 MVM = inv(eyeRbt);
Matrix4 NMVM = normalMatrix(MVM);
sendModelViewNormalMatrix(MVM,NMVM);
safe_glVertexAttrib3f(h_aColor, 0.6, 0.8, 0.6); drawObj(floorVertBO,floorNormBO,6);
MVM = inv(eyeRbt) * objRbt;
NMVM = normalMatrix(MVM);
sendModelViewNormalMatrix(MVM,NMVM);
safe_glVertexAttrib3f(h_aColor, 0.0, 0.0, 1.0); drawObj(cubeVertBO,cubeNormBO,36);
glutSwapBuffers();
if (glGetError() != GL_NO_ERROR){
const GLubyte * errString;
errString=gluErrorString(errCode);
printf("error: %s\n", errString);
}
}
makeProjection返回描述“虛擬相機(jī)”內(nèi)部的特殊種類(lèi)的矩陣。相機(jī)被幾個(gè)參數(shù)-視域、窗口縱橫比率、以及所謂的近值和遠(yuǎn)值-所描述。sendProjectionMatrix把這種“相機(jī)矩陣”發(fā)送到頂點(diǎn)著色器,并且將其放置在被命名為uProjMatrix的變量中。我們?cè)诤竺嬲鹿?jié)中會(huì)學(xué)習(xí)更多關(guān)于這個(gè)矩陣的內(nèi)容,但是現(xiàn)在,你只要在本書(shū)的網(wǎng)站上找到這個(gè)代碼即可。
在我們的程序中,存儲(chǔ)在一個(gè)VBO中的頂點(diǎn)坐標(biāo)為頂點(diǎn)的物體坐標(biāo)。因?yàn)殇秩酒髯罱K需要眼睛坐標(biāo),我們將矩陣也發(fā)送到API。矩陣經(jīng)常被稱(chēng)作MVM或者模型視圖矩陣。(要繪制地面,我們使用
)。頂點(diǎn)著色器(下面會(huì)講述),會(huì)采納這種頂點(diǎn)數(shù)據(jù)并且執(zhí)行乘法
產(chǎn)生用于渲染的眼睛坐標(biāo)。同樣地,用于法線的所有坐標(biāo),需要被乘以一個(gè)關(guān)聯(lián)的法線矩陣,其允許我們從物體坐標(biāo)變換到眼睛坐標(biāo)。
我們的程序sendModelViewNormalMatrix(MVM,NMVM)將MVM和法線矩陣發(fā)送到頂點(diǎn)著色器,并且把它們放置在被命名為uModelViewMatrix和uNormalMatrix的變量中。
順便一提:在計(jì)算機(jī)圖形中,被掛載到三角形一個(gè)頂點(diǎn)的法線矢量,隨后被用于著色,不必須是扁平三角形的真正幾何法線。例如,如果我們使用三角形網(wǎng)格繪制一個(gè)球體形狀近似它,我們可能想讓三角形的三個(gè)頂點(diǎn)3個(gè)有區(qū)別的法線,要更好匹配球體的形狀(參考圖示)。在著色圖片中這會(huì)導(dǎo)致更平滑和更少曲面細(xì)分的外觀。如果我們要各個(gè)面看起來(lái)扁平,就如一個(gè)立方體的各個(gè)面,隨后我們就把每個(gè)三角形的實(shí)際幾何法線傳遞給OpenGL。
對(duì)safe_glVertexAttrib3f的調(diào)用傳遞了3個(gè)浮點(diǎn)數(shù)到頂點(diǎn)著色器中被“指向”到句柄(handle)h_aColor的變量,這個(gè)句柄“指向”頂點(diǎn)著色器中被命名為aColor的屬性變量。任何屬性變量的設(shè)置保留有效直到其被另一個(gè)safe_glVertexAttrib3f調(diào)用所設(shè)置。如此除非被改變,它會(huì)被綁定到每個(gè)隨后的頂點(diǎn)上。被發(fā)送到aColor的數(shù)據(jù)可以用任何我們想借助頂點(diǎn)著色器的方式被解讀。在我們的情形中我們會(huì)解讀這種數(shù)據(jù)為頂點(diǎn)的“rgb色彩”坐標(biāo)。

Figure 6.1: 在圖像中,我們隨意指定頂點(diǎn)處任何我們希望的法線。這些法線(就像所有屬性變量)針對(duì)三角形內(nèi)的所有點(diǎn)被插值。在我們的碎片著色器中我們可以使用這些被插值的法線模擬光照并且確定色彩。當(dāng)三角形的真實(shí)法線被給出,然后我們獲得一個(gè)嵌入的外觀。如果我們指定法線近似某種底層的平滑形狀,我們獲得一個(gè)平滑渲染。
6.3 頂點(diǎn)著色器(The Vertex Shader)
我們的頂點(diǎn)著色器采用每個(gè)頂點(diǎn)位置的物體坐標(biāo),然后把它們變?yōu)檠劬ψ鴺?biāo),就如在小節(jié)5.1中所描述。它同樣變換頂點(diǎn)的法線坐標(biāo)。
這里為頂點(diǎn)著色器的完整代碼:
#version 330
uniform Matrix4 uModelViewMatrix;
uniform Matrix4 uNormalMatrix;
uniform Matrix4 uProjMatrix;
in vec3 aColor;
in vec4 aNormal;
in vec4 aVertex;
out vec3 vColor;
out vec3 vNormal; out vec4 vPosition;
void main() {
vColor = aColor;
vPosition = uModelViewMatrix * aVertex;
vec4 normal = vec4(aNormal.x, aNormal.y, aNormal.z, 0.0);
vNormal = vec3(uNormalMatrix * normal);
gl_Position = uProjMatrix * vPosition;
}
這個(gè)著色器非常易于理解。其不改變地傳遞色彩變量aColor到輸出的vColor。執(zhí)行矩陣-矢量乘法將物體坐標(biāo)轉(zhuǎn)換為眼睛坐標(biāo),并且把它們發(fā)送為輸出。
同時(shí)執(zhí)行矩陣-矢量乘法將法線的物體坐標(biāo)轉(zhuǎn)換為眼睛坐標(biāo)并且把這些發(fā)送為輸出。
最終它使用特殊的(并且仍然沒(méi)有被完全解釋過(guò)的)相機(jī)投射矩陣以獲得新種類(lèi)的被稱(chēng)作頂點(diǎn)的裁切坐標(biāo),并將其作為輸出發(fā)送到gl_Position。在這種情形中,不像我們?cè)诟戒汚中所看到的更簡(jiǎn)單的代碼,gl_Position實(shí)際為4部件坐標(biāo)矢量。在第10-12章,我們會(huì)更深入地確切討論裁切坐標(biāo)數(shù)據(jù)如何被用于放置頂點(diǎn)到屏幕之上。
6.4 接下來(lái)發(fā)生的事情(What Happens Next)
OpenGL接下來(lái)對(duì)頂點(diǎn)著色器的輸出所做事情的細(xì)節(jié)會(huì)在之后的章節(jié)中會(huì)被講述。幾乎每個(gè)之后的段落都將需要被擴(kuò)展為完整的章節(jié)以解釋所要發(fā)生的事情。但是現(xiàn)在,這里是我們需要知道的主要內(nèi)容。
裁切坐標(biāo)被渲染器用于確定在屏幕上哪里放置頂點(diǎn),從而決定三角形會(huì)被繪制在哪里。一旦OpenGL獲得組成一個(gè)三角形的3個(gè)頂點(diǎn)的裁切坐標(biāo),其計(jì)算屏幕上哪些像素落入三角形之內(nèi)。針對(duì)每個(gè)這種像素,它決定這個(gè)像素距離3個(gè)頂點(diǎn)中的每個(gè)有多么“遠(yuǎn)離”。這被用于決定如何混合或者在3個(gè)頂點(diǎn)的變異變量上插值。
在每個(gè)像素上被插值的變量隨后被寫(xiě)著色器的用戶(hù)所使用,著色器針對(duì)每個(gè)像素被獨(dú)立調(diào)用。下面是最簡(jiǎn)單可能的碎片著色器:
in vec3 vColor;
out fragColor;
void main() {
fragColor = vec4(vColor.x, vColor.y, vColor.z, 1.0);
}
這個(gè)著色器接收針對(duì)這個(gè)像素被插值的色彩數(shù)據(jù),然后將其發(fā)送到輸出的變量fragColor。這隨后被發(fā)送到屏幕作為像素的色彩。(關(guān)于這第四個(gè)值1.0被稱(chēng)作alpha,或者透明度值,并且還不會(huì)引起我們的關(guān)注。)裁切坐標(biāo)也被用于決定三角形離屏幕有多遠(yuǎn)。當(dāng)z緩存被開(kāi)啟,這種信息被用于確定,在每個(gè)像素上,哪個(gè)三角形最接近并且因而被繪制。因?yàn)檫@個(gè)決定以逐像素方式在一個(gè)像素上被做出,甚至復(fù)雜的互相滲透的三角形排列會(huì)被正確繪制。
注意當(dāng)使用上面的碎片,我們不使用變異變量vPosition和vNormal,并且在這種情形中,并不真正要發(fā)送它們?yōu)轫旤c(diǎn)著色器中的輸出。下面為一個(gè)稍微更復(fù)雜和更真實(shí)的使用這種數(shù)據(jù)的著色器。
#version 330
uniform vec3 uLight;
in vec3 vColor;
in vec3 vNormal;
in vec4 vPosition;
out fragColor;
void main() {
vec3 toLight = normalize(uLight - vec3(vPosition));
vec3 normal = normalize(vNormal);
float diffuse = max(0.0, dot(normal, toLight));
vec3 intensity = vColor * diffuse;
fragColor = vec4(intensity.x, intensity.y, intensity.z, 1.0);
}
此處我們假設(shè)uLight為點(diǎn)光源的眼睛坐標(biāo),并且這個(gè)數(shù)據(jù)已經(jīng)借助來(lái)自我們的主程序使用對(duì)應(yīng)safe_glVertexUniform3f調(diào)用被恰當(dāng)?shù)貍鬟f到著色器中。存儲(chǔ)于vNormal和vPosition變量中的數(shù)據(jù),就像vColor相關(guān)的數(shù)據(jù),被從頂點(diǎn)數(shù)據(jù)上插值。因?yàn)椴逯当煌瓿傻姆绞剑?code>vPosition中的數(shù)據(jù)表達(dá)了在這個(gè)像素上被看到的三角形內(nèi)的幾何點(diǎn)。其余代碼做了一個(gè)簡(jiǎn)單計(jì)算,計(jì)算多個(gè)矢量并且執(zhí)行點(diǎn)積。目標(biāo)是模擬漫射的反射或模糊(和明亮對(duì)立)材料。我們會(huì)在第14章中回顧這種計(jì)算的細(xì)節(jié)。
6.5 使用矩陣定位和移動(dòng)(Placing and Moving With Matrices)
返回我們最初的代碼,剩下的就是初始化eyeRbt和objRbt變量并且同時(shí)解釋我們?nèi)绾胃滤鼈?。在這種簡(jiǎn)單情形中,我們可以開(kāi)始于
Matrix4 eyeRbt = makeTranslation(Vector3(0.0, 0.0, 4.0));
Matrix4 objRbt = makeTranslation(Vector3(-1,0,-1)) * makeXRotation(22.0);
在這種情形中,我們的所有幀開(kāi)始于和世界幀的軸對(duì)齊的幀。眼睛幀被關(guān)聯(lián)于世界幀的z軸被平移+4單位?;貞浺幌拢覀兊摹跋鄼C(jī)”正看向眼睛幀的負(fù)z軸,因而眼睛正看向世界幀的原點(diǎn)。物體幀在世界幀中被向后和向“左側(cè)”平移一點(diǎn)并且圍繞自己的x-軸旋轉(zhuǎn)。觀察圖像。
讓我們現(xiàn)在允許用戶(hù)移動(dòng)物體,回憶在附錄A中被記錄的運(yùn)動(dòng)回調(diào)函數(shù),我們計(jì)算水平增量deltax。其為當(dāng)鼠標(biāo)左鍵被摁下時(shí)的鼠標(biāo)移位。垂直移位能夠同樣地被計(jì)算。
我們現(xiàn)在可以添加下列行到運(yùn)動(dòng)函數(shù)中移動(dòng)物體。
Matrix4 Q = makeXRotation(deltay) * makeYRotation(deltax);
Matrix4 A = makeMixedFrame(objRbt,EyeRbt);
objRbt = doQtoOwrtA(Q, objRbt, A);
我們也可以使用鼠標(biāo)運(yùn)動(dòng)增強(qiáng)運(yùn)動(dòng)函數(shù),當(dāng)右鍵被摁下,要平移物體,使用代碼Q=makeTranslation(Vector3(deltax, deltay, 0) * 0.01)
當(dāng)中鍵被摁下時(shí),我們能夠用鼠標(biāo)運(yùn)動(dòng)平移物體更近和更遠(yuǎn),使用代碼
Q=makeTranslation(Vector3(0, 0, -deltay) * 0.01)
如果我們希望借助輔助幀移動(dòng)眼睛,那么我們使用代碼:eyeRbt = doQtoOwrtA(inv(Q), eyeRbt, A).
如果我們希望執(zhí)行自我運(yùn)動(dòng)(ego motion),就如我們轉(zhuǎn)動(dòng)頭,那么我們使用代碼:
eyeRbt = doQtoOwrtA(inv(Q), eyeRbt, eyeRbt).
在最后兩種情形的每一種中,我們反轉(zhuǎn)Q以便鼠標(biāo)運(yùn)動(dòng)在更需要的方向上產(chǎn)生圖像運(yùn)動(dòng)。