OpenGL学习笔记(三)着色器
参考资料:OpenGL中文翻译
Shader是什么
着色器(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;
int main()
{
  // 处理输入并进行一些图形操作
  ...
  // 输出处理过的结果到输出变量
  out_variable_name = weird_stuff_we_processed;
}
当我们特别谈论到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute)。我们能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限:
GLint nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
运行结果:

所以我的机器可以使用的顶点属性有4199705个。
数据类型
- 包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。
- 两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix),其中矩阵我们会在之后的教程里再讨论。
向量vec
GLSL中的向量是一个可以包含有1、2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表分量的数量):

即默认分量是float类型的。
一个向量的分量可以通过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;
我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:
vec2 vect = vec2(0.5f, 0.7f);
vec4 result = vec4(vect, 0.0f, 0.0f);
vec4 otherResult = vec4(result.xyz, 1.0f);
输入与输出
我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。
顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。
tips: 你也可以忽略
layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量。
另一个例外是片段着色器,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
顶点着色器向片段着色器发送数据
顶点着色器示例代码
#version 330 core
layout (location = 0) in vec3 position; // position变量的属性位置值为0
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
    gl_Position = vec4(position, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
    vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出变量设置为暗红色
}
片段着色器示例代码
#version 330 core
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
out vec4 color; // 片段着色器输出的变量名可以任意命名,类型必须是vec4
void main()
{
    color = vertexColor;
}
你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。由于它们名字相同且类型相同(传输条件),片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。
运行结果:

即我们成功地从顶点着色器向片段着色器发送数据。
Uniform
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同:
- uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。
- 无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
我们可以在一个着色器中添加uniform关键字至类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色:
#version 330 core
out vec4 color;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
    color = ourColor;
}
声明了一个uniform vec4的变量ourColor,并把片段着色器的输出颜色设置为uniform值的内容。
uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。
tips: 如果你声明了一个uniform却在GLSL代码中没用过,编译器会默认移除这个变量,导致最后编译出的版本中并不会包含它。
在程序中将数据传输给uniform变量
- 找到着色器中uniform属性的索引/位置值。
- 更新它的值。
这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色:
GLfloat timeValue = glfwGetTime();
GLfloat greenValue = (sin(timeValue) / 2) + 0.5; // 将输入的值映射到 0~1
// 1.获取uniform变量的位置
GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
// 2.更新变量的值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
当前主循环代码:
void renderLoop(GLFWwindow *window, GLuint VAO, GLuint shaderProgram) {
    // 设置清屏时填充的颜色
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glUseProgram(shaderProgram);
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window)) {
        // input
        processInput(window);
        // render
        glClear(GL_COLOR_BUFFER_BIT);
        // 更新uniform颜色
        GLfloat timeValue = glfwGetTime();
        GLfloat greenValue = (sin(timeValue) / 2) + 0.5;
        // 1.找uniform变量位置
        GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
        // 2.更新变量值
        glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
        glBindVertexArray(VAO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
}
运行结果即为颜色由黑到绿不断变化的矩形。
制作三色渐变三角形
我们打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:
GLfloat 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 position; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 color;    // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
    gl_Position = vec4(position, 1.0);
    ourColor = color; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
片段着色器:
#version 330 core
in vec3 ourColor;
out vec4 color;
void main()
{
    color = vec4(ourColor, 1.0f);
}
重新配置顶点属性指针:

// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glVertexAttribPointer函数参数:
- 变量的location
- 变量vec的分量数
- 略
- 步长
- 偏移
最后我们修改主循环的代码:
void renderLoop(GLFWwindow *window, GLuint VAO, GLuint shaderProgram) {
    // 设置清屏时填充的颜色
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glUseProgram(shaderProgram);
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window)) {
        // input
        // -----
        processInput(window);
        // render
        // ------
        glClear(GL_COLOR_BUFFER_BIT);
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES,0,3);
        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
}
运行结果:

这个图片可能不是你所期望的那种,因为我们只提供了3个颜色,而不是我们现在看到的大调色板。这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。
这正是在这个三角形中发生了什么。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50000左右的片段,片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了:红首先变成到紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上。
对着色器程序进行封装
我们会发现着色器程序通过OpenGL提供的api操作起来相当麻烦,例如需要先创建两个内部的着色器对象vertexShader和fragmentShader,然后再创建program对象,再将小的Shader附加上去,再链接,甚至使用完之后还需要手动将它们删除,实在是不好管理,故我们可以对Shader程序进行封装。
创建ShaderProgram类:
头文件:
class ShaderProgram {
private:
	// 输入文件名,输出文件中的字符串,我们可以输入存放Shader程序代码的文件路径
    static string getFileString(const GLchar *filePath);
    // 创建内部着色器对象,即vertexShader和fragmentShader
    GLuint createInnerShader(const char ** shaderCodeLoc, GLenum ShaderType);
    // 检查链接是否成功
    bool checkLink();
public:
	// 程序对象id
    GLuint program;
    // 构造函数,参数为两个着色器程序的源代码路径
    ShaderProgram(const GLchar* vertexPath, const GLchar* fragmentPath);
    // 即bind到OpenGL上下文中
    void use();
    // 删除program对象
    ~ShaderProgram();
};
cpp文件;
string ShaderProgram::getFileString(const GLchar *filePath) {
    ifstream shaderFile;
    shaderFile.exceptions(std::ifstream::badbit);
    string total;
    try {
        shaderFile.open(filePath);
        string line;
        while (getline(shaderFile,line)){
            total += line+'\n';
        }
        shaderFile.close();
    }
    catch (ifstream::failure e) {
        cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << endl;
    }
    return total;
}
GLuint ShaderProgram::createInnerShader(const char **shaderCodeLoc, GLenum ShaderType) {
    GLchar infoLog[512];
    GLint success;
    GLuint shaderId = glCreateShader(ShaderType);
    glShaderSource(shaderId,1,shaderCodeLoc, nullptr);
    glCompileShader(shaderId);
    glGetShaderiv(shaderId,GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(shaderId,512, nullptr,infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    return shaderId;
}
bool ShaderProgram::checkLink() {
    // 打印连接错误(如果有的话)
    int success;
    GLchar infoLog[512];
    glGetProgramiv(this->program, GL_LINK_STATUS, &success);
    if(!success)
    {
        glGetProgramInfoLog(this->program, 512, nullptr, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
        return false;
    }
    return true;
}
ShaderProgram::ShaderProgram(const GLchar *vertexPath, const GLchar *fragmentPath) {
    const char *  vertexCode = getFileString(vertexPath).c_str();
    const char *  fragmentCode = getFileString(fragmentPath).c_str();
    GLuint vertexShader = createInnerShader(&vertexCode, GL_VERTEX_SHADER);
    GLuint fragmentShader = createInnerShader(&fragmentCode, GL_FRAGMENT_SHADER);
    program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    glLinkProgram(program);
    checkLink();
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
}
void ShaderProgram::use() {
    glUseProgram(program);
}
ShaderProgram::~ShaderProgram() {
    glDeleteProgram(program);
}
这样我们在客户端,也就是主函数中使用Shader就方便多了:
先将两个Shader程序的源代码放到对应的文件中:



在数据准备阶段时构造shader对象:
ShaderProgram shaderProgram(R"(D:\code\cppCode\CLion\LearnOpenGL\shaderProgram\vertexShader.vert)",
                  R"(D:\code\cppCode\CLion\LearnOpenGL\shaderProgram\fragmentShader.frag)");
然后在主循环中使用shader对象,即把它bind到OpenGL上下文中:
void renderLoop(GLFWwindow *window, GLuint VAO, ShaderProgram &shader) {
    // 设置清屏时填充的颜色
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    shader.use(); <--使用shader
    // render loop
    while (!glfwWindowShouldClose(window)) {
        // input
        // ...
        // render
        // ...
        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // ...
    }
}
这样我们就完成了对ShaderProgram的封装。
OpenGL学习笔记(三)着色器的更多相关文章
- OpenGL学习笔记——求值器和NURBS
		http://codercdy.com/openglxue-xi-bi-ji-qiu-zhi-qi-he-nurbs/ 在最底层,图形硬件所绘制的是点.直线和多边形(通常是三角形和四边形).平滑的曲线 ... 
- [OpenGL ES 02]OpenGL ES渲染管线与着色器
		[OpenGL ES 02]OpenGL ES渲染管线与着色器 罗朝辉 (http://www.cnblogs.com/kesalin/) 本文遵循"署名-非商业用途-保持一致"创 ... 
- java之jvm学习笔记三(Class文件检验器)
		java之jvm学习笔记三(Class文件检验器) 前面的学习我们知道了class文件被类装载器所装载,但是在装载class文件之前或之后,class文件实际上还需要被校验,这就是今天的学习主题,cl ... 
- java学习笔记13--比较器(Comparable、Comparator)
		java学习笔记13--比较器(Comparable.Comparator) 分类: JAVA 2013-05-20 23:20 3296人阅读 评论(0) 收藏 举报 Comparable接口的作用 ... 
- 学习笔记(三)--->《Java 8编程官方参考教程(第9版).pdf》:第十章到十二章学习笔记
		回到顶部 注:本文声明事项. 本博文整理者:刘军 本博文出自于: <Java8 编程官方参考教程>一书 声明:1:转载请标注出处.本文不得作为商业活动.若有违本之,则本人不负法律责任.违法 ... 
- muduo网络库学习笔记(五) 链接器Connector与监听器Acceptor
		目录 muduo网络库学习笔记(五) 链接器Connector与监听器Acceptor Connector 系统函数connect 处理非阻塞connect的步骤: Connetor时序图 Accep ... 
- 物联网学习笔记三:物联网网关协议比较:MQTT 和 Modbus
		物联网学习笔记三:物联网网关协议比较:MQTT 和 Modbus 物联网 (IoT) 不只是新技术,还是与旧技术的集成,其关键在于通信.可用的通信方法各不相同,但是,各种不同的协议在将海量“事物”连接 ... 
- Oracle学习笔记三 SQL命令
		SQL简介 SQL 支持下列类别的命令: 1.数据定义语言(DDL) 2.数据操纵语言(DML) 3.事务控制语言(TCL) 4.数据控制语言(DCL) 
- OpenGL学习笔记3——缓冲区对象
		在GL中特别提出了缓冲区对象这一概念,是针对提高绘图效率的一个手段.由于GL的架构是基于客户——服务器模型建立的,因此默认所有的绘图数据均是存储在本地客户端,通过GL内核渲染处理以后再将数据发往GPU ... 
- OpenGL学习笔记:拾取与选择
		转自:OpenGL学习笔记:拾取与选择 在开发OpenGL程序时,一个重要的问题就是互动,假设一个场景里面有很多元素,当用鼠标点击不同元素时,期待作出不同的反应,那么在OpenGL里面,是怎么知道我当 ... 
随机推荐
- 面试侃集合 | DelayQueue篇
			面试官:好久不见啊,上次我们聊完了PriorityBlockingQueue,今天我们再来聊聊和它相关的DelayQueue吧. Hydra:就知道你前面肯定给我挖了坑,DelayQueue也是一个无 ... 
- Golang中GBK和UTF8编码格式互转
			Golang中GBK和UTF8编码格式互转 需求 已知byte数组的编码格式转换 实现代码 package utils import ( "bytes" "golang. ... 
- js笔记10
			1.闭包 封装:减少代码的冗余,提高代码的重复利用率 继承:本来需要开辟多个空间,只需要开辟一个空间,减少内存的消耗,提高性能 函数归属:函数归属谁,跟他在哪调用没有关系,而跟他在哪定义有关 闭包的定 ... 
- 什么是Mirai僵尸网络
			1.什么是Mirai? Mirai是恶意软件,能够感染在ARC处理器上运行的智能设备,将其转变为远程控制的机器人或"僵尸"并组成网络.这种机器人网络称为僵尸网络,通常用于发动DDo ... 
- Redis的flushall/flushdb误操作
			Redis的flushall/flushdb命令可以做数据清除,对于Redis的开发和运维人员有一定帮助,然而一旦误操作,它的破坏性也是很明显的.怎么才能快速恢复数据,让损失达到最小呢? 假设进行fl ... 
- 流程自动化RPA,Power Automate Desktop系列 - 批量备份Git仓库做好灾备
			一.背景 打个比如,你在Github上的代码库需要批量的定时备案到本地的Gitlab上,以便Github不能访问时,可以继续编写,这时候我们可以基于Power Automate Desktop来实现一 ... 
- 9.5、zabbix高级操作(2)
			4.zabbix的分布式监控: 使用zabbix-proxy主动方式(被动也可),使用zabbix-agent的主动方式(被动也可): Zabbix Server <- Zabbix Proxy ... 
- To_Heart—题解——AT2165
			这是一篇解题报告 首先,看到标签,考虑二分答案. 我们二分答案(即塔顶的值),把大于或等于这个值的变为1,否则变为0. 很容易发现,如果塔顶的答案是1,那么就说明值可以更大,否则相反. 复制一波样例 ... 
- Quartz:Quartz定时代码实现
			1.添加pom.xml <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId> ... 
- Mybatis学习(7)实现mybatis分页
			上一篇文章里已经讲到了mybatis与spring MVC的集成,并且做了一个列表展示,显示出所有article 列表,但没有用到分页,在实际的项目中,分页是肯定需要的.而且是物理分页,不是内存分页. ... 
