着色器(Shader)
- 在之前的随笔中说过,着色器(Shader)是运行在GPU上的小程序。从基本意义上来说,着色器只是一种把输入转化为输出的程序。
- 着色器之间很独立,唯一的共同方式就是通过输入和输出
GLSL
- 一个典型的着色器有以下的结构
#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; }
VertexShader:输入变量也叫顶点属性( Vertex Attribute )。要特别注意以下,我们能声明的顶点属性是有上限的。OpenGL确保至少有16个包含4个分量的顶点属性可用。
( 可以通过GL_MAX_VERTEX_ATTRIBS获取 )。
int nrAttributes; /* 大部分情况下16个够用了,要啥自行车。 /抠鼻 */ glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes); cout << "Maximum nr of vertex attributes supported: " << nrAttributes << sendl;
数据类型
- GLSL的数据类型有三大类
- 基本的数据类型:int float double uint bool
- 向量(Vector)
- 矩阵(Maxtrix)
- 向量
- 一般来说vecn就够了( float 足够满足大部分需求 )
- 一个向量的分量可以通过 vec.x 获取( .x .y .z .w获取第1 2 3 4个分量 )
- GLSL允许对颜色使用rbga,对纹理坐标使用stpq访问相同的分量
- 还有一种骚操作叫重组。
vec2 someVec; vec4 differentVec = someVec.xyxx; vec3 anotherVec = differentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVec.yxzy; vec2 vect = vec2(0.5, 0.7); vec4 result = vec4(vect, 0.0, 0.0); vec4 otherResult = vec4(result.xyz, 1.0); /* 语法不难理解,但确实骚 */
类型 | 含义 |
---|---|
vecn | 包含n个float分量的默认向量 |
bvecn | 包含n个bool分量的向量 |
ivecn | 包含n个int分量的向量 |
uvecn | 包含n个unsigne intd分量的向量 |
dvecn | 包含n个double分量的向量 |
输入与输出
- 之前说过各个着色器之间相互独立,所以要通过输入与输出来实现Shader之间的数据交流与传递。
- GLSL定义了in和out这两个关键字来实现输入和输出。只要上一个阶段的输出变量与下一个阶段的输出变量匹配,它就会传递下去。( 但是在顶点着色器和片段着色器之间会有一些不同 )
下面着重说一下顶点着色器和片段着色器哪里不同
- 顶点着色器:
- 他的输入是从顶点数据中直接接收输入。所以使用location来指定输入变量去定义顶点数据如何管理,这样我们才可以在CPU上配置顶点属性。
- 顶点着色器需要为它的输入提供一个额外的
layout
标识,这样我们才能把它链接到顶点数据。
你也可以忽略layout(location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量。
- 片段着色器:他的输入是一个ve4颜色的输出变量,如果你的片段着色器没有定义输出颜色,默认为黑色或白色。
- 顶点着色器:
all in all: 如果我们打算从一个着色器向另一个着色器发送数据,必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。
举个栗子 ( 🌰三某松鼠的板栗,香死了。 )
/* Vertex Shader */ #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.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色 } /* Fragment Shader */ #version 330 core out vec4 FragColor; in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) void main(){ FragColor = vertexColor; }
Uniform
- 理解
- Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式
- Uniform是全局的。在每个着色器程序对象中都是独一无二的, 可以被着色器程序的任意着色器在任意阶段访问。
- 无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
- 使用( 用uniform关键字声明一个GLSL的uniform )
#version 330 core out vec4 FragColor; /* uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。 */ uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量 void main(){ FragColor = ourColor; }
- 上面只是定义了一个uniform,里面撒子都么的。下面操作一番:
- 找到着色器中uniform属性的索引/位置值。 以便于我们对他的值经行更新。
- 这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色。
- 有几个需要注意的点:
- 更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。
- glUniform函数不支持重载,所以会有后缀
后缀 | 含义 |
---|---|
f | 函数需要一个float作为它的值 |
i | 函数需要一个int作为它的值 |
ui | 函数需要一个unsigned int作为它的值 |
3f | 函数需要3个float作为它的值 |
fv | 函数需要一个float向量/数组作为它的值 |
float timeValue = glfwGetTime(); //获取运行的秒数 float greenValue = (sin(timeValue) / 2.0f) + 0.5f; //使用sin函数让他在0到1之间改变 int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); /* 使用getUniformLocation函数获取uniform中ourcolor的位置的值 返回值为-1时未找到 */ glUseProgram(shaderProgram); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); /* 通Uniform4f函数设置uniform值 */
这里写完之后,你的渲染图形就会变色辣~( 撒花 🌸)
Uniform的妙妙玩儿法
( PS:还是换回三角形吧~正方体太沙雕了。 )
先喝口奶:milk_glass:,最终效果是这样的。⬇️
- 首先你的顶点数据就不能只放位置了,还要加颜色( RGB )
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 // 顶部 };
- 调整顶点着色器
#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设置为我们从顶点数据那里得到的输入颜色 }
- 调整片段着色器
#version 330 core out vec4 FragColor; in vec3 ourColor; void main(){ FragColor = vec4(ourColor, 1.0); }
因为我们添加了另一个顶点属性,更新了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); /* 特别需要注意的是颜色属性的偏移量,也就是最后一个参数 */
点击小鸡获取花里胡哨的憨憨源码~🐤
最后说一下
这个憨憨的彩色三角形可能会引起你的疑惑,毕竟真的是啥都看不出来。
- 因为我们只提供了3个颜色,而不是我们现在看到的大调色板。这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。
- 当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
- 基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。
- 比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。
所以憨憨彩色三角形的出现这个悬案也就结了。