游戏引擎从零开始(25)-glsl封装成文件

427 阅读5分钟

前言

现在的glsl语句是以字符串的形式写在代码中,既显的臃肿、也不方便复用。

这章做一个简单的glsl解析,将glsl语句挪到文件中。

glsl封装与解析

glsl语句存到文件中,分三步处理:

  1. 读取文件
  2. 预处理,解析成单个的shader字符串,存到Map中
  3. 编译链接成真正的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本身也是很大的一个模块,非常满足软件设计接口抽象与封装的思想,实现的非常优雅,有时间的话,值得深入源码学习。

软件设计与开发的过程,本质就是将功能合理的分层、模块化,使得其满足高内聚、低耦合、易拓展。而这看起来既不高大上也不酷炫,反而更多的是朴实和务实的工作。