基于OpenGL编写一个简易的2D渲染框架-09 重构渲染器-Shader

  Shader 只是进行一些简单的封装,主要功能:

    1、编译着色程序

    2、绑定 Uniform 数据

    3、根据着色程序的顶点属性传递顶点数据到 GPU

  着色程序的编译

    GLuint Shader::createShaderProgram(const char* vsname, const char* psname)
    {
        std::string vShaderSource, fShaderSource;
        std::ifstream vShaderFile, fShaderFile;
        vShaderFile.exceptions(std::ifstream::badbit);
        fShaderFile.exceptions(std::ifstream::badbit);

        try {
            vShaderFile.open(PathHelper::fullPath(vsname), std::ios::in);
            fShaderFile.open(PathHelper::fullPath(psname), std::ios::in);

            std::stringstream vShaderStream, fShaderStream;

            vShaderStream << vShaderFile.rdbuf();
            fShaderStream << fShaderFile.rdbuf();

            vShaderSource = vShaderStream.str();
            fShaderSource = fShaderStream.str();

            vShaderFile.close();
            fShaderFile.close();
        }
        catch ( std::ifstream::failure e ) {
            throw std::exception("Error shader: file not succesfully read");
        }
        const GLchar* vShaderCode = vShaderSource.c_str();
        const GLchar* fShaderCode = fShaderSource.c_str();

        /* 创建顶点作色器  */
        GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertexShader, 1, &vShaderCode, NULL);
        glCompileShader(vertexShader);

        GLint success;
        GLchar infoLog[512];

        glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
        if ( !success ) {
            glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
            throw std::exception("");
        }

        /* 创建片段着色器 */
        GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragmentShader, 1, &fShaderCode, NULL);
        glCompileShader(fragmentShader);

        glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
        if ( !success ) {
            glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
            throw std::exception("");
        }

        /* 创建着色程序 */
        GLuint shaderProgram = glCreateProgram();
        glAttachShader(shaderProgram, vertexShader);
        glAttachShader(shaderProgram, fragmentShader);
        glLinkProgram(shaderProgram);

        glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
        if ( !success ) {
            glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
            throw std::exception("");
        }
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);

        /* 使用着色程序 */
        return shaderProgram;
    }

  Simple2D 只支持顶点着色器和片段着色器,暂不支持其他着色器。

  OpenGL 绘制方式

  使用openGL图形库绘制,都需要通过openGL接口向图像显卡提交顶点数据,显卡根据提交的数据绘制出相应的图形。

  其中有四种方式:

    1、立即模式

    2、显示列表

    3、顶点数组

    4、现代VAO、ABO

  立即模式和显示列表是 OpenGL 传统模式的绘制方法(现在都 2017 年了,应该没有人用这种方式了吧?),后两种是现代方式绘制。前面渲染器用的就是第四种方式:现代 VBO 和 VAO。

    VBO 即 Vertex Buffer Object,是一个在高速视频卡中的内存缓冲,用来保存顶点数据,也可用于包含诸如归一化向量、纹理和索引等数据。

    VAO 即 Vertex Array Object ,是一个包含一个或多个VBO的对象,被设计用来存储一个完整被渲染对象所需的信息。

    这里不再对其进行介绍,感兴趣的可以点击这个链接:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/

  

  本次渲染器用的是第三种方式,为了更好地理解这种绘制方式,下面举一个例子,假设顶点着色器的顶点属性为

  layout(location = 0) in vec3 Position;
  layout(location = 1) in vec2 Texcoord;
  layout(location = 2) in vec4 Color;

  这里随机给定一些顶点数据,包含有位置、颜色和纹理坐标

GLfloat vertexes[] = {
  0.0f, 0.0f, 0.0f,
  0.0f, 1.0f, 0.0f,
  1.0f, 1.0f, 0.0f,
  1.0f, 0.0f, 0.0f,
};
GLfloat colors[] = {
  1.0f, 1.0f, 1.0f, 1.0f,
  1.0f, 1.0f, 1.0f, 1.0f,
  1.0f, 1.0f, 1.0f, 1.0f,
  1.0f, 1.0f, 1.0f, 1.0f
};
GLfloat texCoordes[] = {
  0.0f, 1.0f,
  0.0f, 0.0f,
  1.0f, 0.0f,
  1.0f, 1.0f
};

  接下来使用函数 glVertexAttribPointer 把顶点数据传递到 GPU。

void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vertexes);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, colores);
glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, texCoordes);

  然后调用 glDrawArrays 函数进行绘制(使用了顶点索引的可调用 glDrawElements),上面的例子中每个顶点属性的数据储存在不同的数组中。如果你想把数据都储存在一个数组中,就如下面一样

GLfloat data[] = {
  0.0f, 0.0f, 0.0f,   0.0f, 1.0f,   1.0f, 1.0f, 1.0f, 1.0f,
  0.0f, 1.0f, 0.0f,   0.0f, 0.0f,   1.0f, 1.0f, 1.0f, 1.0f,
  1.0f, 1.0f, 0.0f,   1.0f, 0.0f,   1.0f, 1.0f, 1.0f, 1.0f,
  1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   1.0f, 1.0f, 1.0f, 1.0f
};

  这样称之为交错数组,而且使用函数 glVertexAttribPointer 的参数需要作出相应的改变

glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glEnableVertexAttribArray(2);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 9 * sizeof(GLfloat), (char*)data + sizeof(GLfloat) * 0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 9 * sizeof(GLfloat), (char*)data + sizeof(GLfloat) * 3); glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 9 * sizeof(GLfloat), (char*)data + sizeof(GLfloat) * 5);

  第五个参数是跨度,即所有顶点属性的大小,3 + 2 + 4 = 9,最后一个参数是数据的指针,不同顶点属性的数据要加上适当的偏移。最后调用绘制函数即可。

  通过上面的例子可知,每个顶点着色器的顶点属性都不一定一样,所以在传递顶点数据到 GPU 时所执行的操作不一样。所以在创建 Shader 前需要设置 Shader 的顶点属性数组,通过顶点属性数组来执行传递数据到 GPU 的操作。

  分析 glVertexAttribPointer 函数的参数,定义下面顶点属性的结构

    struct VertexAttribute
    {
        int layout;
        int size;
        int type;
        int stride;
        int offset;
    };

  

  Shader 默认有两种顶点属性,分别是 位置-颜色 和 位置-纹理坐标-颜色

    enum CustomVertexAttribute
    {
        CVA_UNKNOEWN,
        CVA_V3F_C4F,
        CVA_V3F_T2F_C4F
    };

  

  设置着色器的顶点属性数组,默认最多存在8个顶点属性

    void Shader::setVertexAttribute(CustomVertexAttribute cva)
    {
        if ( cva == CustomVertexAttribute::CVA_V3F_C4F ) {
            VertexAttribute vertexAttributes[2] = {
                { 0, 3, GL_FLOAT, 7 * sizeof(GL_FLOAT), 0 * sizeof(GL_FLOAT) },
                { 1, 4, GL_FLOAT, 7 * sizeof(GL_FLOAT), 3 * sizeof(GL_FLOAT) }
            };
            this->setVertexAttribute(vertexAttributes, sizeof(vertexAttributes) / sizeof(VertexAttribute));
        }
        else if(cva == CustomVertexAttribute::CVA_V3F_T2F_C4F){
            VertexAttribute vertexAttributes[3] = {
                { 0, 3, GL_FLOAT, 9 * sizeof(GL_FLOAT), 0 * sizeof(GL_FLOAT) },
                { 1, 2, GL_FLOAT, 9 * sizeof(GL_FLOAT), 3 * sizeof(GL_FLOAT) },
                { 2, 4, GL_FLOAT, 9 * sizeof(GL_FLOAT), 5 * sizeof(GL_FLOAT) }
            };
            this->setVertexAttribute(vertexAttributes, sizeof(vertexAttributes) / sizeof(VertexAttribute));
        }
    }

    void Shader::setVertexAttribute(VertexAttribute* attribute, int count)
    {
        assert(count < 8);
        for ( nVertexAttributeCount = 0; nVertexAttributeCount < count; nVertexAttributeCount++ ) {
            vertexAttributes[nVertexAttributeCount] = attribute[nVertexAttributeCount];
        }
    }

  在设置好 Shader 的顶点属性数组后,就可以正确的传递顶点数据到 GPU 了

    void Shader::bindVertexDataToGPU(void* data)
    {
        void* data_offset = nullptr;
        for ( int i = 0; i < nVertexAttributeCount; i++ ) {
            data_offset = static_cast< char* > ( data ) +vertexAttributes[i].offset;

            /* 上传顶点数据 */
            glVertexAttribPointer(
                vertexAttributes[i].layout,
                vertexAttributes[i].size,
                vertexAttributes[i].type,
                GL_FALSE,
                vertexAttributes[i].stride,
                data_offset);

            glEnableVertexAttribArray(vertexAttributes[i].layout);
        }
    }

  函数接受顶点数据的指针,然后根据顶点属性数组调用 glVertexAttribPointer 函数设置顶点属性数组的数据格式和位置,最后调用绘制函数绘制即可。

  绑定 Uniform 数据

  假如你要绑定一个整数 2 到着色器,你可以通过两步完成:

    1、调用函数 glGetUniformLocation 获取着色器中 Uniform 变量的绑定点 location。

    2、调用 glUniform 为当前着色程序对象指定Uniform变量的值。

  由于只是绑定一个整型数 2,可以使用下面的代码绑定

glUniform1i(glGetUniformLocation(shaderProgram, "valueName"), 2);

  值得注意的是,C 语言没有函数重载,所以会有很多名字相同后缀不同的函数版本存在。其中函数名中包含数字(1、2、3、4)表示接受这个数字个用于更改uniform变量的值,i表示32位整形,f表示32位浮点型,ub表示8位无符号byte,ui表示32位无符号整形,v表示接受相应的指针类型。

  下面列举了这些函数

void glUniform1f(GLint location, GLfloat v0);
void glUniform2f(GLint location, GLfloat v0, GLfloat v1);
void glUniform3f(GLint location, GLfloat v0, GLfloat v1,  GLfloat v2);
void glUniform4f(GLint location, GLfloat v0, GLfloat v1,  GLfloat v2,  GLfloat v3); 
void glUniform1i(GLint location, GLint  v0);
void glUniform2i(GLint location, GLint  v0, GLint v1);
void glUniform3i(GLint location, GLint  v0, GLint v1, GLint v2);
void glUniform4i(GLint location, GLint  v0, GLint v1, GLint v2, GLint v3); 

void glUniform1fv(GLint location, GLsizei count, const GLfloat *value);
void glUniform2fv(GLint location, GLsizei count, const GLfloat *value);
void glUniform3fv(GLint location, GLsizei count, const GLfloat *value);
void glUniform4fv(GLint location, GLsizei count, const GLfloat *value);
void glUniform1iv(GLint location, GLsizei count, const GLint   *value);
void glUniform2iv(GLint location, GLsizei count, const GLint   *value);
void glUniform3iv(GLint location, GLsizei count, const GLint   *value);
void glUniform4iv(GLint location, GLsizei count, const GLint   *value); 

void glUniformMatrix2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
void glUniformMatrix3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value); 

  这次 Shader 类对 Uniform 的绑定做一点简单的封装,新定义一个 Uniform 类,实现 Uniform 数据的绑定(一个 Uniform 类对象表示着色器中的一个 Uniform 数据,如果着色器存在多个 Uniform 数据,则 Shader 也相应有 Uniform 对象数组)。首先定义 Uniform 类型枚举

        enum UniformType
        {
            UT_1I, UT_1F,
            UT_2I, UT_2F,
            UT_3I, UT_3F,
            UT_4I, UT_4F,

            UT_TEXTURE
        };

  这次绑定 Uniform 数据不包括数组和矩阵,只是一些绑定简单的数据。

   要绑定一个 Uniform 数据,需要绑定点 location 和值 value,故 Uniform 类成员属性如下

        int nLocation;

        UniformType uniformType;
        float fV0, fV1, fV2, fV3;

  当绑定一个 float 数据时使用 fV0,如果绑定 vec2 数据则使用 fV0 和 fV1(后两个不使用),其他情况类似。

  下面看具体的绑定函数

        bool bind(int tex = 0)
        {
            switch ( uniformType ) {
            case Simple2D::Uniform::UT_1I:
                glUniform1i(nLocation, fV0);
                break;
            case Simple2D::Uniform::UT_1F:
                glUniform1f(nLocation, fV0);
                break;
            case Simple2D::Uniform::UT_2I:
                glUniform2i(nLocation, fV0, fV1);
                break;
            case Simple2D::Uniform::UT_2F:
                glUniform2f(nLocation, fV0, fV1);
                break;
            case Simple2D::Uniform::UT_3I:
                glUniform3i(nLocation, fV0, fV1, fV2);
                break;
            case Simple2D::Uniform::UT_3F:
                glUniform3f(nLocation, fV0, fV1, fV2);
                break;
            case Simple2D::Uniform::UT_4I:
                glUniform4i(nLocation, fV0, fV1, fV2, fV3);
                break;
            case Simple2D::Uniform::UT_4F:
                glUniform4f(nLocation, fV0, fV1, fV2, fV3);
                break;
            case Simple2D::Uniform::UT_TEXTURE:
                glActiveTexture(GL_TEXTURE0 + tex);
                glBindTexture(GL_TEXTURE_2D, fV0);
                glUniform1i(nLocation, tex);
                return true;
            }
            return false;
        }

 

函数的最后实现的是绑定纹理的功能,设置纹理到着色器,我们可以使用 glActiveTexture 激活纹理单元,传入我们需要使用的纹理单元:

 glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
 glBindTexture(GL_TEXTURE_2D, texture);  glUniform1i(location, 0);

激活纹理单元之后,接下来的 glBindTexture 函数调用会绑定这个纹理到当前激活的纹理单元,最后的 glUniform1i 函数传入纹理单元序号 0(纹理单元 GL_TEXTURE8 的序号为 8) 参数实现将纹理设置到着色器。纹理单元 GL_TEXTURE0 默认总是被激活,所以我们只有一张纹理时使用 glBindTexture 的时候,无需激活任何纹理单元。

OpenG L至少保证有 16 个纹理单元供你使用,也就是说你可以激活从 GL_TEXTURE0 到 GL_TEXTURE15。它们都是按顺序定义的,所以我们也可以通过GL_TEXTURE0 + 8 的方式获得 GL_TEXTURE8。

如果我要使用 Shader 绑定一个 vec2 数据,希望可以通过这样的代码实现:

shader->getUniformByName("valueName")->setValue(2, 2);

主要是 getUniformByName 和 setValue 函数的实现:

    Uniform* Shader::getUniformByName(const char* name)
    {
        int location = glGetUniformLocation(program, name);
        assert(location != -1);

        auto it = mUniforms.find(location);
        if ( it != mUniforms.end() ) {
            return &it->second;
        }

        return &mUniforms.insert(std::make_pair(location, Uniform(location))).first->second;
    }

Shader 有一张 Uniform 数据表:

std::map<int, Uniform> mUniforms;

绑定点可以索引到 Uniform 数据,函数 getUniformByName 一开始根据 Uniform 名称查找其绑定点 location,然后通过 location 查找 Uniform 数据表,有就直接返回 Uniform,否则插入一个新的 Uniform 后返回。

接下来就是将值储存到 Uniform 对象中:

        void setValue(int v0) { uniformType = UT_1I; fV0 = v0; }
        void setValue(float v0) { uniformType = UT_1F; fV0 = v0; }

        void setValue(int v0, int v1) { uniformType = UT_2I; fV0 = v0; fV1 = v1; }
        void setValue(float v0, float v1) { uniformType = UT_2F; fV0 = v0; fV1 = v1; }

        void setValue(int v0, int v1, int v2) { uniformType = UT_3I; fV0 = v0; fV1 = v1; fV2 = v2; }
        void setValue(float v0, float v1, float v2) { uniformType = UT_3F; fV0 = v0; fV1 = v1; fV2 = v2; }

        void setValue(int v0, int v1, int v2, int v3) { uniformType = UT_4I; fV0 = v0; fV1 = v1; fV2 = v2; fV3 = v3; }
        void setValue(float v0, float v1, float v2, float v3) { uniformType = UT_4F; fV0 = v0; fV1 = v1; fV2 = v2; fV3 = v3; }

        void setTexture(int v0) { uniformType = UT_TEXTURE; fV0 = v0; }

最后,在 Shader 的 bindUniform 函数实现 Shader 中所有 Uniform 的绑定:

    void Shader::bindUniform()
    {
        int tex = 0;
        for ( auto& ele : mUniforms ) {
            if ( ele.second.bind(tex) ) {
                tex++;
            }
        }
    }

就是调用 Uniform 的 bind 函数而已。

Shader 简易封装到此结束,代码在完成重写渲染器后给出。

时间: 2024-10-11 00:24:21

基于OpenGL编写一个简易的2D渲染框架-09 重构渲染器-Shader的相关文章

基于OpenGL编写一个简易的2D渲染框架-05 渲染文本

阅读文章前需要了解的知识:文本渲染 https://learnopengl-cn.github.io/06%20In%20Practice/02%20Text%20Rendering/ 简要步骤: 获取要绘制的字符的 Unicode 码,使用 FreeType 库获取对应的位图数据,添加到字符表中(后面同样的字符可以再表中直接索引),将字符表上的字符填充到一张纹理上.计算每个字符的纹理坐标,使用渲染器绘制 注意的问题: 对于中英文混合的字符串,使用 char 存储时,英文字符占 1 个字节,而中

基于OpenGL编写一个简易的2D渲染框架01——创建窗口

最近正在学习OpenGL,我认为学习的最快方法就是做一个小项目了. 如果对OpenGL感兴趣的话,这里推荐一个很好的学习网站 https://learnopengl-cn.github.io/ 我用的是 vs2013,使用C++语言编写项目.这个小项目叫Simple2D,意味着简易的2D框架.最终的目的是可以渲染几何图形和图片,最后尝试加上一个2D粒子系统和Box2D物理引擎,并编译一个简单的游戏. 第一步,就是创建一个Win32项目. 接下来,生成一个窗口.编写一个RenderWindow类,

基于OpenGL编写一个简易的2D渲染框架-08 重构渲染器-整体架构

事实上,前面编写的渲染器 Renderer 非常简陋,虽然能够进行一些简单的渲染,但是它并不能满足我们的要求. 当渲染粒子系统时,需要开启混合模式,但渲染其他顶点时却不需要开启混合模式.所以同时渲染粒子系统和其他纹理时会得不到想要的结果,渲染器还存在许多的不足: 1.当渲染许多透明图形时,没有对其进行排序,使得本应透明的图形没有透明. 2.不能对不同的顶点使用不同的状态进行渲染. 渲染器要做的东西很简单,就是 1.传递数据到 GPU 2.设置 OpenGL 状态信息(Alpha测试.模板测试.深

基于OpenGL编写一个简易的2D渲染框架-04 绘制图片

阅读文章前需要了解的知识,纹理:https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/ 过程简述:利用 FreeImage 库加载图像数据,再创建 OpenGL 纹理,通过 Canvas2D 画布绘制,最后又 Renderer 渲染器渲染 本来想用 soil 库加载图像数据的,虽然方便,但是加载有些格式的图像文件时会出现一些问题.最后,改用 FreeImage 库来加载图像了. 添加 FreeImage 库到工

基于OpenGL编写一个简易的2D渲染框架-11 重构渲染器-Renderer

假如要渲染一个纯色矩形在窗口上,应该怎么做? 先确定顶点的格式,一个顶点应该包含位置信息 vec3 以及颜色信息 vec4,所以顶点的结构体定义可以这样: struct Vertex { Vec3 position; Vec4 color; }; 然后填充矩形四个顶点是数据信息: Vertex* data = ( Vertex* ) malloc(sizeof( Vertex ) * 4); data[0].position.set(0, 0, 0); data[1].position.set(

基于OpenGL编写一个简易的2D渲染框架02——搭建OpenGL环境

由于没有使用GLFW库,接下来得费一番功夫. 阅读这篇文章前请看一下这个网页:https://learnopengl-cn.github.io/01%20Getting%20started/02%20Creating%20a%20window/ 以下,我摘取了一点片段 Windows上的OpenGL库 如果你是Windows平台,opengl32.lib已经包含在Microsoft SDK里了,它在Visual Studio安装的时候就默认安装了.由于这篇教程用的是VS编译器,并且是在Windo

基于OpenGL编写一个简易的2D渲染框架-13 使用例子

这是重构渲染器的最后一部分了,将会给出一个 demo,测试模板测试.裁剪测试.半透明排序等等: 上图是本次 demo 的效果图,中间的绿色图形展现的是模板测试. 模板测试 void init(Pass*& p1, Pass*& p2) { p1 = new Pass; p2 = new Pass; Shader* s1 = new Shader("Shader/defaultGeometryShader.vs", "Shader/defaultGeometry

基于OpenGL编写一个简易的2D渲染框架-07 鼠标事件和键盘事件

这次为程序添加鼠标事件和键盘事件 当检测到鼠标事件和键盘事件的信息时,捕获其信息并将信息传送到需要信息的对象处理.为此,需要一个可以分派信息的对象,这个对象能够正确的把信息交到正确的对象. 实现思路: 要实现以上的功能,需要几个对象: 事件分派器:EventDispatcher,负责将 BaseEvent 分派给 EventListener 对象 事件监听器:EventListener,这只是一个接口类,接受 BaseEvent 的对象,真正的处理在它的子类中实现 事件:BaseEvent,储存

基于OpenGL编写一个简易的2D渲染框架-12 重构渲染器-BlockAllocator

BlockAllocator 的内存管理情况可以用下图表示 整体思路是,先分配一大块内存 Chunk,然后将 Chunk 分割成小块 Block.由于 Block 是链表的一个结点,所以可以通过链表的形式把未使用的 Block 连接起来,并保存到 pFreeLists 中.当我们向 BlockAllocator 申请一块内存时,BlockAllocator 会通过 pFreeLists 表索引出一块空闲的 Block,并返回其地址.当我们不断申请内存的时候,BlockAllocator 会不断返