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

posted @ 2019-07-24 23:18  KelvinVS  阅读(15321)  评论(1编辑  收藏  举报