如何实际地将GLSL着色器与您的C++软件一起发布

68

在OpenGL初始化期间,程序应该执行类似以下的操作:

<Get Shader Source Code>
<Create Shader>
<Attach Source Code To Shader>
<Compile Shader>

获取源代码可能就像将其放入字符串中一样简单: (示例来自SuperBible,第6版)

static const char * vs_source[] =
{
    "#version 420 core                             \n"
    "                                              \n"
    "void main(void)                               \n"
    "{                                             \n"
    "    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);   \n"
    "}                                             \n"
};

问题在于直接在字符串中编辑、调试和维护GLSL着色器很困难。因此,从文件中获取源代码的字符串对于开发来说更加容易:

std::ifstream vertexShaderFile("vertex.glsl");
std::ostringstream vertexBuffer;
vertexBuffer << vertexShaderFile.rdbuf();
std::string vertexBufferStr = vertexBuffer.str();
// Warning: safe only until vertexBufferStr is destroyed or modified
const GLchar *vertexSource = vertexBufferStr.c_str();

现在的问题是如何将着色器与程序一起传输?事实上,将源代码与应用程序一起传输可能会成为问题。 OpenGL支持“预编译二进制着色器”,但是Open Wiki指出:

程序二进制格式不应被传输。 不合理的是期望不同的硬件供应商接受相同的二进制格式。 不合理的是期望来自同一供应商的不同硬件接受相同的二进制格式。 [...]

如何实际地将GLSL着色器与C++软件一起传输?


2
我个人会将源文件保存在着色器目录中,并根据需要将它们加载到程序中的字符串中。 - Tim Seguine
3
它们通常存储在单独的文件中。如果你不希望用户玩弄它们,虚拟文件系统是一个不错的选择。 - Bartek Banachewicz
8
只需将着色器作为文件发送即可。这是每个应用程序的做法。甚至大多数AAA游戏都有它们的着色器以某种方式作为直接可用的文件。 - datenwolf
2
即使您将其捆绑在可执行文件中,它仍然是纯文本,并且很容易被发现。 - n0rd
11个回答

72

使用C++11,您还可以使用原始字符串字面值的新功能。将此源代码放入名为shader.vs的单独文件中:

使用C++11,您还可以使用原始字符串字面值的新功能。将此源代码放入名为shader.vs的单独文件中:

R"(
#version 420 core

void main(void)
{
    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)"

然后像这样将其导入为字符串:

const std::string vs_source =
#include "shader.vs"
;

优点是易于维护和调试,并且在OpenGL着色器编译器出现错误时可以得到正确的行号。而且,您仍然不需要发送单独的着色器。

我唯一看到的缺点是文件顶部和底部添加的行(R"))")以及将字符串放入C++代码中的语法有点奇怪。


2
这是一个有趣的想法。你可能可以配置文本编辑器的语法高亮来忽略着色器文件中的原始字符串字面量符号。然后它确实不比包含保护差。 - Tim Seguine
22
使用 R""()"" 可以获得语法高亮。这是一个有效的原始字符串。 - Kenji
@Kenji 你的想法是语法高亮比编译器“笨”,而 R""( 可以愚弄语法高亮让其认为字符串已经结束,但实际上编译器知道它还没有结束吗?在我的 VS2013 中无效 - 语法高亮知道字符串还没有结束。 - Karu
1
@Karu,你说得对。它只适用于“笨拙”的高亮器(尽管在glsl中不存在原始字符串,所以实际上,它应该适用于任何glsl高亮器)。好观点。我仍在寻找更一致的解决方案来解决这个问题。我希望像 R"(#include shader.glsl)" 这样的东西是可能的,但当然,原始字符串甚至忽略编译器指令。但坦白地说,这只是一个小问题,我还有更重要的事情要做,比如完成我的项目! - Kenji
1
在Visual Studio 2017中,这会导致智能感知错误。但是如果你这样做: inline constexpr char vs_source = #include "shader.vs" ""; (加上额外的双引号),错误就消失了。 - Elad Maimoni
这是使用 include 的最糟糕的方式。 - Никита Самоуков

55

只有两种选择,一是“将它们直接存储在可执行文件中”,二是“将它们存储在(一个)单独的文件中”,中间没有其他选择。如果想要一个自包含的可执行文件,把它们放入二进制文件中是个好主意。请注意,您可以将它们添加为资源或调整构建系统以将着色器字符串从单独的开发文件中嵌入源文件以便更容易开发(并在开发版本中可能能够直接加载单独的文件)。

您认为发布着色器源代码会有问题吗?在GL中根本没有其他方法。预编译二进制文件仅对于在目标计算机上缓存编译结果有用。随着GPU技术的快速发展,不同的GPU架构和完全不兼容的ISA的不同供应商,预编译着色器二进制根本没有意义。

请注意,即使加密,将您的着色器源代码放入可执行文件中也不能“保护”它们。用户仍然可以钩入GL库并拦截您指定给GL的源代码。而且那里的GL调试器正是这样做的。

更新2016年

在SIGGRAPH 2016上,OpenGL架构审查委员会发布了GL_ARB_gl_spirv扩展。这将允许GL实现使用SPIRV二进制中间语言。这具有一些潜在的好处:

  1. 着色器可以离线预“编译”(最终针对目标GPU的编译仍然由驱动程序完成)。您无需发布着色器源代码,而只需发布二进制中间表示。
  2. 有一个标准的编译器前端(glslang),它进行解析,因此可以消除不同实现的解析器之间的差异。
  3. 可以添加更多的着色器语言,而无需更改GL实现。
  4. 它在某种程度上增加了到vulkan的可移植性。

这个方案使得GL在这方面更加类似于D3D和Vulkan。但是它并没有改变整体的情况。SPIRV字节码仍然可以被拦截、反汇编和反编译。这确实让逆向工程变得更困难一些,但实际上并没有太多变化。在着色器中,通常不能承受大量的混淆措施,因为这会极大地降低性能 - 这与着色器的目的相违背。

此外,请注意,这个扩展现在(2016年秋季)还没有得到广泛的支持。而且苹果在4.1之后停止了对GL的支持,所以这个扩展可能永远不会出现在OSX上。

2017年小更新

GL_ARB_gl_spirv 现在是 OpenGL 4.6 的官方核心功能,我们可以预期该功能越来越受欢迎,但它并没有太大的变化。


4
我们只需要软件足够安全,以使“老实人保持老实”;) 没有其他要求。因此,某种加密实际上是无用的。感谢您的回答。 - Korchkidu
1
由于SPIRV是一种中间语言,如果某人或公司公然窃取了您的知识产权,并且您能够证明它只能作为SPIRV进行交付,则可以采取法律行动。 - user8991265

25

OpenGL支持预编译的二进制文件,但不具备可移植性。与HLSL不同,后者由微软的编译器编译成标准的字节码格式,然后由驱动程序将其翻译为GPU的本地指令集。 OpenGL没有这样的格式。你不能将预编译的二进制文件用于除加快加载时间以外的任何更多操作,即使是这样,也不能保证编译的二进制文件会在驱动程序版本更改甚至机器上的实际GPU更改时正常工作。

如果你真的很担心,你可以隐蔽你的着色器。问题是,除非你正在做一些真正独特的事情,否则没有人会关心你的着色器,我是真的这么说的。这个行业靠开放发展,在业内所有大牌都会在GDC、SIGGRAPH等会议上讨论最新和最有趣的技术。实际上,着色器是如此的实现特定,以至于从逆向工程中得到的信息通常并不比通过参加上述会议来得到的信息更多。

如果你关心的是别人修改你的软件,那么我建议你实施一个简单的哈希测试或校验和测试。许多游戏已经这样做来防止作弊,你想要走多远就看你自己了。但底线是,OpenGL的二进制着色器旨在减少着色器编译时间,而不是用于可移植的重新分发。


14

我的建议是将着色器的整合作为您二进制构建过程的一部分。我在我的代码中使用CMake扫描文件夹以查找着色器源文件,然后生成一个包含所有可用着色器枚举的头文件:

#pragma once
enum ShaderResource {
    LIT_VS,
    LIT_FS,
    // ... 
    NO_SHADER
};

const std::string & getShaderPath(ShaderResource shader);

同样地,CMake会创建一个CPP文件,该文件可以根据资源返回着色器的文件路径。

const string & getShaderPath(ShaderResource res) {
  static map<ShaderResource, string> fileMap;
  static bool init = true;
  if (init) {
   init = false;
   fileMap[LIT_VS] =
    "C:/Users/bdavis/Git/OculusRiftExamples/source/common/Lit.vs";
   // ...
  }
  return fileMap[res];
}

让 CMake 脚本在发布版本中不提供文件路径,而是提供着色器的源代码,并将着色器内容存储在 cpp 文件中(或者在 Windows 或 Apple 目标的情况下,将它们作为可执行资源/可执行捆绑包的一部分)。虽然需要进行调试,但是如果不将其内置到可执行文件中,则可以轻松修改着色器。实际上,我的 GLSL 程序获取代码会查看着色器的编译时间与源文件的修改时间戳,如果文件自上次编译以来发生了更改,则会重新加载着色器(这仍处于起步阶段,因为这意味着您会失去之前绑定到该着色器的任何 uniform,但我正在努力解决这个问题)。

这实际上不是一个着色器问题,而是一个通用的“非 C++ 资源”问题。对于您想要加载和处理的所有内容,都存在相同的问题...纹理图像、声音文件、关卡等。


谢谢。CMake解决方案确实很有趣。 - Korchkidu

10

作为将GLSL着色器直接保存在字符串中的替代方案,我建议考虑一下我正在开发的这个库:ShaderBoiler (遵循Apache-2.0许可证)。

它目前处于alpha版本,并且具有一些可能会限制使用的限制。

主要概念是编写类似于GLSL代码的C++结构来构建计算图,从而生成最终的GLSL代码。

例如,让我们看一下以下C++代码:

#include <shaderboiler.h>
#include <iostream>

void main()
{
    using namespace sb;

    context ctx;
    vec3 AlbedoColor           = ctx.uniform<vec3>("AlbedoColor");
    vec3 AmbientLightColor     = ctx.uniform<vec3>("AmbientLightColor");
    vec3 DirectLightColor      = ctx.uniform<vec3>("DirectLightColor");
    vec3 LightPosition         = ctx.uniform<vec3>("LightPosition");

    vec3 normal   = ctx.in<vec3>("normal");
    vec3 position = ctx.in<vec3>("position");
    vec4& color   = ctx.out<vec4>("color");

    vec3 normalized_normal = normalize(normal);

    vec3 fragmentToLight = LightPosition - position;

    Float squaredDistance = dot(fragmentToLight, fragmentToLight);

    vec3 normalized_fragmentToLight = fragmentToLight / sqrt(squaredDistance);

    Float NdotL = dot(normal, normalized_fragmentToLight);

    vec3 DiffuseTerm = max(NdotL, 0.0) * DirectLightColor / squaredDistance;

    color = vec4(AlbedoColor * (AmbientLightColor + DiffuseTerm), 1.0);

    std::cout << ctx.genShader();
}

控制台的输出为:

uniform vec3 AlbedoColor;
uniform vec3 AmbientLightColor;
uniform vec3 LightPosition;
uniform vec3 DirectLightColor;

in vec3 normal;
in vec3 position;

out vec4 color;

void main(void)
{
        vec3 sb_b = LightPosition - position;
        float sb_a = dot(sb_b, sb_b);
        color = vec4(AlbedoColor * (AmbientLightColor + max(dot(normal, sb_b / sqrt(sb_a)), 0.0000000) * DirectLightColor / sb_a), 1.000000);
}

使用GLSL代码创建的字符串可以与OpenGL API一起使用以创建着色器。


6
问题在于直接在字符串中编辑、调试和维护GLSL着色器很困难。
奇怪的是,到目前为止所有“答案”都完全忽略了这句话,而那些答案的反复主题是:“你无法解决问题;只能处理它。”
使它们更易于编辑,同时直接从字符串中加载它们的答案很简单。考虑以下字符串字面量:
    const char* gonFrag1 = R"(#version 330
// Shader code goes here
// and newlines are fine, too!)";

就内容而言,其他评论都是正确的。确实,正如他们所说,最好的安全措施是隐蔽性,因为GL可以被拦截。但是为了让诚实的人保持诚实,并在意外程序损坏的情况下阻止某些操作,您可以在C ++中执行上述操作,并轻松维护您的代码。

当然,如果您确实想要保护世界上最具革命性的着色器不受盗窃,那么隐蔽性可以被采取到非常有效的极端程度。但这是另一个线程的另一个问题。


你的解决方案在我看来仍然难以编辑和维护。实际上,它并没有增加太多价值... - Korchkidu
5
我必须说,从一个程序员的角度来看,这令我惊讶。你真的认为用实际的换行符比起手动添加新行要简单八倍吗?别开玩笑了!不过,我会接受挑战并尝试添加更多内容。也许只是在开发阶段,你可以将着色器保存在文件中,以保持不同语言的分离,并增加语法高亮的可能性,而无需在IDE中进行太多调整。只有在开发结束时才转移到C++中。使用我提供的字符串文字语法,您可以在那个阶段轻松地复制和粘贴。我真的很难想象这不是解决维护问题的方法! - Thomas Poole
不,这与问题无关。使用资源或脚本实际上从文件到文件传输源代码比您的解决方案更好。 - Korchkidu
1
我认为我在这里的“答案”最大的问题实际上是它应该是一条评论。我很高兴有些人发现我的贡献对他们的应用程序有用,但我可以看出这不是对你关注的问题的回答。如果我没记错的话,我当时还没有评论的权限。 - Thomas Poole
@ Thomas Poole,我觉得你的“答案”非常棒。 - durduvakis

3

2
一个建议: 在你的程序中,将着色器放在:
const char shader_code = {
#include "shader_code.data"
, 0x00};

在shader_code.data中,应该有着以十六进制数字列表形式,用逗号分隔的着色器源代码。在编译之前,应该使用通常在文件中编写的着色器代码创建这些文件。在Linux中,我会在Makefile中放置指令来运行代码:
cat shader_code.glsl | xxd -i > shader_code.data

我曾经做过类似的事情,将字符串编码为32位整数数组,以隐藏可以被打印出来的源代码中的签名。只需注意Windows和Linux之间的字节序差异即可。 - KANJICODER
有没有像xxd -i这样的程序,可以生成一个包含转义C字符串文字的文件,而不是字节数组? - Justin Meiners

1
另一种存储glsl文本文件或预编译glsl文件的替代方案是着色器生成器,它以着色树为输入并输出glsl(或hlsl,...)代码,然后在运行时进行编译和链接...采用这种方法,您可以更轻松地适应gfx硬件的任何功能。如果您有足够的时间,还可以支持hlsl,无需使用cg着色语言。如果您深入思考glsl / hlsl,就会发现将着色树转换为源代码是语言设计师的初衷。

1
我不知道这是否有效,但您可以使用类似g2bin的binutils程序将.vs文件嵌入可执行文件中,并将着色器程序声明为外部资源,然后像访问常规内嵌在可执行文件中的资源一样访问它们。请参见Qt中的qrc,或者您可以查看我的一个用于嵌入可执行文件中的东西的小程序https://github.com/heatblazer/binutil,该程序作为IDE的预构建命令调用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接