如何用C ++软件实际运送GLSL着色器

在OpenGL初始化过程中,程序应该执行如下操作:

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

获取源代码可以像把它放在一个string一样简单:(例子取自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" }; 

问题在于很难直接在string中编辑,debugging和维护GLSL着色器。 因此,从文件中获取源代码string更容易进行开发:

 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指出:

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

如何用C ++软件实际运送GLSL着色器?

只是“将它们直接存储在可执行文件中”或“将它们存储在(a)单独的文件中”,而没有任何介入。 如果你想要一个自包含的可执行文件,把它们放入二进制文件是个好主意。 请注意,您可以将它们添加为资源或调整您的构build系统,以将单独的开发文件中的着色器stringembedded到源文件中,从而使开发更容易(可以直接将独立文件加载到开发版本中)。

为什么你认为运送着色器的来源将是一个问题? GL中根本没有别的办法。 预编译的二进制文件仅用于在目标机器上caching编译结果。 随着GPU技术的快速发展,GPU体系结构的改变以及不同供应商的ISAs的完全不兼容,预编译的着色器二进制文件根本就没有意义。

请注意,将着色器源设置为可执行不会“保护”它们,即使您对它们进行了encryption。 用户仍然可以挂钩到GL库,并截取您指定给GL的来源。 那里的GLdebugging器就是这么做的。

更新2016年

在SIGGRAPH 2016上,OpenGL Architecture Review Board发布了GL_ARB_gl_spirv扩展。 这将允许GL实现使用SPIRV二进制中间语言。 这有一些潜在的好处:

  1. 着色器可以预先“编译”为脱机状态(目标GPU的最终编译过程仍然由驱动程序发生)。 您不必运送着色器源代码,而仅需要二进制中间表示。
  2. 有一个标准的编译器前端( glslang )进行parsing,因此可以消除不同实现的parsing器之间的差异。
  3. 可以添加更多着色器语言,而无需更改GL实现。
  4. 它有点增加便携性vulcan。

通过这个scheme,GL在这方面变得更加类似于D3D和Vulkan。 但是,这并没有改变更大的情况。 SPIRV字节码仍然可以被拦截,反汇编和反编译。 它确实使逆向工程有点困难,但实际上并没有太多。 在着色器中,您通常无法承受广泛的混淆措施,因为这会显着降低性能 – 这与着色器的效果相反。

另外请记住,这个扩展现在不是广泛可用(2016年秋季)。 苹果公司已经停止支持4.1之后的GL,所以这个扩展可能永远不会到OSX。

less数更新2017年

GL_ARB_gl_spirv现在是OpenGL 4.6的核心function,所以我们可以期待这个function的使用率不断提高,但是它并没有改变更大的图片。

使用c ++ 11,您也可以使用原始string文字的新function。 把这个源代码放在一个名为shader.vs的单独文件中:

 R"( #version 420 core void main(void) { gl_Position = vec4(0.0, 0.0, 0.0, 1.0); } )" 

然后像下面这样将其导入为一个string:

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

优点是易于维护和debugging,并且在OpenGL着色器编译器出现错误的情况下可以得到正确的行数。 而且你还不需要运送单独的着色器。

我能看到的唯一缺点是在文件( R"))"的顶部和底部添加的行,以及将string转换为C ++代码有点奇怪的语法。

OpenGL支持预编译的二进制文件,但不支持移植。 与HLSL不同,HLSL被微软的编译器编译成标准的bytcode格式,后来被驱动程序转换成GPU的原生指令集,OpenGL没有这样的格式。 你不能在一台机器上使用预编译的二进制文件来caching已编译的GLSL着色器,以加快加载时间,即使如此,也不能保证编译后的二进制文件在驱动程序版本改变的情况下工作。机器上实际的GPU改变。

如果你真的偏执,你总是可以混淆你的着色器。 事情是,除非你正在做一件真正的事情,否则没有人会关心你的着色器,而我的意思是真正的。 这个行业是开放的,所有的行业巨头都会定期讨论GDC,SIGGRAPH等会议上的最新和最有趣的技术。实际上,着色器是特定于实现的,所以通常没有太多可以做的逆向工程,你不能只听一个会议。

如果你关心的是修改软件的人,那么我build议你实施一个简单的哈希或校验和testing。 许多游戏已经做到这一点,以防止作弊,你想要多远,取决于你。 但底线是OpenGL中的二进制着色器是为了减less着色器的编译时间,而不是为了便携式的重新分配。

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

 #pragma once enum ShaderResource { LIT_VS, LIT_FS, // ... NO_SHADER }; const std::string & getShaderPath(ShaderResource shader); 

同样,CMake创build一个CPP文件,给定一个资源,将文件path返回给着色器。

 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脚本中改变它的行为不会太困难(这里有太多的handwake),所以在发布版本中,不是提供文件path,而是提供了着色器的源代码,并且在cpp文件中存储了着色器本身(或者在Windows或Apple目标的情况下使它们成为可执行资源/可执行包的一部分)。

这种方法的优点是,在debugging过程中,如果不将它们烧写到可执行文件中,则更容易修改着色器。 实际上,我的GLSL程序获取代码实际上是查看着色器的编译时间与源文件的修改时间戳,如果自上次编译以来文件发生了变化,将重新加载着色器(这仍然处于起步阶段,因为这意味着你失去了以前与着色器绑定的任何制服,但是我正在努力)。

这是一个比通用的“非C ++资源”问题更less的着色器问题。 同样的问题,你可能想要加载和处理的所有东西都存在…纹理,声音文件,关卡的图像,你有什么。

问题在于很难直接在string中编辑,debugging和维护GLSL着色器。

这个句子到目前为止都被“答案”完全忽略了,这个答案反复出现的主题是“你解决不了问题,只是处理了”。

让他们更容易编辑,而直接从string中加载它们的答案很简单。 考虑下面的string:

  const char* gonFrag1 = R"(#version 330 // Shader code goes here // and newlines are fine, too!)"; 

所有其他的评论是正确的。 实际上,正如他们所说的那样,现有的最好的安全措施是隐晦的,因为GL可以被拦截。 但为了保持诚实的人诚实,并阻止意外的程序损坏的方式,你可以做如上的C ++,仍然很容易维护你的代码。

当然,如果你想保护世界上最革命性的着色器免于被盗,那么默默无闻就可以采取相当有效的极端措施。 但这是另一个线程的另一个问题。

作为将GLSL着色器直接保存为string的替代方法,我build议考虑我正在开发的这个库: ShaderBoiler (Apache-2.0)。

它是在alpha版本,并有一些限制,可能会限制它的使用。

主要的概念是编写类似于GLSL代码的C ++结构,这将构build一个计算图,从中生成最终的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代码创build的string可以与OpenGL API一起使用来创build着色器。

如果您不想将它们分开,也可以使用预处理器指令将多个着色器源组合成一个文件(或string)。 这也可以让你避免重复(例如共享声明) – 编译器大部分时间都会优化未使用的variables。

http://www.gamedev.net/topic/651404-shaders-glsl-in-one-file-is-it-practical/

我不知道这是否会起作用,但是可以使用binutils(比如像g2bin这样的程序)将.vs文件embedded到可执行文件中,并且可以将着色器程序声明为外部程序,然后将其作为embedded在可执行文件中的普通资源来访问它们。 在Qt中查看qrc,或者你可以在这里查看我的小程序,用于在可执行文件中embedded东西: https : //github.com/heatblazer/binutil ,它被作为IDE的预生成命令调用。

一条build议:

在你的程序中,把着色器放在:

 const char shader_code = { #include "shader_code.data" , 0x00}; 

在shader_code.data中,应该有着色器源代码作为由逗号分隔的hex数字列表。 这些文件应该在编译前使用通常在文件中写入的着色器代码来创build。 在Linux中,我将在Makefile中放置指令来运行代码:

 cat shader_code.glsl | xxd -i > shader_code.data 

存储glsl文本文件或预编译的glsl文件的另一种方法是着色器生成器,它将阴影树作为input,并输出glsl(或hlsl,…)代码,然后在运行时编译并链接…按照此方法您可以更轻松地适应gfx硬件的任何function。 如果你有很多时间,你也可以支持hlsl,不需要cg着色语言。 如果你深入思考glsl / hlsl,你会发现,将阴影树转化为源代码是在语言devise师的思想背后。