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

How to Practically Ship GLSL Shaders with your C++ Software

本文关键字:GLSL 何使用 C++ 软件      更新时间:2023-10-16

在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声明:

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

如何使用C++软件实际交付GLSL着色器

使用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++代码的语法有点奇怪。

只有"将它们直接存储在可执行文件中"或"将它们存储在(a)个单独的文件中",两者之间没有任何内容。如果您想要一个自包含的可执行文件,将它们放入二进制文件中是个好主意。请注意,您可以将它们作为资源添加,也可以调整构建系统,将单独开发文件中的着色器字符串嵌入到源文件中,以使开发更容易(还可以在开发构建中直接加载单独的文件)。

为什么你认为运送着色器源会是个问题?GL中根本没有其他方法。预编译的二进制文件只适用于在目标计算机上缓存编译结果。随着GPU技术的快速进步,GPU体系结构的不断变化,以及不同的供应商使用完全不兼容的ISAs,预编译的着色器二进制文件根本没有意义。

请注意,将着色器源置于可执行状态并不能"保护"它们,即使对它们进行加密也是如此。用户仍然可以挂接到GL库并截取您指定给GL的源。而GL调试器正是这样做的。

更新2016

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

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

通过该方案,GL在这方面与D3D和Vulkan越来越相似。然而,这并不能改变大局。SPIRV字节码仍然可以被拦截、反汇编和反编译。它确实让逆向工程变得有点困难,但实际上并没有多大困难。在着色器中,通常无法承担大量的混淆措施,因为这会显著降低性能,这与着色器的用途相反。

请记住,目前(2016年秋季)还没有广泛提供此延期。苹果在4.1之后已经停止支持GL,所以这个扩展可能永远不会出现在OSX上。

2017年最小更新

GL_ARB_gl_spirv现在是OpenGL 4.6的官方核心功能,因此我们可以预期该功能的采用率会越来越高,但它并没有改变大局。

OpenGL支持预编译的二进制文件,但不支持移植。HLSL是由微软的编译器编译成标准bytcode格式的,后来由驱动程序将翻译成GPU的本机指令集,与此不同,OpenGL没有这样的格式。除了在一台机器上缓存已编译的GLSL着色器以加快加载时间之外,您不能将预编译的二进制文件用于任何其他用途,即使这样,如果驱动程序版本发生更改,也不能保证已编译的二进制文件能够工作。。。更不用说机器上的实际GPU发生变化了。

如果真的是偏执狂,则始终可以模糊着色器。问题是,除非你正在做一些真正独一无二的事情,否则没有人会关心你的着色器,我是认真的。这个行业在开放性的基础上蓬勃发展,行业中的所有大公司都会定期在GDC、SIGGRAPH等会议上讨论最新、最有趣的技术。事实上,着色器是如此特定于实现,以至于你通常无法通过逆向工程来做很多事情,而仅仅听一次上述会议就无法做到这一点。

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

我的建议是将着色器合并到二进制文件中作为构建过程的一部分。我在代码中使用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程序获取代码实际上会查看着色器的编译时间与源文件的修改时间戳,如果文件自上次编译以来发生了更改,则会重新加载着色器(这仍处于初级阶段,因为这意味着你会丢失以前绑定到着色器的任何统一,但我正在处理这一问题)。

与一般的"非C++资源"问题相比,这实际上不是着色器问题。您可能想要加载和处理的所有内容都存在相同的问题。。。图像的纹理,声音文件,级别,你有什么。

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

它是阿尔法版本,有一些限制,可能会限制它的使用

主要概念是用类似于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一起使用来创建着色器。

问题是很难编辑、调试和维护GLSL着色器直接在字符串中。

奇怪的是,到目前为止,这句话被所有的"答案"完全忽略了,而这些答案反复出现的主题是,"你无法解决问题,只需处理它。"

让它们更容易编辑,同时直接从字符串加载,答案很简单。考虑以下字符串文字:

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

所有其他评论都是正确的。事实上,正如他们所说,最好的安全性是隐蔽性,因为GL可以被拦截。但是,为了让诚实的人保持诚实,并在程序意外损坏的过程中设置一些块,您可以在C++中执行上述操作,并且仍然可以轻松地维护您的代码。

当然,如果你确实想保护世界上最具革命性的着色器免受盗窃,那么默默无闻可能会达到相当有效的极端。但这是另一个问题。

如果不想将多个着色器源分开,也可以使用预处理器指令将它们组合到一个文件(或字符串)中。这也可以避免重复(例如,共享声明)——未使用的变量大部分时间都由编译器优化。

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

建议:

在程序中,将着色器放入:

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

在shader_code.data中,着色器源代码应该是用逗号分隔的十六进制数字列表。这些文件应该在编译之前使用通常在文件中编写的着色器代码创建。在Linux中,我会在Makefile中放入运行代码的指令:

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

我不知道这是否可行,但您可以使用类似于binutils的程序(如g2bin)将.vs文件嵌入到可执行文件中,并且您可以将着色器程序声明为外部程序,然后将其作为嵌入可执行文件的普通资源进行访问。请参阅Qt中的qrc,或者您可以在此处查看我在可执行文件中嵌入内容的小程序:https://github.com/heatblazer/binutil其被调用作为IDE的预构建命令。

存储glsl文本文件或预编译glsl文件的另一种选择是着色器生成器,它以阴影树作为输入并输出glsl(或hlsl,…)代码,然后在运行时对其进行编译和链接。。。遵循这种方法,您可以更容易地适应gfx硬件的任何功能。你也可以支持hlsl,如果你有很多时间,不需要cg着色语言。如果你深入思考glsl/hlsl,你会发现,将阴影树转换为源代码是语言设计者的想法。

在C99/C11中,您可以通过两个简单的步骤来完成

## Bash build script:
## STEP #1: Convert your [C99/C11] code to GLSL by quoting it:
    awk 'NF { print """$0"\""n""""}' GLSL_AS_C99.C11 > QUOTED.TXT
## STEP #2: Compile your project:
    gcc -x c -c MY_PROJECT_FILE.C11 -o object_file.o -std=c11 -m64
    gcc -o EXE.exe object_file.o
    rm object_file.o
    ./EXE.exe
    rm EXE.exe

是的。还有更多。你必须用一种通用的风格来编写你的C99也编译为GLSL。例如:

#ifdef THIS_IS_BEING_COMPILED_AS_OPEN_GL_SHADER_CODE
    #define I32 int
#endif
#ifdef THIS_IS_BEING_COMPILED_AS_C99_CODE
    #define I32 uint32_t
#endif

在C99中以这种方式编写的代码可以剪切并粘贴到GLSL着色器中代码没有问题。它还允许您对GLSL着色器代码进行单元测试。要包含AWK命令字符串化的代码,请执行以下操作:

//:AAC2020_PAINT5D_DEFAULT_001:==============================://
const char* AAC2020_PAINT5D_DEFAULT_001=( //:////////////////://
//://////////////////////////////////////////////////////////://
"#version 330 core                           n"//://////////://
"#define AAC2020_MACRO_THIS_IS_OPEN_GL (1)   n"//://////////://
//://////////////////////////////////////////////////////////://
//|Everything Below Is Cut+Pasted From       |////://////////://
//|The C99 File: P5D_OGL._                   |////://////////://
//://////////////////////////////////////////////////////////://
    #include "../QUOTED.TXT"
                                                
); //:///////////////////////////////////////////////////////://
//:==============================:AAC2020_PAINT5D_DEFAULT_001://

如果您熟悉C代码和bash脚本。这应该足以解释一下。但如果你需要更多的解释,我拍了一个30分钟的演示以及youtube上的解说视频。

https://www.youtube.com/watch?v=kQfSL4kv5k0&list=PLN4rUakF78aCdRxjMU8_JBGAKIrtt_7N5&索引=115

如果你更喜欢工作规范。。。这是我的游戏引擎版本,它使用系统打开AAC202.SH构建脚本;awk";指挥与工作从那里向后。

https://github.com/KanjiCoder/AAC2020

对当前问题特别感兴趣的其他文件有:

  1. P5D1OGL.FRA._<--可以作为GLSL运行并经过单元测试的C99源代码
  2. P5D1OGL.FRA.STRING._<--awk命令引用的P5D1OGL.FRA._
  3. P5D_001._<--P5D1OGL.FRA.STRING._#包含在常量字符*中以嵌入到exe中
  4. 多边形D._<--OpenGL Polyfills.D(数据/结构)
  5. POLOGL.F._<--OpenGL Polyfills.F(函数)

或者,如果你调到我的抽搐流,并要求一个面对面的概述关于我是如何做到这一点的,我可以给你一个现场演示和进一步的解释。

www.twitch.com/kanjicoder

你也可以给我发电子邮件:HeavyMetalCookies@Gmail.com我现在35岁,今年是2021年。如果我不回复这意味着我要么已经死了,要么太出名而无法回答,要么两者兼而有之。我将把它作为一个练习留给读者来弄清楚就是其中之一。

-John Mark