上次讀書筆記主要是學習了應用三維座標變換矩陣對二維的圖形進行變換,並附帶介紹了GLSL語言的編譯、連接相關的知識,以後介紹了GLSL中變量的修飾符,着重介紹了uniform修飾符,來向着色器程序傳入輸入參數。ios
此次讀書筆記的內容相對有趣一些,主要是和園友們分享討論三維座標變換矩陣在三維幾何體上的應用,以及介紹一下如何實現三維圖形與用戶操做的交互。這一次筆記在三維編程中也是很是重要的——咱們最後開發的三維程序最終就是要和目標用戶進行交互的。算法
以前一直沒有在博客上放過gif格式的動畫圖片,此次由於涉及交互操做,因此想在博文上貼幾張gif格式的動畫圖片。爲此,我得找一款好的錄屏軟件和格式轉換軟件,結果找了半天沒找到使用比較方便且製做出來效果比較好的軟件,還下到了流氓軟件,難免吐槽下——找PC好軟件仍是得多長几個心眼啊,最好是上官網去下載!最後找到了一個直接能夠錄屏並生成gif,且支持編輯,在此推薦給你們——摳摳視頻秀。不過,最後生成的動畫圖片比較大,考慮到在手機上逛園子童鞋,博主就不上傳了,太多圖片仍是很費流量的,我把代碼和可執行文件傳到網盤上了,須要的童鞋能夠自行去下載。哎,瞎忙乎了一場~~~~(>_<)~~~~。編程
好,談正事,讓咱們繼續踏上OpenGL學習之路——第四站。數組
既然是學習三維座標的變換,總得有一個變爲對象——三維場景(幾何體)。在讀書筆記(三)中咱們用的是正方形——一個簡單得不能再簡單的正方形。這一次仍然使用一個最簡單的三維圖形——立方體。首先仍是讓咱們來看看例子程序代碼,其實和以前講的幾乎沒什麼區別,也就是那麼幾步:開闢緩衝區、上傳頂點數據、設置頂點屬性,最後渲染圖形,具體程序代碼以下:緩存
1 #include <iostream> 2 3 #include "GameFramework/StdAfx.h" 4 #include "Resource/GPUProgram.h" 5 #include "AlgebraicEntity/Matrix.h" 6 7 GPUProgram program; 8 9 void initialize_04() 10 { 11 // --------------準備立方體頂點數據-------------- 12 // 0/----------------/1 13 // /| /| 14 // / | / | 15 //4/--|-------------/5 | 16 // | | | | 17 // | | | | 18 // | | | | 19 // | 3/-------------|--/2 20 // | / | / 21 // |/ |/ 22 //7/----------------/6 23 GLfloat cube_vertices[8][4] = { 24 { -0.5f, 0.5f, -0.5f, 1.0f }, 25 { 0.5f, 0.5f, -0.5f, 1.0f }, 26 { 0.5f, -0.5f, -0.5f, 1.0f }, 27 { -0.5f, -0.5f, -0.5f, 1.0f }, 28 { -0.5f, 0.5f, 0.5f, 1.0f }, 29 { 0.5f, 0.5f, 0.5f, 1.0f }, 30 { 0.5f, -0.5f, 0.5f, 1.0f }, 31 { -0.5f, -0.5f, 0.5f, 1.0f } 32 }; 33 34 // --------------準備頂點顏色數據-------------- 35 GLfloat cube_colors[8][4] = { 36 { 1.0f, 1.0f, 1.0f, 1.0f }, 37 { 1.0f, 1.0f, 0.0f, 1.0f }, 38 { 1.0f, 0.0f, 1.0f, 1.0f }, 39 { 0.0f, 1.0f, 1.0f, 1.0f }, 40 { 0.0f, 0.0f, 1.0f, 1.0f }, 41 { 0.0f, 1.0f, 0.0f, 1.0f }, 42 { 1.0f, 0.0f, 0.0f, 1.0f }, 43 { 0.0f, 0.0f, 0.0f, 1.0f } 44 }; 45 46 // --------------建立緩衝區對象,分配空間,上傳數據-------------- 47 GLuint buffer_ID; 48 glGenBuffers(1, &buffer_ID); 49 glBindBuffer(GL_ARRAY_BUFFER, buffer_ID); 50 glBufferData(GL_ARRAY_BUFFER, 51 sizeof(cube_vertices) + sizeof(cube_colors), NULL, GL_STATIC_DRAW); 52 glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(cube_vertices), cube_vertices); 53 glBufferSubData(GL_ARRAY_BUFFER, sizeof(cube_vertices), sizeof(cube_colors), cube_colors); 54 55 // --------------建立並設置頂點屬性對象-------------- 56 GLuint VAO_ID; 57 glGenVertexArrays(1, &VAO_ID); 58 glBindVertexArray(VAO_ID); 59 glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0)); 60 glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(sizeof(cube_vertices))); 61 glEnableVertexAttribArray(0); 62 glEnableVertexAttribArray(1); 63 64 // --------------準備頂點索引數據-------------- 65 GLushort vertex_indices[] = { 66 1, 5, 0, 4, 3, 7, 2, 6, 67 7, 4, 6, 5, 1, 2, 0, 3 68 }; 69 GLuint EBO_ID; 70 glGenBuffers(1, &EBO_ID); 71 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_ID); 72 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(vertex_indices), vertex_indices, GL_STATIC_DRAW); 73 74 program.AddShader(GL_VERTEX_SHADER, "F:/VC++遊戲編程/OpenGLGuide/OpenGL05/cube.vert"); 75 program.AddShader(GL_FRAGMENT_SHADER, "F:/VC++遊戲編程/OpenGLGuide/OpenGL05/cube.frag"); 76 glUseProgram(program.CreateGPUProgram()); 77 } 78 79 void display_04() 80 { 81 glClear(GL_COLOR_BUFFER_BIT); 82 83 glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, BUFFER_OFFSET(0)); 84 glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, BUFFER_OFFSET(8 * sizeof(GLushort))); 85 } 86 87 int main(int argc, char **argv) 88 { 89 glutInit(&argc, argv); 90 glutInitDisplayMode(GLUT_RGBA); 91 glutInitWindowSize(512, 512); 92 glutInitContextVersion(3, 3); 93 glutInitContextProfile(GLUT_CORE_PROFILE); 94 glutCreateWindow("學習之路(四)"); 95 96 glewExperimental = TRUE; 97 if (glewInit()) 98 { 99 std::cerr << "Unable to initialize GLEW... Exiting..." << std::endl; 100 std::exit(EXIT_FAILURE); 101 } 102 103 initialize_04(); 104 glutDisplayFunc(display_04); 105 106 glutMainLoop(); 107 return 0; 108 }
能夠看出:程序的總體框架與以前是相似的,由初始化函數(initialize_04)和繪製函數(display_04)構成。其中初始化函數負責數據的準備和輸入給OpenGL,這裏的數據包括立方體頂點數據(這裏給出的是齊次座標)、頂點顏色數據以及頂點索引數據(在頂點數組中的索引位置,從0開始)。在咱們的示例中,數據準備是比較簡單的,但在實際項目開發時,數據的準備一般會很複雜。繪製函數則調用OpenGL繪製相關的API來實現幾何體的繪製,這裏咱們將三角形拆開爲兩個三角形條帶(見下圖),這樣繪製立方體的過程就是繪製兩個三角形帶(triangle strip)。數據結構
固然,爲了繪製,還須要加入頂點着色器和片元着色器。這兩個着色器都很簡單,其中頂點着色器是從傳遞輸入頂點數據到GLSL的內置變量gl_Position中,並將頂點顏色數據直接輸出,由OpenGL進行插值,而後傳到片元着色器;片元着色器獲得光柵化(插值)以後的顏色值,直接輸出便可,兩個着色程序代碼以下:框架
頂點着色程序:ide
1 #version 330 core 2 3 layout(location = 0) in vec4 vertex_position; 4 layout(location = 1) in vec4 vertex_color; 5 6 out vec4 temp_color; 7 8 void main() 9 { 10 gl_Position = vertex_position; 11 temp_color = vertex_color; 12 }
片元着色程序:函數
1 #version 330 core 2 3 in vec4 temp_color; 4 5 out vec4 out_color; 6 7 void main() 8 { 9 out_color = temp_color; 10 }
下圖即是上述程序的執行結果,看上去像一個正方形,但其實它是立方體,稍後咱們對它作了旋轉操做以後就能夠看到廬山真面目了。oop
看完了繪製結果,讓咱們繼續學習上述程序中用到的新的OpenGL API。第一個新的API就是往緩存中分段上傳數據,例子中咱們往緩存中分別上傳了頂點座標數據和頂點顏色數據。咱們以前用glBufferData來分配並上傳數據,但該函數只能一次性輸入全部數據,若是客戶端的數據保存在不一樣數據結構中,但想上傳到同一個目標緩衝區對象時,就得使用下面這個新的API了:
void glBufferSubData(GLenum target offset, GLintptr offset, GLsizeiptr size, const GLvoid *data) 函數功能:將data指針所指向的、大小爲size個字節的數據上傳到當前綁定的緩衝區對象所管理的、偏移量爲offset的內存區域。 target ——目標緩衝區對象類型 offset ——緩衝區對象所管理內存的偏移量 size ——上傳數據的字節數 data ——指向數據的指針
用示意圖更形象的表達以下:
經過上述接口,就能夠將頂點座標數據、頂點的其它屬性(如顏色)一些數據分開存放,分段上傳至緩衝區對象管理的顯存中。
以前博文中,咱們都是使用glDrawArray來繪製幾何體的。這個繪製API的頂點數據是從綁定到目標爲GL_ARRAY_BUFFER緩衝區對象獲得。除此以外,OpenGL還支持經過頂點的索引值來間接獲得頂點數據來繪製幾何體,這樣作的好處很顯然的——避免頂點數組中的重複項。所使用的OpenGL繪製API爲glDrawElements(),使用這種繪製方式,除了須要提供頂點數組數據外,還須要須要告訴OpenGL,繪製時所使用的頂點索引數組,這時就要使用另外一個綁定目標——GL_ELEMENT_ARRAY_BUFFER(見程序71~72行)——的緩衝區對象了,具體使用方法與綁定目標爲GL_ARRAY_BUFFER的緩衝區對象是如出一轍的。好了,到此爲止,初始化部分已經完成了,我的感受有了讀書筆記(一)做爲基礎,這裏理解起來就很簡單了。下面,來看看剛纔提到的、使用索引來繪製幾何體的API,其函數簽名爲:
void glDrawElement(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices) 函數功能:使用count個索引值來繪製一個mode類型的圖元,索引值類型爲type,索引值數據保存在GL_ELEMENT_ARRAY_BUFFER綁定的緩衝區對象中,繪製使用的索引數據爲偏移量indices開始的count個索引值 mode ——繪製圖元的類型 count ——用到的索引值個數 type ——索引值的類型 indices ——索引值在緩衝區所管理內存中的偏移量
這個命令的功能和glDrawArray同樣,區別在於所獲取頂點數據的方式不一樣:glDrawArray直接使用頂點數組來繪製圖元;glDrawElements則使用頂點索引值來間接獲取繪製圖元的頂點來繪製幾何體。
上一小節繪製出來的立方體是靜止不動的,那麼如何讓它動起來呢?這就須要將變換矩陣應用到幾何圖像的頂點數據上,爲此要修改源程序。首先,修改頂點着色器,在頂點着色器中將頂點數據傳遞給內置變量gl_Position變量前,對它作一次矩陣變換,改變以後的着色器程序以下(改動部分已由紅色字符標出):
1 #version 330 core 2 3 uniform mat4 mat_transform; 4 5 layout(location = 0) in vec4 vertex_position; 6 layout(location = 1) in vec4 vertex_color; 7 8 out vec4 temp_color; 9 10 void main() 11 { 12 gl_Position = mat_transform * vertex_position; 13 temp_color = vertex_color; 14 }
着色器程序中用到的矩陣數據要由客戶端輸入,本文中立方體體的變換方式由用戶決定,客戶端修改包括兩部分:
1. 這須要在main函數中向GLUT註冊接收鍵盤輸入回調函數來接收用戶的輸入,以下(紅色標出部分):
1 int main(int argc, char **argv) 2 { 3 glutInit(&argc, argv); 4 glutInitDisplayMode(GLUT_RGBA); 5 glutInitWindowSize(512, 512); 6 glutInitContextVersion(3, 3); 7 glutInitContextProfile(GLUT_CORE_PROFILE); 8 glutCreateWindow("學習之路(四)"); 9 10 glewExperimental = TRUE; 11 if (glewInit()) 12 { 13 std::cerr << "Unable to initialize GLEW... Exiting..." << std::endl; 14 std::exit(EXIT_FAILURE); 15 } 16 17 initialize_04(); 18 glutDisplayFunc(display_04); 19 glutKeyboardFunc(display_keydown); 20 21 glutMainLoop(); 22 return 0; 23 }
2. 定義鍵盤輸入回調函數display_keydown,以下:
1 void display_keydown(unsigned char key, int x, int y) 2 { 3 // --------------準備矩陣數據-------------- 4 Matrix4X4 mat; 5 if (key == 'x') // 繞x軸順時針旋轉 6 { 7 mat = Matrix4X4::CreateRotateMatrix(PI / 12, Vector3D(1.0, 0.0, 0.0)); 8 } 9 else if (key == 'y') // 繞y軸順時針旋轉 10 { 11 mat = Matrix4X4::CreateRotateMatrix(PI / 12, Vector3D(0.0, 1.0, 0.0)); 12 } 13 else if (key == 'z') // 繞z軸順時針旋轉 14 { 15 mat = Matrix4X4::CreateRotateMatrix(PI / 12, Vector3D(0.0, 0.0, 1.0)); 16 } 17 else if (key == '+') // 放大 18 { 19 mat = Matrix4X4::CreateScaleMatrix(1.1); 20 } 21 else if (key == '-') // 縮小 22 { 23 mat = Matrix4X4::CreateScaleMatrix(0.9); 24 } 25 else if (key == 'l') // 左平移 26 { 27 mat = Matrix4X4::CreateTranslateMatrix(Vector3D(-0.1, 0.0, 0.0)); 28 } 29 else if (key == 'r') // 右平移 30 { 31 mat = Matrix4X4::CreateTranslateMatrix(Vector3D(0.1, 0.0, 0.0)); 32 } 33 mat_transform = mat * mat_transform; 34 35 // --------------上傳矩陣數據-------------- 36 glUniformMatrix4fv(mat_location, 1, GL_TRUE, mat_transform._m); 37 38 // --------------繪製圖像-------------- 39 glutPostRedisplay(); 40 }
各輸入字符所表明的含義在代碼註釋中已經詳細說明了,在此再也不贅述。回調函數中,根據不一樣的用戶輸入,生成讀書筆記(二)中介紹的旋轉、平移、縮放矩陣;而後讓它與原有的變換矩陣(保存在全局變量mat_transform中)相乘,須要注意的是:新的變換矩陣必定要放在乘號(*)的左邊——矩陣乘法不具備交換性;經過函數glUniformMatrix4fv向OpenGL上傳新的複合變換矩陣,最後調用glutPostRedisplay()函數從新繪製圖像便可。另外,在初始化函數initialize_04中也須要傳入單位矩陣做爲初始的變換矩陣,不然一開始會沒有圖像。運行程序,經過鍵盤輸入回調函數中給出的字符,能夠獲得下圖所示的運行結果:
第二節中的操做均由鍵盤輸入的,但在實際使用中,用戶經常是用鼠標來操做。下面咱們來瞅瞅,如何經過鼠標怎麼驅動物體的平移、旋轉和縮放的。
平移操做能夠說是最簡單的了,起始點和終止點至關於肯定了平移向量。不過鼠標給出的是像素座標,在求平移向量以前須要將像素座標轉換爲世界座標系,轉換公式以下:
特別注意的是,屏幕像素座標系xx軸與OpenGL的世界座標系下的xx軸是同向的,而yy軸剛好相反——是反響的,因此上述變換公式從形式上看,xx和yy相差一個負號,將其封裝爲一個輔助函數,以下:
1 Point3D Point2DHelper::ConvertPointFromScreenToWorld( const Point2D& pt2D ) 2 { 3 double dWorldX = 2 * pt2D.x / m_iScreenWidth - 1.0; 4 double dWorldY = 1.0 - 2 * pt2D.y / m_iScreenHeight; 5 double dWorldZ = 0.0; 6 return Point3D(dWorldX, dWorldY, dWorldZ); 7 }
這裏,m_iScreenWidth和m_iScreenHeight分別爲窗口的寬和高。獲得起始點和終止點的座標後,根據讀書筆記(二)中所講的知識點就能夠求得平移變換矩陣。客戶端的代碼將在3.4節與縮放、旋轉變換一併給出。
鼠標驅動縮放形式有許多種——只要將向量映射爲一個數便可,這裏咱們採用的策略是住右下方向拖拽是縮小,往左上方向拖拽爲放大,獲得以下映射變換公式:
獲得縮放係數以後,根據讀書筆記(二)所介紹的知識即可求得縮放矩陣,客戶端代碼將在3.4節給出。
剛纔主要闡述了經過鼠標實現物體的平移、旋轉。平移只要獲得平移向量就能夠了,縮放只要獲得縮放係數便可,但對於旋轉,則沒有那麼簡單了,下面就來具體學習一下吧!經過鼠標來驅動物體的旋轉,本質就是經過屏幕上的兩個點,肯定旋轉角度和旋轉軸,肯定這兩個參數的算法稱爲ArcBall算法。此算法能夠用下面示意圖來分析:
已知:黑色的二維座標系OxyOxy是屏幕座標系,橙色的三維座標系OxyzOxyz是OpenGL的世界座標系;SS點是鼠標的起始點,EE點爲鼠標的終止點;肯定S到E的旋轉矩陣。ArcBall算法是按下面步驟執行的:
第一步:將屏幕當作一個"z>0z>0"的半球面(這樣就能旋轉了嘛!),將鼠標起點SS和終點EE往半球面上投影,投影方法很簡單,xx和yy保持不變,zz按下述規則求出:
第二步:求旋轉軸,也很簡單,即向量OS′→OS′→和向量OE′→OE′→張成的平面的法向量,即:
第三步:求旋轉角度,也不難,即向量OS′→OS′→和向量OE′→OE′→的夾角,即:
第四步:根據旋轉軸和旋轉角度,求得旋轉矩陣(具體方法見讀書筆記(二))。
將上述步驟封裝在ArcBall類中,具體程序代碼以下:
1 Matrix4X4 ArcBall::CreateRotateMatrix( 2 const Point3D& ptPlaneStart, const Point3D& ptPlaneEnd ) 3 { 4 // ----------------相等,直接返回單位矩陣---------------- 5 if (ptPlaneStart == ptPlaneEnd) 6 { 7 Matrix4X4 mat; 8 return mat; 9 } 10 11 // ----------------投影至半球---------------- 12 Point3D ptSphereStart = ProjectPointToSemiSphere_i(ptPlaneStart); 13 Point3D ptSphereEnd = ProjectPointToSemiSphere_i(ptPlaneEnd); 14 15 // ----------------求旋轉軸---------------- 16 Point3D ptOrigin(0.0, 0.0, 0.0); 17 Vector3D vOriginToStart(ptSphereStart - ptOrigin); 18 Vector3D vOriginToEnd(ptSphereEnd - ptOrigin); 19 Vector3D vRotate = vOriginToEnd.CrossProduct(vOriginToStart); 20 vRotate.Normalize(); 21 22 // ----------------求旋轉角度---------------- 23 double dTheta = std::acos(std::max(std::min(vOriginToStart.DotProduct(vOriginToEnd), 1.0), -1.0)); 24 25 // ----------------生成旋轉矩陣---------------- 26 return Matrix4X4::CreateRotateMatrix(dTheta, vRotate); 27 } 28 29 Point3D ArcBall::ProjectPointToSemiSphere_i(const Point3D& ptPlane) 30 { 31 double dSquare = ptPlane.x * ptPlane.x + ptPlane.y * ptPlane.y; 32 double dSphereZ = dSquare >= 1.0 ? 0.0 : std::sqrt(1 - dSquare); 33 34 return Point3D(ptPlane.x, ptPlane.y, dSphereZ); 35 }
代碼實現不是很難,在此不做過多解釋。有一點說明一下,輸入的頂點是變換後世界座標系下的座標點。關於怎麼把屏幕像素座標點轉換爲OpenGL座標系下的點咱們已經在3.1中做了說明。
在客戶端的main函數中須要向OpenGL註冊鼠標狀態改變時的回調函數和鼠標按住時移動的回調函數,以下:
glutMouseFunc(display_mouse_state_changed); glutMotionFunc(display_mouse_move);
在鼠標狀態改變回調函數中,鼠標左鍵按住表示旋轉,鼠標中鍵按住表示平移,鼠標右鍵按住表示縮放。兩個回調函數的具體代碼以下(紅色是左鍵旋轉的邏輯、綠色是中鍵平移的邏輯、藍色爲右鍵縮放的代碼邏輯):
1 void display_mouse_state_changed(int button, int state, int x, int y) 2 { 3 ptStart = Point2DHelper::ConvertPointFromScreenToWorld(Point2D(x, y)); 4 if (GLUT_LEFT_BUTTON == button) 5 { 6 if (state == GLUT_DOWN) 7 { 8 bIsRotate = true; 9 } 10 else if (state == GLUT_UP) 11 { 12 bIsRotate = false; 13 } 14 } 15 else if (GLUT_MIDDLE_BUTTON == button) 16 { 17 if (state == GLUT_DOWN) 18 { 19 bIsMove = true; 20 } 21 else if (state == GLUT_UP) 22 { 23 bIsMove = false; 24 } 25 } 26 else if (GLUT_RIGHT_BUTTON == button) 27 { 28 if (state == GLUT_DOWN) 29 { 30 bIsScale = true; 31 } 32 else if (state == GLUT_UP) 33 { 34 bIsScale = false; 35 } 36 } 37 } 38 39 void display_mouse_move(int x, int y) 40 { 41 if (!bIsMove && !bIsRotate && !bIsScale) 42 { 43 return; 44 } 45 46 // ------像素座標點的XY座標轉換爲世界座標點的XY座標------ 47 ptEnd = Point2DHelper::ConvertPointFromScreenToWorld(Point2D(x, y)); 48 49 // --------------準備矩陣數據-------------- 50 Matrix4X4 mat; 51 if (bIsRotate) 52 { 53 mat = ArcBall::CreateRotateMatrix(ptStart, ptEnd); 54 } 55 else if (bIsMove) 56 { 57 mat = Matrix4X4::CreateTranslateMatrix(ptEnd - ptStart); 58 } 59 else if (bIsScale) 60 { 61 double dLength = (ptStart.x - ptEnd.x) + (ptEnd.y - ptStart.y); 62 double dScale = std::exp(dLength); 63 mat = Matrix4X4::CreateScaleMatrix(dScale); 64 } 65 mat_transform = mat * mat_transform; 66 67 // --------------上傳矩陣數據-------------- 68 glUniformMatrix4fv(mat_location, 1, GL_TRUE, mat_transform._m); 69 glutPostRedisplay(); 70 71 ptStart = ptEnd; 72 }
本想在貼上交互效果動畫的,無奈圖片較大,上傳多次不成功,且考慮到一些園友流量有限,因此就把這些實驗結果放到百度網盤上,以供園友們學習借鑑~地址爲:http://pan.baidu.com/s/1hsbcGK0,這是讀書筆記(一)~讀書筆記(四)的全部代碼。若是想直接運行的話,須要解壓至 F:\VC++遊戲編程 路徑下,不然會致使着色器程序找不到。下次博主將會更新,解壓後能夠存放在任意位置。
解壓後有兩個文件夾:
OpenGLGuide:用於存放源代碼;
Package:用於存放編譯出來的lib、dll以及頭文件。
至此,咱們陸陸續續學習了三維變換的一些知識,包括平移、旋轉和縮放矩陣的理論推導與應用。在下一次讀書筆記中,咱們將繼續三維變換的內容——投影變換矩陣的推導與應用,這應該是三維變換的最後一塊內容了。五一三天假期立刻結束了,又要開始上班了。