读取带有 std::ifstream 的文件来编译 OpenGL 着色器不起作用

reading a file with std::ifstream to compile a OpenGL shader doesn't work

这是我遇到问题的小函数:

GLuint LoadShaders()
{
    // open and compile vertex shader
    std::ifstream VSS("glfwtest.vert", std::ifstream::ate);
    int len = VSS.tellg() + 1;
    GLchar* vertexSource = new GLchar[len];
    VSS.clear();
    VSS.seekg(0, VSS.beg);
    for(int i=0; i<len ; i++) { VSS.get(vertexSource[i]); }

    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexSource, NULL);
    glCompileShader(vertexShader);
    VSS.close();

    // open and compile the fragment shader
    std::ifstream FSS("glfwtest.frag", std::ifstream::ate);
    len = FSS.tellg() + 1;
    GLchar* fragmentSource = new GLchar[len];
    FSS.clear();
    FSS.seekg(0, FSS.beg);
    for(int i=0; i<len ; i++) { FSS.get(fragmentSource[i]); }

    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
    glCompileShader(fragmentShader);
    FSS.close();

    // Link the vertex and fragment shader into a shader program
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glBindFragDataLocation(shaderProgram, 0, "outColor");
    glLinkProgram(shaderProgram);
    return shaderProgram;
}

我遵循了 this 指南,并且成功地正确渲染了白色三角形。在那之后,我认为将整个事情拆分成函数以便以后重用它们可能是一个很好的练习。所以我将两个着色器的着色器源代码放在单独的文件中(而不是直接将它们放在我的 C++ 源代码中),并编写上面的函数以从它们各自的文件中读取它们。

问题来了:我的顶点着色器可以正常加载和编译,而我的片段着色器却不能。但是当我像这样将片段着色器源代码放入代码中时

const GLchar* fragmentSource =
    "#version 150 core\n"
    "out vec4 outColor;"
    "void main()"
    "{"
    "    outColor = vec4(1.0, 1.0, 1.0, 1.0);"
    "}";

GLuint LoadShaders()
{

    [...]

    // open and compile the fragment shader
    //std::ifstream FSS("glfwtest.frag", std::ifstream::ate);
    //len = FSS.tellg() + 1;
    //GLchar* fragmentSource = new GLchar[len];
    //FSS.clear();
    //FSS.seekg(0, FSS.beg);
    //for(int i=0; i<len ; i++) { FSS.get(fragmentSource[i]); }

    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentSource, NULL);
    glCompileShader(fragmentShader);

    [...]

    return shaderProgram;
}

它工作正常并显示一个白色三角形。因此,从 "glfwtest.vert" 加载和编译我的顶点着色器的相同代码不适用于片段着色器。

我以前从未在 C++ 中使用过 <fstream> 并从我发现的 online documentation example 中将其组合在一起。我想错误就在那里,但我找不到它,因为我还不是 C++ 向导。

编辑:

在评论中提出建议后,我仍然卡住了。这是我得到的 gdb 输出:

(gdb) break main.cpp:55
Breakpoint 1 at 0x4017dc: file main.cpp, line 55.
(gdb) r
Starting program: /home/georg/code/redbook/glfwtest/glfwtest 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".

Breakpoint 1, LoadShaders () at main.cpp:56
56      GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
(gdb) p vertexSource
 = (GLchar *) 0x8277e0 "#version 420 core\n\nin vec2 position;\n\nvoid main()\n{\n    gl_Position = vec4(position, 0.0, 1.0);\n}"
(gdb) p fragmentSource
 = (GLchar *) 0x82a300 "#version 150 core\nout vec4 outColor;\nvoid main()\n{\n   outColor = vec4(1.0, 1.0, 1.0, 1.0);\n}77"
(gdb) p len
 = 92
(gdb) 

如您所见,长度正确存储在 len 中,源中有 92 个字符。尽管如此,不知何故还有两个额外的字符。我用十六进制编辑器检查了源文件,这两个字符肯定不在文件中。

(跳到最近的 C++17 解决方案)

虽然此答案不会调试您的代码,但您表示有兴趣使您的例程更加模块化。这是使用 std::stringstream 并消除显式动态内存管理的更强大的东西:

static std::string read_shader_file (const char *shader_file)
{
    // no feedback is provided for stream errors / exceptions.

    std::ifstream file (shader_file);
    if (!file) return std::string ();

    file.ignore(std::numeric_limits<std::streamsize>::max());
    auto size = file.gcount();

    if (size > 0x10000) // 64KiB sanity check for shaders:
        return std::string ();

    file.clear();
    file.seekg(0, std::ios_base::beg);

    std::stringstream sstr;
    sstr << file.rdbuf();
    file.close();

    return sstr.str();
}

std::string 是可移动的,因此您无需担心 'value' 返回的成本 - 尽管在任何情况下与 GL 着色器编译和链接相比这都是微不足道的。

GLuint LoadShaders ()
{
    ...

    std::string shader_source = read_shader_file("glfwtest.vert");

    const char *shader_source_ptr = shader_source.c_str();
    glShaderSource(vertexShader, 1, & shader_source_ptr, NULL);
    ...
}

C++17

我正在查看我的一些旧答案,并意识到我已经更新了我自己的 glShader::load 代码,我以此答案为基础。首先,我们现在有一个 可移植 路径:std::filesystem::path,允许 Windows 上的 UTF-16 (?) 文件名编码(例如 wchar_t 字符串)例如。

其次,GLSL使用UTF-8方案。因此,没有理由std::wifstream。从 C++17 开始,std::ifstream 接受 (std::filesystem::path)(...path::value_type *) 作为构造函数,或 open 方法。参见:path

通过为 ifstream 启用异常,我们可以希望捕获 std::ios_base::failure 异常,如果我们只需要诊断 what() 消息,它会继承 std::runtime_error。否则 code() 可能会提供更多信息 - 规范没有说明诊断信息的信息量...

无论如何,这是将 GLSL 文件读取到 std::string 的惯用方法:

#include <filesystem>
#include <limits>

#include <fstream>
#include <iterator>

static std::string read_shader_file
(
    const std::filesystem::path::value_type *shader_file)
{
    std::ifstream ifs;

    auto ex = ifs.exceptions();
    ex |= std::ios_base::badbit | std::ios_base::failbit;
    ifs.exceptions(ex);

    ifs.open(shader_file);
    ifs.ignore(std::numeric_limits<std::streamsize>::max());
    auto size = ifs.gcount();

    if (size > 0x10000) // 64KiB sanity check:
        return false;

    ifs.clear();
    ifs.seekg(0, std::ios_base::beg);

    return std::string {std::istreambuf_iterator<char> {ifs}, {}});
}

如果发生异常,资源将被展开并且(可能)抛出 std::ios_base::failure 异常。否则,我们有一个 std::string,我们现在可以使用 c_str() 方法将着色器源传递给 OpenGL。

@derhass 已在评论中诊断出问题,但由于 OP 在实际修复代码时仍然存在问题:

在将字符串传递给 glShaderSource() 之前,您需要确保该字符串是 null-terminated。如果你不这样做,字符串末尾将有一个未定义的字符,几乎任何事情都可能发生:

  • 如果角色恰好是 '[=12=]'.
  • 就可以了
  • 它可能会给出基于无效字符的语法错误。
  • 如果在用尽可访问内存之前没有找到空字符,它可能会崩溃。

要通过对代码进行最少的更改来修复它,请使用:

std::ifstream VSS("glfwtest.vert", std::ifstream::ate);
int len = VSS.tellg();
GLchar* vertexSource = new GLchar[len + 1];
VSS.clear();
VSS.seekg(0, VSS.beg);
for(int i=0; i<len ; i++) { VSS.get(vertexSource[i]); }
vertexSource[len] = '[=10=]';

片段着色器也一样。