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