从0开始的OpenGL学习(四)-着色器类

本文主要解决两个问题:

  • 1、着色器和应用、着色器之间是如何进行数据传递的?
  • 2、如何封装一个着色器类?

一、开胃小菜GLSL

先来点开胃菜,扯两句GLSL。之前我们就用过GLSL搞了个顶点着色器和片元着色器,也算是对它不陌生了。语法上,它和C语言十分类似,所以使用起来的时候感觉还是很友好的(笔者是学C语言出生的,吼吼)。不说太多关于语法、操作符、数据类型这些废话,学了一门语言之后,所有的语言都会往自己熟悉的方向去用,学一门新语言的时候最讨厌的就是先看语法,烦透了。我们直接从关键点着手!

1、着色器代码的格式:

#version 版本号
in 数据类型 变量名;
in 数据类型 变量名;
out 数据类型 变量名;
uniform 数据类型 变量名;
void main() {
        //处理过程
         输出变量 = 处理结果;
}

in表示从上一个阶段输入的数据,out表示这个阶段需要输出的数据,uniform表示全局的数据(应用里也能读取和写入这个变量,这就是着色器和应用之间互通数据的方法),主函数main中包含了处理过程,将处理结果赋值给输出变量。
2、向量
用的最多的数据类型就是向量,反正到现在为止我们用的是最多的,以后用的也是最多的。已经用过的类型有:vec3,vec4。表示有3个、4个float数据的向量。当然还有vec1,vec2。(vec2可以理解,这个vec1是什么鬼?别问我,我也是新手,别说用过,见都没见过。)这只是包含float类型的向量,还有包含布尔类型的向量bvec1234,包含整型数的向量ivec1234,包含无符号整型数的向量uvec1234,包含双精度浮点型数的向量dvec1234。除了包含的类型不同,操作方式都一样,用到的时候再说。

二、着色器之间的数据互通

说穿了很简单,上一个阶段的输出变量就会成为下一个阶段的输入变量,只要变量名相同就行了。注意,必须要相同变量名,否则接收不到。

//顶点着色器代码
const char * vertexShaderSource = "#version 330 core\n" 
"layout (location = 0) in vec3 aPos;\n" 
"out vec3 ourColor;\n"
"void main()\n" 
"{\n"
"       gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"       ourColor = vec3(0.5f, 0.0f, 0.0f);\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"
"}\0";

在顶点着色器中定义一个输出的颜色,片元着色器中接收,然后直接将这个颜色赋值给了片元颜色输出量,这样三角形的颜色就成了顶点着色器中定义的暗红色了。
在这里插入图片描述
参考源代码:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter4/triangle_4_2.cc

三、着色器和应用之间的数据互通

说穿了也很简单,定义成uniform方式的变量就是全局的变量,可以在应用中访问,不过需要一些特殊的访问方式。

//片元着色器代码
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"uniform vec4 ourColor;\n"
"in vec3 ourColor;\n"
"void main()\n"
"{\n"
"       FragColor = vec4(ourColor, 1.0f);\n"
"}\0";

(坑提醒:如果申明了一个uniform但是在GLSL中没有使用,编译器会自动将这个变量删除的,这会引起一系列诡谲的BUG,到时候慢慢填吧,哇哈哈)
要使用这个变量,需要两步:

  • 1、获取该变量在着色器程序中的位置(这里是着色器程序,不是片元着色器,是顶点着色器和片元着色器链接进入的那个着色器程序)。
  • 2、通过glUniform4fv函数对其进行赋值。
float greenValue = 1.0;
int outColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUserProgram(shaderProgram);
glUniform4f(outColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

这里的glUniform4f函数调用可以放到glUserProgram之前,在使用着色器程序之前就可以完成对颜色的赋值操作。
运行之后的效果显示了一个绿色三角形:
运行效果
运行效果
参考代码:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter4/triangle_4_3.cc

四、封装一个着色器类

直接在源代码中写着色器实在是太麻烦了,不好管理不说,可读性也很差。作为一个上进的好程序员,怎么能不封装一下节省时间呢?
这个类需要什么功能?从文件中读取并且编译是必要的,也要可以设置着色器中变量的功能,使用着色器的功能也不能少,嗯,干脆就弄一个编译链接之后的着色器程序类。

不多解释了,按照我们的思路来编码,代码本身很简单。

#ifndef SHADER_H
#define SHADER_H
 
#include <glad/glad.h>    //
 
#include <string>
#include <iostream>
#include <fstream>
#include <sstream>
 
class Shader {
public:
    //程序ID
    unsigned int ID;
 
    Shader(const GLchar* vertexPath, const GLchar* 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

其他的实现都很简单,主要是Shader这个构造函数,要完成两个主要步骤:

  • 其一、从文件中读取着色器代码
  • 其二、编译链接这些代码

先实现读取代码的功能:

    //读取着色器的代码
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;

    //确保文件流会输出异常
    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();

        vertexCode = vShaderStream.str();
        fragmentCode = fShaderStream.str();
    }
    catch(const std::ifstream::failure e)
    {
        std::cerr << "错误:读取文件失败,请检查文件是否存在!" << std::endl;
    }
    

简单地使用C++的STL库来读取文件,保存到流中,再从流中转换到字符串里保存,这样,我们就得到了所需要的代码。

再把代码进行编译:

    //2 编译着色器
    const char *vShaderCode = vertexCode.c_str();
    const char *fShaderCode = fragmentCode.c_str();

    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 << "编译顶点着色器失败,错误信息:" << 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 << "编译片元着色器失败,错误信息:" << infoLog << std::endl;
    }
    // //着色器程序
    // ID = glad_glCreateProgram();
    // glAttachShader(ID, vertex);
    // glAttachShader(ID, fragment);
    // glLinkProgram(ID);
    // glGetProgramiv(ID, GL_LINK_STATUS, &success);
    // if (!success)
    // {
    //     glad_glGetProgramInfoLog(ID, 512, NULL, infoLog);
    //     std::cout << "编译片元着色器失败,错误信息:" << infoLog << std::endl;
    // }
    //链接着色器
    ID = glCreateProgram();
    glAttachShader(ID, vertex);
    glAttachShader(ID, fragment);
    glLinkProgram(ID);
    glGetProgramiv(ID, GL_LINK_STATUS, &success);
    if (!success)
    {
        glad_glGetProgramInfoLog(ID, 512, NULL, infoLog);
        std::cout << "链接着色器失败,错误信息:" << infoLog << std::endl;
    }
    //删除着色器
    glDeleteShader(vertex);
    glDeleteShader(fragment);

代码很简单,出了添加了一个编译和连接的成功判断,都是我们之前写过的东西,没啥难度。

使用着色器程序以及设置简单数据的功能也没啥好说的,就是单纯的函数调用而已,直接放代码:

好,功能实现完毕,编译一下,没有问题。Very Good!

等等,这就好了?还没完呢!弄了个东西不弄不就浪费了,我们来改造一下代码。
参考代码:

#include "shader.h"

Shader::Shader(const GLchar* vertexPath, const GLchar* fragmentPath)
{
    //读取着色器的代码
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;

    //确保文件流会输出异常
    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();

        vertexCode = vShaderStream.str();
        fragmentCode = fShaderStream.str();
    }
    catch(const std::ifstream::failure e)
    {
        std::cerr << "错误:读取文件失败,请检查文件是否存在!" << std::endl;
    }
    
    //2 编译着色器
    const char *vShaderCode = vertexCode.c_str();
    const char *fShaderCode = fragmentCode.c_str();

    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 << "编译顶点着色器失败,错误信息:" << 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 << "编译片元着色器失败,错误信息:" << infoLog << std::endl;
    }
    // //着色器程序
    // ID = glad_glCreateProgram();
    // glAttachShader(ID, vertex);
    // glAttachShader(ID, fragment);
    // glLinkProgram(ID);
    // glGetProgramiv(ID, GL_LINK_STATUS, &success);
    // if (!success)
    // {
    //     glad_glGetProgramInfoLog(ID, 512, NULL, infoLog);
    //     std::cout << "编译片元着色器失败,错误信息:" << infoLog << std::endl;
    // }
    //链接着色器
    ID = glCreateProgram();
    glAttachShader(ID, vertex);
    glAttachShader(ID, fragment);
    glLinkProgram(ID);
    glGetProgramiv(ID, GL_LINK_STATUS, &success);
    if (!success)
    {
        glad_glGetProgramInfoLog(ID, 512, NULL, infoLog);
        std::cout << "链接着色器失败,错误信息:" << infoLog << std::endl;
    }
    //删除着色器
    glDeleteShader(vertex);
    glDeleteShader(fragment);
}
//使用着色器
void Shader::use()
{
    glUseProgram(ID);
}
//设置uniform变量
void Shader::setBool(const std::string& name, bool value) const
{
    glUniformli(glGetUniformLocation(ID, name.c_str()), (int)value));
}
void Shader::setInt(const std::string& name, int value) const
{
    glUniformli(glGetUniformLocation(ID, name.c_str(), value));
}
void Shader::setFloat(const std::string& name, float value) const
{
    glUniformlf(glGetUniformLocation(ID, name.c_str()), value);
}

五、使用着色器类
先创建两个文件,Shader.vs和Shader.fs,分别表示顶点着色器和片元着色器。使用本文第二节中显示为红色的着色器代码。
参考:
顶点着色器:

//顶点着色器代码
#version 330 core
layout (location = 0) in vec3 aPos;
//"layout (location = 1) in vec3 aColor;\n"
out vec3 ourColor;
void main()
{
	gl_Position = vec4(aPos, 1.0);
	ourColor = vec3(0.5f, 0.0f, 0.0f);
}

片元着色器:

//片元着色器代码
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
//uniform vec4 ourColor;
void main()
{
	FragColor = vec4(ourColor, 1.0f);
}

然后,将冗长的着色器实现代码和加载编译的代码删除掉,用Shader对象来代替:

最后,将使用着色器程序的代码删除,调用着色器对象的use函数

使用着色器对象

编译运行,效果和第二节的一样,说明我们的工作是有成效的!

代码参考:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter4/triangle_4_2.cc

总结
嗯,到现在为止,我们的着色器程序算是走上正轨了,手写一遍代码的读者想必也累了,休息一下吧。

本章节工程源代码:

https://gitee.com/pengrui2009/open-gl-study/blob/master/chapter4

原文链接

作者:闪电的蓝熊猫
链接:https://www.jianshu.com/p/a7bc9020f0a3

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值