本文主要解决两个问题:
- 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