前言
现在的glsl语句是以字符串的形式写在代码中,既显的臃肿、也不方便复用。
这章做一个简单的glsl解析,将glsl语句挪到文件中。
glsl封装与解析
glsl语句存到文件中,分三步处理:
- 读取文件
- 预处理,解析成单个的shader字符串,存到Map中
- 编译链接成真正的shader program
流程如下图所示:
下面我们就按这个流程来修改代码,go~
glsl语句存储到文件
以textureShader为例,将SandBoxApp中textreShader的glsl语句放到文件Texture.glsl中,以"#type"开头标识shader类别,暂时我们仅支持vertex、fragment两种长江的类型:
Sandbox/assets/shaders/Texture.glsl
// Basic Texture Shader
#type vertex
#version 330 core
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec2 a_TexCoord;
uniform mat4 u_ViewProjection;
uniform mat4 u_Transform;
out vec2 v_TexCoord;
void main()
{
v_TexCoord = a_TexCoord;
gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0);
}
#type fragment
#version 330 core
layout(location = 0) out vec4 color;
in vec2 v_TexCoord;
uniform sampler2D u_Texture;
void main()
{
color = texture(u_Texture, v_TexCoord);
}
基类Shader增加接口,支持读取glsl文件
Shader中增加Create接口,支持从文件读取shader语句:
Sandbox/Hazel/src/Hazel/Renderer/Shader.h
static Shader* Create(const std::string& filePath);
static Shader* Create(const std::string& vertexSrc, const std::string&
同样,实现中也要注意区分平台:
Sandbox/Hazel/src/Hazel/Renderer/Shader.cpp
Shader *Shader::Create(const std::string& filePath) {
switch (Renderer::GetAPI()) {
case RendererAPI::API::None:
HZ_CORE_ASSERT(false, "RendererAPI::None is currently not supported!");
return nullptr;
case RendererAPI::API::OpenGL:
return new OpenGLShader(filePath);
}
HZ_CORE_ASSERT(false, "Unknown RendererAPI!");
}
子类OpenGLShader实现读取.glsl文件
OpenGLShader中增加读取文件、编译Shader program的接口,参考下面代码注释:
Sandbox/Hazel/src/Hazel/Platform/OpenGL/OpenGLShader.h
#include "Renderer/Shader.h"
// TODO:REMOVE!
typedef unsigned int GLenum;
namespace Hazel {
class OpenGLShader : public Shader {
public:
// 支持参数为文件路径的构造函数
OpenGLShader(const std::string& filePath);
OpenGLShader(const std::string& vertexSrc, const std::string& fragmentSrc);
...
private:
// 读取文件到字符串
std::string ReadFile(const std::string& filePath);
// shader存储到map中,key为shader类别
std::unordered_map<GLenum, std::string> PreProcess(const std::string& source);
// 编译Shader生成Shader program
void Compile(const std::unordered_map<GLenum, std::string>& shaderSources);
private:
uint32_t m_RendererID;
};
OpenGLShader的实现中改动较大,涉及到文件IO,不熟悉c++ io,可以参考:C++ 文件和流
Sandbox/Hazel/src/Hazel/Platform/OpenGL/OpenGLShader.cpp
使用到文件流
include <fstream>
用字符串区分shader类别
static GLenum ShaderTypeFromString(const std::string& type) {
if (type == "vertex") {
return GL_VERTEX_SHADER;
}
if (type == "fragment" || type == "pixel") {
return GL_FRAGMENT_SHADER;
}
HZ_CORE_ASSERT(false, "Unknown shader type!");
return 0;
}
ReadFile实现
std::string OpenGLShader::ReadFile(const std::string &filePath) {
std::string result;
std::ifstream in(filePath, std::ios::in | std::ios::binary);
if (in) {
// 移动到文件末尾
in.seekg(0, std::ios::end);
// tellg()当前索引,即获取文件的二进制长度,按照字节来计算偏移
// string.resize()按照char为单位,也是字节
result.resize(in.tellg());
// 移动到文件开头
in.seekg(0, std::ios::beg);
// 读取文件内容到result
// 注意! &result[0]获取的是真正放字符串内容的内存地址,和std::string的地址是不一样的
in.read(&result[0], result.size());
in.close();
} else {
HZ_CORE_ERROR("Could not open file '{0}'", filePath);
}
return result;
}
注意,ifstream接口中,类似seekg、seekp是成对的接口,xxx_g表示get型操作,声明该操作后可进行读取,xxx_p表示put型操作,声明该操作后可进行写入。
PreProcess实现
解析.glsl文件,这里的代码要耐心点理解
std::unordered_map<GLenum, std::string> OpenGLShader::PreProcess(const std::string &source) {
std::unordered_map<GLenum, std::string> shaderSource;
const char* typeToken = "#type";
size_t typeTokenLength = strlen(typeToken);
// pos: 指向"#type"开头
// eol:指向"vertex"末尾
// begin:指向"vertex"开头
// nextLinePos:指向"#version"开头
size_t pos = source.find(typeToken, 0);
while(pos != std::string::npos) {
size_t eol = source.find_first_of("\r\n", pos); //#type后面的换行
HZ_CORE_ASSERT(eol != std::string::npos, "Syntax error") // 没有换行,则后面没有逻辑,语法错误
size_t begin = pos + typeTokenLength + 1; // glsl第一行代码 +1表示跳过一个空格
std::string type = source.substr(begin, eol - begin); //
HZ_CORE_ASSERT(ShaderTypeFromString(type), "Invalid shader type specified!")
size_t nextLinePos = source.find_first_not_of("\r\n", eol);
// 找到下一个"#type"
pos = source.find(typeToken, nextLinePos);
shaderSource[ShaderTypeFromString(type)] = source.substr(nextLinePos, pos-nextLinePos);
// shaderSource[ShaderTypeFromString(type)] = source.substr(nextLinePos, pos-(nextLinePos == std::string::npos ? source.size() - 1 : nextLinePos));
}
return shaderSource;
}
Compile实现
Compile就是将之之前构造函数中的逻辑抠出。
void OpenGLShader::Compile(const std::unordered_map<GLenum, std::string> &shaderSources) {
GLuint program = glCreateProgram();
std::vector<GLenum> glShaderIDs(shaderSources.size());
for (auto& kv : shaderSources) {
GLenum type = kv.first;
const std::string& source = kv.second;
GLuint shader = glCreateShader(type);
const GLchar* sourceCStr = source.c_str();
glShaderSource(shader, 1, &sourceCStr, 0);
glCompileShader(shader);
GLint isCompiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
if (isCompiled == GL_FALSE) {
GLint maxLength = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
std::vector<GLchar> infoLog(maxLength);
glGetShaderInfoLog(shader, maxLength, &maxLength, &infoLog[0]);
glDeleteShader(shader);
HZ_CORE_ERROR("{0}", infoLog.data());
HZ_CORE_ASSERT(false, "Shader compilation failure!");
break;
}
glAttachShader(program, shader);
glShaderIDs.push_back(shader);
}
m_RendererID = program;
glLinkProgram(program);
GLint isLinked = 0;
glGetProgramiv(program, GL_LINK_STATUS, (int*)&isLinked);
if (isLinked == GL_FALSE) {
GLint maxLength = 0;
glGetProgramiv(program ,GL_INFO_LOG_LENGTH, &maxLength);
std::vector<GLchar> infoLog(maxLength);
glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]);
glDeleteProgram(program);
for (auto id : glShaderIDs) {
glDeleteShader(id);
}
HZ_CORE_ERROR("{0}", infoLog.data());
HZ_CORE_ASSERT(false, "Shader link failure!");
return;
}
for (auto id : glShaderIDs) {
glDetachShader(program, id);
}
}
代码看着比较长,实际就是处理编译链接,接近一半的代码都是处理异常。
替换SandBoxApp中的TextureShader加载
删掉之前的shader 字符串,改用加载文件的方式,代码整洁很多。
m_TextureShader.reset(Hazel::Shader::Create("../assets/shaders/Texture.glsl"));
完整代码&总结
本次代码修改:
github.com/summer-go/H…
我们不断的在调整代码的逻辑,逐步朝高内聚、低耦合的方向完善。
C++ IO本身也是很大的一个模块,非常满足软件设计接口抽象与封装的思想,实现的非常优雅,有时间的话,值得深入源码学习。
软件设计与开发的过程,本质就是将功能合理的分层、模块化,使得其满足高内聚、低耦合、易拓展。而这看起来既不高大上也不酷炫,反而更多的是朴实和务实的工作。