目錄
- 簡介
- 核心模式與立即渲染模式
- 狀態機
- 對象
- GLFW和GLAD
- Hello OpenGL
- Triangle 三角形
- 頂點緩沖對象 VBO
- 頂點數組對象 VAO
- 元素緩沖對象 EBO/ 索引緩沖對象 IEO
- 著色器
- GLSL
- 數據類型
- 輸入輸出
- Uniform
- 紋理
- 紋理過濾
- Mipmap 多級漸遠紋理
- 實際使用方式
- 紋理單元
- 坐標系統
- 裁剪空間
- 攝像機
- 自由移動
- 視角移動
GitHub主頁:https://github.com/sdpyy
OpenGL學習倉庫:https://github.com/sdpyy1/CppLearn/tree/main/OpenGL
簡介
在學習完Games101,以及手擼一個軟光柵之后,來學習一下OpenGL~
OpenGL常被視作提供圖形操作函數的API,但其本質是由Khronos制定的規范(Specification),僅嚴格定義函數行為及輸出標準,具體實現(如底層優化、硬件適配)由開發者(如顯卡廠商)自行完成。
核心模式與立即渲染模式
OpenGL早期采用立即渲染模式(固定管線),簡化了圖形繪制但效率低且控制受限。隨著版本迭代,OpenGL 3.2起廢棄此模式,轉向?核心模式?,強制使用現代函數并移除舊特性。核心模式雖需深入理解圖形編程(如手動管理渲染流程),但顯著提升了靈活性與性能,同時迫使開發者掌握底層細節,犧牲易用性換取更高效的硬件控制能力。
狀態機
OpenGL自身是一個巨大的狀態機(State Machine):一系列的變量描述OpenGL此刻應當如何運行。OpenGL的狀態通常被稱為OpenGL上下文(Context)。我們通常使用如下途徑去更改OpenGL狀態:設置選項,操作緩沖。最后,我們使用當前OpenGL上下文來渲染。下面是一個例子
// 綁定opengl當前狀態的vaoGL_CALL(glBindVertexArray(vao));// 指定下一次屬性設置在vao中的位置GL_CALL(glEnableVertexAttribArray(posAttrib));
當使用OpenGL的時候,我們會遇到一些狀態設置函數(State-changing Function),這類函數將會改變上下文。以及狀態使用函數(State-using Function),這類函數會根據當前OpenGL的狀態執行一些操作。只要你記住OpenGL本質上是個大狀態機,就能更容易理解它的大部分特性。
對象
在OpenGL中一個對象是指一些選項的集合,它代表OpenGL狀態的一個子集。這塊的解釋還看不明白,等學習后再補充
GLFW和GLAD
GLFW是一個專門針對OpenGL的C語言庫,它提供了一些渲染物體所需的最低限度的接口。它允許用戶創建OpenGL上下文、定義窗口參數以及處理用戶輸入,對我們來說這就夠了。如果沒有 GLFW 這樣的庫,開發者需要自己針對不同的操作系統編寫復雜的代碼來創建窗口和處理用戶輸入,這將大大增加開發的難度和工作量。使用 GLFW 可以簡化這些底層的操作,讓開發者能夠更加專注于 OpenGL 的圖形渲染部分,提高開發效率
由于 OpenGL 驅動版本眾多,不同的顯卡廠商和操作系統對 OpenGL 函數的實現可能會有所不同,而且 OpenGL 中的大多數函數的位置在編譯時無法確定,需要在運行時查詢。GLAD 的主要作用就是管理 OpenGL 的函數指針,它會根據當前的系統和顯卡環境,動態地加載正確的 OpenGL 函數地址,并將這些函數地址保存在函數指針中,以便開發者在程序運行時能夠正確地調用 OpenGL 函數。這樣可以確保程序在不同的硬件和操作系統環境下都能正常運行,提高了程序的兼容性。
至于GLFW和glad的安裝這里不會提及
Hello OpenGL
??兩個頭文件的導入順序。GLAD的頭文件包含了正確的OpenGL頭文件(例如GL/gl.h),所以需要在其它依賴于OpenGL的頭文件之前包含GLAD。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
int main()
{glfwInit();// 對GLFW的配置 版本號、次版本號、選擇核心模式glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);return 0;
}
進一步利用GLFW進行窗口創建,并將OpenGL上下文設置到這個窗口上
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);if (window == nullptr){std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();return -1;}glfwMakeContextCurrent(window);
下一步就是用glad加載函數指針
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}
緊接著需要提供渲染的窗口大小,這個就是進行視口變換時的依據
// 前兩個參數控制窗口左下角的位置。第三個和第四個參數控制渲染窗口的寬度和高度(像素)
glViewport(0, 0, 800, 600);
下面介紹回調函數
void framebuffer_size_callback(GLFWwindow* window, int width, int height){// 當窗口大小變化時,重新調整視口glViewport(0, 0, width, height);
}
---
// 綁定窗口變化事件到framebuffer_size_callback這個函數
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
最終就是渲染循環了,畢竟不只是渲染一幅圖片
while(!glfwWindowShouldClose(window))
{// 雙緩沖glfwSwapBuffers(window);// 事件處理glfwPollEvents();
}
雙緩沖(Double Buffer)
應用程序使用單緩沖繪圖時可能會存在圖像閃爍的問題。 這是因為生成的圖像不是一下子被繪制出來的,而是按照從左到右,由上而下逐像素地繪制而成的。最終圖像不是在瞬間顯示給用戶,而是通過一步一步生成的,這會導致渲染的結果很不真實。為了規避這些問題,我們應用雙緩沖渲染窗口應用程序。前緩沖保存著最終輸出的圖像,它會在屏幕上顯示;而所有的的渲染指令都會在后緩沖上繪制。當所有的渲染指令執行完畢后,我們交換(Swap)前緩沖和后緩沖,這樣圖像就立即呈顯出來,之前提到的不真實感就消除了。
最后一件事,窗口關閉的處理
glfwTerminate();
return 0;
渲染操作都是寫在while循環中的,現在簡單寫一下清屏操作,并通過glClearColor設置清屏后顯示的顏色
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
當前的完整代碼如下
在這里插入代碼片//
// Created by 劉卓昊 on 2025/4/3.
//
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height){std::cout << "Failed to create GLFW window" << std::endl;glViewport(0, 0, width, height);
}int main()
{glfwInit();// 對GLFW的配置 版本號、次版本號、選擇核心模式glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);if (window == nullptr){std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();return -1;}glfwMakeContextCurrent(window);if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}// 前兩個參數控制窗口左下角的位置。第三個和第四個參數控制渲染窗口的寬度和高度(像素)glViewport(0, 0, 800, 600);glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);while(!glfwWindowShouldClose(window)){// 雙緩沖glfwSwapBuffers(window);// 事件處理glfwPollEvents();glClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);}glfwTerminate();return 0;}
運行結果如下圖
Triangle 三角形
首先了解三個概念
頂點數組對象:Vertex Array Object,VAO
頂點緩沖對象:Vertex Buffer Object,VBO
元素緩沖對象:Element Buffer Object,EBO 或 索引緩沖對象 Index Buffer Object,IBO
頂點緩沖對象 VBO
OpenGL不是簡單地把所有的3D坐標變換為屏幕上的2D像素;OpenGL僅當3D坐標在3個軸(x、y和z)上-1.0到1.0的范圍內時才處理它。所有在這個范圍內的坐標叫做標準化設備坐標(Normalized Device Coordinates),此范圍內的坐標最終顯示在屏幕上(在這個范圍以外的坐標則不會顯示)。
這里為了簡單,沒有設置深度
float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f, 0.5f, 0.0f
};
我們通過頂點緩沖對象(Vertex Buffer Objects, VBO)管理這個內存,它會在GPU內存(通常被稱為顯存)中儲存大量頂點。使用這些緩沖對象的好處是我們可以一次性的發送一大批數據到顯卡上,而不是每個頂點發送一次。從CPU把數據發送到顯卡相對較慢,所以只要可能我們都要嘗試盡量一次性發送盡可能多的數據。當數據發送至顯卡的內存中后,頂點著色器幾乎能立即訪問頂點,這是個非常快的過程。
頂點緩沖對象是第一個接觸的OpenGL對象,需要有一個獨一無二的ID,通過glGenBuffers函數來生成一個帶有緩沖ID的VBO對象。```cpp// 就是unsigned intGLuint VBO;// 此函數會為VBO分配一個未使用的ID(例如 1, 2 等)但此時?并未實際創建緩沖對象?,僅預留了標識符glGenBuffers(1, &VBO);// 綁定到OpenGL上下文中,此時GPU才會真正分配內存glBindBuffer(GL_ARRAY_BUFFER, VBO);
從此刻起,我們對GL_ARRAY_BUFFER上的操作都屬于對當前綁定的VBO進行操作。
glBufferData是一個專門用來把用戶定義的數據復制到當前綁定緩沖的函數。第一個參數是目標緩沖的類型:頂點緩沖對象當前綁定到GL_ARRAY_BUFFER目標上。第二個參數指定傳輸數據的大小(以字節為單位);用一個簡單的sizeof計算出頂點數據大小就行。第三個參數是我們希望發送的實際數據。至于第四個參數有三種情況:
- GL_STATIC_DRAW :數據不會或幾乎不會改變。
- GL_DYNAMIC_DRAW:數據會被改變很多。
- GL_STREAM_DRAW :數據每次繪制時都會改變。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices),vertices,GL_STATIC_DRAW);
現在我們已經把頂點數據儲存在顯卡的內存中,用VBO這個頂點緩沖對象管理。下面我們會創建一個頂點著色器和片段著色器來真正處理這些數據。現在我們開始著手創建它們吧。
先簡單來進行硬編碼寫一個簡單的頂點著色器
const char * vertexShaderSource = "#version 330 core\n""layout (location=0) in vec3 aPos;\n""void main()\n""{\n"" gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);\n""}\0";
為了運行glsl,需要在運行時動態編譯源代碼。需要先創建著色器對象,并編譯
// 創建著色器GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);// 源碼添加在shader上glShaderSource(vertexShader,1,&vertexShaderSource, nullptr);// 編譯源碼glCompileShader(vertexShader);
如果想檢查運行編譯是否正確,以及報錯信息也是比較麻煩
int success;char infoLog[512];glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);if(!success){glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}
片段著色器操作類似,主要處理光柵化后每個像素的著色
const char * fragmentShaderSource = "#version 330 core\n""out vec4 FragColor;\n""\n""void main()\n""{\n"" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n""} ";unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);glCompileShader(fragmentShader);
最終還需要鏈接成一個程序
unsigned int shaderProgram;shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);
檢查報錯信息
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);if(!success) {glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);}
最后還需要use這個程序
glUseProgram(shaderProgram);
頂點數組對象 VAO
頂點著色器允許我們指定任何以頂點屬性為形式的輸入。這使其具有很強的靈活性的同時,它還的確意味著我們必須手動指定輸入數據的哪一個部分對應頂點著色器的哪一個頂點屬性。所以,我們必須在渲染前指定OpenGL該如何解釋頂點數據。頂點數據的解析方式如下
我們需要告訴OpenGL如何解析頂點數據,比如緊密的字節序列的一段代表一個頂點的數據,但數據中可能不止頂點的位置,可能還有頂點的顏色之類的,需要告訴OpenGL一個頂點數據有多大,頂點位置的偏移量是多少(因為不一定一開始就是位置信息)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
上邊代碼中的0就是之前寫頂點著色器代碼中的(location=0)
如果把這些狀態配置存儲在一個對象中,通過綁定他來啟動該狀態,這就是VAO。
頂點數組對象(Vertex Array Object, VAO)可以像頂點緩沖對象那樣被綁定。VAO中存儲的內容如下
VAO創建過程與VBO類似
GLuint VAO;glGenVertexArrays(1, &VAO);glBindVertexArray(VAO);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);
當創建并綁定好VAO和VBO后就可以進行渲染了
?? 渲染函數要寫到While循環里
// while中glDrawArrays(GL_TRIANGLES, 0, 3);
元素緩沖對象 EBO/ 索引緩沖對象 IEO
其實就是保存頂點的順序,比如如果VAO中是這樣的數據
float vertices[] = {// 第一個三角形0.5f, 0.5f, 0.0f, // 右上角0.5f, -0.5f, 0.0f, // 右下角-0.5f, 0.5f, 0.0f, // 左上角// 第二個三角形0.5f, -0.5f, 0.0f, // 右下角-0.5f, -0.5f, 0.0f, // 左下角-0.5f, 0.5f, 0.0f // 左上角
};
我們指定了右下角和左上角兩次!一個矩形只有4個而不是6個頂點,這樣就產生50%的額外開銷。EBO是一個緩沖區,就像一個頂點緩沖區對象一樣,它存儲 OpenGL 用來決定要繪制哪些頂點的索引。
float vertices[] = {0.5f, 0.5f, 0.0f, // 右上角0.5f, -0.5f, 0.0f, // 右下角-0.5f, -0.5f, 0.0f, // 左下角-0.5f, 0.5f, 0.0f // 左上角
};unsigned int indices[] = {// 注意索引從0開始! // 此例的索引(0,1,2,3)就是頂點數組vertices的下標,// 這樣可以由下標代表頂點組合成矩形0, 1, 3, // 第一個三角形1, 2, 3 // 第二個三角形
};
EBO創建和綁定方法與VBO基本一致
GLuint EBO;glGenBuffers(1, &EBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glDrawElements繪制的時候就不是直接繪制了,而是使用EBO獲取索引來繪制
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
**另外可以使用VAO中的數據來保存EBO。在綁定VAO時,綁定的最后一個元素緩沖區對象存儲為VAO的元素緩沖區對象。然后,綁定到VAO也會自動綁定該EBO。**這步操作是自動進行的,但需要先綁定VAO。
如果想繪制線框模型可以用以下代碼來決定是線框模型還是填充模型
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
著色器
在Hello Triangle教程中提到,著色器(Shader)是運行在GPU上的小程序。這些小程序為圖形渲染管線的某個特定部分而運行。從基本意義上來說,著色器只是一種把輸入轉化為輸出的程序。著色器也是一種非常獨立的程序,因為它們之間不能相互通信;它們之間唯一的溝通只有通過輸入和輸出。
GLSL
著色器是使用一種叫GLSL的類C語言寫成的。GLSL是為圖形計算量身定制的,它包含一些針對向量和矩陣操作的有用特性。
著色器的開頭總是要聲明版本,接著是輸入和輸出變量、uniform和main函數。每個著色器的入口點都是main函數,在這個函數中我們處理所有的輸入變量,并將結果輸出到輸出變量中。
一個典型的著色器結構如下
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;void main()
{// 處理輸入并進行一些圖形操作...// 輸出處理過的結果到輸出變量out_variable_name = weird_stuff_we_processed;
}
當我們特別談論到頂點著色器的時候,每個輸入變量也叫頂點屬性(Vertex Attribute),我們能聲明的頂點屬性是有上限的,它一般由硬件來決定。OpenGL確保至少有16個包含4分量的頂點屬性可用
數據類型
基礎數據類型有:int、float、double、uint、bool
另外還有向量
向量可以有一些有趣的重組操作
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
輸入輸出
GLSL定義了in
和out
關鍵字專門來實現這個目的。每個著色器使用這兩個關鍵字設定輸入和輸出,只要一個輸出變量與下一個著色器階段的輸入匹配,它就會傳遞下去。在頂點著色器和片段著色器有一些不同。
頂點著色器的輸入特殊在,它從頂點數據中直接接收輸入。為了定義頂點數據該如何管理,我們使用location這一元數據指定輸入變量,這樣我們才可以在CPU上配置頂點屬性。例如layout (location = 0)
,除此之外也可以這樣設置
// 根據glsl的參數輸入順序來獲取vao中數據定義的順序GLint posAttrib = glGetAttribLocation(program, "aPos");GLint colorAttrib = glGetAttribLocation(program, "aColor");GLint uvAttrib = glGetAttribLocation(program, "aUV");
這樣設置的情況下,glsl就不要寫location了
const char* vertexShaderSource = "#version 330 core\n"// "layout (location = 0) in vec3 aPos;\n" // 輸入位置1的3維坐標"in vec3 aPos;\n" // 輸入位置1的3維坐標(不指定位置版本,就按從頭開始)// "layout (location = 1) in vec3 aColor;\n" // 輸入位置2的顏色數據
// "in vec3 aColor;\n" // 輸入位置2的顏色數據(不指定位置版本,就是從上一個變量輸入完成之后的3個數據)‘"in vec2 aUV;\n"
在片段著色器中,需要一個vec4顏色的輸出變量,因為他的目的就是生成最終的顏色。如果你在片段著色器沒有定義輸出顏色,OpenGL會把你的物體渲染為黑色(或白色)。所以,如果我們打算從一個著色器向另一個著色器發送數據,我們必須在發送方著色器中聲明一個輸出,在接收方著色器中聲明一個類似的輸入。當類型和名字都一樣的時候,OpenGL就會把兩個變量鏈接到一起,它們之間就能發送數據了(這是在鏈接程序對象時完成的),下面是一個例子,vertexColor變量就是從頂點著色器跑到片段著色器的
// 頂點
#version 330 core
layout (location = 0) in vec3 aPos; // 位置變量的屬性位置值為0out vec4 vertexColor; // 為片段著色器指定一個顏色輸出void main()
{gl_Position = vec4(aPos, 1.0); // 注意我們如何把一個vec3作為vec4的構造器的參數vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把輸出變量設置為暗紅色
}//片段
#version 330 core
out vec4 FragColor;in vec4 vertexColor; // 從頂點著色器傳來的輸入變量(名稱相同、類型相同)void main()
{FragColor = vertexColor;
}
Uniform
uniform是全局的(Global)。全局意味著uniform變量必須在每個著色器程序對象中都是獨一無二的,而且它可以被著色器程序的任意著色器在任意階段訪問。第二,無論你把uniform值設置成什么,uniform會一直保存它們的數據,直到它們被重置或更新。
#version 330 core`在這里插入代碼片`
out vec4 FragColor;uniform vec4 ourColor; // 在OpenGL程序代碼中設定這個變量void main()
{FragColor = ourColor;
}
如果你聲明了一個uniform卻在GLSL代碼中沒用過,編譯器會靜默移除這個變量,導致最后編譯出的版本中并不會包含它,這可能導致幾個非常麻煩的錯誤,記住這點!
這個uniform現在還是空的;我們還沒有給它添加任何數據,所以下面我們就做這件事。我們首先需要找到著色器中uniform屬性的索引/位置值。當我們得到uniform的索引/位置值后,我們就可以更新它的值了。調用glUseProgram來設置uniform的值
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
最后提一點,如果在VBO中不止位置屬性,還有顏色屬性如下圖
可以這樣設置
// 位置屬性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 顏色屬性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);
把原本的代碼寫在這里記錄一下
//
// Created by 劉卓昊 on 2025/4/3.
//
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <valarray>void framebuffer_size_callback(GLFWwindow* window, int width, int height){std::cout << "Failed to create GLFW window" << std::endl;glViewport(0, 0, width, height);
}
GLFWwindow * InitWindowAndFunc(){glfwInit();// 對GLFW的配置 版本號、次版本號、選擇核心模式glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);if (window == nullptr){std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();return nullptr;}glfwMakeContextCurrent(window);if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return nullptr;}// 前兩個參數控制窗口左下角的位置。第三個和第四個參數控制渲染窗口的寬度和高度(像素)glViewport(0, 0, 800, 600);glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);return window;
}int main()
{float vertices[] = {0.5f, 0.5f, 0.0f, // 右上角0.5f, -0.5f, 0.0f, // 右下角-0.5f, -0.5f, 0.0f, // 左下角-0.5f, 0.5f, 0.0f // 左上角};unsigned int indices[] = {// 注意索引從0開始!// 此例的索引(0,1,2,3)就是頂點數組vertices的下標,// 這樣可以由下標代表頂點組合成矩形0, 1, 3, // 第一個三角形1, 2, 3 // 第二個三角形};GLFWwindow * window = InitWindowAndFunc();// 創建一個IDGLuint VBO;// 此函數會為VBO分配一個未使用的ID(例如 1, 2 等),但此時?并未實際創建緩沖對象?,僅預留了標識符glGenBuffers(1, &VBO);// 綁定到OpenGL上下文中,此時GPU才會真正分配內存glBindBuffer(GL_ARRAY_BUFFER, VBO);//glBufferData是一個專門用來把用戶定義的數據復制到當前綁定緩沖的函數。它的第一個參數是目標緩沖的類型:頂點緩沖對象當前綁定到GL_ARRAY_BUFFER目標上。第二個參數指定傳輸數據的大小(以字節為單位);用一個簡單的sizeof計算出頂點數據大小就行。第三個參數是我們希望發送的實際數據。glBufferData(GL_ARRAY_BUFFER, sizeof(vertices),vertices,GL_STATIC_DRAW);// 頂點著色器const char * vertexShaderSource = "#version 330 core\n""layout (location=0) in vec3 aPos;\n""void main()\n""{\n"" gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);\n""}\0";// 創建著色器GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);// 源碼添加在shader上glShaderSource(vertexShader,1,&vertexShaderSource, nullptr);// 編譯源碼glCompileShader(vertexShader);int success;char infoLog[512];glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);if(!success){glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;}const char * fragmentShaderSource = "#version 330 core\n""out vec4 FragColor;\n""uniform vec4 ourColor; // 在OpenGL程序代碼中設定這個變量\n""\n""void main()\n""{\n"" FragColor = ourColor;\n""} ";unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);glCompileShader(fragmentShader);// 整合成一個程序unsigned int shaderProgram;shaderProgram = glCreateProgram();glAttachShader(shaderProgram, vertexShader);glAttachShader(shaderProgram, fragmentShader);glLinkProgram(shaderProgram);// 鏈接階段報錯處理glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);if(!success) {glGetProgramInfoLog(shaderProgram, 512, nullptr, infoLog);}// 設置使用glUseProgram(shaderProgram);// VAOGLuint VAO;glGenVertexArrays(1, &VAO);glBindVertexArray(VAO);glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);glEnableVertexAttribArray(0);// EBOGLuint EBO;glGenBuffers(1, &EBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);while(!glfwWindowShouldClose(window)){// 雙緩沖glfwSwapBuffers(window);// 事件處理glfwPollEvents();glClearColor(0.2f, 0.3f, 0.3f, 1.0f);glClear(GL_COLOR_BUFFER_BIT);// 更新uniform顏色float timeValue = glfwGetTime();float greenValue = sin(timeValue) / 2.0f + 0.5f;int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);// 通過綁定好的VAO和VBO和EBO畫三角形glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);}glfwTerminate();return 0;}
下來給出一個封裝的shader類
//
// Created by Administrator on 2025/4/4.
//#ifndef OPENGL_SHADER_H
#define OPENGL_SHADER_H#include <glad/glad.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>class Shader {
public:GLuint ID;Shader(const char*vertexPath, const char*fragmentPath);void use();// uniform工具函數void setBool(const std::string &name, bool value) const;void setInt(const std::string &name, int value) const;void setFloat(const std::string &name, float value) const;void setVec4f(const std::string &name, float v0, float v1, float v2, float v3) const;};#endif //OPENGL_SHADER_H
//
// Created by Administrator on 2025/4/4.
//#include "Shader.h"Shader::Shader(const char *vertexPath, const char *fragmentPath) {// 1. 從文件路徑中獲取頂點/片段著色器std::string vertexCode;std::string fragmentCode;std::ifstream vShaderFile;std::ifstream fShaderFile;// 保證ifstream對象可以拋出異常:vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);try{// 打開文件vShaderFile.open(vertexPath);fShaderFile.open(fragmentPath);std::stringstream vShaderStream, fShaderStream;// 讀取文件的緩沖內容到數據流中vShaderStream << vShaderFile.rdbuf();fShaderStream << fShaderFile.rdbuf();// 關閉文件處理器vShaderFile.close();fShaderFile.close();// 轉換數據流到stringvertexCode = vShaderStream.str();fragmentCode = fShaderStream.str();}catch(std::ifstream::failure e){std::cout << "ERROR::SHADER::FILE_NOT_READ" << std::endl;}const char* vShaderCode = vertexCode.c_str();const char* fShaderCode = fragmentCode.c_str();// 2. 編譯著色器unsigned int vertex, fragment;int success;char infoLog[512];// 頂點著色器vertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);// 打印編譯錯誤(如果有的話)glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);if(!success){glGetShaderInfoLog(vertex, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;};// 片段著色器fragment = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragment, 1, &fShaderCode, NULL);glCompileShader(fragment);// 打印編譯錯誤(如果有的話)glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);if(!success){glGetShaderInfoLog(fragment, 512, NULL, infoLog);std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;}// 著色器程序ID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);// 打印連接錯誤(如果有的話)glGetProgramiv(ID, GL_LINK_STATUS, &success);if(!success){glGetProgramInfoLog(ID, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;}// 刪除著色器,它們已經鏈接到我們的程序中了,已經不再需要了glDeleteShader(vertex);glDeleteShader(fragment);
}void Shader::use() {glUseProgram(ID);
}void Shader::setBool(const std::string &name, bool value) const
{glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void Shader::setInt(const std::string &name, int value) const
{glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void Shader::setFloat(const std::string &name, float value) const
{glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}void Shader::setVec4f(const std::string &name, float v0, float v1, float v2, float v3) const {glUniform4f(glGetUniformLocation(ID, name.c_str()), v0, v1, v2, v3);
}
紋理
首先需要給每個頂點一個紋理坐標
float texCoords[] = {0.0f, 0.0f, // 左下角1.0f, 0.0f, // 右下角0.5f, 1.0f // 上中
};
紋理坐標的范圍通常是從(0, 0)到(1, 1),當我們設置成別的區域時,OpenGL通過參數調整有不同的表達方式
可以對每個坐標軸的行為進行單獨控制
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
紋理過濾
首先介紹一個概念:紋理像素,打開一張圖片的每個像素就是紋理像素。OpenGL以一個頂點設置的紋理坐標數據去查找紋理圖像上的像素,然后進行采樣提取紋理像素的顏色。當我需要渲染到一片很大的光柵時,每個像素點插值出來的紋理坐標沒法直接對應紋理圖片的一個像素(即紋理像素),比如紋理坐標為(0.1,0.1)時,如果原紋理圖片是10x10的,這就表示第一行第一列的那個紋理像素的顏色作為該像素點的顏色(當然真實情況是0+0.5才是一個紋理像素的中心,這里只是便于理解),但是如果紋理坐標插值出來是(0.11,0.11)那就沒法直接對應了,就需要特殊處理了。
OpenGL也有對于紋理過濾(Texture Filtering)的選項。紋理過濾有很多個選項,但是現在我們只討論最重要的兩種:GL_NEAREST和GL_LINEAR。
- GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL默認的紋理過濾方式,即選擇離紋理坐標最接近的紋理像素的顏色
- GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會基于紋理坐標附近的紋理像素,計算出一個插值,近似出這些紋理像素之間的顏色。一個紋理像素的中心距離紋理坐標越近,那么這個紋理像素的顏色對最終的樣本顏色的貢獻越大。下圖中你可以看到返回的顏色是鄰近像素的混合色
Mipmap 多級漸遠紋理
有些物體會很遠,但其紋理會擁有與近處物體同樣高的分辨率。比如一個物體光柵化后只占了2x2的像素,但他的紋理圖片有10x10,它需要對一個跨過紋理很大部分的片段只拾取一個紋理顏色。在小物體上這會產生不真實的感覺,更不用說對它們使用高分辨率紋理浪費內存的問題了。
OpenGL使用一種叫做多級漸遠紋理(Mipmap)的概念來解決這個問題,它簡單來說就是一系列的紋理圖像,后一個紋理圖像是前一個的二分之一。多級漸遠紋理背后的理念很簡單:距觀察者的距離超過一定的閾值,OpenGL會使用不同的多級漸遠紋理,即最適合物體的距離的那個。由于距離遠,解析度不高也不會被用戶注意到。同時,多級漸遠紋理另一加分之處是它的性能非常好。讓我們看一下多級漸遠紋理是什么樣子的
離攝像機距離較遠的物體采樣時,在小紋理上采樣效果更好。對Mipmap的使用也有多種過濾方式
最后一種參數額外介紹一下GL_LINEAR_MIPMAP_LINEAR
(三線性過濾),在最接近的兩個mipmap上進行線性插值,最后混合結果,計算量時最大的
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
第一行是對渲染物體小于紋理圖片時,使用Mipmap,第二個是當渲染物體大于紋理圖片時的設置
實際使用方式
stb_image.h是Sean Barrett的一個非常流行的單頭文件圖像加載庫,下載源代碼后使用方式如下
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
首先加載一個紋理圖片。這個函數首先接受一個圖像文件的位置作為輸入。接下來它需要三個int作為它的第二、第三和第四個參數,stb_image.h將會用圖像的寬度、高度和顏色通道的個數填充這三個變量。我們之后生成紋理的時候會用到的圖像的寬度和高度的。
int width, height, nrChannels;unsigned char *data = stbi_load("./assert/container.jpg", &width, &height, &nrChannels, 0);
對紋理的管理與之前那些Object類似,也得創建id
GLuint texture;glGenTextures(1, &texture);
緊接著也需要進行綁定,這樣之后對紋理的設置都是設置綁定的紋理
glBindTexture(GL_TEXTURE_2D, texture);
下來就是把載入的圖片生成紋理了,上邊的綁定就是為了下邊設置時不需要再考慮是給誰設置了
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
// 釋放掉圖片數據,因為已經轉移好了
stbi_image_free(data);
下來設置一下紋理的環繞和過濾方式
// 為當前綁定的紋理對象設置環繞、過濾方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
這樣就設置好紋理了,當然頂點數據還要額外存儲紋理坐標,不然每個像素去紋理圖哪個位置找還不知道,在VBO中存儲頂點位置、顏色、紋理坐標,同時需要告訴VAO
float vertices[] = {
// ---- 位置 ---- ---- 顏色 ---- - 紋理坐標 -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
告訴VAO,紋理數據是如何存儲的,即每個頂點數據站8個字節,紋理數據的偏移為6
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));glEnableVertexAttribArray(2);
同時需要在頂點著色器中接收參數,接收了每個頂點的紋理坐標,并輸出到片段著色器中
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;out vec3 ourColor;
out vec2 TexCoord;void main()
{gl_Position = vec4(aPos, 1.0);ourColor = aColor;TexCoord = aTexCoord;
}
片段著色器接收插值后的紋理坐標和紋理圖片(uniform )
texture是本身就有的函數,根據輸入的紋理圖片和紋理坐標,根據之前對紋理圖片的設置進行采樣
#version 330 core
out vec4 FragColor;in vec3 ourColor;
in vec2 TexCoord;uniform sampler2D ourTexture;void main()
{FragColor = texture(ourTexture, TexCoord);
}
現在只剩下在調用glDrawElements之前綁定紋理了,它會自動把紋理賦值給片段著色器的采樣器
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
如果你的紋理代碼不能正常工作或者顯示是全黑,請繼續閱讀,并一直跟進我們的代碼到最后的例子,它是應該能夠工作的。在一些驅動中,必須要對每個采樣器uniform都附加上紋理單元才可以,這個會在下面介紹。
紋理單元
我們通過uniform設置的紋理,但是沒有在代碼中給他賦值呀。使用glUniform1i,我們可以給紋理采樣器分配一個位置值,這樣的話我們能夠在一個片段著色器中設置多個紋理。一個紋理的默認紋理單元是0,它是默認的激活紋理單元,所以教程前面部分我們沒有分配一個位置值。紋理單元的主要目的是讓我們在著色器中可以使用多于一個的紋理。通過把紋理單元賦值給采樣器,我們可以一次綁定多個紋理,只要我們首先激活對應的紋理單元。
glActiveTexture(GL_TEXTURE0); // 在綁定紋理之前先激活紋理單元
glBindTexture(GL_TEXTURE_2D, texture);
激活紋理單元之后,綁定的紋理會綁定到激活的紋理單元。OpenGL至少保證有16個紋理單元供你使用,也就是說你可以激活從GL_TEXTURE0到GL_TEXTRUE15。
就這塊東西可以理解為一個像素點的顏色不一定只來自于一張紋理圖,比如普通貼圖、法線貼圖、高光貼圖共同配合才能完成一個很好的效果,具體例子可以看我之前的tinyrenderer相關博客的例子。https://blog.csdn.net/lzh804121985/article/details/146939272?spm=1001.2014.3001.5502(直接拉到最后看各種圖片)
當設置兩個不同的紋理圖時,要修改片段著色器的內容,這里就是混合兩張紋理圖的內容
uniform sampler2D texture1;
uniform sampler2D texture2;void main()
{FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2);
}
因為這里定義的其實是兩個采樣器,而不是兩個紋理,我覺得修改名字比較直觀
uniform sampler2D sampler1;
uniform sampler2D sampler2;void main()
{FragColor = mix(texture(sampler1, TexCoord), texture(sampler2, TexCoord), 0.2);
}}
最后就需要指定每個sample對應的是哪個紋理單元了
// 設置 Uniform(目的是給片段著色器中定義的兩個采樣器,告訴他們分別對應的是哪個紋理單元)
// glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手動設置ourShader.setInt("sampler1", 0);ourShader.setInt("sampler2", 1);
完整代碼如下
//
// Created by 劉卓昊 on 2025/4/3.
//
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>#include "Shader.h"
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>#define GL_CALL(x) \do { \x; \GLenum error = glGetError(); \if (error != GL_NO_ERROR) { \std::cerr << "OpenGL error: " << error << " at " << __FILE__ << ":" << __LINE__ << std::endl; \} \} while (0)void framebuffer_size_callback(GLFWwindow* window, int width, int height) {GL_CALL(glViewport(0, 0, width, height));
}GLFWwindow * InitWindowAndFunc() {glfwInit();// 對GLFW的配置 版本號、次版本號、選擇核心模式glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);if (window == nullptr) {std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();return nullptr;}glfwMakeContextCurrent(window);if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {std::cout << "Failed to initialize GLAD" << std::endl;return nullptr;}// 前兩個參數控制窗口左下角的位置。第三個和第四個參數控制渲染窗口的寬度和高度(像素)GL_CALL(glViewport(0, 0, 800, 600));glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);return window;
}int main()
{float vertices[] = {
// ---- 位置 ---- ---- 顏色 ---- - 紋理坐標 -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上};unsigned int indices[] = {// 注意索引從0開始!// 此例的索引(0,1,2,3)就是頂點數組vertices的下標,// 這樣可以由下標代表頂點組合成矩形0, 1, 3, // 第一個三角形1, 2, 3 // 第二個三角形};GLFWwindow * window = InitWindowAndFunc();// shaderShader ourShader("./shader/shader.vs", "./shader/shader.fs");// 創建Object的IDGLuint VBO, VAO, EBO;GL_CALL(glGenVertexArrays(1, &VAO));GL_CALL(glGenBuffers(1, &VBO));GL_CALL(glGenBuffers(1, &EBO));GL_CALL(glBindVertexArray(VAO));// 綁定到OpenGL上下文中,此時GPU才會真正分配內存GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, VBO));//glBufferData是一個專門用來把用戶定義的數據復制到當前綁定緩沖的函數。它的第一個參數是目標緩沖的類型:頂點緩沖對象當前綁定到GL_ARRAY_BUFFER目標上。第二個參數指定傳輸數據的大小(以字節為單位);用一個簡單的sizeof計算出頂點數據大小就行。第三個參數是我們希望發送的實際數據。GL_CALL(glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW));// EBOGL_CALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO));GL_CALL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW));// VAOGL_CALL(glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0));GL_CALL(glEnableVertexAttribArray(0));GL_CALL(glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))));GL_CALL(glEnableVertexAttribArray(1));GL_CALL(glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))));GL_CALL(glEnableVertexAttribArray(2));// 紋理部分GLuint texture1, texture2;// 讓加載的圖片y軸反轉stbi_set_flip_vertically_on_load(true);// 紋理單元 0GL_CALL(glActiveTexture(GL_TEXTURE0));int width, height, nrChannels;GL_CALL(glGenTextures(1, &texture1));GL_CALL(glBindTexture(GL_TEXTURE_2D, texture1));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));unsigned char *data1 = stbi_load("./assets/container.jpg", &width, &height, &nrChannels, 0);if (!data1){std::cout << "Failed to load texture" << std::endl;}GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data1));GL_CALL(glGenerateMipmap(GL_TEXTURE_2D));stbi_image_free(data1);// 紋理單元 1GL_CALL(glActiveTexture(GL_TEXTURE1));GL_CALL(glGenTextures(1, &texture2));GL_CALL(glBindTexture(GL_TEXTURE_2D, texture2));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));unsigned char *data2 = stbi_load("./assets/awesomeface.png", &width, &height, &nrChannels, 0);if (!data2){std::cout << "Failed to load texture" << std::endl;}GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2));GL_CALL(glGenerateMipmap(GL_TEXTURE_2D));stbi_image_free(data2);ourShader.use();// 設置 Uniform(目的是給片段著色器中定義的兩個采樣器,告訴他們分別對應的是哪個紋理單元)GL_CALL(ourShader.setInt("sampler1", 0));GL_CALL(ourShader.setInt("sampler2", 1));while (!glfwWindowShouldClose(window)){// 清理窗口GL_CALL(glClearColor(0.2f, 0.3f, 0.3f, 1.0f));GL_CALL(glClear(GL_COLOR_BUFFER_BIT));// 綁定紋理GL_CALL(glActiveTexture(GL_TEXTURE0));GL_CALL(glBindTexture(GL_TEXTURE_2D, texture1));GL_CALL(glActiveTexture(GL_TEXTURE1));GL_CALL(glBindTexture(GL_TEXTURE_2D, texture2));// 激活著色器GL_CALL(ourShader.use());// 綁定VAOGL_CALL(glBindVertexArray(VAO));// 繪制GL_CALL(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0));// 事件處理glfwPollEvents();// 雙緩沖glfwSwapBuffers(window);// 解綁GL_CALL(glBindVertexArray(0));GL_CALL(glUseProgram(0));}glfwTerminate();return 0;
}
用Clion的一定要注意,要修改debug文件夾下的著色器代碼才能生效。。。
最終圖片
坐標系統
GLM是OpenGL Mathematics的縮寫,它是一個只有頭文件的庫,也就是說我們只需包含對應的頭文件就行了,不用鏈接和編譯。GLM可以在它們的網站上下載。把頭文件的根目錄復制到你的includes文件夾,然后你就可以使用這個庫了。
OpenGL希望在每次頂點著色器運行后,我們可見的所有頂點都為標準化設備坐標(Normalized Device Coordinate, NDC)。也就是說,每個頂點的x,y,z坐標都應該在-1.0到1.0之間,超出這個坐標范圍的頂點都將不可見。也就是說OpenGL頂點著色器執行完后,會對不在NDC范圍內的點進行剔除。
games101學過很多了,MVP+透視除法+視口變換,讓一個三維物體轉為二維坐標
局部空間(Local Space,或者稱為物體空間(Object Space))
世界空間(World Space)
觀察空間(View Space,或者稱為視覺空間(Eye Space))
裁剪空間(Clip Space)
屏幕空間(Screen Space)
對于局部空間、世界空間、觀察空間這里就不做解釋了
裁剪空間
在一個頂點著色器運行的最后,OpenGL期望所有的坐標都能落在一個特定的范圍內,且任何在這個范圍之外的點都應該被裁剪掉(Clipped)。被裁剪掉的坐標就會被忽略,所以剩下的坐標就將變為屏幕上可見的片段。這也就是裁剪空間(Clip Space)名字的由來。
如果只是圖元(Primitive),例如三角形,的一部分超出了裁剪體積(Clipping Volume),則OpenGL會重新構建這個三角形為一個或多個三角形讓其能夠適合這個裁剪范圍。
例如下圖
一旦所有頂點被變換到裁剪空間,最終的操作——透視除法(Perspective Division)將會執行,在這個過程中我們將位置向量的x,y,z分量分別除以向量的齊次w分量。透視除法是將4D裁剪空間坐標變換為3D標準化設備坐標的過程。這一步會在每一個頂點著色器運行的最后被自動執行。
在OpenGL中只需要處理MVP矩陣,裁剪、透視除法和視口變換會自動處理。
來直接開干把!
- 模型變換矩陣,通過將頂點坐標乘以這個模型矩陣,我們將該頂點坐標變換到世界坐標。我們的平面看起來就是在地板上,代表全局世界里的平面。
glm::mat4 model;
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
- 視圖變換矩陣
glm::mat4 view;
// 注意,我們將矩陣向我們要進行移動場景的反方向移動。
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
- 透視投影矩陣
glm::mat4 projection;
projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);
把這些矩陣傳入頂點著色器
#version 330 core
layout (location = 0) in vec3 aPos;
...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;void main()
{// 注意乘法要從右向左讀gl_Position = projection * view * model * vec4(aPos, 1.0);...
}
整體如下
// 構建MVP矩陣auto model = glm::mat4(1.0f);auto view = glm::mat4(1.0f);auto projection = glm::mat4(1.0f);model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);ourShader.setMat4("model", model);ourShader.setMat4("view", view);ourShader.setMat4("projection", projection);
setMat4是新添加的方法
void Shader::setMat4(const std::string &name, const glm::mat4 &mat) const {glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), // 獲取Uniform位置1, // 上傳矩陣數量GL_FALSE, // 是否轉置(GLM默認列主序,無需轉置)glm::value_ptr(mat) // 使用GLM提供的指針獲取方法);
}
現在圖片躺下了
后邊這個立方體就不做了,就是頂點都寫進去,然后設置好VAO,之后在循環中修改MVP矩陣的M矩陣,他就轉起來了,但目前還沒有考慮Z-buffer。**GLFW會自動為你生成這樣一個緩沖)**我們想要確定OpenGL真的執行了深度測試,首先我們要告訴OpenGL我們想要啟用深度測試;它默認是關閉的。我們可以通過glEnable函數來開啟深度測試。glEnable和glDisable函數允許我們啟用或禁用某個OpenGL功能
// 開啟Z-buffer
glEnable(GL_DEPTH_TEST);// 當模型發生變化時,需要在循環中清楚之前緩存的zbuffer
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
攝像機
OpenGL本身沒有攝像機(Camera)的概念,但我們可以通過把場景中的所有物體往相反方向移動的方式來模擬出攝像機,產生一種我們在移動的感覺,而不是場景在移動。在上一節中我們通過把所有物體向后平移3格,來模擬攝像機在(0,0,3)的位置上。
要定義一個攝像機,我們需要它在世界空間中的位置、觀察的方向、一個指向它右側的向量以及一個指向它上方的向量。實際上創建了一個三個單位軸相互垂直的、以攝像機的位置為原點的坐標系。
首先來指定一個攝像機位置
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
攝像機的方向就指向場景的原點即可,相減就得到了攝像機的指向方向,但是取反轉方向,也就是是指向+Z的,與攝像機的朝向相反
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
之后還需要一個右向量作為x方向,可以通過上向量與指向方向叉乘得到
// 上方向
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
// 叉乘得右方向,表示x軸的正方向
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
最后y方向就可以通過上和右的叉乘得到
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
使用上面得到的x\y\z向量可以構建lookAt矩陣,可以用這個矩陣乘以任何向量來將其變換到攝像機坐標空間
其中R
是右向量,U
是上向量,D
是方向向量,P
是攝像機位置向量,可以直接通過GLM生成這個矩陣
glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
glm::LookAt函數需要一個位置、目標和上向量
如果我們在View矩陣構建時使用時間,就會讓整個場景開始旋轉
float camX = sin(glfwGetTime()) * radius;float camZ = cos(glfwGetTime()) * radius;glm::mat4 view;view = glm::lookAt(glm::vec3(camX, 0.0, camZ), glm::vec3(0.0, 0.0, 0.0), glm::vec3(0.0, 1.0, 0.0));
自由移動
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
通過上邊參數控制,就用這些vec3來控制lookAt矩陣的生成了,下來就用監聽來綁定按鍵修改參數,就是改變Pos來實現的
void processInput(GLFWwindow *window)
{float cameraSpeed = 0.05f; // adjust accordinglyif (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)cameraPos += cameraSpeed * cameraFront;if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)cameraPos -= cameraSpeed * cameraFront;if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
}
這里設置一個固定的移動速度是有bug的,因為每一幀處理一次移動,如果硬件比較好的條件下,幀數高,速度就會快一點。圖形程序和游戲通常會跟蹤一個時間差(Deltatime)變量,它儲存了渲染上一幀所用的時間。我們把所有速度都去乘以deltaTime值
float deltaTime = 0.0f; // 當前幀與上一幀的時間差
float lastFrame = 0.0f; // 上一幀的時間
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
void processInput(GLFWwindow *window)
{float cameraSpeed = 2.5f * deltaTime;...
}
視角移動
為了能夠改變視角,我們需要根據鼠標的輸入改變cameraFront向量。這里需要一點三角學的知識
歐拉角:俯仰角(Pitch)、偏航角(Yaw)和滾轉角(Roll),每個歐拉角都有一個值來表示,把三個角結合起來我們就能夠計算3D空間中任何的旋轉向量了。
假設我們現在在XZ屏幕,往Y偏移就是俯仰角
假設俯仰角為pitch
direction.y = sin(glm::radians(pitch)); // 注意我們先把角度轉為弧度
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));
對偏航角的處理一個道理
通過俯仰角的計算已經知道了斜邊長是cos(pitch),一結合,就得如果已知俯仰角和偏航角,就可以知道(x,y,z)坐標了
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 譯注:direction代表攝像機的前軸(Front),這個前軸是和本文第一幅圖片的第二個攝像機的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
偏航角和俯仰角是通過鼠標(或手柄)移動獲得的,水平的移動影響偏航角,豎直的移動影響俯仰角。它的原理就是,儲存上一幀鼠標的位置,在當前幀中我們當前計算鼠標位置與上一幀的位置相差多少。如果水平/豎直差別越大那么俯仰角或偏航角就改變越大,也就是攝像機需要移動更多的距離。
首先讓光標消失
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
之后注冊一個鼠標移動監聽的回調
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
glfwSetCursorPosCallback(window, mouse_callback);
我們必須在程序存儲上一幀鼠標的位置,再看這一幀的變化,計算出角度進而計算出攝像機位置,修改LookAt矩陣
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{if(firstMouse){lastX = xpos;lastY = ypos;firstMouse = false;}float xoffset = xpos - lastX;float yoffset = lastY - ypos;lastX = xpos;lastY = ypos;float sensitivity = 0.05;xoffset *= sensitivity;yoffset *= sensitivity;yaw += xoffset;pitch += yoffset;if(pitch > 89.0f)pitch = 89.0f;if(pitch < -89.0f)pitch = -89.0f;glm::vec3 front;front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));front.y = sin(glm::radians(pitch));front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));cameraFront = glm::normalize(front);
}
另外場景的放大縮小可以通過控制透視矩陣的fov來控制
最后提供一個封裝好的攝像機類
#ifndef CAMERA_H
#define CAMERA_H#include <glad/glad.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>// Defines several possible options for camera movement. Used as abstraction to stay away from window-system specific input methods
enum Camera_Movement {FORWARD,BACKWARD,LEFT,RIGHT
};// Default camera values
const float YAW = -90.0f;
const float PITCH = 0.0f;
const float SPEED = 2.5f;
const float SENSITIVITY = 0.1f;
const float ZOOM = 45.0f;// An abstract camera class that processes input and calculates the corresponding Euler Angles, Vectors and Matrices for use in OpenGL
class Camera
{
public:// camera Attributesglm::vec3 Position;glm::vec3 Front;glm::vec3 Up;glm::vec3 Right;glm::vec3 WorldUp;// euler Anglesfloat Yaw;float Pitch;// camera optionsfloat MovementSpeed;float MouseSensitivity;float Zoom;// constructor with vectorsCamera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), float yaw = YAW, float pitch = PITCH) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM){Position = position;WorldUp = up;Yaw = yaw;Pitch = pitch;updateCameraVectors();}// constructor with scalar valuesCamera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM){Position = glm::vec3(posX, posY, posZ);WorldUp = glm::vec3(upX, upY, upZ);Yaw = yaw;Pitch = pitch;updateCameraVectors();}// returns the view matrix calculated using Euler Angles and the LookAt Matrixglm::mat4 GetViewMatrix(){return glm::lookAt(Position, Position + Front, Up);}// processes input received from any keyboard-like input system. Accepts input parameter in the form of camera defined ENUM (to abstract it from windowing systems)void ProcessKeyboard(Camera_Movement direction, float deltaTime){float velocity = MovementSpeed * deltaTime;if (direction == FORWARD)Position += Front * velocity;if (direction == BACKWARD)Position -= Front * velocity;if (direction == LEFT)Position -= Right * velocity;if (direction == RIGHT)Position += Right * velocity;}// processes input received from a mouse input system. Expects the offset value in both the x and y direction.void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true){xoffset *= MouseSensitivity;yoffset *= MouseSensitivity;Yaw += xoffset;Pitch += yoffset;// make sure that when pitch is out of bounds, screen doesn't get flippedif (constrainPitch){if (Pitch > 89.0f)Pitch = 89.0f;if (Pitch < -89.0f)Pitch = -89.0f;}// update Front, Right and Up Vectors using the updated Euler anglesupdateCameraVectors();}// processes input received from a mouse scroll-wheel event. Only requires input on the vertical wheel-axisvoid ProcessMouseScroll(float yoffset){Zoom -= (float)yoffset;if (Zoom < 1.0f)Zoom = 1.0f;if (Zoom > 45.0f)Zoom = 45.0f;}private:// calculates the front vector from the Camera's (updated) Euler Anglesvoid updateCameraVectors(){// calculate the new Front vectorglm::vec3 front;front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch));front.y = sin(glm::radians(Pitch));front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch));Front = glm::normalize(front);// also re-calculate the Right and Up vectorRight = glm::normalize(glm::cross(Front, WorldUp)); // normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement.Up = glm::normalize(glm::cross(Right, Front));}
};
#endif
最終能運行的整體代碼
//
// Created by 劉卓昊 on 2025/4/3.
//
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>#include "Shader.h"
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "camera.h"
#define GL_CALL(x) \do { \x; \GLenum error = glGetError(); \if (error != GL_NO_ERROR) { \std::cerr << "OpenGL error: " << error << " at " << __FILE__ << ":" << __LINE__ << std::endl; \} \} while (0)
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
// camera
Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));
float lastX = SCR_WIDTH / 2.0f;
float lastY = SCR_HEIGHT / 2.0f;
bool firstMouse = true;// timing
float deltaTime = 0.0f; // time between current frame and last frame
float lastFrame = 0.0f;GLFWwindow * InitWindowAndFunc() {glfwInit();// 對GLFW的配置 版本號、次版本號、選擇核心模式glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);if (window == nullptr) {std::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();return nullptr;}glfwMakeContextCurrent(window);if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {std::cout << "Failed to initialize GLAD" << std::endl;return nullptr;}// 前兩個參數控制窗口左下角的位置。第三個和第四個參數控制渲染窗口的寬度和高度(像素)GL_CALL(glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT));glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);glfwSetCursorPosCallback(window, mouse_callback);glfwSetScrollCallback(window, scroll_callback);glfwSetScrollCallback(window, scroll_callback);return window;
}int main()
{float vertices[] = {
// ---- 位置 ---- ---- 顏色 ---- - 紋理坐標 -0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上};unsigned int indices[] = {// 注意索引從0開始!// 此例的索引(0,1,2,3)就是頂點數組vertices的下標,// 這樣可以由下標代表頂點組合成矩形0, 1, 3, // 第一個三角形1, 2, 3 // 第二個三角形};GLFWwindow * window = InitWindowAndFunc();// shaderShader ourShader("./shader/shader.vs", "./shader/shader.fs");// 創建Object的IDGLuint VBO, VAO, EBO;GL_CALL(glGenVertexArrays(1, &VAO));GL_CALL(glGenBuffers(1, &VBO));GL_CALL(glGenBuffers(1, &EBO));GL_CALL(glBindVertexArray(VAO));// 綁定到OpenGL上下文中,此時GPU才會真正分配內存GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, VBO));//glBufferData是一個專門用來把用戶定義的數據復制到當前綁定緩沖的函數。它的第一個參數是目標緩沖的類型:頂點緩沖對象當前綁定到GL_ARRAY_BUFFER目標上。第二個參數指定傳輸數據的大小(以字節為單位);用一個簡單的sizeof計算出頂點數據大小就行。第三個參數是我們希望發送的實際數據。GL_CALL(glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW));// EBOGL_CALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO));GL_CALL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW));// VAOGL_CALL(glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0));GL_CALL(glEnableVertexAttribArray(0));GL_CALL(glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))));GL_CALL(glEnableVertexAttribArray(1));GL_CALL(glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))));GL_CALL(glEnableVertexAttribArray(2));// 紋理部分GLuint texture1, texture2;// 讓加載的圖片y軸反轉stbi_set_flip_vertically_on_load(true);// 紋理單元 0GL_CALL(glActiveTexture(GL_TEXTURE0));int width, height, nrChannels;GL_CALL(glGenTextures(1, &texture1));GL_CALL(glBindTexture(GL_TEXTURE_2D, texture1));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));unsigned char *data1 = stbi_load("./assets/container.jpg", &width, &height, &nrChannels, 0);if (!data1){std::cout << "Failed to load texture" << std::endl;}GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data1));GL_CALL(glGenerateMipmap(GL_TEXTURE_2D));stbi_image_free(data1);// 紋理單元 1GL_CALL(glActiveTexture(GL_TEXTURE1));GL_CALL(glGenTextures(1, &texture2));GL_CALL(glBindTexture(GL_TEXTURE_2D, texture2));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));unsigned char *data2 = stbi_load("./assets/awesomeface.png", &width, &height, &nrChannels, 0);if (!data2){std::cout << "Failed to load texture" << std::endl;}GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2));GL_CALL(glGenerateMipmap(GL_TEXTURE_2D));stbi_image_free(data2);ourShader.use();// 設置 Uniform(目的是給片段著色器中定義的兩個采樣器,告訴他們分別對應的是哪個紋理單元)GL_CALL(ourShader.setInt("sampler1", 0));GL_CALL(ourShader.setInt("sampler2", 1));// 光標消失glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);while (!glfwWindowShouldClose(window)){float currentFrame = static_cast<float>(glfwGetTime());deltaTime = currentFrame - lastFrame;lastFrame = currentFrame;processInput(window);// 清理窗口GL_CALL(glClearColor(0.2f, 0.3f, 0.3f, 1.0f));GL_CALL(glClear(GL_COLOR_BUFFER_BIT));// 綁定紋理GL_CALL(glActiveTexture(GL_TEXTURE0));GL_CALL(glBindTexture(GL_TEXTURE_2D, texture1));GL_CALL(glActiveTexture(GL_TEXTURE1));GL_CALL(glBindTexture(GL_TEXTURE_2D, texture2));// 激活著色器GL_CALL(ourShader.use());// 構建MVP矩陣auto model = glm::mat4(1.0f);auto view = glm::mat4(1.0f);auto projection = glm::mat4(1.0f);model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));view = camera.GetViewMatrix();projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);ourShader.setMat4("model", model);ourShader.setMat4("view", view);ourShader.setMat4("projection", projection);// 綁定VAOGL_CALL(glBindVertexArray(VAO));// 繪制GL_CALL(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0));// 事件處理glfwPollEvents();// 雙緩沖glfwSwapBuffers(window);// 解綁GL_CALL(glBindVertexArray(0));GL_CALL(glUseProgram(0));}glfwTerminate();return 0;
}
// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)glfwSetWindowShouldClose(window, true);if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)camera.ProcessKeyboard(FORWARD, deltaTime);if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)camera.ProcessKeyboard(BACKWARD, deltaTime);if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)camera.ProcessKeyboard(LEFT, deltaTime);if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)camera.ProcessKeyboard(RIGHT, deltaTime);
}// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{// make sure the viewport matches the new window dimensions; note that width and// height will be significantly larger than specified on retina displays.glViewport(0, 0, width, height);
}// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xposIn, double yposIn)
{float xpos = static_cast<float>(xposIn);float ypos = static_cast<float>(yposIn);if (firstMouse){lastX = xpos;lastY = ypos;firstMouse = false;}float xoffset = xpos - lastX;float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to toplastX = xpos;lastY = ypos;camera.ProcessMouseMovement(xoffset, yoffset);
}// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{camera.ProcessMouseScroll(static_cast<float>(yoffset));
}