3

我在这里看到了有关此主题的堆栈溢出的许多问题和答案。从这些答案中,我想出了一个可能的解决方案,将 GLSL 属性绑定到用户定义的语义。我想获得一些关于它的意见和讨论,并检查它是否是一个有效的想法。

首先,假设我们有一些用户定义的语义列表:

enum VertexElementSemantic
{
  POSITION, NORMAL, AMBIENT, DIFFUSE, SPECULAR,
  TEX_COORD0, TEX_COORD1, TEX_COORD2, TEX_COORD3,
  INDICES
};

以及封装设置顶点属性指针所需数据的结构。

struct VertexElement
{
  unsigned int m_source;
  unsigned int m_offset;
  unsigned int m_stride;
}

现在,一些 RenderOperation 类将包含 VertexElementSemantics 到 VertexElements 的映射。VertexElement 的格式、大小以及是否归一化都可以由它的 Semantic 来确定。

为了设置这个指针,我们需要的最后一点信息是属性位置本身。这是我们想要将 VertexElementSemantic 绑定到特定位置的地方。

这个问题的第一个答案中,我们了解到我们可以像这样明确地声明每个属性的期望位置:

layout(location = 0) in vec3 position;

所以我们可以将我们的语义映射到这些硬编码的位置,但是我们需要在每个着色器中对这个位置进行硬编码。对这些位置的任何更改都需要我们检查和编辑每个着色器。

但是,这个值根本不必由着色器源提供。从这个问题的答案中,我们了解到我们可以在外部将#defines 添加到我们的着色器中,如下所示:

char *sources[2] = { "#define FOO\n", sourceFromFile };
glShaderSourceARB(shader, 2, sources, NULL);

使用它,我们可以构建一个字符串,为每个语义的所需位置#defines 变量。例如,我们可以构建一个字符串,最终将以下内容插入到每个着色器的开头:

#define POSITION_LOCATION 0
#define NORMAL_LOCATION 1
#define AMBIENT_LOCATION 2
...

回到明确声明我们的属性位置,我们现在应该能够这样声明它们:

layout(location = POSITION_LOCATION) in vec3 position;
layout(location = NORMAL_LOCATION) in vec3 normal;
layout(location = AMBIENT_LOCATION) in vec4 ambient;

此方法允许我们在代码中设置每个语义的所需属性位置。它还为着色器本身提供了一种语义绑定感觉。像这样的系统是朝着正确方向迈出的一步来解决为属性位置提供意义的问题吗?

4

1 回答 1

4

让我们考虑一下这个想法的后果。

我们可以构建一个字符串,为每个语义的所需位置#defines 变量。例如,我们可以构建一个字符串,最终将以下内容插入到每个着色器的开头:

嗯,这在两个方面都很糟糕。首先,有#version问题。如果您想使用1.10 以外的任何GLSL 版本,您必须提供#version声明。并且该声明必须在 shader 中的第一件事,在评论和空白之外。

通过将这些#defines 放入您的着色器源(无论是通过字符串连接,还是像您一样使用多个字符串),那么您必须接受某些后果。通常,每个单独的着色器文件都有自己的#version声明,指定它使用的 GLSL 版本。但是如果你想使用 GLSL 1.10 以外的东西,你就不能这样做。你必须让你的 C++ 源代码#version在你#define的 s之前生成 ,

这意味着您的着色器源现在与其编译的版本分离。这是可行的,但这意味着您的着色器源现在不清楚,不知道它是什么版本。您可以通过其他方式传达版本,例如使用文件名(例如,lit_transform_330.vert将使用版本 3.30)。但是你必须设计这样一个系统。

既然版本问题已经解决了,那么下一个问题:你所做的是多余的。

您使用诸如“语义”之类的术语,这对 OpenGL 没有任何意义。您似乎正在尝试将某种形式的名称分配给特定的顶点属性,以便您可以在着色器和 C++ 代码中看到该名称的使用,从而知道它用于什么属性。

也就是说,您要定义“名称”和“属性索引”之间的映射。您希望它在一个地方定义,以便它自动传播到每个着色器并在整个 C++ 源代码中一致使用。

好吧,我们已经有了名称和属性索引之间的映射。它被称为“属性名称和属性索引之间的映射”。每个着色器都必须为其属性提供名称。这就是您在定义中看到的字符串名称,例如in vec4 position;属性的名称是position. 这就是 GLSL 在使用变量时所称的变量。

如您链接到的答案中所述,您可以在链接程序之前将特定属性名称与 C++ 代码中的属性索引相关联。这是通过函数完成glBindAttribLocation。您可以设置任意数量的映射。当程序链接时,与指定位置匹配的属性将被分配到该位置。

您所需要的只是一个“语义”列表(又名:属性索引)以及您需要着色器用于这些属性的字符串名称。

你可能会说,“好吧,我希望着色器可以随意调用变量。” 我的回答是……有什么区别?您建议的方案已经要求用户遵守特定的命名约定。只是他们必须使用的名称不是变量的名称;它是您在声明时与变量关联的某个标签的名称。

那么具体有什么区别呢?着色器的编写者必须遵守顶点属性变量名称的设置命名方案?所有着色器中相同概念的一致名称不是一件好事吗?

一个区别是,如果他们在你的方案下错误地输入了“语义”,他们会得到一个着色器编译错误(因为他们输入错误的“语义”名称与任何实际#define的 s 不匹配)。然而,如果他们错误地输入了属性的名称,那么只有在使用属性时没有错误地输入该名称时,他们才会得到编译器错误。

有办法抓住它。它需要使用程序自省来遍历活动属性列表,并根据预期的属性名称检查它们。

您可以将其归结为一组非常简单的约定。使用您的“语义”定义:

enum VertexElementSemantic
{
  POSITION, NORMAL, AMBIENT, DIFFUSE, SPECULAR,
  TEX_COORD0, TEX_COORD1, TEX_COORD2, TEX_COORD3,
  INDICES, NUM_SEMANTICS
};

//in the C++ file you use to link your shaders
const char *AttributeNames[] =
{
  "position", "normal", "ambient", "diffuse", "specular", 
  "tex_coord0", "tex_coord1", "tex_coord2", "tex_coord3", 
  "indices",
}

static_assert(ARRAY_COUNT(AttributeNames) == NUM_SEMANTICS); //Where `ARRAY_COUNT` is a macro that computes the number of elements in a static array.

GLuint CreateProgram(GLuint vertexShader, GLuint fragmentShader)
{
  GLuint prog = glCreateProgram();
  //Attach shaders
  for(int attrib = 0; attrib < NUM_SEMANTICS; ++attrib)
  {
    glBindAttribLocation(prog, attrib, AttributeNames[attrib]);
  }

  glLinkProgram(prog);

  //Detach shaders
  //Check for linking errors

  //Verify that attribute locations are as expected.
  //Left as an exercise for the reader.

  return prog;
}

就个人而言,我只会使用一个数字。无论您使用什么,编写着色器的人都必须遵守一些约定。这意味着当他们去编写一个获取位置的顶点着色器时,他们将不得不查找如何说“这是一个位置”。因此,无论如何,他们都必须在某处的表格中查找某些内容。

在这一点上,它归结为最可能的问题是什么。最有可能的问题要么是认为他们知道答案但实际上是错误的人(即:没有查),要么是错误输入答案的人。打错数字真的很难(尽管它肯定会发生),而打错数字要容易得多POSITION_LOCATION。前一个问题可能发生在一个或多或少相等的人身上。

所以在我看来,如果你的约定是基于数字而不是文字,你更有可能得到更少的约定不匹配问题。

于 2013-04-05T07:18:09.887 回答