OpenGL教程(五)

1,005 阅读7分钟

前言

正如之前章节所提到的,着色器就是运行在GPU上的小程序,简单来说,着色器就是仅仅是一个将输入数据经过一定转换然后输出的过程,着色器之间是非常独立的,彼此之间除了输入输出之外没有其他交流,这篇文章将会详细介绍着色器以及编写着色器的语言GLSL。

GLSL

着色器是使用一种类似C语言的语言GLSL编写的,GLSL是为显卡使用而设计的,包含了很多针对向量和矩阵操作的特性。着色器一般以版本声明为开头,紧接着就是一些输入和输出的变量,uniform以及main函数,每一个着色器的入口都是main函数,在main函数中处理输入变量然后将处理的结果输出到输出变量,至于uinform,之后我们会详细介绍,一个典型的着色器的结构如下

#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
void main()
    {
    // process input(s) and do some weird graphics stuff
        ...
    // output processed stuff to output variable
        out_variable_name = weird_stuff_we_processed;
    }

当提及顶点着色器时我们都知道它的输入变量是顶点属性,硬件支持我们声明的顶点属性的数量有一个最大值,OpenGL规定至少有16个4分量的顶点属性可用,一些硬件可能会提供更多,这个值可以通过GL_MAX_VERTEX_ATTRIBS查询:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout<< "Maxium nr of vertex attributes supported: " <<nrAttributes<<std::endl

类型

与其他编程语言相同,GLSL也拥有数据类型去指定我们使用的数据的类型,GLSL提供了C语言提供的默认基本类型如int, float, double, uint以及bool。GLSL同样提供了两种容器类型vectors和matrices,matrices之后我们再讨论。

Vectors

GLSL的vector拥有1,2,3或4个之前提到的基本类型的分量,它们的形式有以下几种:

  • vecn:拥有n个浮点数的vector
  • bvecn:包含n个布尔值的vector
  • ivecn:包含n个整型的vector
  • uvecn:包含n个无符号整型的vector
  • dvecn:包含n个double的vector 一般情况下vecn已经能够满足使用需求。

vector分量的获取有很多方式,也非常的灵活,例如vec.x就可以获取vector的第一个分量,使用.x, .y, .z 和.w可以分别获取vector的第一、第二、第三和第四个分量,同样的GLSL也允许通过rgba去获取颜色分量,通过stpq获取纹理的坐标,vector的赋值与计算也非常的灵活,就像这样:

vec2 someVec;
vec4 differentVec;
vec3 anotherVec = differentVec.zyw;
vec4 othorVec = someVec.xxxx + anotherVec.yxzy;

输入与输出

着色器本身是非常小的程序,但是是整个图形管线的一部分,所以每一个着色器都必须有输入和输出,GLSL提供了in和out关键字来指定输入和输出,一个着色器的输出将会是另一个着色器的输入。

顶点着色器需要接受特定形式的输入否则效率会特别低下,而且顶点着色器是直接从顶点数据获取输入,为了解决这个问题,可以通过指定变量的位置就像layout(location = 0)来说明顶点数据是如何组织的。

其实也可以通过glGetAttribLocation省略指定布局(layout(location=0)),但是还是建议写在顶点着色器中,这样易于理解,也可以减少我们和OpenGL的工作。

除了指定输入有时候我们还需要指定输出,例如片段着色器需要输出每一个像素的颜色,如果片段着色器没有输出颜色,OpenGL将会渲染成白色或黑色。所以如果想要从一个着色器向另一个着色器传递数据,我们需要在一个着色器声明输出,在一个着色器声明输入,当变量的类型和名字都相同时,OpenGL将会链接这些数据从而实现数据的传递(这个过程一般是在链接项目对象时实现的),为了说明这个过程,我们通过一下例子说明,在顶点着色器就定义颜色: 顶点着色器

#version 330 core
layout(location = 0) in vec3 aPos;

out vec4 vertexColor;

void main()
{
    gl_Postion = vec4(aPos, 1.0);
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0);
}

片段着色器

out vec4 FragColor;

in vec4 vertexColor;

void main() {
    FragColor = vertexColor;
}

Uniforms

uniform是另一种方式将CPU上的数据传递到GPU的着色器上,uniform与顶点属性差异很大,首先,uniform是全局的。全局则意味着在每一个着色器程序(shader program)中都是唯一的,其次无论给uniform设置了什么值,uniform都会保持这个值除非重置或更新这个值。

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor;

void main() {
    FragColor = ourColor;
}

我们需要使用uniform关键字去声明,从上面代码可以看出我们使用在着色器中使用uniform,并且使用uniform定义三角形的颜色,由于uniform是一个全局变量,我们可以在任意的着色器中去定义。

需要注意的是如果声明了一个uniform变量,但是GLSL代码中并没有使用,那么在编译阶段将会移除变量,这会引发严重错误。

那么该如何给一个uniform变量赋值呢,首先需要知道uniform属性在着色器中的位置,然后就可以更行它的值了,用一个例子来说明这个过程,我们不再是直接传递一个颜色给片段着色器,而是根据时间来改变颜色,其代码如下:

float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

首先我们需要通过glfwGetTime()获取到当时的运行时间(以秒为单位),然后通过sin函数将greenValue的值设置在0.0到1.0之间,然后通过glGetUniformLocation函数查询uniform变量的位置,其第一个参数是着色器程序,第二个参数是uinform变量的名称,如如果其返回值是-1则说明没有找到位置。然后通过glUniform4f函数来设置uniform变量的值。需要注意的是在查询uniform变量的位置时不需要使用该着色器程序,但是在更新uniform变量的之前需要使用着色器程序(通过调用glUseProgram),这是因为设置uniform变量是在当前活跃的着色器程序。

因为OpenGL是一个C库,所以不支持函数的重载,所以一个函数支持不同的数据类型,就需要为每一个数据类型定义一个新的函数,glUniform函数就是一个很好的例子,这个函数通过不同的后缀来区分不同类型的参数。

  • f:入参是一个float
  • i:入参是一个int
  • ui:入参是一个unsigned int
  • 3分:入参是3个float
  • fv:入参是一个float类型的vector或array 我们使用的glUniform4f就是需要四个float类型入参。

现在我们已经知道了如何设置uniform变量的值,我们可以使用它们去渲染,如果我们想改变颜色,还需要在每一帧渲染的时候更新uniform,所以我们需要在渲染循环中计算和跟新greenValue。

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;
i   nt vertexColorLocation = glGetUniformLocation(shaderProgram,     "ourColor");
    glUseProgram(shaderProgram);
    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);

    glfwSwapBuffers(window);
    glfwPollEvents();
}

以上可以看到uinform是为每一帧动态设置属性很好的方法,但是如果我们想要为每一个顶点设置颜色呢,通过以上的方法可以需要为每一个顶点声明一个uniform变量,显然这种方法比较繁琐,更好的方式是在顶点属性中定义更多的数据。

更多的属性

之前的章节我们介绍了如何填充VBO、配置顶点属性指针以及将它们全部存储到VAO中,这一次我们也可以将颜色数据添加到顶点数据中,所以我们需要将3个浮点数颜色数据添加到顶点数组中。

float vertices[] = {
// positions         // colors
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left
0.0f,  0.5f, 0.0f, 0.0f, 0.0f, 1.0f  // top
};

因为我们需要传递更多的数据给顶点着色器,所以我们需要调整顶点着色器去接受颜色值作为顶点属性的输入,以下代码则是修改后的顶点着色器,可以注意到,我们将颜色属性的layout设置为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, 1.0);
    ourColor = aColor;
}

由于我们不在片段着色器中使用uniform而是使用ourColor变量设置颜色,所以我们也需要修改片段着色器的内容:

#version 330 core
out vec4 FragColor;
in vec3 ourColor;

void main() {
    FragColor = vec4(ourColor, 1.0);
}

因为我们新增了其他的顶点属性,我们需要配置顶点属性指针,在VBO中更新后的数据的组织情况如下图所示:

image.png

已经知道了现在数据的布局,我们可以通过glVertexAttribPointer来格式化。

glVertexAttribPointer(0, 3, GL_FLOAT, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_POINTER, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

需要注意的是我们将位置属性的layout设置为0,颜色属性的layout设置为1,颜色属性在位置属性的厚片,位置属性是三个float,所以颜色属性的偏移量就是3 * sizeof(float)。

最后的结果如下

image.png

代码如下

# include<glad/glad.h>
# include<GLFW/glfw3.h>
# include<iostream>

void framebuffer_size_callback(GLFWwindow *window, int width, int height);

const unsigned int WIDTH = 800;
const unsigned int 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() {
    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);

    GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, "QStackOpenGL", NULL, NULL);
    if (window == NULL) {
        std::cout<<"creat window failed"<<std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout<<"failed to init glad"<<std::endl;
        glfwTerminate();
        return -1;
    }

    //定义顶点着色器
    unsigned int 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::COMPLIATION_FAILED\n"<<infoLog<<std::endl;
    }
    //定义片段着色器
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"<<infoLog<<std::endl;
    }

    //链接着色器
    unsigned 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;
    }

    //删除着色器
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    //顶点数据
    float vertices[] = {
        // positions         // colors
         0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // bottom right
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // bottom left
         0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f   // top 

    };

    unsigned int VAO, VBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    //先绑定VAO
    glBindVertexArray(VAO);

    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);

    while (!glfwWindowShouldClose(window))
    {
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shaderProgram);
        //因为我们只有一个三角形,不需要每次绑定VAO,为了看起更合理,我依然每次都绑定
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    //清除数据
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    glfwTerminate();
    return -1;

}

void framebuffer_size_callback(GLFWwindow *window, int width, int height) {
        glViewport(0, 0, width, height);
    }

定义自己的着色器类

编写、编译和管理着色器十分的繁琐,为了方便管理我们通过建立一个着色器的类从硬盘读取着色器,然后编译并且链接它们,检查是否有错误,十分方便使用。

出于学习的目的,我们将着色器的类定义在一个头文件内,其结构如下

#ifndef SHADER_H
#define SHADER_H
#include <glad/glad.h> // include glad to get the required OpenGL headers
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
class Shader
{
public:
// 项目ID
unsigned int ID;
// 构造函数,读取并且构建着色器
Shader(const char* vertexPath, const char* 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

着色器的类需要获取着色器项目的ID,它的构造函数需要顶点着色器和片段着色器源码文件的地址。

最后

这篇文章主要介绍了编写着色器的语言GLSL,更多内容可以关注公众号QStack。