OpenGL入門1.3:着色器 GLSL


前言

經過之前一段時間的學習(渲染管線簡介)我們已經知道了着色器(Shader)是運行在GPU上的程序,這些小程序為圖形渲染管線的某個特定部分而運行,着色器只是一種把輸入轉化為輸出的程序,着色器也是一種非常獨立的程序,因為它們之間不能相互通信,它們之間唯一的溝通只有通過輸入和輸出

之前我們簡要地觸及了一點着色器的皮毛,並了解了如何恰當使用它們,現在我們要用一種更加廣泛的形式詳細解釋着色器,特別是OpenGL着色器語言(GLSL)

GLSL簡介

我們現在討論的着色器是使用OpenGL着色器語言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;	//uniform

int main()
{
  // 處理輸入並進行一些圖形操作
  ...
  // 輸出處理過的結果到輸出變量
  out_variable_name = weird_stuff_we_processed;
}

回看我們之前的圖:

可以看到,對於頂點着色器(Vertex Shader)來說,輸入變量是頂點數據(Vertex Data),頂點數據是用頂點屬性(Vertex Attribute)表示的

我們能聲明的頂點屬性是有上限的(一般是由硬件決定),OpenGL確保至少有16個包含4分量的頂點屬性可用,但是有些硬件或許允許更多的頂點屬性,你可以查詢GL_MAX_VERTEX_ATTRIBS來獲取具體的上限:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

通常情況下它至少會返回16個,大部分情況下是夠用了

數據類型

GLSL中包含C等其它語言大部分的默認基礎數據類型:intfloatdoubleuintbool

GLSL也有兩種容器類型,它們會在這個教程中使用很多,分別是向量(Vector)矩陣(Matrix),其中矩陣我們之后再討論

向量

GLSL中的向量是一個可以包含有1、2、3或者4個分量的容器,分量的類型可以是前面默認基礎類型的任意一個。它們可以是下面的形式(n代表分量的數量):

類型 含義
vecn 包含n個float分量的默認向量
bvecn 包含n個bool分量的向量
ivecn 包含n個int分量的向量
uvecn 包含n個unsigned int分量的向量
dvecn 包含n個double分量的向量

大多數時候我們使用vecn,因為float足夠滿足大多數要求了

一個向量的分量可以分別使用.x.y.z.w來獲取它們的第1、2、3、4個分量
GLSL也允許你對顏色使用rgba,或是對紋理坐標使用stpq訪問相同的分量

向量這一數據類型也允許一些有趣而靈活的分量選擇方式:重組(Swizzling)

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

你可以使用上面4個字母任意組合來創建一個和原來向量一樣長的(同類型)新向量,只要原來向量有那些分量即可(當然,不允許在一個vec2向量中去獲取.z元素)

我們也可以把一個向量作為一個參數傳給不同的向量構造函數,以減少需求參數的數量:

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

向量是一種靈活的數據類型,我們可以把用在各種輸入和輸出上

輸入與輸出

雖然着色器是各自獨立的小程序,但是它們都是一個整體的一部分,所以我們希望每個着色器都有獨立的輸入和輸出,這樣才能進行高效的數據交流和傳遞

GLSL定義了inout關鍵字專門來實現這個目的,每個着色器使用這兩個關鍵字設定輸入和輸出,只要一個輸出變量與下一個着色器階段的輸入匹配,它就會傳遞下去,但在頂點和片段着色器中會有點不同

頂點着色器應該接收的是一種特殊形式的輸入,否則就會效率低下,頂點着色器的輸入特殊在,它從頂點數據中直接接收輸入,為了定義頂點數據該如何管理,我們使用location這一元數據指定輸入變量,這樣我們才可以在CPU上配置頂點屬性,我們已經在前面的教程看過這個了,layout (location = 0),頂點着色器需要為它的輸入提供一個額外的layout標識,這樣我們才能把它鏈接到頂點數據

你也可以忽略layout (location = 0)標識符,通過在OpenGL代碼中使用glGetAttribLocation查詢屬性位置值(Location),但是直接在着色器中設置它們會更容易理解而且節省你和OpenGL的工作量

片段着色器需要一個vec4顏色輸出變量,因為片段着色器需要生成一個最終輸出的顏色,如果你在片段着色器沒有定義輸出顏色,OpenGL會把你的物體渲染為黑色(或白色)

所以,如果我們打算從一個着色器向另一個着色器發送數據,我們必須在發送方着色器中聲明一個輸出,在接收方着色器中聲明一個類似的輸入,當類型和名字都一樣的時候,OpenGL就會把兩個變量鏈接到一起(鏈接程序對象時),它們之間就能發送數據了

現在我們稍微改動一下之前寫的着色器,讓頂點着色器為片段着色器決定顏色

頂點着色器

#version 330 core
layout (location = 0) in vec3 aPos; //位置變量的屬性位置值為0

out vec4 vertexColor; 	//指定一個顏色輸出作為片段着色器輸入

void main()
{
    gl_Position = vec4(aPos, 1.0); 			//把一個vec3作為vec4的構造器的參數
    vertexColor = vec4(0.0, 0.0, 1.0, 1.0); //把輸出變量設置為藍色
}

片段着色器

#version 330 core
out vec4 FragColor;

in vec4 vertexColor; 	//從頂點着色器傳來的輸入變量(名稱相同、類型相同)

void main()
{
    FragColor = vertexColor;
}

我們在頂點着色器中聲明了一個vertexColor變量作為vec4輸出,並在片段着色器中聲明了一個類似的vertexColor輸入,由於它們名字相同且類型相同,片段着色器中的vertexColor就和頂點着色器中的vertexColor鏈接了

由於我們在頂點着色器中將顏色設置為藍色,最終的片段也是藍色的

那我們更進一步,看看能否從應用程序中直接給片段着色器發送一個顏色

Uniform

Uniform是一種從CPU中的應用向GPU中的着色器發送數據的方式,但uniform和頂點屬性有些不同,首先,uniform是全局的(Global),全局意味着uniform變量必須在每個着色器程序對象中都是獨一無二的,而且它可以被着色器程序的任意着色器在任意階段訪問,然后,無論你把uniform值設置成什么,uniform會一直保存它們的數據,直到它們被重置或更新

我們可以在一個着色器中添加uniform關鍵字至類型和變量名前來聲明一個GLSL的uniform
從此處開始我們就可以在着色器中使用新聲明的uniform了,以下是片段着色器的代碼

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; // 在OpenGL程序代碼中設定這個變量

void main()
{
    FragColor = ourColor;
}

我們在片段着色器中聲明了一個uniform vec4的ourColor,並把片段着色器的輸出顏色設置為uniform值的內容,因為uniform是全局變量,我們可以在任何着色器中定義它們,而無需通過頂點着色器作為中介,頂點着色器中不需要這個uniform,所以我們不用頂點着色器里定義它

如果你聲明了一個uniform卻在GLSL代碼中沒用過,編譯器會靜默移除這個變量,導致最后編譯出的版本中並不會包含它,這可能導致幾個非常麻煩的錯誤

這個uniform現在還是空的;我們還沒有給它添加任何數據,所以下面我們就做這件事

我們首先需要找到着色器中uniform屬性的索引/位置值,當我們得到uniform的索引/位置值后,我們就可以更新它的值了,這次我們不去給像素傳遞單獨一個顏色,而是讓它隨着時間改變顏色:

//渲染循環內
glUseProgram(shaderProgram);

float timeValue = glfwGetTime();//獲取運行的秒數
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;//讓顏色在0.0到1.0之間改變,結果儲存到greenValue里
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");//用glGetUniformLocation查詢uniform ourColor的位置值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);//通過glUniform4f函數設置uniform值

首先我們通過glfwGetTime()獲取運行的秒數
然后我們使用sin函數讓顏色在0.0到1.0之間改變,最后將結果儲存到greenValue里

接着,我們用glGetUniformLocation查詢uniform ourColor的位置值,我們為查詢函數提供着色器程序和uniform的名字(這是我們希望獲得的位置值的來源),如果glGetUniformLocation返回-1就代表沒有找到這個位置值

最后,我們可以通過glUniform4f函數設置uniform值,注意,查詢uniform地址不要求你之前使用過着色器程序,但是更新一個uniform之前你必須先使用程序(調用glUseProgram),因為它是在當前激活的着色器程序中設置uniform的

因為OpenGL在其核心是一個C庫,所以它不支持類型重載,在函數參數不同的時候就要為其定義新的函數;glUniform是一個典型例子。這個函數有一個特定的后綴,標識設定的uniform的類型。可能的后綴有:

后綴 含義
f 函數需要一個float作為它的值
i 函數需要一個int作為它的值
ui 函數需要一個unsigned int作為它的值
3f 函數需要3個float作為它的值
fv 函數需要一個float向量/數組作為它的值

在我們的代碼中,我們希望分別設定uniform的4個float值,所以我們通過glUniform4f傳遞我們的數據(也可以使用fv)

現在你知道如何設置uniform變量的值了,我們可以使用它們來渲染了,如果我們打算讓顏色慢慢變化,我們就要在渲染循環的每一次迭代中(所以他會逐幀改變)更新這個uniform,否則三角形就不會改變顏色
下面我們就計算greenValue然后每個渲染迭代都更新這個uniform:

while (!glfwWindowShouldClose(window))
{
    // 輸入
    processInput(window);

    // 渲染指令
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(shaderProgram);

    float timeValue = glfwGetTime();//獲取運行的秒數
    float greenValue = (sin(timeValue) / 2.0f) + 0.5f;//讓顏色在0.0到1.0之間改變,結果儲存到greenValue里
    int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");//用glGetUniformLocation查詢uniform ourColor的位置值
    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);//通過glUniform4f函數設置uniform值

    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);

    // 檢查並調用事件,交換緩沖
    glfwSwapBuffers(window);

    // 檢查觸發什么事件,更新窗口狀態
    glfwPollEvents();
}

這里的代碼對之前代碼是一次非常直接的修改。這次,我們在每次迭代繪制三角形前先更新uniform值

正確更新了uniform,就可以看到我們的矩形逐漸由綠變黑再變回綠色:

XxdlOt9fBq

現在我們的代碼如下:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
"   gl_Position = vec4(aPos, 1.0);\n"
"}\0";

const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"uniform vec4 ourColor;\n"
"void main()\n"
"{\n"
"   FragColor = ourColor;\n"
"}\n\0";

int main()
{
	// 實例化GLFW窗口
	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);
	//(寬,高,窗口名)返回一個GLFWwindow類的實例:window
	if (window == NULL)
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);

	// 告訴GLFW我們希望每當窗口調整大小的時候調用改變窗口大小的函數
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// glad管理opengl函數指針,初始化glad
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		// 生成錯誤則輸出錯誤信息
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	//build and compile 着色器程序
	//頂點着色器
	unsigned int vertexShader;
	vertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
	glCompileShader(vertexShader);
	//檢查頂點着色器是否編譯錯誤
	int  success;
	char infoLog[512];
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "vertexShader complie SUCCESS" << std::endl;
	}
	//片段着色器
	unsigned int fragmentShader;
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
	glCompileShader(fragmentShader);
	//檢查片段着色器是否編譯錯誤
	glGetShaderiv(fragmentShader, GL_LINK_STATUS, &success);
	if (!success) {
		glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "fragmentShader complie SUCCESS" << std::endl;
	}
	//連接着色器
	unsigned int shaderProgram;
	shaderProgram = glCreateProgram();
	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);
	glLinkProgram(shaderProgram);
	//檢查片段着色器是否編譯錯誤
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "shaderProgram complie SUCCESS" << std::endl;
	}
	//連接后刪除
	glDeleteShader(vertexShader);
	glDeleteShader(fragmentShader);

	//頂點數據
	float vertices[] = {
		0.5f, 0.5f, 0.0f,   // 0號點
		0.5f, -0.5f, 0.0f,  // 1號點
		-0.5f, -0.5f, 0.0f, // 2號點
		-0.5f, 0.5f, 0.0f   // 3號點
	};
	unsigned int indices[] = { // 注意索引從0開始!
		0, 1, 3, // 第一個三角形
		1, 2, 3  // 第二個三角形
	};

	unsigned int VBO;
	glGenBuffers(1, &VBO);
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	unsigned int EBO;
	glGenBuffers(1, &EBO);

	// 初始化代碼
	// 1. 綁定頂點數組對象
	glBindVertexArray(VAO);
	// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
	// 4. 設定頂點屬性指針
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);

	////線框模式wireframe
	//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

	// 渲染循環
	while (!glfwWindowShouldClose(window))
	{
		// 輸入
		processInput(window);

		// 渲染指令
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		glUseProgram(shaderProgram);

		float timeValue = glfwGetTime();//獲取運行的秒數
		float greenValue = (sin(timeValue) / 2.0f) + 0.5f;//讓顏色在0.0到1.0之間改變,結果儲存到greenValue里
		int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");//用glGetUniformLocation查詢uniform ourColor的位置值
		glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);//通過glUniform4f函數設置uniform值

		glBindVertexArray(VAO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
		glBindVertexArray(0);

		// 檢查並調用事件,交換緩沖
		glfwSwapBuffers(window);

		// 檢查觸發什么事件,更新窗口狀態
		glfwPollEvents();
	}

	// 釋放之前的分配的所有資源
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &EBO);
	glfwTerminate();
	
	return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	// 每當窗口改變大小,GLFW會調用這個函數並填充相應的參數供你處理
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window)
{
	// 返回這個按鍵是否正在被按下
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
		glfwSetWindowShouldClose(window, true);
}

更多屬性

原版教程(一個三角形)

我們已經了解了如何填充VBO、配置頂點屬性指針以及如何把它們都儲存到一個VAO里

這次,我們同樣打算把顏色數據加進頂點數據中,我們將把顏色數據添加為3個float值至vertices數組,我們將把三角形的三個角分別指定為紅色、綠色和藍色:

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    // 頂部
};

由於現在有更多的數據要發送到頂點着色器,我們有必要去調整一下頂點着色器,使它能夠接收顏色值作為一個頂點屬性輸入。需要注意的是我們用layout標識符來把aColor屬性的位置值設置為1:

#version 330 core
layout (location = 0) in vec3 aPos;   // 位置變量的屬性位置值為 0 
layout (location = 1) in vec3 aColor; // 顏色變量的屬性位置值為 1

out vec3 ourColor; // 向片段着色器輸出一個顏色

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor; // 將ourColor設置為我們從頂點數據那里得到的輸入顏色
}

由於我們不再使用uniform來傳遞片段的顏色了,現在使用ourColor輸出變量,我們必須再修改一下片段着色器:

#version 330 core
out vec4 FragColor;  
in vec3 ourColor;

void main()
{
    FragColor = vec4(ourColor, 1.0);
}

因為我們也不用變換顏色了,激活着色器glUseProgram可以放到渲染循環外:

// 激活着色器
glUseProgram(shaderProgram);

// 渲染循環
while (!glfwWindowShouldClose(window))
{
    // 輸入
    processInput(window);

    // 渲染指令
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    // 交換緩沖並查詢IO事件
    glfwSwapBuffers(window);
    glfwPollEvents();
}

因為我們添加了另一個頂點屬性,並且更新了VBO的內存,我們就必須重新配置頂點屬性指針。更新后的VBO內存中的數據現在看起來像這樣:

知道了現在使用的布局,我們就可以使用glVertexAttribPointer函數更新頂點格式,

// 位置屬性
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);

glVertexAttribPointer函數的前幾個參數比較明了。這次我們配置屬性位置值為1的頂點屬性,顏色值有3個float那么大,我們不去標准化這些值

由於我們現在有了兩個頂點屬性,我們不得不重新計算步長值,為獲得數據隊列中下一個屬性值(比如位置向量的下個x分量)我們必須向右移動6個float,其中3個是位置值,另外3個是顏色值。這使我們的步長值為6乘以float的字節數(=24字節)
同樣,這次我們必須指定一個偏移量,對於每個頂點來說,位置頂點屬性在前,所以它的偏移量是0,顏色屬性緊隨位置數據之后,所以偏移量就是3 * sizeof(float),用字節來計算就是12字節

運行程序:

源碼:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream> 

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
"   gl_Position = vec4(aPos, 1.0);\n"
"   ourColor = aColor;\n"
"}\0";

const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 ourColor;\n"
"void main()\n"
"{\n"
"   FragColor = vec4(ourColor, 1.0f);\n"
"}\n\0";

int main()
{
	//glfw初始化
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);//MacOS

	//glfw window creation
	GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	//glad: load all OpenGL function pointers
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	//build and compile 着色器程序

		//頂點着色器
	unsigned int vertexShader;
	vertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
	glCompileShader(vertexShader);
	//檢查頂點着色器是否編譯錯誤
	int  success;
	char infoLog[512];
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "vertexShader complie SUCCESS" << std::endl;
	}
	//片段着色器
	unsigned int fragmentShader;
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
	glCompileShader(fragmentShader);
	//檢查片段着色器是否編譯錯誤
	glGetShaderiv(fragmentShader, GL_LINK_STATUS, &success);
	if (!success) {
		glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "fragmentShader complie SUCCESS" << std::endl;
	}

	//連接到着色器程序
	unsigned int shaderProgram;
	shaderProgram = glCreateProgram();
	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);
	glLinkProgram(shaderProgram);
	//檢查片段着色器是否編譯錯誤
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "shaderProgram complie SUCCESS" << std::endl;
	}
	//連接后刪除
	glDeleteShader(vertexShader);
	glDeleteShader(fragmentShader);

	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    // 頂部
	};

	unsigned int VBO;
	glGenBuffers(1, &VBO);
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);

	//初始化代碼(只運行一次 (除非你的物體頻繁改變))	
	// 1. 綁定VAO
	glBindVertexArray(VAO);
	// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	// 位置屬性
	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);
	// 激活着色器
	glUseProgram(shaderProgram);

	//線框模式wireframe
	//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

	// 渲染循環
	while (!glfwWindowShouldClose(window))
	{
		// 輸入
		processInput(window);

		// 渲染指令
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		// 更新uniform顏色
		glBindVertexArray(VAO);
		glDrawArrays(GL_TRIANGLES, 0, 3);

		// 交換緩沖並查詢IO事件
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);

	glfwTerminate();
	return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow *window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
		glfwSetWindowShouldClose(window, true);
}

這個圖片可能不是你所期望的那種,因為我們只提供了3個顏色,而不是我們現在看到的大調色板,這是在片段着色器中進行的所謂片段插值(Fragment Interpolation)的結果

當渲染一個三角形時,光柵化(Rasterization)階段通常會造成比原指定頂點更多的片段,光柵會根據每個片段在三角形形狀上所處相對位置決定這些片段的位置
基於這些位置,它會插值(Interpolate)所有片段着色器的輸入變量,比如說,我們有一個線段,上面的端點是綠色的,下面的端點是藍色的。如果一個片段着色器在線段的70%的位置運行,它的顏色輸入屬性就會是一個綠色和藍色的線性結合;更精確地說就是30%藍 + 70%綠

這正是在這個三角形中發生了什么。我們有3個頂點,和相應的3個顏色,從這個三角形的像素來看它可能包含50000左右的片段,片段着色器為這些像素進行插值顏色。如果你仔細看這些顏色就應該能明白了:紅首先變成到紫再變為藍色。片段插值會被應用到片段着色器的所有輸入屬性上

矩形(兩個三角形)

我把矩形的四個角分別指定為白色,紅色、綠色和藍色:

float vertices[] = {
    // 位置              // 顏色
     0.5f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f, // 0右上角
	 0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f, // 1右下角
	-0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f, // 2左下角
	-0.5f,  0.5f, 0.0f,  1.0f, 1.0f, 1.0f  // 3左上角
};

unsigned int indices[] = { // 注意索引從0開始
	0, 1, 3, // 第一個三角形
	1, 2, 3  // 第二個三角形
};

運行程序:

源碼:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream> 

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
"   gl_Position = vec4(aPos, 1.0);\n"
"   ourColor = aColor;\n"
"}\0";

const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 ourColor;\n"
"void main()\n"
"{\n"
"   FragColor = vec4(ourColor, 1.0f);\n"
"}\n\0";

int main()
{
	//glfw初始化
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	//glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);//MacOS

	//glfw window creation
	GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
	if (window == NULL)
	{
		std::cout << "Failed to create GLFW window" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	//glad: load all OpenGL function pointers
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}

	//build and compile 着色器程序

		//頂點着色器
	unsigned int vertexShader;
	vertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
	glCompileShader(vertexShader);
	//檢查頂點着色器是否編譯錯誤
	int  success;
	char infoLog[512];
	glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "vertexShader complie SUCCESS" << std::endl;
	}
	//片段着色器
	unsigned int fragmentShader;
	fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
	glCompileShader(fragmentShader);
	//檢查片段着色器是否編譯錯誤
	glGetShaderiv(fragmentShader, GL_LINK_STATUS, &success);
	if (!success) {
		glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "fragmentShader complie SUCCESS" << std::endl;
	}

	//連接到着色器程序
	unsigned int shaderProgram;
	shaderProgram = glCreateProgram();
	glAttachShader(shaderProgram, vertexShader);
	glAttachShader(shaderProgram, fragmentShader);
	glLinkProgram(shaderProgram);
	//檢查片段着色器是否編譯錯誤
	glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
		std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
	}
	else {
		std::cout << "shaderProgram complie SUCCESS" << std::endl;
	}
	//連接后刪除
	glDeleteShader(vertexShader);
	glDeleteShader(fragmentShader);

	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.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f, // 左下角
		-0.5f,  0.5f, 0.0f,  1.0f, 1.0f, 1.0f  // 左上角
	};

	unsigned int indices[] = { // 注意索引從0開始!
		0, 1, 3, // 第一個三角形
		1, 2, 3  // 第二個三角形
	};

	unsigned int VBO;
	glGenBuffers(1, &VBO);
	unsigned int VAO;
	glGenVertexArrays(1, &VAO);
	unsigned int EBO;
	glGenBuffers(1, &EBO);

	//初始化代碼(只運行一次 (除非你的物體頻繁改變))	
	// 1. 綁定VAO
	glBindVertexArray(VAO);
	// 2. 把我們的頂點數組復制到一個頂點緩沖中,供OpenGL使用
	glBindBuffer(GL_ARRAY_BUFFER, VBO);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
	// 3. 復制我們的索引數組到一個索引緩沖中,供OpenGL使用
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
	// 4. 設定頂點屬性指針

	// 位置屬性
	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);
	// 激活着色器
	glUseProgram(shaderProgram);

	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);

	//線框模式wireframe
	//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

	// 渲染循環
	while (!glfwWindowShouldClose(window))
	{
		// 輸入
		processInput(window);

		// 渲染指令
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		glBindVertexArray(VAO);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

		// 交換緩沖並查詢IO事件
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	glDeleteVertexArrays(1, &VAO);
	glDeleteBuffers(1, &VBO);
	glDeleteBuffers(1, &EBO);

	glfwTerminate();
	return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow *window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//是否按下了返回鍵
		glfwSetWindowShouldClose(window, true);
}

着色器類

編寫、編譯、管理着色器很麻煩,所以我們要寫一個類專門處理這件事,它可以從硬盤讀取着色器,然后編譯並鏈接它們,並對它們進行錯誤檢測

我們會把着色器類全部放在在頭文件里,主要是為了學習用途,當然也方便移植,我們先建立一個shader_s.h

img

#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h>; // 包含glad來獲取所有的必須OpenGL頭文件

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

class Shader
{
public:
    // 生成一個ID
    unsigned int ID;
    // 構造函數
    Shader(const GLchar* vertexPath, const GLchar* 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;
};

#endif

在上面,我們在頭文件頂部使用了幾個預處理指令(Preprocessor Directives),這些預處理指令會告知你的編譯器只在它沒被包含過的情況下才包含和編譯這個頭文件,即使多個文件都包含了這個着色器頭文件,它是用來防止鏈接沖突的

着色器類儲存了着色器程序的ID,它的構造器需要頂點和片段着色器源代碼的文件路徑,這樣我們就可以把源碼的文本文件儲存在硬盤上了
除此之外,為了讓我們的生活更輕松一點,還加入了一些工具函數:use用來激活着色器程序,所有的set…函數能夠查詢一個unform的位置值並設置它的值

然后我們要在原來的Test.cpp里include我們自己寫的這個頭文件

從文件讀取

我們使用C++文件流讀取着色器內容,儲存到幾個string對象里:

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();
        // 轉換數據流到string
        vertexCode   = vShaderStream.str();
        fragmentCode = fShaderStream.str();     
    }
    catch(std::ifstream::failure e)
    {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_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;
};

// 片段着色器也類似
[...]

// 着色器程序
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);

use函數非常簡單:

void use() 
{ 
    glUseProgram(ID);
}

uniform的setter函數也很類似:

void setBool(const std::string &name, bool value) const
{
    glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); 
}
void setInt(const std::string &name, int value) const
{ 
    glUniform1i(glGetUniformLocation(ID, name.c_str()), value); 
}
void setFloat(const std::string &name, float value) const
{
    glUniform1f(glGetUniformLocation(ID, name.c_str()), value); 
} 

以下幾個set的寫法,之后會用到,先不用寫:

void setVec2(const std::string & name, const glm::vec2 & value) const
{
    glUniform2fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}
void setVec2(const std::string & name, float x, float y) const
{
    glUniform2f(glGetUniformLocation(ID, name.c_str()), x, y);
}
// ------------------------------------------------------------------------
void setVec3(const std::string & name, const glm::vec3 & value) const
{
    glUniform3fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}
void setVec3(const std::string & name, float x, float y, float z) const
{
    glUniform3f(glGetUniformLocation(ID, name.c_str()), x, y, z);
}
// ------------------------------------------------------------------------
void setVec4(const std::string & name, const glm::vec4 & value) const
{
    glUniform4fv(glGetUniformLocation(ID, name.c_str()), 1, &value[0]);
}
void setVec4(const std::string & name, float x, float y, float z, float w)
{
    glUniform4f(glGetUniformLocation(ID, name.c_str()), x, y, z, w);
}
// ------------------------------------------------------------------------
void setMat2(const std::string & name, const glm::mat2 & mat) const
{
    glUniformMatrix2fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
// ------------------------------------------------------------------------
void setMat3(const std::string & name, const glm::mat3 & mat) const
{
    glUniformMatrix3fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}
// ------------------------------------------------------------------------
void setMat4(const std::string & name, const glm::mat4 & mat) const
{
    glUniformMatrix4fv(glGetUniformLocation(ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);
}

現在我們就寫完了一個完整的着色器類,使用這個着色器類就很簡單了:

Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
...
while(...)
{
    ourShader.use();
    ourShader.setFloat("someUniform", 1.0f);
    DrawStuff();
}

我們把頂點和片段着色器儲存為兩個叫做shader.vsshader.fs的文件(包括后綴都可以隨意改,在你的代碼里也做相應的改動就行了)

shader_s.h的完整代碼:

#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h>; // 包含glad來獲取所有的必須OpenGL頭文件

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>


class Shader
{
public:
	// 程序ID
	unsigned int ID;

	// 構造器讀取並構建着色器 
	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();
			// 轉換數據流到string
			vertexCode = vShaderStream.str();
			fragmentCode = fShaderStream.str();
		}
		catch (std::ifstream::failure e)
		{
			std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
		}
		const char* vShaderCode = vertexCode.c_str();
		const char* fShaderCode = fragmentCode.c_str();

		// 2. 編譯着色器
		unsigned int vertex, fragment;
		// 頂點着色器
		vertex = glCreateShader(GL_VERTEX_SHADER);
		glShaderSource(vertex, 1, &vShaderCode, NULL);
		glCompileShader(vertex);
		checkCompileErrors(vertex, "VERTEX");
		// 片段着色器也類似
		fragment = glCreateShader(GL_FRAGMENT_SHADER);
		glShaderSource(fragment, 1, &fShaderCode, NULL);
		glCompileShader(fragment);
		checkCompileErrors(fragment, "FRAGMENT");
		// 着色器程序
		ID = glCreateProgram();
		glAttachShader(ID, vertex);
		glAttachShader(ID, fragment);
		glLinkProgram(ID);
		checkCompileErrors(ID, "PROGRAM");
		// 刪除着色器,它們已經鏈接到我們的程序中了,已經不再需要了
		glDeleteShader(vertex);
		glDeleteShader(fragment);
	}
	// 激活着色器
	void use()
	{
		glUseProgram(ID);
	}
	// uniform的setter函數
	void setBool(const std::string &name, bool value) const
	{
		glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
	}
	void setInt(const std::string &name, int value) const
	{
		glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
	}
	void setFloat(const std::string &name, float value) const
	{
		glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
	}

private:
	// 檢查編譯或鏈接是否出錯
	void checkCompileErrors(unsigned int shader, std::string type)
	{
		int success;
		char infoLog[1024];
		if (type != "PROGRAM")
		{
			glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
			if (!success) //打印連接錯誤(如果有的話)
			{
				glGetShaderInfoLog(shader, 1024, NULL, infoLog);
				std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
			}
		}
		else
		{
			glGetProgramiv(shader, GL_LINK_STATUS, &success);
			if (!success) //打印連接錯誤(如果有的話)
			{
				glGetProgramInfoLog(shader, 1024, NULL, infoLog);
				std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
			}
		}
	}

};

#endif

應用着色器類

建立Shader類之后就輕松多了

首先建兩個文件分別存頂點着色器代碼和片段着色器代碼

着色器代碼直接寫進去,再也不用“”引來引去了

注意VS的C++工程里,篩選器(就是這個看上去像文件夾的)不是真的文件夾,在項目路徑下是沒有Shader這個文件夾的

所以我們在Main.cpp(我工程里的Test.cpp)里要這樣寫:

Shader ourShader("vertexSource.txt", "fragmentSource.txt");

當然這一堆你也不用寫了,全部注釋掉

你要做的只是把

glUseProgram(shaderProgram);

替換成

ourShader.use();

就大功告成了

擴展練習

1.3.1倒置三角形

修改頂點着色器倒置三角形

這是我們原來的頂點着色器代碼

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main()
{
   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
   ourColor = aColor;
}

修改后

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main()
{
    gl_Position = vec4(aPos.x, -aPos.y, aPos.z, 1.0); // just add a - to the y position
    ourColor = aColor;
}

效果如下

1.3.2移動三角形

使用uniform定義一個水平偏移量,在頂點着色器中使用這個偏移量把三角形移動到屏幕右側

先寫好頂點着色器:

// vertex shader:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

uniform float xOffset;

void main()
{
    gl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0); // add the xOffset to the x position of the vertex position
    ourColor = aColor;
}

在我們沒部署好着色器類的時候,我們需要這么寫:

// 渲染循環中		
// 更新uniform
float timeValue = glfwGetTime();
float rightValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "xOffset");//用glGetUniformLocation查詢uniform xOffset的位置值
glUseProgram(shaderProgram);//通過glUniform4f函數設置uniform值
glUniform1f(vertexColorLocation, rightValue);//通過glUniform4f函數設置uniform值

其中shaderProgram這個ID值是在渲染循環外獲得的:

int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}

但是我們在Shader類里已經寫好了這些東西,和幾個Set的函數

那就舒服多了,在渲染循環中直接這么來:

// 更新uniform
float timeValue = glfwGetTime();
float rightValue = (sin(timeValue) / 2.0f);
ourShader.setFloat("xOffset", rightValue);

awesome~!

LM3o1hGErd

結合之前變換顏色的套路,我們加點花樣:

// fragment Shader
#version 330 core
out vec4 FragColor;  
uniform vec4 ourColor;

void main()
{
    FragColor = ourColor;
}

由於這個uniform是個vec4,我們在shader_s頭文件里反手給它一個超級加倍

然后我們在渲染循環里給他個字:明牌

然后就是我們最最激動的rewarding moment:

4cpZ8a4iAY

END


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM