[learnOpenGL学习笔记_5] 着色器

简介: [learnOpenGL学习笔记_5] 着色器

learnOpenGL网站是学习openGL非常有用的网站,作为一个小白,为了方便后续回顾同时给大家提供借鉴,在此记录学习的过程。
本文参考:着色器


0. 概念

该部分为着色器的基本概念,包含着色器语言GLSL,数据类型,读者可以选择暂时跳过,直接从第1部分代码开始阅读。

0.1 GLSL

着色器是使用一种叫GLSL的类C语言写成的。

着色器的开头总是要声明版本,接着是输入和输出变量、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;
}

0.2 数据类型

GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。

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分量的向量

一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量。你可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4个分量。

向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(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);

1. 着色器的输入与输出

着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。

如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。 当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。

例如下面的程序,将之前代码的顶点着色器和片段着色器代码修改:

  • 顶点着色器
const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "out vec4 vertexColor;\n" //为片段着色器指定一个颜色输出
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "   vertexColor = vec4(0.5, 0.0, 0.0, 1.0);\n" //把输出变量设置为暗红色
    "}\0";
  • 片段着色器
const char *fragmentShaderSource1 = "#version 330 core\n"
    "in vec4 vertexColor;\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vertexColor;\n"
    "}\n\0";
  • 运行结果如下:
    左边的三角形,通过在顶点着色器中定义了一个out vec4 vertexColor传递出颜色值给片段着色器,片段着色器通过定义一个in vec4 vertexColor接收颜色值。

2. Uniform

到目前位置我们都是在着色器中定义计算颜色值,这样很不灵活。如果我们想要从应用程序中直接给片段着色器发送一个颜色呢?

2.1 概念

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式。

  • uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。
  • 无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。

2.2 使用Uniform

下面通过修改代码,使用uniform设置三角形的颜色:

- 片段着色器代码修改:

声明一个uniform vec4类型的ourColor,并令片段着色器的输出颜色等于uniform值的内容。

const char *fragmentShaderSource2 = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "uniform vec4 ourColor;\n"  //在OpenGL程序代码中设定这个变量
    "void main()\n"
    "{\n"
    "   FragColor = ourColor;\n"
    "}\n\0";

如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!

  • 通过程序给uniform赋值
  • (1) 首先需要找到着色器中uniform属性的索引/位置值。用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序uniform的名字(这是我们希望获得的位置值的来源)。如果glGetUniformLocation返回-1就代表没有找到这个位置值。
  • (2) glUniform4f函数设置uniform值。glUniform的说明请看这里
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram2, "ourColor");
glUseProgram(shaderProgram2);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。

  • 运行结果:
    右边的三角形由绿变黑再变绿

3. 更多属性

uniform对于设置一个在渲染迭代中会改变的属性是一个非常有用的工具,它也是一个在程序和着色器间数据交互的很好工具,但假如我们打算为每个顶点设置一个颜色的时候该怎么办?这种情况下,我们就不得不声明和顶点数目一样多的uniform了。在这一问题上更好的解决方案是在顶点属性中包含更多的数据

1. 三角形顶点数据中加入颜色数据

float firstTriangle[] = {
        -0.9f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下
        -0.0f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下
        -0.45f, 0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
    };

2. 调整顶点着色器

由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用layout标识符来把aColor属性的位置值设置为1:

const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n" // 位置变量的属性位置值为 0
    "layout (location = 1) in vec3 aColor;\n" // 颜色变量的属性位置值为 1
    "out vec3 vertexColor;\n" //为片段着色器指定一个颜色输出
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos, 1.0);\n"
    "   vertexColor = aColor;\n" // 将ourColor设置为我们从顶点数据那里得到的输入颜色
    "}\0";

3. 调整片段着色器

需要稍微调整片段着色器,使其输入颜色数据能够与顶点着色器的输出颜色相对应。

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

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);
  • 由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。
  • 同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3 * sizeof(float),用字节来计算就是12字节。

5. 运行结果

左边三角形为程序运行结果。

这个图片可能不是你所期望的那种,因为我们只提供了3个颜色,而不是我们现在看到的大调色板。这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。

6. 源代码

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <cmath>
const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n" // 位置变量的属性位置值为 0
    "layout (location = 1) in vec3 aColor;\n" // 颜色变量的属性位置值为 1
    "out vec3 vertexColor;\n" //为片段着色器指定一个颜色输出
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos, 1.0);\n"
    "   vertexColor = aColor;\n" // 将ourColor设置为我们从顶点数据那里得到的输入颜色
    "}\0";
const char *fragmentShaderSource1 = "#version 330 core\n"
    "in vec3 vertexColor;\n"
    "out vec4 FragColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = vec4(vertexColor, 1.0f);\n"
    "}\n\0";
const char *fragmentShaderSource2 = "#version 330 core\n"
    "out vec4 FragColor;\n"
    "uniform vec4 ourColor;\n"  //在OpenGL程序代码中设定这个变量
    "void main()\n"
    "{\n"
    "   FragColor = ourColor;\n"
    "}\n\0";
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
void draw(GLFWwindow *window);
int main()
{
    glfwInit(); //初始化GLFW
    
    //配置GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  //告知系统我们使用的opengl版本是3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //同样明确告诉GLFW我们使用的是核心模式(Core-profile)
    
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); //MAC环境下需加这一句才能使以上配置生效
    
    GLFWwindow *window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);
    if (window == nullptr)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    
    //初始化glad
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    
    //设置视口
    glViewport(0, 0, 800, 600);
    
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    
    float firstTriangle[] = {
        -0.9f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下
        -0.0f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下
        -0.45f, 0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
    };
    float secondTriangle[] = {
        0.0f, -0.5f, 0.0f,  // left
        0.9f, -0.5f, 0.0f,  // right
        0.45f, 0.5f, 0.0f   // top
    };
    
    unsigned int VBO[2];
    glGenBuffers(2, VBO);
    
    unsigned int VAO[2];
    glGenVertexArrays(2, VAO);
    
    // bind the Vertex Array Object first
    glBindVertexArray(VAO[0]);
    //绑定一个VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
    //将顶点数据复制到缓冲内存中
    glBufferData(GL_ARRAY_BUFFER, sizeof(firstTriangle), firstTriangle, 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);
    
    // bind the Vertex Array Object first
    glBindVertexArray(VAO[1]);
    //绑定一个VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
    //将顶点数据复制到缓冲内存中
    glBufferData(GL_ARRAY_BUFFER, sizeof(secondTriangle), secondTriangle, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    
    //---------> 5. 创建并编译顶点着色器
    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;
    }
    
    //--------> 6. 创建并编译片段着色器
    //创建片段着色器对象
    unsigned int fragmentShader1;
    fragmentShader1 = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader1, 1, &fragmentShaderSource1, NULL);
    glCompileShader(fragmentShader1);
    glGetShaderiv(fragmentShader1, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader1, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    
    unsigned int fragmentShader2;
    fragmentShader2 = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader2, 1, &fragmentShaderSource2, NULL);
    glCompileShader(fragmentShader2);
    glGetShaderiv(fragmentShader2, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader2, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    
    //--------> 7. 链接着色器
    unsigned int shaderProgram1;
    shaderProgram1 = glCreateProgram();
    glAttachShader(shaderProgram1, vertexShader);
    glAttachShader(shaderProgram1, fragmentShader1);
    glLinkProgram(shaderProgram1);
    glGetProgramiv(shaderProgram1, GL_LINK_STATUS, &success);
    if(!success) {
        glGetProgramInfoLog(shaderProgram1, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::LINK_FAILED\n" << infoLog << std::endl;
    }
    
    unsigned int shaderProgram2;
    shaderProgram2 = glCreateProgram();
    glAttachShader(shaderProgram2, vertexShader);
    glAttachShader(shaderProgram2, fragmentShader2);
    glLinkProgram(shaderProgram2);
    glGetProgramiv(shaderProgram2, GL_LINK_STATUS, &success);
    if(!success) {
        glGetProgramInfoLog(shaderProgram2, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::LINK_FAILED\n" << infoLog << std::endl;
    }
    
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader1);
    glDeleteShader(fragmentShader2);
    
    //循环渲染
    while(!glfwWindowShouldClose(window))
    {
        //检测输入事件
        processInput(window);
        
        //渲染指令
        draw(window);
        
        //------> 8. 画三角形
        glUseProgram(shaderProgram1);
        glBindVertexArray(VAO[0]);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        
        float timeValue = glfwGetTime();
        float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
        int vertexColorLocation = glGetUniformLocation(shaderProgram2, "ourColor");
        glUseProgram(shaderProgram2);
        glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
        glBindVertexArray(VAO[1]);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
    
    //正确释放/删除之前的分配的所有资源
    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);
}
void draw(GLFWwindow *window)
{
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
}

4. 补充知识

glUniform函数

因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;glUniform是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有:

后缀 含义
f 函数需要一个float作为它的值
i 函数需要一个int作为它的值
ui 函数需要一个unsigned int作为它的值
3f 函数需要3个float作为它的值
fv 函数需要一个float向量/数组作为它的值

每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在我们的例子里,我们希望分别设定uniform的4个float值,所以我们通过glUniform4f传递我们的数据(注意,我们也可以使用fv版本)。

相关文章
|
SQL 数据库
nested exception is dm.jdbc.driver.DMException: 字符串截断
nested exception is dm.jdbc.driver.DMException: 字符串截断 问题处理
|
消息中间件 存储 Kafka
Kafka日志处理:深入了解偏移量查找与切分文件
**摘要:** 本文介绍了如何在Kafka中查找偏移量为23的消息,涉及ConcurrentSkipListMap的查询、索引文件的二分查找及日志分段的物理位置搜索。还探讨了Kafka日志分段的切分策略,包括大小、时间、索引大小和偏移量达到特定阈值时的切分条件。理解这些对于优化Kafka的性能和管理日志至关重要。
436 2
|
XML 传感器 机器人
Isaac Sim详细安装使用教程
NVIDIA Isaac Sim 4.0版本的安装和使用教程,包括了下载、安装、配置环境、使用ROS/ROS2扩展、设置工作空间以及编译和运行示例程序的步骤。同时,还提供了Isaac Sim的架构图和USD文件格式的介绍,以及相关的资源链接和教程视频。
3490 1
Isaac Sim详细安装使用教程
|
自然语言处理 关系型数据库 数据管理
阿里云百炼|析言GBI产品入门指导
析言基础入门的操作体验,带领大家一起探索阿里云百炼|析言GBI的奥秘。
3633 19
|
Java API Spring
Spring Boot 中的 AOP 处理
对 Spring Boot 中的切面 AOP 做了详细的讲解,主要介绍了 Spring Boot 中 AOP 的引入,常用注解的使用,参数的使用,以及常用 api 的介绍。AOP 在实际项目中很有用,对切面方法执行前后都可以根据具体的业务,做相应的预处理或者增强处理,同时也可以用作异常捕获处理,可以根据具体业务场景,合理去使用 AOP。
|
存储 SQL 监控
OceanBase 的水平扩展与性能优化
【8月更文第31天】随着业务的增长,单一数据库服务器往往难以满足日益增长的数据存储和处理需求。OceanBase 作为一款分布式数据库解决方案,通过其独特的水平扩展能力,能够在不牺牲性能的前提下支持海量数据存储和高并发事务处理。本文将详细介绍 OceanBase 的水平扩展机制,并提供一些性能优化的建议。
1001 0
|
编译器 C++ 开发者
C++一分钟之-多重继承与菱形问题
【7月更文挑战第19天】C++的多重继承允许类从多个基类派生,但引入了菱形问题,即类D通过B和C(都继承自A)双重继承A,可能导致数据冗余和二义性。解决这个问题的关键是**虚继承**,通过`virtual`关键字确保基类A只被继承一次,消除冲突。理解并适当使用虚继承是处理这类问题的关键,有助于保持代码的清晰和正确性。
270 0
|
安全 网络协议 网络安全
SSL(Secure Sockets Layer)是一种安全协议,用于在客户端和服务器之间建立加密的通信通道。
SSL(Secure Sockets Layer)是一种安全协议,用于在客户端和服务器之间建立加密的通信通道。
|
Java 数据库连接 数据库
Spring之JdbcTemplate(带你一篇文章搞定JdbcTemplate)
Spring之JdbcTemplate(带你一篇文章搞定JdbcTemplate)
149 0
|
缓存 编解码 算法
Skia深入分析10——Skia库的性能与优化潜力
Skia库性能与优化潜力 图形/渲染 算法/架构 作为图形渲染引擎,性能上是非常重要的,按通常Android手机60帧的刷新率,绘制一帧的总时间只有16ms,可谓是毫厘必争。提升性能到最后,就必然跟不同CPU的特性打交道,毕竟一个SIMD下去,好做的提升5、6倍,不那么好做的也达到2、3倍,收益极其可观。 SIMD,在intel上是SSE,在arm上是neon,在
8853 0