參考資料:
主頁 - LearnOpenGL CN
https://blog.csdn.net/qq_40120946/category_12566573.html
由于OpenGL的大多數實現都是由顯卡廠商編寫的,當產生一個bug時通常可以通過升級顯卡驅動來解決。
OpenGL中的名詞解釋
OpenGL 上下文(Context)
-
作用:
-
OpenGL上下文就像是一個畫家的工具箱,它包含了所有當前可用的工具(如畫筆、顏料、畫布等),它存儲了OpenGL的所有狀態(如當前顏色、線條粗細、是否啟用混合等)。
-
每個窗口/線程通常有一個獨立的上下文,每個上下文是獨立的,就像一個“沙盒”,不同上下文的資源默認不共享(除非顯式設置共享)。
-
上下文必須綁定到某個“窗口”或“繪圖表面”(如
HWND
、QOpenGLWidget
等),才能進行渲染。
-
為什么需要多個上下文?
多線程渲染(每個線程通常需要一個獨立的上下文)。
不同窗口可能需要不同的OpenGL版本(如一個用OpenGL 2.1,另一個用OpenGL 4.6)。
-
例子:
-
如果你有兩個畫家(兩個OpenGL上下文),他們各自有自己的畫筆和顏料,互不影響。
-
當你切換上下文(比如從窗口A切換到窗口B),就像換了一個畫家,他用的工具和之前的不一樣。
-
OpenGL 狀態機(State Machine)
OpenGL狀態機決定了當前如何繪制圖形,它由一系列可開關的設置組成,就像畫家的當前工作模式。
-
作用:
-
控制OpenGL的繪制行為,比如:
-
當前顏色(
glColor3f(1.0, 0.0, 0.0)
?→ 紅色) -
是否啟用深度測試(
glEnable(GL_DEPTH_TEST)
) -
當前綁定的紋理(
glBindTexture(GL_TEXTURE_2D, textureId)
)
-
-
這些狀態會一直保持,直到你手動改變它們。
-
-
例子:
-
畫家當前選擇的是紅色顏料(
glColor3f(1.0, 0.0, 0.0)
),那么接下來他畫的所有東西都是紅色,直到他換顏色。 -
畫家決定是否使用尺子(
glEnable(GL_LINE_SMOOTH)
),如果啟用,畫線會更平滑。
-
OpenGL (ObjectID)
對象ID(如VAO
、VBO
、Texture
的ID)就像是工具箱里的每件工具的編號。
-
作用:
-
OpenGL管理的資源(如緩沖區、紋理、著色器)都有一個唯一的
GLuint
類型的ID。 -
你需要綁定(Bind)這些ID到OpenGL的某個目標(Target),才能使用它們。
-
-
例子:
-
畫家有3支畫筆(3個
VBO
),編號分別是1、2、3。 -
他必須拿起某支筆(
glBindBuffer(GL_ARRAY_BUFFER, vboId)
)才能用它畫畫。 -
如果他不綁定任何筆(
glBindBuffer(GL_ARRAY_BUFFER, 0)
),就沒法畫。
-
著色器 ——?著色器 - LearnOpenGL CN
頂點著色器
核心作用:處理每個頂點的坐標變換和屬性計算。
- 坐標變換:將輸入的頂點坐標(如模型空間坐標)轉換為裁剪空間坐標(Clip Space),這是透視投影和視口變換的基礎。
- 屬性傳遞:處理頂點屬性(如位置、法線、紋理坐標、顏色),并將處理后的數據傳遞給后續階段(如幾何著色器或片段著色器)。
- 應用場景:
- 實現模型的位移、旋轉、縮放(通過矩陣變換)。
- 計算頂點光照效果的初始值(如法線變換)。
- 實現頂點動畫(如骨骼動畫、波浪效果)。
特點:
- 對每個頂點執行一次,并行計算效率高。
- 必須輸出裁剪空間坐標(
gl_Position
)。
// 頂點著色器示例(簡化)
#version 330 core
layout (location = 0) in vec3 aPosition; ?// 頂點位置
layout (location = 1) in vec3 aColor; ? ? // 頂點顏色
out vec3 vColor; ? ? ? ? ? ? ? ? ? ? ? ? ?// 傳遞給片段著色器的顏色uniform mat4 model; ? ? ? // 模型矩陣
uniform mat4 view; ? ? ? ?// 視圖矩陣
uniform mat4 projection; ?// 投影矩陣void main() {
? ? // 坐標變換:模型空間 → 世界空間 → 視圖空間 → 裁剪空間
? ? gl_Position = projection * view * model * vec4(aPosition, 1.0);
? ? vColor = aColor; ? ? ?// 傳遞顏色屬性
}
片段著色器
核心作用:計算每個片段(Fragment,可理解為潛在像素)的最終顏色。
- 顏色計算:基于插值后的頂點屬性(如顏色、紋理坐標)和外部數據(如光照、材質、紋理),計算像素的 RGBA 值。
- 高級效果:實現光照模型(如 Phong、PBR)、陰影、紋理采樣、透明度、后處理(如模糊、色調調整)。
- 應用場景:
- 渲染物體材質和紋理。
- 實現光照、反射、折射效果。
- 創建特效(如霧效、發光、粒子)。
特點:
- 對每個光柵化后的片段執行一次,計算量通常比頂點著色器大。
- 必須輸出一個顏色值(
out vec4 FragColor
)。
#version 330 core
in vec3 vColor; ? ? ? ? ? // 從頂點著色器接收的顏色
out vec4 FragColor; ? ? ? // 輸出的最終顏色void main() {
? ? // 直接使用插值后的顏色
? ? FragColor = vec4(vColor, 1.0);
}
VBO(Vertex Buffer Object)—— 頂點緩沖區對象
VBO?是OpenGL中用于存儲頂點數據(如位置、顏色、紋理坐標等)的緩沖區對象。它直接在GPU內存中分配空間,避免CPU到GPU的重復數據傳輸,提高渲染效率。
作用
-
存儲頂點屬性數據(例如:
float vertices[] = {x1,y1,z1, x2,y2,z2, ...}
)。 -
通過
glBindBuffer
綁定后,OpenGL才知道從哪里讀取數據。
類比
-
VBO = 顏料管
-
每個顏料管(VBO)存儲一種原始數據(如紅色顏料管、藍色顏料管)。
-
顏料管本身不知道如何被使用,只是數據的容器。
-
VAO(Vertex Array Object)—— 頂點數組對象
VAO?是一個“配置容器”,用于記錄:
-
當前綁定的VBO。
-
頂點屬性的解析方式(如位置是3個float,顏色是4個float等)。
-
頂點屬性的啟用狀態(通過
glEnableVertexAttribArray
)。
作用
-
避免重復配置:每次繪制時無需重新綁定VBO和設置屬性格式。
-
提高性能:OpenGL可以直接讀取預定義的頂點數據布局。
類比
-
VAO = 調色板(或畫筆套裝)
-
調色板(VAO)記錄了:
-
哪些顏料管(VBO)被擠到調色板上。
-
每種顏料的用途(如紅色用于輪廓,藍色用于填充)。
-
-
畫家(OpenGL)拿起調色板后,直接知道如何作畫。
-
VBO 和 VAO 的聯系與區別
聯系
-
VAO 依賴 VBO
-
VAO本身不存儲頂點數據,它只是記錄“如何從VBO中讀取數據”。
-
必須先綁定VBO,再配置VAO(就像先擠顏料到調色板,再定義用途)。
-
區別
特性 | VBO | VAO |
---|---|---|
存儲內容 | 原始頂點數據(如位置、顏色) | 頂點屬性的配置(如何讀數據) |
是否直接參與繪制 | 是(數據源) | 否(只是數據格式的封裝) |
綁定目標 | GL_ARRAY_BUFFER | 無目標(直接綁定) |
性能優化 | 減少CPU-GPU數據傳輸 | 減少繪制時的狀態切換開銷 |
一個VAO綁定多個VBO嗎?——可以
VAO可以關聯多個VBO(例如:一個VBO存位置,另一個存顏色)。
只需在綁定VAO后,依次綁定不同VBO并配置屬性:glBindVertexArray(vaoId);
// 綁定位置VBO
glBindBuffer(GL_ARRAY_BUFFER, vboPos);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
// 綁定顏色VBO
glBindBuffer(GL_ARRAY_BUFFER, vboColor);
glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, NULL);
基礎光照
環境光照(Ambient Lighting):
即使在黑暗的情況下,世界上通常也仍然有一些光亮(月亮、遠處的光),所以物體幾乎永遠不會是完全黑暗的。為了模擬這個,我們會使用一個環境光照常量,它永遠會給物體一些顏色。
漫反射光照(Diffuse Lighting):
模擬光源對物體的方向性影響(Directional Impact)。它是風氏光照模型中視覺上最顯著的分量。物體的某一部分越是正對著光源,它就會越亮。
鏡面光照(Specular Lighting):
模擬有光澤物體上面出現的亮點。鏡面光照的顏色相比于物體的顏色會更傾向于光的顏色。
法向量
法向量是一個垂直于頂點表面的(單位)向量。由于頂點本身并沒有表面(它只是空間中一個獨立的點),我們利用它周圍的頂點來計算出這個頂點的表面。我們能夠使用一個小技巧,使用叉乘對立方體所有的頂點計算法向量,但是由于3D立方體不是一個復雜的形狀,所以我們可以簡單地把法線數據手工添加到頂點數據中。更新后的頂點數據數組可以在這里找到。試著去想象一下,這些法向量真的是垂直于立方體各個平面的表面的(一個立方體由6個平面組成)。
計算漫反射光照需要什么?
- 法向量:一個垂直于頂點表面的向量。
- 定向的光線:作為光源的位置與片段的位置之間向量差的方向向量。為了計算這個光線,我們需要光的位置向量和片段的位置向量。
OpenGL 圖形渲染管線全流程解析
OpenGL 狀態機與對象管理基礎
- 狀態機特性:OpenGL 通過上下文(Context)維護運行狀態,通過設置選項、操作緩沖修改狀態,以當前上下文執行渲染。
- 對象引用機制:對象通過整數 ID(objectId)綁定至上下文目標位置,存儲選項配置,避免重復設置(如模型數據綁定)。
階段 | 像素位置相關 | 像素顏色相關 |
---|---|---|
頂點著色器 | 轉換 3D 坐標至裁剪空間,為投影做準備 | 處理頂點顏色等屬性(可傳遞給片段著色器) |
幾何著色器(可選) | 修改圖元形狀(如生成更多頂點) | 可輸出頂點顏色屬性 |
圖元裝配 | 定義幾何形狀的輪廓 | 無直接顏色處理 |
光柵化 | 確定像素的屏幕坐標(x, y) | 傳遞片段位置給片段著色器 |
片段著色器 | 無直接位置處理 | 計算像素的 RGBA 顏色值 |
Alpha 測試與混合 | 基于深度判斷像素可見性 | 混合顏色值,確定最終顯示的顏色 |
數據流:
一、像素位置的確定:3D 到 2D 坐標轉換與光柵化階段
1.?頂點著色器(Vertex Shader):初步坐標變換
- 作用:將輸入的 3D 頂點坐標(如模型本地坐標)轉換為裁剪空間坐標(Clip Space),并完成頂點屬性(如位置、法線)的基礎處理。
- 與像素位置的關系:此階段將頂點從模型空間映射到裁剪空間,為后續投影到 2D 屏幕做準備,但尚未直接確定像素位置。
2.?圖元裝配(Primitive Assembly):幾何形狀定義
- 作用:將頂點按指定圖元類型(如三角形、線段)裝配成幾何形狀,確定渲染的基本單元(如一個三角形由 3 個頂點組成)。
- 與像素位置的關系:明確了 3D 空間中幾何圖形的輪廓,但仍處于抽象的幾何階段。
3.?光柵化(Rasterization):像素位置生成
- 作用:將圖元(如三角形)映射到屏幕的 2D 像素網格,計算每個圖元覆蓋的像素坐標,并生成對應的片段(Fragment)。
- 關鍵細節:
- 此階段通過插值計算圖元在屏幕上的覆蓋范圍,確定每個像素的位置(x, y 坐標)。
- 裁切(Clipping)會丟棄視圖外的像素,減少無效計算。
- 結論:光柵化階段直接確定了像素的屏幕位置。
二、像素顏色的計算:片段處理與最終輸出階段
1.?片段著色器(Fragment Shader):顏色值計算
- 作用:接收光柵化生成的片段(包含位置、紋理坐標等信息),結合 3D 場景數據(如光照、陰影、材質屬性、紋理采樣),計算每個像素的最終顏色值(RGBA)。
- 關鍵細節:
- 可通過光照模型(如蘭伯特、Phong)計算光照對顏色的影響。
- 支持紋理采樣,將紋理貼圖的顏色映射到像素。
- 結論:片段著色器是像素顏色計算的核心階段。
2.?Alpha 測試與混合(Alpha Test & Blending):顏色最終確定
- 作用:
- 深度測試:比較當前片段與已渲染像素的深度值,決定是否保留當前像素(近物覆蓋遠物)。
- Alpha 混合:根據透明度(Alpha 值)混合當前像素與背景像素的顏色,實現半透明效果(如玻璃、煙霧)。
- 與顏色的關系:此階段不直接計算顏色,而是基于深度和 Alpha 值對片段著色器輸出的顏色進行篩選或混合,最終確定屏幕上顯示的像素顏色。
圖形渲染管線核心流程拆解
-
3D 到 2D 坐標轉換階段
- 頂點著色器(Vertex Shader):輸入單個頂點,將 3D 坐標轉換為特定空間坐標(如裁剪空間),并處理頂點屬性(如位置、顏色)。
- 幾何著色器(Geometry Shader)(可選):輸入頂點組(圖元),通過生成新頂點擴展或修改圖元形狀(如從單個三角形生成兩個三角形)。
-
圖元裝配與光柵化階段
- 圖元裝配(Primitive Assembly):將頂點按指定圖元類型(如三角形、點)裝配為幾何形狀,確定渲染基本單元。
- 光柵化(Rasterization):將圖元映射為屏幕像素,生成片段(Fragment);裁切(Clipping)丟棄視圖外像素,提升效率。
-
片段處理與最終輸出階段
- 片段著色器(Fragment Shader):計算像素最終顏色,結合 3D 場景數據(光照、陰影、材質等)生成顏色值。
- Alpha 測試與混合(Alpha Test & Blending):通過深度 / 模板測試判斷像素可見性,基于 Alpha 值實現透明度混合,確定最終渲染結果。
管線核心邏輯總結
“狀態機管理對象狀態 → 管線各階段逐級處理頂點與圖元 → 從 3D 坐標映射至 2D 像素并渲染”,這一流程構成了 OpenGL 將幾何數據轉化為屏幕圖像的完整技術鏈路。
完整OpenGL繪圖流程
OpenGL步驟 | Qt (QOpenGLWidget) | 畫家類比 |
---|---|---|
1. 初始化OpenGL上下文 |
| 畫家準備好畫布、工具箱(上下文=工具箱+畫布)。 |
2. 定義頂點數據 | 在initializeGL() 中定義數組(如float vertices[] ) | 畫家決定畫什么(如“畫一個三角形,左下紅、右下綠、頂部藍”)。 |
3. 創建VBO | glGenBuffers() ?+?glBufferData() ?在initializeGL() 中完成 | 把顏料擠到調色盤上(VBO=顏料管,存儲原始顏色和位置數據)。 |
4. 創建VAO | glGenVertexArrays() ?+ 配置屬性(glVertexAttribPointer )在initializeGL() 中完成 | 調色盤(VAO)記錄: - 紅色顏料用于輪廓 - 藍色顏料用于填充。 |
5. 清空屏幕 | 在paintGL() 中調用?glClear() | 畫家擦干凈畫布 |
6. 綁定VAO | 在paintGL() 中調用?glBindVertexArray(VAO) | 畫家拿起調色盤(綁定VAO后,OpenGL知道如何讀取VBO數據)。 |
7. 繪制調用 | 在paintGL() 中調用?glDrawArrays() | 畫家用調色盤上的顏料開始作畫 |
8. 解綁VAO | 可調用?glBindVertexArray(0) ,但Qt通常省略 | 畫家放下調色盤 |
9.調整畫布大小 | 重寫?resizeGL(int w, int h) ,自動調用 | 畫家根據畫布大小調整畫筆范圍。 |
10.清理資源 | QOpenGLWidget ?析構函數自動釋放VAO/VBO | 畫家收工,清理調色盤和顏料。 |
?原生OpenGL對比Qt繪圖流程關鍵區別總結
特性 | Qt (QOpenGLWidget ) | 原生OpenGL (GLFW) |
---|---|---|
上下文管理 | 自動創建和管理OpenGL上下文 | 需手動初始化(如GLFW/GLEW) |
窗口集成 | 直接嵌入Qt窗口體系 | 需手動處理窗口事件(如GLFW) |
函數調用 | 通過?QOpenGLFunctions ?調用OpenGL API | 直接調用OpenGL API(如?glDrawArrays ) |
生命周期 | 自動調用?initializeGL() 、paintGL() | 需手動編寫渲染循環 |
適用場景 | Qt應用程序中嵌入3D繪圖 | 跨平臺游戲/獨立圖形應用 |
Qt 的?QOpenGLWidget
?繪圖完整代碼——制一個三色三角形
1.1 創建自定義?QOpenGLWidget
?子類
// MyGLWidget.h #include <QOpenGLWidget> #include <QOpenGLFunctions>class MyGLWidget : public QOpenGLWidget, protected QOpenGLFunctions { public:MyGLWidget(QWidget* parent = nullptr) : QOpenGLWidget(parent) {}protected:void initializeGL() override; // 初始化OpenGLvoid paintGL() override; // 繪制void resizeGL(int w, int h) override; // 窗口大小變化時調整private:GLuint VAO, VBO; // OpenGL對象 };
1.2 實現?initializeGL
(初始化階段)
// MyGLWidget.cpp void MyGLWidget::initializeGL() {initializeOpenGLFunctions(); // 初始化Qt的OpenGL函數綁定// 定義頂點數據(位置 + 顏色)float vertices[] = {// 位置 // 顏色-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 // 頂部(藍)};// 創建并綁定VBOglGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// 創建并綁定VAOglGenVertexArrays(1, &VAO);glBindVertexArray(VAO);// 配置位置屬性(前3個float)glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// 配置顏色屬性(后3個float,偏移量=3*sizeof(float))glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(1);// 解綁(非必須,但安全)glBindVertexArray(0);glBindBuffer(GL_ARRAY_BUFFER, 0); }
1.3 實現?paintGL
(渲染階段)
void MyGLWidget::paintGL() {glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 設置清屏顏色(深綠色)glClear(GL_COLOR_BUFFER_BIT); // 清空顏色緩沖glBindVertexArray(VAO); // 綁定VAO(自動關聯VBO和屬性)glDrawArrays(GL_TRIANGLES, 0, 3); // 繪制三角形 }
1.4 實現?resizeGL
(窗口調整)
void MyGLWidget::resizeGL(int w, int h) {glViewport(0, 0, w, h); // 調整視口大小 }
1.5 在Qt窗口中使用?MyGLWidget
// main.cpp #include <QApplication> #include "MyGLWidget.h"int main(int argc, char** argv) {QApplication app(argc, argv);MyGLWidget widget;widget.show();return app.exec(); }