ping-code

导航

OpenGL入门——着色器类

着色器的编写、编译、管理是个很繁琐的事。所以就需要写一个类,这个类可以从文件读取着色器源码,可以编译链接它们,可以对它们进行错误检测,可以设置Uniform值。

 

1. 类的声明

#pragma once

#include <glad/glad.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

//着色器对象类型
#define SHADER_TYPE_VERTEX        0    //顶点着色器对象
#define SHADER_TYPE_FRAGMENT    1    //片段着色器对象
#define SHADER_TYPE_PROGRAM        2    //着色器程序对象


class CShader
{
public:
    CShader(std::string vertex_path, std::string fragment_path);

    ~CShader();

    //激活着色器程序
    void run();

    //设置uniform值
    void setUniformBool(const std::string val_name, bool val);

    void setUniformInt(const std::string val_name, int val);

    void setUniformFloat(const std::string val_name, float val);

private:
    //编译、链接着色器
    void initialize(const char *vertexShaderSource, const char *fragmentShaderSource);

    //编译检测
    void checkShader(unsigned int shader_id, int type);

private:
    unsigned int        ID;
};

2. 类的定义

#include "shader.h"

CShader::CShader(std::string vertex_path, std::string fragment_path)
{
    // 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(vertex_path);
        fShaderFile.open(fragment_path);
        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();

    initialize(vShaderCode, fShaderCode);
}

CShader::~CShader()
{
    glDeleteProgram(ID);
}

void CShader::initialize(const char *vertexShaderSource, const char *fragmentShaderSource)
{
    //创建一个顶点着色器对象
    unsigned int vertexShader;
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//着色器源码附加到着色器对象上
    glCompileShader(vertexShader);//编译源码
    checkShader(vertexShader, SHADER_TYPE_VERTEX);

    //创建一个片段着色器对象,注意还是用ID来引用的
    unsigned int fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);//编译源码
    checkShader(fragmentShader, SHADER_TYPE_FRAGMENT);

    //创建一个着色器的程序
    ID = glCreateProgram();
    glAttachShader(ID, vertexShader);//把之前编译的着色器附加到程序对象上
    glAttachShader(ID, fragmentShader);
    glLinkProgram(ID);//glLinkProgram链接它们
    checkShader(ID, SHADER_TYPE_PROGRAM);
    
    //链接后即可删除
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

}

void CShader::checkShader(unsigned int shader_id, int type)
{
    int  success;
    char infoLog[512];
    if (type != SHADER_TYPE_PROGRAM)
    {
        glGetShaderiv(shader_id, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功
        if (!success)
        {
            glGetShaderInfoLog(shader_id, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << std::endl;
        }
    }
    else
    {
        glGetProgramiv(shader_id, GL_COMPILE_STATUS, &success);//用glGetProgramiv检查是否编译成功
        if (!success)
        {
            glGetShaderInfoLog(shader_id, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl;
        }
    }
}

void CShader::run()
{
    glUseProgram(ID);
}

void CShader::setUniformBool(const std::string val_name, bool val)
{
    glUniform1i(glGetUniformLocation(ID, val_name.c_str()), (int)val);
}

void CShader::setUniformInt(const std::string val_name, int val)
{
    glUniform1i(glGetUniformLocation(ID, val_name.c_str()), val);
}

void CShader::setUniformFloat(const std::string val_name, float val)
{
    glUniform1i(glGetUniformLocation(ID, val_name.c_str()), val);
}

 

3. 类的使用

把着色器源码保存到文件中,后缀可以自定义,如下:

顶点着色器文件hello_triangle.vs

//vertex shader source
#version 330 core
layout(location = 0) in vec3 position;    //位置变量的属性位置为0
layout(location = 1) in vec3 color;        //颜色变量的属性位置为1
out vec3 vertexColor;                    //为片段着色器指定一个颜色输出

void main()
{
    gl_Position = vec4(position, 1.0);    //opengl顶点坐标
    vertexColor = color;                //从顶点数据那里得到的输入颜色
}

片段着色器文件hello_triangle.fs

//fragment shader source
#version 330 core
out vec4 fragColor;        //像素的最终颜色
in vec3 vertexColor;    //从顶点着色器传来的输入变量(名称、类型必须相同)
//uniform vec3 ourColor;//程序中设定这个变量

void main()
{
    fragColor = vec4(vertexColor, 1.0);
}

应用程序中使用着色器类,只需要2步:定义着色器和激活程序对象

    ...//初始化窗口

    ///定义着色器
    CShader shader("hello_triangle.vs", "hello_triangle.fs");
    
    ...//定义顶点对象及其属性

        
    while (!glfwWindowShouldClose(window))
    {
        processInput(window);


        //清空屏幕
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);


        ///绘制物体
        shader.run();//激活程序对象

        ...

        glfwPollEvents();//检查有没有触发什么事件
    }

    //释放对象
    ...

 

完整代码示例

//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "shader.h"

//改变窗口大小
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)//点击ESC键退出绘制
        glfwSetWindowShouldClose(window, true);
}

GLFWwindow* init_window()
{
    ///窗口初始化
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号,当API以不兼容的方式更改时,该值会增加。
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号,当特性被添加到API中时,它会增加,但是它保持向后兼容。
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//使用核心模式,不兼容已废弃函数

    //创建glfw窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "ping-window", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "failed to create GLFW window" << std::endl;
        glfwTerminate();//释放/删除之前的分配的所有资源
        return nullptr;
    }
    glfwMakeContextCurrent(window);//将窗口的上下文设置为当前线程的主上下文
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//注册为调整窗口回调函数

    //GLAD是用来管理OpenGL的函数指针的,在调用任何OpenGL的函数之前初始化GLAD
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))//给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数
    {
        std::cout << "failed to intialize GLAD" << std::endl;
        return nullptr;
    }

    glViewport(0, 0, 800, 600);//处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)

    return window;
}

int hello_triangle()
{
    GLFWwindow* window = init_window();

    ///定义着色器
    CShader shader("hello_triangle.vs", "hello_triangle.fs");

    ///定义顶点对象
    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    // 顶部
    };
    //生成VAO对象,缓冲ID为VAO
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);//绑定VAO,从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO,供之后使用

    //生成VBO对象,缓冲ID为VBO
    unsigned int VBO;
    glGenBuffers(1, &VBO);//第一个参数GLsizei是要生成的缓冲对象的数量,第二个GLuint是要输入用来存储缓冲对象名称的数组

    //绑定到目标对象,VBO变成了一个顶点缓冲类型
    glBindBuffer(GL_ARRAY_BUFFER, VBO);//第一个就是缓冲对象的类型,第二个参数就是要绑定的缓冲对象的名称
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//数据传入缓冲内存中,GL_STATIC_DRAW:数据不会或几乎不会改变; GL_DYNAMIC_DRAW:数据会被改变很多; GL_DYNAMIC_DRAW:数据会被改变很多

    //设置顶点属性指针,如何解析顶点数据
    /*
    第一个参数指定我们要配置的顶点属性,顶点着色器中使用layout(location = 0)定义
    第二个参数指定顶点属性的大小
    第三个参数指定数据的类型
    第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间
    第五个参数步长(Stride),它告诉我们在连续的顶点属性组之间的间隔
    最后一个参数的类型是void*,数据在缓冲中起始位置的偏移量(Offset)
    */
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);//启用顶点属性layout(location = 0),顶点属性默认是禁用的
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);//启用顶点属性layout(location = 1),顶点属性默认是禁用的
    glBindBuffer(GL_ARRAY_BUFFER, 0);//设置完属性,解绑VBO

    glBindVertexArray(0);//配置完VBO及其属性,解绑VAO


    //绘制模式为线条GL_LINE,填充面GL_FILL
    //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);


        ///绘制物体
        shader.run();

        glBindVertexArray(VAO);
        //使用VAO绘制
        glDrawArrays(GL_TRIANGLES, 0, 3);//绘制图元为三角形,起始索引0,绘制顶点数量3
        
        glfwSwapBuffers(window);//交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲)
        glfwPollEvents();//检查有没有触发什么事件
    }

    //释放对象
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

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

int main()
{
    hello_triangle();

    return 0;
}

 

运行结果

 

posted on 2023-09-17 10:38  一只小瓶子  阅读(135)  评论(0编辑  收藏  举报