基于C++的OpenGL 02 之着色器

1. 概述

本文基于C++语言,描述OpenGL的着色器

环境搭建以及绘制流程可参考:

笔者这里不过多描述每个名词、函数和细节,更详细的文档可以参考:

2. 着色器

着色器(Shader)是运行在GPU上的小程序,这些小程序为图形渲染管线的某个特定部分而运行

从基本意义上来说,着色器只是一种把输入转化为输出的程序

着色器也是一种非常独立的程序,因为它们之间不能相互通信,它们之间唯一的沟通只有通过输入和输出

着色器语言(英语:Shader Language)也叫着色语言(英语:Shading Language),是一类专门用来为着色器编程的编程语言

Shader Language目前主要有3种语言:

  1. 基于 OpenGL 的 OpenGL Shading Language,简称 GLSL
  2. 基于 DirectX 的 High Level Shading Language,简称 HLSL
  3. 还有 NVIDIA 公司的 C for Graphic,简称 Cg 语言

3. GLSL

OpenGL着色器是使用一种叫GLSL的类C语言写成的,为图形计算量身定制的,包含一些针对向量和矩阵操作的有用特性

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数

每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中

uniform可以理解为globe,即全局变量

一个典型的OpenGL着色器的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 = result_we_processed;
}

4. 数据类型

GLSL包含的基础变量类型:

  • int
  • uint
  • float
  • double
  • bool

GLSL包含的容器类型:

  • Vector(向量)
  • Matrix(矩阵)

4.1 向量

GLSL中的向量可以是包含2-4个分量的容器,分量的类型可以是任意基础类型

向量的类型大致可以表示为:<基础类型首字母>+vec+<分量个数>

如包含3个double分量的向量:dvec3

例外的是float类型,float向量是GLSL中最常用的变量,其类型为:vec+<分量个数>

向量的分量支持.x.y.z的方式获取,向量支持重组,例如:

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

5. 输入与输出

OpenGL中着色器是独立的小程序,每个着色器具有输入与输出,从而进行数据传递

GLSL定义了inout两个关键字实现这种流程

前面的着色器使用out定义的变量会传递到后面的着色器中用in声明且类型与变量名相同的变量

例外的是:

  • 顶点着色器使用layout (location = <n>)指定输入变量(也可以用glGetAttribLocation()查询属性位置)
  • 片段着色器需要vec4类型的颜色输出

综述就是,一般而言:

  • inout两个关键字主要用于顶点着色器与片段着色器的数据传递
  • 顶点着色器需要使用location进行输入,输出中要指定gl_Position
  • 片段着色器需要指定输出颜色

一个简单的示例代码如下:

顶点着色器:

#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(1.0, 0.0, 0.0, 1.0); // 把输出变量设置为红色
}

片段着色器:

#version 330 core
out vec4 FragColor;

in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

void main()
{
    FragColor = vertexColor;
}

这里只是修改GLSL,环境代码在这片文章的结尾:

实现的效果:

image-20220728102803721

6. Uniform

inout两个关键字主要用于着色器的数据传递,Uniform则主要用于CPU与GPU的数据传递

Uniform是全局变量,变量名不可重复,可以被任何着色器随时读取

以下是在片段着色器中声明一个Uniform的颜色变量:

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量

void main()
{
    FragColor = ourColor;
}

接下来是在渲染中动态设置这个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);

一切正常的话,将会出现一个三角形逐渐由绿变黑再变回绿色:

动画

一些想法:

  • 既然说Uniform是全局变量,那是否可以在没有声明Uniform的着色器中访问呢?经过笔者的实验,似乎不可以

7. 绑定更多属性

利用Uniform可以实现颜色从CPU到GPU的传递,但是变量多时声明很多Uniform就不那么合适,另一个方案是将颜色属性绑定到顶点数据中,从顶点着色器传递到片段着色器中,代码如下

设置顶点与对应的颜色:

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内存布局如下:

img

绑定VBO的顶点格式:

// 位置属性
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);

运行程序结果:

image-20220728115416055

8. 封装着色器

着色器程序的生成步骤大致都是:

  • 编写GLSL
  • 创建Shader
  • 加载GLSL
  • 编译Shader
  • 创建着色器程序
  • 附加Shader到着色器程序
  • 链接着色器程序

封装一个着色器类,可以有效简化创建一个着色器程序的步骤

这是封装的Shader.hpp:

#ifndef SHADER_HPP
#define SHADER_HPP

#include <glad/glad.h> // 包含glad来获取所有的必须OpenGL头文件

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

class Shader
{
public:
    // 程序ID
    unsigned int ID;

    // 构造器读取并构建着色器
    Shader(const char *vertexPath, const char *fragmentPath)
    {
        // 1. 从文件路径中获取顶点/片段着色器
        std::string vertexCode;
        std::string fragmentCode;
        std::ifstream vShaderFile;
        std::ifstream fShaderFile;
        // 保证ifstream对象可以抛出异常:
        vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
        try
        {
            // 打开文件
            vShaderFile.open(vertexPath);
            fShaderFile.open(fragmentPath);
            std::stringstream vShaderStream, fShaderStream;
            // 读取文件的缓冲内容到数据流中
            vShaderStream << vShaderFile.rdbuf();
            fShaderStream << fShaderFile.rdbuf();
            // 关闭文件处理器
            vShaderFile.close();
            fShaderFile.close();
            // 转换数据流到string
            vertexCode = vShaderStream.str();
            fragmentCode = fShaderStream.str();
        }
        catch (std::ifstream::failure e)
        {
            std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
        }
        const char *vShaderCode = vertexCode.c_str();
        const char *fShaderCode = fragmentCode.c_str();

        // 2. 编译着色器
        unsigned int vertex, fragment;
        int success;
        char infoLog[512];

        // 顶点着色器
        vertex = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertex, 1, &vShaderCode, NULL);
        glCompileShader(vertex);
        // 打印编译错误(如果有的话)
        glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
        if (!success)
        {
            glGetShaderInfoLog(vertex, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"
                      << infoLog << std::endl;
        };

        // 片段着色器也类似
        fragment = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragment, 1, &fShaderCode, NULL);
        glCompileShader(fragment);
        // 打印编译错误(如果有的话)
        glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
        if (!success)
        {
            glGetShaderInfoLog(fragment, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"
                      << infoLog << std::endl;
        };

        // 着色器程序
        ID = glCreateProgram();
        glAttachShader(ID, vertex);
        glAttachShader(ID, fragment);
        glLinkProgram(ID);
        // 打印连接错误(如果有的话)
        glGetProgramiv(ID, GL_LINK_STATUS, &success);
        if (!success)
        {
            glGetProgramInfoLog(ID, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n"
                      << infoLog << std::endl;
        }

        // 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
        glDeleteShader(vertex);
        glDeleteShader(fragment);
    }
    // 使用/激活程序
    void use()
    {
        glUseProgram(ID);
    }
    // uniform工具函数
    void setBool(const std::string &name, bool value) const
    {
        glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
    }
    void setInt(const std::string &name, int value) const
    {
        glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
    }
    void setFloat(const std::string &name, float value) const
    {
        glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
    }
};

#endif

以下是一个简单的调用程序test.cpp

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <math.h>
#include "Shader.hpp"

void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void process_input(GLFWwindow *window);
unsigned int *renderInit();
void render(unsigned int shaderProgram, unsigned int VAO);
bool checkCompile(unsigned int shader);
bool checkProgram(unsigned int shaderProgram);

int main()
{
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    GLFWwindow *window = glfwCreateWindow(800, 600, "hello triangle", nullptr, nullptr);

    if (window == nullptr)
    {
        std::cout << "Faild to create window" << std::endl;
        glfwTerminate();
    }
    glfwMakeContextCurrent(window);

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Faild to initialize glad" << std::endl;
        return -1;
    }
    glad_glViewport(0, 0, 800, 600);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    unsigned int *arr = renderInit();
    while (!glfwWindowShouldClose(window))
    {
        process_input(window);

        // render

        std::cout << arr[0] << " " << arr[1] << " " << arr[2] << std::endl;
        render(arr[0], arr[1]);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteProgram(arr[0]);
    glDeleteVertexArrays(1, &arr[1]);
    glDeleteBuffers(1, &arr[2]);

    glfwTerminate();
    return 0;
}

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

void process_input(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
    {
        glfwSetWindowShouldClose(window, true);
    }
}

unsigned int *renderInit()
{
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    float vertices[] = {
        0.0f, 0.5f, 0.0f,
        -0.5f, -0.5f, 0.0f,
        0.5f, -0.5f, 0.0f};

    unsigned int VBO;
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0);
    glEnableVertexAttribArray(0);

    Shader shaderProgram = Shader("test.vs", "test.fs");
    shaderProgram.use();

    return new unsigned int[3]{shaderProgram.ID, VAO, VBO};
}
void render(unsigned int shaderProgram, unsigned int VAO)
{
    glClearColor(0.2, 0.3, 0.3, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(shaderProgram);

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

test.vs如下:

#version 330 core
layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0 
void main()
{
    gl_Position = vec4(aPos, 1.0);
}

test.fs如下:

#version 330 core
out vec4 FragColor;  
void main()
{
    FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}

9. 参考资料

[1]着色器 - LearnOpenGL CN (learnopengl-cn.github.io)

[2]着色器语言_百度百科 (baidu.com)

[3]三大 Shader 编程语言(CG/HLSL/GLSL) - 知乎 (zhihu.com)

[4]OpenGL学习笔记(四)着色器 - 知乎 (zhihu.com)

[5]着色器语言 GLSL (opengl-shader-language)入门大全 - 善未易明 - 博客园 (cnblogs.com)

posted @ 2022-07-26 22:44  当时明月在曾照彩云归  阅读(466)  评论(0编辑  收藏  举报