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

Kor*_*idu 46 c++ opengl glsl

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

<Get Shader Source Code>
<Create Shader>
<Attach Source Code To Shader>
<Compile Shader>
Run Code Online (Sandbox Code Playgroud)

获取源代码可能就像将它放在一个字符串中一样简单:(示例摘自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"
};
Run Code Online (Sandbox Code Playgroud)

问题是很难直接在字符串中编辑,调试和维护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();
Run Code Online (Sandbox Code Playgroud)

现在的问题是如何使用您的程序发送着色器?实际上,使用您的应用程序运送源代码可能是个问题.OpenGL支持"预编译的二进制着色器",但Open Wiki声明:

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

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

Jan*_*egg 46

使用c ++ 11,您还可以使用原始字符串文字的新功能.将此源代码放在一个名为的单独文件中shader.vs:

R"(
#version 420 core

void main(void)
{
    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)"
Run Code Online (Sandbox Code Playgroud)

然后将其作为字符串导入,如下所示:

const std::string vs_source =
#include "shader.vs"
;
Run Code Online (Sandbox Code Playgroud)

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

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

  • 使用`R""(`和`)""`来突出显示.这是一个有效的原始字符串. (10认同)
  • 这是一个有趣的想法。您可能会配置文本编辑器的语法突出显示,以忽略着色器文件中的原始字符串文字表示法。那么,这实际上并不比包括后卫还要糟糕。 (2认同)

der*_*ass 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的便携性.

通过该方案,GL在这方面变得与D3D和Vulkan更相似.但是,它并没有改变更大的图景.SPIRV字节码仍然可以被拦截,反汇编和反编译.它确实使逆向工程变得更难,但实际上并没有太多.在着色器中,您通常无法承受大量的混淆措施,因为这会大大降低性能 - 这与着色器的效果相反.

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

MINOR UPDATE 2017

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

  • 我们只需要我们的软件足够安全,以保持"诚实的人诚实";)没有更多.所以某种加密实际上是无用的.感谢您的回答. (2认同)

And*_*man 19

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

如果你真的是偏执狂,你总是可以模糊你的着色器.问题是,除非你正在做一些真正独一无二的事情,否则没有人会关心你的着色器,我的意思是真的.这个行业在开放性方面茁壮成长,业内所有大型企业都定期在GDC,SIGGRAPH等会议上讨论最新,最有趣的技术.事实上,着色器是如此特定于实现,通常没有太多你可以做的通过收听其中一个会议来反向设计你不能做的事情.

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


Jhe*_*ico 12

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

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

const std::string & getShaderPath(ShaderResource shader);
Run Code Online (Sandbox Code Playgroud)

类似地,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];
}
Run Code Online (Sandbox Code Playgroud)

使CMake脚本改变它的行为并不是太难(在这里花很多时间),以便在发布版本中而不是提供它提供着色器源的文件路径,并在cpp文件中存储内容着色器本身(或者在Windows或Apple目标的情况下使它们成为可执行资源/可执行包的一部分).

这种方法的优点是,如果它们没有被烘焙到可执行文件中,那么在调试期间动态修改着色器要容易得多.事实上,我的GLSL程序获取代码实际上是查看着色器的编译时间与源文件的修改时间戳,并且如果文件自上次编译后已经更改,则将重新加载着色器(这仍处于初期阶段,因为这意味着你丢失了以前绑定到着色器的任何制服,但我正在努力).

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


Tho*_*ole 6

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

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

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

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

就它们而言,所有其他评论都是正确的。事实上,正如他们所说,可用的最佳安全性是默默无闻,因为 GL 可以被拦截。但是为了让诚实的人保持诚实,并防止意外损坏程序,您可以在 C++ 中执行上述操作,并且仍然可以轻松维护您的代码。

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

  • 好吧,我必须说,来自一个令我惊讶的程序员。你真的认为新行的实际换行不是容易 8 倍吗?下车!不过,我将迎接添加更多内容的挑战。也许_just_在开发过程中,您可以将着色器保存在文件中,以将不同的语言分开并使语法突出显示更有可能,而无需在 IDE 中进行过多的修改。仅在开发结束时转移到 C++。使用我提供的字符串文字语法,您可以在该阶段复制和粘贴。我真的很难想象这不是维护问题的解决方案! (5认同)

Pod*_*kiy 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();
}
Run Code Online (Sandbox Code Playgroud)

控制台的输出将是:

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);
}
Run Code Online (Sandbox Code Playgroud)

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