QT使用openGL绘制一个三角形
對于opengl的學習來說,繪制一個三角形是學習一種計算機語言時的一個hello world級的入門程序,個人覺得相比主流語言的helloworld,openGL的入門確實是有一些勸退,雖然說有不錯的教程,但簡明與全面不可兼得,很容易面對教程中一大堆概念和術語而摸不到頭腦,本文試圖用“相對”簡單和直觀的方式讓人成功的繪制出第一個三角形。對于使用QT的同學,可以直接從文末的鏈接下載完整代碼,自己修改其中的參數(shù)觀察變化,這樣理解起來更快。希望能讓使用QT并且想學習openGL的人踏出第一步,而不是被畫一個三角形拒之門外。
文章目錄
- 前言
- 環(huán)境
- 論繪制一個三角形都需要什么
- 比較重要的組成部分
- 各部分關系圖
- 一種可行的繪制流程
- 編寫代碼
- 關于QT使用openGL
- 按步驟來
- 創(chuàng)建openGL程序
- 編寫頂點著色器程序和片段著色器并添加到openGL程序中去
- 創(chuàng)建并綁定VAO
- 創(chuàng)建并綁定VBO
- 將數(shù)據(jù)存入VBO
- 告訴openGL該如何分配和使用數(shù)據(jù)
- 繪制
- 完整的MyWidget.cpp代碼
- 繪制最終效果
- 完整項目代碼
前言
本人也是初學openGL,而且代碼部分會出現(xiàn)QT封裝類和openGL原生函數(shù)混用的情況(QT的一些封裝對比原生確實方便一些,但是缺點是文檔不夠親民,示例代碼少,有時會不知該如何使用),對于并不使用QT的同學來說,可以僅參考各部分組成關系和繪制流程,我覺得應該還是有助于初期對openGL的理解的。
隔了一段時間回頭來修改了一下這篇文章,想讓他更加詳細親民。不過仔細想了想,感覺學習openGL最好的入門方法就是找一個在你的環(huán)境下可以編譯運行的繪制三角形或正方形的openGL代碼,結合著各種教程,修改其中參數(shù),觀察變化來理解其中的意義。
雖然只是繪制一個三角形,但是需要相對較多的準備知識,而且直到最后你能將這些知識組合起來并正確組織代碼之前,你無法得到任何的反饋,因為這個過程幾乎已經沒法再分解了,你沒辦法先畫出一個點,再畫出一條線,進而畫出一個三角形,因為openGL的繪制過程并不是這種邏輯,如果你只是對著文字教程一點一點編寫代碼,可能很久都沒法得到正確的結果,也不知道是哪里出錯了,如此既會浪費時間,又會有挫敗感。所以如果你對這個入門感到頭痛,那么就先去找一份適合自己的可用代碼吧。
本文不會介紹的很全面,只是希望能夠通過此文讓同學們跨過opengl的門檻,更加輕松的去理解和學習其他人的教程,這里也給出兩個教程鏈接:
一個比較全面系統(tǒng)的教程: LearnOpenGL CN
和我一樣使用QT的同學在學習上方教程時如果想知道QT做了哪些封裝以及如何使用相應的類時,可以參考這里:基于QT的openGL學習,這一系列的主要問題是基本就是示例代碼,而沒有解釋。不過入了門之后直接看代碼可能反而比文字描述直觀,也是不錯的參考。
環(huán)境
Windows7
QT 5.10.1 (MSVC2017_x64)
論繪制一個三角形都需要什么
對于openGL來說,繪制一個三角形需要我們通過計算機語言向其提供 頂點 和 顏色 的信息。
比較重要的組成部分
關于頂點和顏色信息的存儲:openGL使用簡稱為 VAO(Vertex Array Object,頂點數(shù)組對象) 的對象來存儲這些信息。
向VAO傳遞信息的過程中,我們會使用簡稱為 VBO(Vertex Buffer Object,頂點緩沖對象) 的對象。
對VAO中存儲的信息進行處理:openGL使用一個 程序(program) 對象來決定使用VAO中信息的方式并進行最終的繪制。程序包含所謂的 “著色器”(shader) ,你可以把著色器理解成是由openGL語言(基本獨立于你所使用的語言)編寫而成的程序。
一個能夠繪制圖形的openGL程序至少包含頂點著色器(Vertex Shader)和片段著色器(Fragment Shader),其中頂點著色器會決定繪制頂點的位置,而片段著色器用來決定這些位置的顏色
上述的組成部分如果按照一定的流程全部正確設置完畢,我們就可以繪制出我們的第一個三角形了。
各部分關系圖
一種可行的繪制流程
當你熟悉了openGL的基礎知識之后,你會知道繪制流程的順序并不是固定的,只需滿足一些必要條件即可,但是現(xiàn)在知道這一點就可以了,然后暫且認為流程就是如下固定的,否則容易頭暈。
編寫代碼
了解了上述知識后,現(xiàn)在要做的就是學習如何通過代碼實現(xiàn)上述的步驟來繪制了,到現(xiàn)在為止可以說第一步只邁出了小半,因為openGL并沒有特別符合直覺和方便使用的函數(shù)接口,像如下
#include <openGL> void main() {setVertex(xxxx);setColor(xxxx);paint(); }這樣的使用方法并不存在,必須結合前述知識去學習openGL存儲和處理數(shù)據(jù)的方法。
關于QT使用openGL
編寫一個自定義類,繼承QOpenGLWidget和QOpenGLFunctions即可,我們的繪制便會在這個Widget內進行。
頭文件MyWidget.h示例如下:
繼承后的類會包含initializeGL(),paintGL(),resizeGL()這三個需要重寫的函數(shù)。
顧名思義,initializeGL用來初始化各項openGL相關的部件,設置openGL程序,存儲數(shù)據(jù)等操作的代碼通常在此函數(shù)內進行編寫,而不是在類的構造函數(shù)中。
paintGL用來編寫繪制相關操作的代碼。
resizeGL用來在Widget尺寸發(fā)生改變時修改設置以得到預期的效果。
注意在此三個函數(shù)之外的自定義函數(shù)中調用openGL的函數(shù)功能時,通常需要先調用makeCurrent() 函數(shù)來獲得上下文(context)。
按步驟來
創(chuàng)建openGL程序
QT創(chuàng)建和綁定openGL程序比較簡單直觀
QOpenGLShaderProgram* program = new QOpenGLShaderProgram; program->bind();但是這僅僅是創(chuàng)建了一個空的程序,如前述,想要繪制圖形,openGL程序中至少包含頂點著色器程序和片段著色器程序,于是我們接著往下來。
編寫頂點著色器程序和片段著色器并添加到openGL程序中去
著色器程序的名稱和后綴沒有固定要求,方便區(qū)分功能用途即可,你甚至可以直接在QT代碼中用字符串的形式編寫并傳入著色器程序,不過我覺得額外編寫文件好修改一些,這里便介紹從其他文件添加著色器程序的方法。
頂點著色器程序 triangle.vert:
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aColor;out vec3 ourColor;void main() {ourColor = aColor;gl_Position =vec4(aPos, 1.0); }第一行#version 330 core為openGL版本聲明,你可能會看到不含這一行的openGL程序代碼,變量的定義方法可能會與此不同
layout 和 location是什么現(xiàn)在可以不管,事實上沒有這兩個也沒關系。文章后面會有簡單解釋
in 代表此變量會由我們的程序傳入
vec3 代表3維向量的數(shù)據(jù)類型
aPos和aColor和ourColor是我們自己定義的變量名
out 代表此變量會由此著色器傳出給片段著色器 (其實著色器并不只有兩種,他們會按一定的順序將數(shù)據(jù)傳遞下去,由于我們只使用了兩種著色器程序,此處直接理解為傳給片段著色器即可) 供其使用。
gl_Position 是頂點著色器的內建變量,是一個4維向量,前3維代表頂點的空間位置,第4維目前我們一律設置為1.0即可。后續(xù)我們將通過自己的程序,借助頂點著色器中定義的aPos變量將頂點的空間坐標傳遞進來。
openGL的繪圖空間為一個長寬高均為2的立方體:
只有在這個空間范圍內的頂點才能夠被繪制出來。
片段著色器程序triangle.frag:
#version 330 core in vec3 ourColor; void main() {gl_FragColor = vec4(ourColor,1.0); }in代表此變量由其他著色器程序傳入,在本文中會接收由頂點著色器傳入的ourColor變量,注意想要正確的傳出傳入變量,需要保證不同著色器中的變量名稱和類型一致,而且傳遞方向不要搞反。
gl_FrageColor為內建變量,用來表示顏色,也是一個4維變量(4個分量分別代表RGBA:紅,綠,藍,α值),α值現(xiàn)在統(tǒng)一設置為1.0即可。
接下來就可以將以上兩個程序添加到program中了
//向program中添加頂點著色器if(!program->addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){qDebug()<< (program->log());return;}//向program中添加片段著色器if(!program->addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){qDebug()<< (program->log());return;}if(!program->link()){qDebug()<< (program->log());return;}創(chuàng)建并綁定VAO
QT中VAO的創(chuàng)建和綁定也比較簡單
QOpenGLVertexArrayObject m_vao;m_vao.create();m_vao.bind();創(chuàng)建并綁定VBO
基本同上
QOpenGLBuffer m_vbo;m_vbo.create();m_vbo.bind();將數(shù)據(jù)存入VBO
注意我們并不會直接對VAO進行操作,VBO可以看做是某一項數(shù)據(jù),而VAO則是這些數(shù)據(jù)的集合,可以理解為在program從VBO中取數(shù)據(jù)時會將這些數(shù)據(jù)自動存入VAO,所以我們也需要在綁定VBO之前綁定一個VAO。
我們所需要的數(shù)據(jù)可以在代碼中使用一個靜態(tài)數(shù)組創(chuàng)建
這里應該也算是個難理解的地方,這里數(shù)組中每一行代表一個頂點數(shù)據(jù),而其中每一行的前三個數(shù)代表空間坐標,后三個數(shù)代表顏色分量。
然而這種區(qū)分是我們自行規(guī)定的,對于VAO和VBO來說,目前他們就只是二進制數(shù)據(jù)而已,openGL本身并沒有規(guī)定各項數(shù)據(jù)必須以何種形式組織起來,我們將通過一些方式來告訴openGL如何來分配和使用這些數(shù)據(jù)。(這里可以在成功畫出三角形后再回頭來理解)
將這些數(shù)據(jù)存入VBO只需一行代碼
這個語句也在一定程度上表示vbo只關心數(shù)據(jù)的大小,你準備了一個數(shù)組,VBO把這個數(shù)組中的內容一股腦復制進來,數(shù)據(jù)存儲就算完成了。這里跟memcpy函數(shù)有一定的相似性,他只管拷貝數(shù)據(jù),至于數(shù)據(jù)是什么意義,則需要程序員來控制。
告訴openGL該如何分配和使用數(shù)據(jù)
現(xiàn)在搬來我們之前寫好的頂點著色器程序
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aColor;out vec3 ourColor;void main() {ourColor = aColor;gl_Position =vec4(aPos, 1.0); }
我們需要告訴頂點著色器程序,我們希望aPos這個變量中持有頂點的空間坐標信息,這是一個三維向量,所以我們讓他持有靜態(tài)數(shù)組中每一行的前三個數(shù)據(jù),這可以通過以下代碼來完成
上方這種方式更加貼近原生openGL的方法,在QT中,你也可以使用下方這種更易于理解的方式(至少你暫時不用思考上面方法中那個整形變量是做什么的)
program->setAttributeBuffer("aPos",GL_FLOAT, 0, 3,6*sizeof(GLfloat));program->enableAttributeArray("aPos");在這里解釋一下這個整形變量m_attr。openGL會把變量名和一個整型數(shù)字一一對應起來,原生openGL的函數(shù)需要一個傳出參數(shù),用來告訴你變量名"aPos"應該對應哪個整型值。假設你現(xiàn)在需要一個變量"aPos",openGL通過這個函數(shù)告訴你,在我的內部用數(shù)字"1"代表這個變量,那么在這之后,當你想要使用"aPos"時,你需要告訴openGL,我現(xiàn)在要操作"1"所代表的變量了。
還記得前面寫的layout (location=0)嗎?這里其實就是相當于手動分配了變量和數(shù)字的對應關系。
比如layout (location = 0) in vec3 aPos;
就代表我們想讓數(shù)字"0"來代表"aPos"這個變量,這個屬于程序員與openGL的約定,當使用數(shù)字"0"時,雙方都明白所指的是什么。當你想使用aPos時,你告訴openGL我要使用"aPos"和我要使用"0"意思是一樣的。區(qū)別在于對于前者,openGL先要查找aPos所對應的數(shù)字,所以事先手動分配數(shù)字并直接使用數(shù)字可以節(jié)省一步從而一定程度上提高性能。
setAttributeBuffer這個函數(shù)中的參數(shù)還是需要好好解釋一下的,而且主要是后三個參數(shù)
GL_FLOAT是用來告訴著色器VAO中的數(shù)據(jù)類型。
舉個相似的例子來說明著色器是如何看待這些數(shù)據(jù)的。
假設我們內存中有16進制數(shù)據(jù)(0x)123456789ABC,這數(shù)據(jù)本身目前并沒有意義
那么我們用get(short,0,2,3sizeof(short))可以取出[12,34],[78,9A](16進制)兩組數(shù)據(jù)
get(short,0,2,2sizeof(short))可以取出[12,34],[56,78],[9A,BC]
get(short,2,2,2sizeof(short))可以取出[56,78],[9A,BC]
get(int,0,1,1sizeof(int))則可以取出[1234],[5678],[9ABC]三組數(shù)據(jù)
可以看到不同的參數(shù)會賦予數(shù)據(jù)不同的意義,而對于openGL的setAttributeBuffer函數(shù)來說,你要做的就是通過這個函數(shù)及其參數(shù)來確保openGL可以按照你的要求讀出正確的數(shù)據(jù)。
通過上述語句,openGL就知道了名為"aPos"的變量表示三組頂點數(shù)據(jù):(0.5f,-0.5f,0.0f),(-0.5f,-0.5f,0.0f)以及(0.0f,0.5f,0.0f)。
如果你理解了我們如何通過上述代碼告訴openGL在當前VBO的數(shù)據(jù)中取每行的前三個數(shù)據(jù)給aPos,那么每行后三個數(shù)據(jù)如何傳給aColor也就應該清楚了
int m_color=program->attributeLocation("aColor");program->setAttributeBuffer(m_color,GL_FLOAT,3*sizeof(GLfloat),3,6*sizeof(GLfloat));program->enableAttributeArray(m_color);注:其實你也可以通過兩個數(shù)組,兩個VBO分別來傳遞信息,兩種方式難理解的點不同,我覺得都是入門需要掌握的,但是這里先只給出這一種方式吧。
這里還是應該多看幾遍,力求理解著色器程序是如何取到數(shù)據(jù)的,如果覺得講得不清楚可以留言,我會嘗試說得再細致一些。
至此,我們的openGL程序設置已經完成,頂點位置和顏色數(shù)據(jù)也都存到了VAO中,接下來,終于可以在崩潰前進行激動人心的繪制了……
繪制
上述編碼基本都是在initializeGL()函數(shù)中編寫的,繪制通常在paintGL()函數(shù)中完成,繪制時需要設置完整的program(決定如何確定頂點和顏色)和VAO(其中存儲了繪制過程中所需要的數(shù)據(jù))
因此在繪制函數(shù)之前(一般為glDrawXXXX)確保我們綁定了正確的openGL程序和VAO。
于是在paintGL()函數(shù)中,我們可以編寫如下代碼:
通過這個程序解釋glDrawArrays各項參數(shù)的意義既麻煩又不好理解,但是還是適當說明下。
GL_TRIANGLES 表示我們要繪制的是三角形(然而并不是說繪制矩形就有GL_RECTANGLE可以用…)可以認為這表示一種排列頂點的方式。
0 表示從第0個頂點開始繪制
3 表示我們一共要繪制3個頂點
至此,三角形的繪制可以說已經結束了。后面會給出完整項目代碼的鏈接,當你成功繪制出三角形,并更進一步的可以參照其他教程畫出正方形,正方體等等圖形時,修改幾次其中的參數(shù)即可讓你有個直觀的認識。
完整的MyWidget.cpp代碼
#include "mywidget.h" #include <QDebug>static GLfloat vertices[] = {//我們所準備的需要提供給openGL的頂點數(shù)據(jù)// 位置 // 顏色0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 頂部 };MyWidget::MyWidget(QWidget* parent):QOpenGLWidget(parent) {}MyWidget::~MyWidget() //建議在析構函數(shù)中手動銷毀openGL相關的對象, //文檔中特意提到QT的回收機制難以保證回收所有openGL使用的資源 //不銷毀的話在關閉程序時可能會出現(xiàn)異常 {makeCurrent();m_vao.destroy();m_vbo.destroy();doneCurrent(); }void MyWidget::initializeGL() {initializeOpenGLFunctions();// 創(chuàng)建并綁定著色器程序program = new QOpenGLShaderProgram;program->bind();//向program中添加頂點著色器if(!program->addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){qDebug()<< (program->log());return;}//向program中添加片段著色器if(!program->addShaderFromSourceFile(QOpenGLShader::Fragment,":/triangle.frag")){qDebug()<< (program->log());return;}if(!program->link()){qDebug()<< (program->log());return;}//創(chuàng)建并綁定VAOm_vao.create();m_vao.bind();//創(chuàng)建并綁定VBOm_vbo.create();m_vbo.bind();m_vbo.allocate(vertices, sizeof(vertices));//向VBO傳遞我們準備好的數(shù)據(jù)(本文件起始部分的靜態(tài)數(shù)組)//向頂點著色器傳遞其中定義為"aPos"的變量所需的數(shù)據(jù)m_attr=program->attributeLocation("aPos");program->setAttributeBuffer(m_attr,GL_FLOAT, 0, 3,6*sizeof(GLfloat));program->enableAttributeArray(m_attr);//向頂點著色器傳遞其中定義為"aColor"的變量所需的數(shù)據(jù)m_color=program->attributeLocation("aColor");program->setAttributeBuffer(m_color,GL_FLOAT,3*sizeof(GLfloat),3,6*sizeof(GLfloat));program->enableAttributeArray(m_color);program->release();//解綁程序}void MyWidget::paintGL() {//glClearColor(0.2f, 0.3f, 0.3f, 1.0f);//glClear(GL_COLOR_BUFFER_BIT);program->bind();//綁定繪制所要使用的openGL程序m_vao.bind();//綁定包含openGL程序所需信息的VAOglDrawArrays(GL_TRIANGLES, 0, 3);//繪制m_vao.release();//解綁VAOprogram->release();//解綁程序//update();//調用update()函數(shù)會執(zhí)行paintGL,現(xiàn)在繪制一個靜態(tài)的三角形可以不使用//也可以用定時器連接update()函數(shù)來控制幀率,直接在paintGL函數(shù)中調用update()大概是60幀 }void MyWidget::resizeGL(int width, int height) {}繪制最終效果
如果你一切順利,你就可以得到自己通過openGL繪制的第一個三角形啦,效果如下:
完整項目代碼
完整的QT項目已上傳至github
希望同學們都能夠順利地邁出學習openGL的第一步!如果覺得本文中有寫的不清楚或者是錯誤的地方,歡迎留言指出。
總結
以上是生活随笔為你收集整理的QT使用openGL绘制一个三角形的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 消费者购买动机
- 下一篇: 解读游戏“仙股”飞鱼科技年内涨幅超400