OpenGL 入门(三)— Shader(着色器)

文章目录

前言

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

从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信。它们之间唯一的沟通只有通过输入和输出。

GLSL

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

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

每个输入变量也叫顶点属性(Vertex Attribute)。能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限:

cpp 复制代码
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

数据类型

GLSL中包含C等其它语言大部分的默认基础数据类型:intfloatdoubleuintbool

GLSL也有两种容器类型:向量(Vector)和矩阵(Matrix)

这个重点介绍一下向量(Vector):

GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。

类型 含义
vecn 包含n个float分量的默认向量
bvecn 包含n个bool分量的向量
ivecn 包含n个int分量的向量
uvecn 包含n个unsigned int分量的向量
dvecn 包含n个double分量的向量

向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。重组允许这样的语法:

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

输入与输出

GLSL定义了inout关键字专门来实现这个目的。

每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。

为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。

顶点着色器

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

片段着色器

#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
    FragColor = vertexColor;
}

完整脚本

cpp 复制代码
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>

void InitGLFW();
bool CreateWindow();
bool InitGLAD();
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);

// settings 窗口宽高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

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 *fragmentShaderSource = "#version 330 core\n"
                                   "out vec4 FragColor;\n"
                                   " in vec4 vertexColor;\n"
                                   "void main()\n"
                                   "{\n"
                                   "   FragColor =vertexColor;\n"
                                   "}\n\0";

GLFWwindow *window;

int main()
{
    InitGLFW();                      // 初始化GLFW
    bool isCreated = CreateWindow(); // 创建一个窗口对象
    if (!isCreated)
        return -1;
    bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数
    if (!isGLAD)
        return -1;

    // TODO:创建一个三角形,使用着色器语言GLSL编写顶点着色器和片段着色器,将它们编译链接到一个着色器程序中,并使用这个着色器程序绘制三角形。

    // 1.顶点着色器(vertex shader) 用于处理顶点
    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::COMPILATION_FAILED\n"
                  << infoLog << std::endl;
    }

    // 2.片段着色器(fragment shader) 用于处理片段/片元(一个片段是OpenGL渲染一个像素所需的所有数据)
    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;
    }

    // 3.着色器程序(shader program) 用来管理着色器
    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); // 删除着色器

    // 设置顶点数据(和缓冲区)并配置顶点属性
    // 1.顶点输入
    float vertices[] = {
        -0.5f, -0.5f, 0.0f, // 左
        0.5f, -0.5f, 0.0f,  // 右
        0.0f, 0.5f, 0.0f    // 上
    };

    // 2.创建一个顶点缓冲对象(Vertex Buffer Objects, VBO) 顶点数组对象(Vertex Array Object, VAO)
    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO); // 生成一个VAO对象
    glGenBuffers(1, &VBO);      // 生成一个VBO对象
    // 3.首先绑定顶点数组对象,然后绑定和设置顶点缓冲区,然后配置顶点属性。
    glBindVertexArray(VAO);                                                    // 绑定VAO
    glBindBuffer(GL_ARRAY_BUFFER, VBO);                                        // 绑定VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中

    // 4.配置顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0); // 解析顶点数据
    glEnableVertexAttribArray(0);                                                  // 启用顶点属性

    // 注意这是允许的,调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 你可以在之后解除对VAO的绑定,这样其他VAO调用就不会意外地修改这个VAO,但这种情况很少发生。修改其他
    //  VAOs无论如何都需要调用glBindVertexArray,所以当不直接需要时,我们通常不会取消绑定VAOs(也不会取消绑定vbo)。
    glBindVertexArray(0);

    // 取消此调用的注释以绘制线框多边形。
    // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // 循环渲染
    while (!glfwWindowShouldClose(window))
    {

        // 输入
        processInput(window);

        // 渲染指令
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 绘制三角形
        glUseProgram(shaderProgram);      // 使用着色器程序
        glBindVertexArray(VAO);           // 绑定VAO
        glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形
        // glBindVertexArray(0); // 不需要每次都绑定VAO

        // 检查并调用事件,交换缓冲
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 可选:一旦资源超出其用途,就取消分配所有资源:
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    // 释放/删除之前的分配的所有资源
    glfwTerminate();
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

void InitGLFW()
{
    // 初始化GLFW
    glfwInit();
    // 配置GLFW  第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;
    // 第二个参数接受一个整型,用来设置这个选项的值。
    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);
}
bool CreateWindow()
{
    // 创建一个窗口对象
    window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        // 创建失败,终止程序
        glfwTerminate();
        return false;
    }
    // 将我们窗口的上下文设置为当前线程的主上下文
    glfwMakeContextCurrent(window);
    // 设置窗口大小改变时的回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    return true;
}
bool InitGLAD()
{
    // 初始化GLAD,传入加载系统相关opengl函数指针的函数
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        // 初始化失败,终止程序
        return false;
    }
    return true;
}

// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
    // 设置窗口的维度
    glViewport(0, 0, width, height);
}

// 输入
void processInput(GLFWwindow *window)
{
    // 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true
    // 关闭应用程序
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

运行效果:

Uniform

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。

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

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

在片段着色器中声明一个带有uniform关键字的GLSL脚本:

#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
    FragColor = ourColor;
}

此时,这个uniform现在还是空的,然后,我们在C++程序中访问更新它的值:

cpp 复制代码
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
//用glGetUniformLocation查询uniform ourColor的位置值。
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
//更新一个uniform之前你必须先使用程序(调用glUseProgram),
//因为它是在当前激活的着色器程序中设置uniform的。
glUseProgram(shaderProgram);
//通过glUniform4f函数设置uniform值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

完整代码

cpp 复制代码
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <cmath>

void InitGLFW();
bool CreateWindow();
bool InitGLAD();
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);

// settings 窗口宽高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

const char *vertexShaderSource = "#version 330 core\n"
                                 "layout (location = 0) in vec3 aPos;\n"
                                 "void main()\n"
                                 "{\n"
                                 "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
                                 "}\0";
const char *fragmentShaderSource = "#version 330 core\n"
                                   "out vec4 FragColor;\n"
                                   "uniform vec4 ourColor;\n"
                                   "void main()\n"
                                   "{\n"
                                   "   FragColor =ourColor;\n"
                                   "}\n\0";

GLFWwindow *window;

int main()
{
    InitGLFW();                      // 初始化GLFW
    bool isCreated = CreateWindow(); // 创建一个窗口对象
    if (!isCreated)
        return -1;
    bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数
    if (!isGLAD)
        return -1;

    // TODO:创建一个三角形,使用着色器语言GLSL编写顶点着色器和片段着色器,将它们编译链接到一个着色器程序中,并使用这个着色器程序绘制三角形。

    // 1.顶点着色器(vertex shader) 用于处理顶点
    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::COMPILATION_FAILED\n"
                  << infoLog << std::endl;
    }

    // 2.片段着色器(fragment shader) 用于处理片段/片元(一个片段是OpenGL渲染一个像素所需的所有数据)
    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;
    }

    // 3.着色器程序(shader program) 用来管理着色器
    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); // 删除着色器

    // 设置顶点数据(和缓冲区)并配置顶点属性
    // 1.顶点输入
    float vertices[] = {
        -0.5f, -0.5f, 0.0f, // 左
        0.5f, -0.5f, 0.0f,  // 右
        0.0f, 0.5f, 0.0f    // 上
    };

    // 2.创建一个顶点缓冲对象(Vertex Buffer Objects, VBO) 顶点数组对象(Vertex Array Object, VAO)
    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO); // 生成一个VAO对象
    glGenBuffers(1, &VBO);      // 生成一个VBO对象
    // 3.首先绑定顶点数组对象,然后绑定和设置顶点缓冲区,然后配置顶点属性。
    glBindVertexArray(VAO);                                                    // 绑定VAO
    glBindBuffer(GL_ARRAY_BUFFER, VBO);                                        // 绑定VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中

    // 4.配置顶点属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void *)0); // 解析顶点数据
    glEnableVertexAttribArray(0);                                                  // 启用顶点属性

    // 注意这是允许的,调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 你可以在之后解除对VAO的绑定,这样其他VAO调用就不会意外地修改这个VAO,但这种情况很少发生。修改其他
    //  VAOs无论如何都需要调用glBindVertexArray,所以当不直接需要时,我们通常不会取消绑定VAOs(也不会取消绑定vbo)。
    glBindVertexArray(0);

    // 取消此调用的注释以绘制线框多边形。
    // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // 循环渲染
    while (!glfwWindowShouldClose(window))
    {

        // 输入
        processInput(window);

        // 渲染
        // 清除颜色缓冲
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 记得激活着色器
        glUseProgram(shaderProgram);

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

        // 绘制三角形
        glBindVertexArray(VAO);           // 绑定VAO
        glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形
        // glBindVertexArray(0); // 不需要每次都绑定VAO

        // 检查并调用事件,交换缓冲
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 可选:一旦资源超出其用途,就取消分配所有资源:
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    // 释放/删除之前的分配的所有资源
    glfwTerminate();
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

void InitGLFW()
{
    // 初始化GLFW
    glfwInit();
    // 配置GLFW  第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;
    // 第二个参数接受一个整型,用来设置这个选项的值。
    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);
}
bool CreateWindow()
{
    // 创建一个窗口对象
    window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        // 创建失败,终止程序
        glfwTerminate();
        return false;
    }
    // 将我们窗口的上下文设置为当前线程的主上下文
    glfwMakeContextCurrent(window);
    // 设置窗口大小改变时的回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    return true;
}
bool InitGLAD()
{
    // 初始化GLAD,传入加载系统相关opengl函数指针的函数
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        // 初始化失败,终止程序
        return false;
    }
    return true;
}

// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
    // 设置窗口的维度
    glViewport(0, 0, width, height);
}

// 输入
void processInput(GLFWwindow *window)
{
    // 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true
    // 关闭应用程序
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

多个属性

把位置数据和颜色数据添加到顶点数据中:

声明vertices数组:

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函数更新顶点格式:

cpp 复制代码
// 位置属性
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);

完整脚本

cpp 复制代码
#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <cmath>

void InitGLFW();
bool CreateWindow();
bool InitGLAD();
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);

// settings 窗口宽高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_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.0);\n"
                                   "}\n\0";

GLFWwindow *window;

int main()
{
    InitGLFW();                      // 初始化GLFW
    bool isCreated = CreateWindow(); // 创建一个窗口对象
    if (!isCreated)
        return -1;
    bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数
    if (!isGLAD)
        return -1;

    // TODO:创建一个三角形,使用着色器语言GLSL编写顶点着色器和片段着色器,将它们编译链接到一个着色器程序中,并使用这个着色器程序绘制三角形。

    // 1.顶点着色器(vertex shader) 用于处理顶点
    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::COMPILATION_FAILED\n"
                  << infoLog << std::endl;
    }

    // 2.片段着色器(fragment shader) 用于处理片段/片元(一个片段是OpenGL渲染一个像素所需的所有数据)
    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;
    }

    // 3.着色器程序(shader program) 用来管理着色器
    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); // 删除着色器

    // 设置顶点数据(和缓冲区)并配置顶点属性
    // 1.顶点输入
    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    // 顶部
    };

    // 2.创建一个顶点缓冲对象(Vertex Buffer Objects, VBO) 顶点数组对象(Vertex Array Object, VAO)
    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO); // 生成一个VAO对象
    glGenBuffers(1, &VBO);      // 生成一个VBO对象
    // 3.首先绑定顶点数组对象,然后绑定和设置顶点缓冲区,然后配置顶点属性。
    glBindVertexArray(VAO);                                                    // 绑定VAO
    glBindBuffer(GL_ARRAY_BUFFER, VBO);                                        // 绑定VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中

    // 4.配置位置属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
    glEnableVertexAttribArray(0);
    // 5.   颜色属性
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    // 注意这是允许的,调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 你可以在之后解除对VAO的绑定,这样其他VAO调用就不会意外地修改这个VAO,但这种情况很少发生。修改其他
    //  VAOs无论如何都需要调用glBindVertexArray,所以当不直接需要时,我们通常不会取消绑定VAOs(也不会取消绑定vbo)。
    glBindVertexArray(0);

    // 取消此调用的注释以绘制线框多边形。
    // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // 循环渲染
    while (!glfwWindowShouldClose(window))
    {

        // 输入
        processInput(window);

        // 渲染
        // 清除颜色缓冲
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 记得激活着色器
        glUseProgram(shaderProgram);
        // 绘制三角形
        glBindVertexArray(VAO);           // 绑定VAO
        glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形
        // glBindVertexArray(0); // 不需要每次都绑定VAO

        // 检查并调用事件,交换缓冲
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 可选:一旦资源超出其用途,就取消分配所有资源:
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    // 释放/删除之前的分配的所有资源
    glfwTerminate();
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

void InitGLFW()
{
    // 初始化GLFW
    glfwInit();
    // 配置GLFW  第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;
    // 第二个参数接受一个整型,用来设置这个选项的值。
    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);
}
bool CreateWindow()
{
    // 创建一个窗口对象
    window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        // 创建失败,终止程序
        glfwTerminate();
        return false;
    }
    // 将我们窗口的上下文设置为当前线程的主上下文
    glfwMakeContextCurrent(window);
    // 设置窗口大小改变时的回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    return true;
}
bool InitGLAD()
{
    // 初始化GLAD,传入加载系统相关opengl函数指针的函数
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        // 初始化失败,终止程序
        return false;
    }
    return true;
}

// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
    // 设置窗口的维度
    glViewport(0, 0, width, height);
}

// 输入
void processInput(GLFWwindow *window)
{
    // 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true
    // 关闭应用程序
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

效果展示:

着色器类模板

每创建一个着色器编写、编译、管理很麻烦,封装通用的Shader模板类可帮助我们省去很多时间,重点关注在效果实现上。

创建着色器程序

创建Shader程序模板头文件shader_s.h, 并放在指定头文件文件夹:

cpp 复制代码
#ifndef SHADER_H
#define SHADER_H

#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();
            std::cout << "vertexCode: " << vertexCode << std::endl;
            std::cout << "fragmentCode: " << fragmentCode << std::endl;
        }
        catch (std::ifstream::failure &e)
        {
            std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
        }
        const char *vShaderCode = vertexCode.c_str();
        const char *fShaderCode = fragmentCode.c_str();
        // 2. 编译着色器
        unsigned int vertex, fragment;
        // 顶点着色器
        vertex = glCreateShader(GL_VERTEX_SHADER);     // 创建顶点着色器
        glShaderSource(vertex, 1, &vShaderCode, NULL); // 把着色器源码附加到着色器对象上
        glCompileShader(vertex);                       // 编译着色器
        checkCompileErrors(vertex, "VERTEX");          // 检查编译错误
        // 片段着色器
        fragment = glCreateShader(GL_FRAGMENT_SHADER);   // 创建片段着色器
        glShaderSource(fragment, 1, &fShaderCode, NULL); // 把着色器源码附加到着色器对象上
        glCompileShader(fragment);                       // 编译着色器
        checkCompileErrors(fragment, "FRAGMENT");        // 检查编译错误
                                                         // 着色器程序
        ID = glCreateProgram();
        glAttachShader(ID, vertex);
        glAttachShader(ID, fragment);
        glLinkProgram(ID);
        checkCompileErrors(ID, "PROGRAM");
        // 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
        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); // 获取uniform变量的位置值,设置值
    }
    void setInt(const std::string &name, int value) const
    {
        glUniform1i(glGetUniformLocation(ID, name.c_str()), value); // 获取uniform变量的位置值,设置值
    }
    void setFloat(const std::string &name, float value) const
    {
        glUniform1f(glGetUniformLocation(ID, name.c_str()), value); // 获取uniform变量的位置值,设置值
    }

private:
    // 检查着色器编译/连接是否成功的工具函数
    void checkCompileErrors(unsigned int shader, std::string type)
    {
        int success;
        char infoLog[1024];
        // 检查着色器是否编译/链接成功了
        if (type != "PROGRAM")
        {
            glGetShaderiv(shader, GL_COMPILE_STATUS, &success); // 获取着色器编译/连接状态
            if (!success)
            {
                glGetShaderInfoLog(shader, 1024, NULL, infoLog); // 获取着色器编译/连接错误信息
                std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n"
                          << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
        else
        {
            glGetProgramiv(shader, GL_LINK_STATUS, &success); // 获取着色器编译/连接状态
            if (!success)
            {
                glGetProgramInfoLog(shader, 1024, NULL, infoLog); // 获取着色器编译/连接错误信息
                std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n"
                          << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
            }
        }
    }
};
#endif

创建Shader脚本

顶点着色器文件:shader.vs

#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;
}

片元着色器文件:shader.fs

#version 330 core
out vec4 FragColor;

in vec3 ourColor;

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

然后把这两个文件放到指定的文件夹下:

使用模板和Shader脚本文件

需要注意的是,在加载Shader脚本时要,指定正确的脚本路径:

完整脚本

cpp 复制代码
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <shader_s.h>

void InitGLFW();
bool CreateWindow();
bool InitGLAD();
// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
void processInput(GLFWwindow *window);

// settings 窗口宽高
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

GLFWwindow *window;

int main()
{
    InitGLFW();                      // 初始化GLFW
    bool isCreated = CreateWindow(); // 创建一个窗口对象
    if (!isCreated)
        return -1;
    bool isGLAD = InitGLAD(); // 初始化GLAD,传入加载系统相关opengl函数指针的函数
    if (!isGLAD)
        return -1;

    // 构建和编译着色程序
    Shader ourShader("shader/P1_Basic/03_Shader/shader.vs", "shader/P1_Basic/03_Shader/shader.fs");

    // 设置顶点数据(和缓冲区)并配置顶点属性
    // 1.顶点输入
    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    // 顶部
    };

    // 2.创建一个顶点缓冲对象(Vertex Buffer Objects, VBO) 顶点数组对象(Vertex Array Object, VAO)
    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO); // 生成一个VAO对象
    glGenBuffers(1, &VBO);      // 生成一个VBO对象
    // 3.首先绑定顶点数组对象,然后绑定和设置顶点缓冲区,然后配置顶点属性。
    glBindVertexArray(VAO);                                                    // 绑定VAO
    glBindBuffer(GL_ARRAY_BUFFER, VBO);                                        // 绑定VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 将顶点数据复制到缓冲区中

    // 4.配置位置属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)0);
    glEnableVertexAttribArray(0);
    // 5.   颜色属性
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void *)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);

    // 注意这是允许的,调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 你可以在之后解除对VAO的绑定,这样其他VAO调用就不会意外地修改这个VAO,但这种情况很少发生。修改其他
    //  VAOs无论如何都需要调用glBindVertexArray,所以当不直接需要时,我们通常不会取消绑定VAOs(也不会取消绑定vbo)。
    glBindVertexArray(0);

    // 取消此调用的注释以绘制线框多边形。
    // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    // 循环渲染
    while (!glfwWindowShouldClose(window))
    {

        // 输入
        processInput(window);

        // 渲染
        // 清除颜色缓冲
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 记得激活着色器
        ourShader.use();
        // 绘制三角形
        glBindVertexArray(VAO);           // 绑定VAO
        glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形
        // glBindVertexArray(0); // 不需要每次都绑定VAO

        // 检查并调用事件,交换缓冲
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 可选:一旦资源超出其用途,就取消分配所有资源:
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    // 释放/删除之前的分配的所有资源
    glfwTerminate();
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

void InitGLFW()
{
    // 初始化GLFW
    glfwInit();
    // 配置GLFW  第一个参数代表选项的名称,我们可以从很多以GLFW_开头的枚举值中选择;
    // 第二个参数接受一个整型,用来设置这个选项的值。
    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);
}
bool CreateWindow()
{
    // 创建一个窗口对象
    window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        // 创建失败,终止程序
        glfwTerminate();
        return false;
    }
    // 将我们窗口的上下文设置为当前线程的主上下文
    glfwMakeContextCurrent(window);
    // 设置窗口大小改变时的回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    return true;
}
bool InitGLAD()
{
    // 初始化GLAD,传入加载系统相关opengl函数指针的函数
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        // 初始化失败,终止程序
        return false;
    }
    return true;
}

// 窗口大小改变时调用
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
    // 设置窗口的维度
    glViewport(0, 0, width, height);
}

// 输入
void processInput(GLFWwindow *window)
{
    // 当用户按下esc键,我们设置window窗口的windowShouldClose属性为true
    // 关闭应用程序
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}
相关推荐
2401_8574396921 分钟前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna1 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_1 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
Dream_Snowar2 小时前
速通Python 第三节
开发语言·python
唐诺2 小时前
几种广泛使用的 C++ 编译器
c++·编译器
jinhuazhe20133 小时前
如何解决vscode powershell乱码
ide·vscode·编辑器
高山我梦口香糖3 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
mahuifa3 小时前
混合开发环境---使用编程AI辅助开发Qt
人工智能·vscode·qt·qtcreator·编程ai
冷眼看人间恩怨3 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
信号处理学渣3 小时前
matlab画图,选择性显示legend标签
开发语言·matlab