LearnOpenGL学习笔记—入门04:Shaders(上)
0 前言
本节笔记对应的内容 着色器
在入门01中我们配置好了环境
在入门02中我们可以检测输入并出现一个有颜色的窗口
在入门03中我们初步学习了图形渲染管线,尝试用不同方法画出了三角形和四边形
在这一节,我们会开始讲讲着色器(Shader)的内容,以及OpenGL着色器语言(GLSL)。
在入门03的程序中,我们有用过shader,没有仔细讲,所以在Shaders(上)这节,我们可以尝试着具体的分析运用一下。
1 GLSL
着色器(Shader)是运行在GPU上的小程序,是使用一种叫GLSL的类C语言写成的。一个典型的着色器有下面的结构:
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
int main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
- 第一行是声明所用的OpenGL版本
- 接着是输入和输出这个shader的变量
- 接下来是uniform
什么是uniform呢?,根据上面的这幅图我们可以来理解。
上图中的Vertex Shader的in和out已经接好了,从VAO中输入,输出到Fragment Shader。
但是除此之外,如果还需要从cpu中拿什么参数(比如时间什么的),那么就要从额外的管道(uniform)输入进来。 - 底下具体的shader代码也是包含在main函数当中,然后我们做一些操作之后,输出变量。
输出的变量就会自动地往图形渲染管线的下一部分输送过去(在我们的例子中就是从Vertex Shader到了Fragment Shader),所以输出这端的名字也要和输入那端的变量名一样。 - Vertex Shader一定会输出gl_Position给下面去处理。并且当我们特别谈论到Vertex Shader的时候,每个输入变量也叫顶点属性(Vertex Attribute)。
- 我们能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用。
2 GLSL的数据类型
和其他编程语言一样,GLSL也有数据类型可以来指定变量的种类。
GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。
GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix),其中矩阵会在之后的学习里再讨论。
- 向量
GLSL中的向量是一个可以包含1到4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表分量的数量):
一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量。我们可以分别使用.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 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
3 输入与输出
虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,每个着色器都有各自的输入和输出,这样才能进行数据交流和传递。
GLSL定义了in和out关键字专门来实现输入输出。
每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。
但在顶点和片段着色器中会有点不同。
- 顶点着色器接收的是一种特殊形式的输入。
顶点着色器的输入特殊在,它从顶点数据中直接接收输入。
为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。
我们已经在前面的教程看过这个了,layout (location = 0)。
顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。 - 片段着色器它需要一个vec4颜色输出变量,因为片段着色器的功能是生成一个最终输出的颜色。
如果在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)
如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。
当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了。
4 给图形上色
我们把顶点着色器改成这样
const char* vertexShaderSource =
"#version 330 core \n"
"layout(location = 0) in vec3 aPos; \n"
"out vec4 vertexColor; \n"
"void main(){ \n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n"
" vertexColor = vec4(0.5, 0.0, 0.0, 1.0); \n"
"}
可以看到我们增加了一个输出变量vertexColor,指定为暗红色。
与之对应的我们改一下片段着色器的代码
const char* fragmentShaderSource =
"#version 330 core \n"
"out vec4 FragColor; \n"
"in vec4 vertexColor; \n"
"void main(){ \n"
" FragColor = vertexColor;} \n";
我们增加了一个输入变量,来接收顶点着色器传输出来的数据,并把这个数据赋值给FragColor,这样我们实现了简单的传递。
然后再上节笔记的基础上运行代码,得到一个红色的四边形
5 让图形随着时间变色
之前我们是从顶点着色器丢颜色到片段着色器,现在我们使用uniform直接丢给片段着色器。
const char* fragmentShaderSource =
"#version 330 core \n"
"in vec4 vertexColor; \n"
"uniform vec4 ourColor; \n"
"out vec4 FragColor; \n"
"void main(){ \n"
" FragColor = ourColor;} \n";
现在这个uniform还是空的,我们还没有给它添加任何数据,所以下面我们就做这件事。
我们首先需要找到着色器中uniform属性的索引/位置值。
当我们得到uniform的索引/位置值后,我们就可以更新它的值了。
这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色,在渲染循环中加入以下代码(可以再上一节完整代码的glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
之后加上):
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);
- 首先我们通过glfwGetTime()获取运行的秒数。然后我们使用sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。
- 接着,我们用glGetUniformLocation查询uniform ourColor的位置值。
我们为查询函数提供着色器程序和uniform的名字。
如果glGetUniformLocation返回-1就代表没有找到这个位置值。 - 最后,我们可以通过glUniform4f函数设置uniform值。
- 注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。
现在知道如何设置uniform变量的值,我们就可以使用它们来渲染了。
我们打算让颜色慢慢变化,所以上面我们是在游戏循环的每一次迭代中加入它,更新这个uniform(所以颜色会逐帧改变)。
如果正确更新了uniform,那么我们会看到我们的图形逐渐由绿变黑再变回绿色。(动态过程就不附视频了_(:з」∠)_)
6 绘制一个彩色的图形
在前面的学习中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。
这次,我们想加一些内容,打算把颜色数据加进顶点数据中。
我们将把颜色数据添加为3个float值至vertices数组。
我们通过修改顶点数组,把四边形的四个角分别指定为红色,绿色,蓝色以及一个其他颜色:
float vertices[] = {
-0.5f, -0.5f, 0.0f, 1.0f, 0, 0, //0
0.5f, -0.5f, 0.0f, 0, 1.0f, 0, //1
0.0f, 0.5f, 0.0f, 0, 0, 1.0f, //2
//0.5f, -0.5f, 0.0f,
//0.0f, 0.5f, 0.0f,
0.8f, 0.8f, 0.0f, 0.3f, 0.5f, 0.7f //3
};
这时候VBO中我们的数据看起来是这样的:
在原本只有位置信息的基础上,多了三个栏位给RGB。
所以我们通过顶点属性挖数据的时候,就要挖两个东西,一个是顶点位置,一个是顶点色。
我们0号栏位给位置,1号栏位给颜色,并且由于数据变多了,我们也要修改对应的偏移量和步长值
// 位置属性
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);
对应的着色器也要进行修改,加入了顶点色
const char* vertexShaderSource =
"#version 330 core \n"
"layout(location = 0) in vec3 aPos; // 位置变量的属性位置值为 0\n"
"layout(location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1\n"
"out vec4 vertexColor; \n"
"void main(){ \n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n"
" vertexColor = vec4(aColor,1.0); \n"
"} \n";
const char* fragmentShaderSource =
"#version 330 core \n"
"in vec4 vertexColor; \n"
"uniform vec4 ourColor; \n"
"out vec4 FragColor; \n"
"void main(){ \n"
" FragColor = vertexColor;} \n";
点选运行
我们只提供了4个颜色,现在却看到了一个大大的调色板。
这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。比如当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。
光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。
- 比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。
如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。
这正是在这个四边形中发生了什么。我们有4个顶点,和相应的4个颜色,从这个三角形的像素来看它可能包含很多的片段,片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了:红首先变成到紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上。
此时的完整代码应该是:
#include <iostream>
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
float vertices[] = {
-0.5f, -0.5f, 0.0f, 1.0f, 0, 0, //0
0.5f, -0.5f, 0.0f, 0, 1.0f, 0, //1
0.0f, 0.5f, 0.0f, 0, 0, 1.0f, //2
//0.5f, -0.5f, 0.0f,
//0.0f, 0.5f, 0.0f,
0.8f, 0.8f, 0.0f, 0.3f, 0.5f, 0.7f //3
};
//0 1 2 2 1 3
unsigned int indices[] = {
0,1,2,
2,1,3
};
const char* vertexShaderSource =
"#version 330 core \n"
"layout(location = 0) in vec3 aPos; // 位置变量的属性位置值为 0\n"
"layout(location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1\n"
"out vec4 vertexColor; \n"
"void main(){ \n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); \n"
" vertexColor = vec4(aColor,1.0); \n"
"} \n";
const char* fragmentShaderSource =
"#version 330 core \n"
"in vec4 vertexColor; \n"
"uniform vec4 ourColor; \n"
"out vec4 FragColor; \n"
"void main(){ \n"
" FragColor = vertexColor;} \n";
void processInput(GLFWwindow* window){
if (glfwGetKey(window, GLFW_KEY_ESCAPE )== GLFW_PRESS)
{
glfwSetWindowShouldClose(window, true);
}
}
int main() {
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR,3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//Open GLFW Window
GLFWwindow* window = glfwCreateWindow(800,600,"My OpenGL Game",NULL,NULL);
if(window == NULL)
{
printf("Open window failed.");
glfwTerminate();
return - 1;
}
glfwMakeContextCurrent(window);
//Init GLEW
glewExperimental = true;
if (glewInit() != GLEW_OK)
{
printf("Init GLEW failed.");
glfwTerminate();
return -1;
}
glViewport(0, 0, 800, 600);
//glEnable(GL_CULL_FACE);
//glCullFace(GL_BACK);
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
unsigned int fragmentShader;
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);
// 位置属性
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);
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
//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);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//glDrawArrays(GL_TRIANGLES, 0, 6);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}