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

时间:2013-12-07 16:17:24

标签: c++ opengl glsl

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

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

获取源代码可能就像将其放入字符串一样简单: (摘自SuperBible, 6th Edition

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着色器?

11 个答案:

答案 0 :(得分:41)

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

为什么您认为出货着色器来源会有问题? 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的便携性。
  5. 通过该计划,GL在这方面与D3D和Vulkan变得更加相似。但是,它并没有改变更大的图景。 SPIRV字节码仍然可以被拦截,反汇编和反编译。它确实使逆向工程变得更难,但实际上并没有那么多。在着色器中,您通常无法承受大量的混淆措施,因为这会大大降低性能 - 这与着色器的效果相反。

    另请注意,此扩展程序目前尚未广泛使用(2016年秋季)。并且Apple在4.1之后已经停止支持GL,因此这个扩展可能永远不会来到OSX。

    MINOR UPDATE 2017

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

答案 1 :(得分:40)

使用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 :(得分:18)

OpenGL支持预编译的二进制文件,但不支持。与HLSL不同,HLSL由Microsoft的编译器编译为标准的代码格式,后来 翻译 编译为驱动程序的GPU本机指令集,而OpenGL没有这样的格式。您不能将预编译的二进制文件用于在单个机器上缓存已编译的GLSL着色器以加快加载时间,即使这样,如果驱动程序版本发生更改,也无法保证已编译的二进制文件能够正常工作...更不用说了机器上的实际GPU会发生变化。

如果您真的偏执,您可以随时对着色器进行模糊处理。问题是,除非你正在做一些真正独一无二的事情,否则没有人会关心你的着色器,我的意思是真的。这个行业在开放性方面茁壮成长,业内所有大型企业都定期在GDC,SIGGRAPH等会议上讨论最新,最有趣的技术。事实上,着色器是如此特定于实现,通常没有太多你可以做的通过聆听其中一个会议来对他们进行逆向工程。

如果您关注的是人们修改您的软件,那么我建议您实施一个简单的哈希或校验和测试。许多游戏已经这样做以防止作弊,你想要取多远取决于你。但最重要的是,OpenGL中的二进制着色器旨在减少着色器编译时间,而不是便携式重新分发。

答案 3 :(得分:11)

我的建议是在构建过程中将着色器合并到二进制文件中。我在我的代码中使用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 ++资源”问题相比,这实际上不是一个着色器问题。您可能想要加载和处理的所有内容都存在同样的问题...图像纹理,声音文件,关卡,你有什么。

答案 4 :(得分:6)

作为将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一起使用来创建着色器。

答案 5 :(得分:4)

  

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

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

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

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

所有其他评论都是正确的。事实上,正如他们所说,最好的安全措施是默默无闻,因为GL可以被截获。但是为了保持诚实的人诚实,并在意外程序损坏的方式上设置一些障碍,你可以在C ++中完成上述操作,并且仍然可以轻松维护你的代码。

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

答案 6 :(得分:1)

如果您不希望将多个着色器源分开,则还可以使用预处理器指令将多个着色器源合并为一个文件(或字符串)。这也可以避免重复(例如共享声明) - 大多数时候编译器会优化未使用的变量。

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

答案 7 :(得分:1)

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

答案 8 :(得分:1)

建议:

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

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

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

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

答案 9 :(得分:1)

存储glsl文本文件或预编译glsl文件的另一种替代方法是着色器生成器,它将树荫树作为输入并输出glsl(或hlsl,...)代码,然后在运行时编译和链接...按照这种方法,您可以更轻松地适应gfx硬件具有的任何功能。你也可以支持hlsl,如果你有很多时间,不需要cg着色语言。如果你仔细考虑glsl / hlsl,你会看到,将树荫转化为源代码是语言设计师心中的背后。

答案 10 :(得分:-1)

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

## 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&index=115

如果你更喜欢工作代码......这是我的游戏引擎构建,它使用了这个 系统。打开AAC2020.SH构建脚本,找到“awk”命令并工作 从那里向后。

https://github.com/KanjiCoder/AAC2020

与手头问题特别相关的其他文件是:

  1. P5D1OGL.FRA._ <-- 可以作为 GLSL 运行和单元测试的 C99 源代码
  2. P5D1OGL.FRA.STRING._ <-- P5D1OGL.FRA._ 由 awk 命令引用。
  3. P5D_001._ <-- P5D1OGL.FRA.STRING._ #included into const char* 以嵌入到 exe 中
  4. POLYOGL.D._ <-- OpenGL Polyfills .D(数据/结构)
  5. POLYOGL.F._ <-- OpenGL Polyfills .F(函数)

或者,如果您收听我的 twitch 视频流并请求面对面的概述 关于我如何做到这一点,我可以给你一个现场演示和进一步的解释。

www.twitch.com/kanjicoder

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

-约翰马克