OpenGL-Vertex Shader Inputs(Vertex Shader输入)
全球图形学领域教育的领先者、自研引擎的倡导者、底层技术研究领域的技术公开者,东汉书院在致力于使得更多人群具备内核级竞争力的道路上,将带给小伙伴们更多的公开技术教学和视频,感谢一路以来有你的支持。我们正在用实际行动来帮助小伙伴们构建一套成体系的图形学知识架构,你在我们这里获得的不止于那些毫无意义的代码,我们这里更多的是代码背后的故事,以及精准、透彻的理解。
Vertex Shader Inputs(Vertex Shader输入)
The first step in any OpenGL graphics pipeline is actually the vertex fetch stage, unless the configuration does not require any vertex attributes, as was the case in some of our earliest examples. This stage runs before your vertex shader and is responsible for forming its inputs. You have already been introduced to the glVertexAttribPointer() function and we have explained how it hooks data in buffers up to vertex shader inputs. Now we’ll take a closer look at vertex attributes. In the example programs presented thus far, we’ve used only a single vertex attribute and have filled it with four-component floating-point data, which matches the data types we have used for our uniforms, uniform blocks, and hard-coded constants. However,OpenGL supports a large number of vertex attributes, and each can have its own format, data type, number of components, and so on. Also, OpenGL can read the data for each attribute from a different buffer object. glVertexAttribPointer() is a handy way to set up virtually everything about a vertex attribute. However, it can actually be considered more of a helper function that sits on top of a few lower-level functions: glVertexAttribFormat() glVertexAttribBinding(), and glBindVertexBuffer(). Their prototypes are
除非Vertex Shader不需要什么顶点属性,否则OpenGL渲染管线的第一步是vertex fetch。这个步骤在vertex shader执行之前执行,它负责给vertex shader提供输入数据。我们已经介绍过glVertexAttribPointer 这个函数了,你也知道了它是如何把数据传送给shader作为输入的。现在我们进一步看看顶点属性和我们给它输入的那些由四个浮点数组成的与我们的uniform,uniform blocks和硬编码数据匹配的数据。实际上,OpenGL 支持很多顶点属性,并且我们可以给它们各自不同的格式,这就包括数据格式和组成浮点数的个数。并且OpenGL可以从buffer object为每个属性读取数据。glVertexAttribPointer是一种设置好顶点属性的方法。我们其实 还有很多方法可以做这件事:glVertexAttribFormat() glVertexAttribBinding(), 和 glBindVertexBuffer(),他们的函数申明如下:
void glVertexAttribFormat(GLuint attribindex, GLint size,GLenum type, GLboolean normalized,GLuint relativeoffset);
void glVertexAttribBinding(GLuint attribindex,GLuint bindingindex);
void glBindVertexBuffer(GLuint bindingindex,GLuint buffer,GLintptr offset,GLintptr stride);
To understand how these functions work, first let’s consider a simple vertex shader fragment that declares a number of inputs. In Listing 7.1, notice the use of the location layout qualifier to set the locations of the inputs explicitly in the shader code.
为了理解这些函数如何工作的,首先我们设想一个简单的申明了又多个输入的vertex shader。在清单7.1中,注意到我们使用了location layout修饰符去在shader中显式的设置输入的位置。
#version 450 core
// Declare a number of vertex attributes
layout (location = 0) in vec4 position;
layout (location = 1) in vec3 normal;
layout (location = 2) in vec2 tex_coord;
// Note that we intentionally skip location 3 here
layout (location = 4) in vec4 color;
layout (location = 5) in int material_id;
Listing 7.1: Declaration of multiple vertex attributes
The shader fragment in Listing 7.1 declares five inputs: position, normal, tex_coord, color, and material_id. Now, consider that we are using a data structure to represent our vertices, which is defined in C as follows:
清单7.1的代码中申明了5个输入的顶点属性:position, normal,tex_coord, color, 和 material_id。现在我们结合我们在C语言里我们用来描述顶点的结构体,它的定义如下所示:
typedef struct VERTEX_t
vmath::vec4 position;
vmath::vec3 normal;
vmath::vec2 tex_coord;
GLubyte color[3];
int material_id;
} VERTEX;
Notice that our vertex structure in C mixes use of vmath types and plain-old data (for color).
注意我们C语言里的顶点结构体使用vmath里面的类和比较原始的数据类型(颜色属性)
The first attribute is pretty standard and should be familiar to you—it’s the position of the vertex, specified as a four-component floating-point vector. To describe this input using the glVertexAttribFormat() function, we would set size to 4 and type to GL_FLOAT. The second attribute, the normal of the geometry at the vertex, is in normal and would be passed to glVertexAttribFormat() with size set to 3 and type set to GL_FLOAT. Likewise, tex_coord can be used as a two-dimensional texture coordinate and might be specified by setting size to 2 and type to GL_FLOAT.
第一个属性对你来说应该不陌生了,是顶点的位置。我们使用glVertexAttribFormat去描述顶点有这样的一个输入,我们将会设置size为4,type参数为GL_FLOAT。第二个属性是法线,在设置这个属性的时候我们会设置 size为3然后type传入GL_FLOAT。相似的,tex_coord是二维的纹理坐标,我们会设置size为2,type为GL_FLOAT。
The color input to the vertex shader is declared as a vec4, but the color member of our VERTEX structure is actually an array of 3 bytes. Both the size (number of elements) and the data type are different. OpenGL can convert the data for you as it reads it into the vertex shader. To hook our 3-byte color member up to our four-component vertex shader input, we call glVertexAttribFormat() with size set to 3 and type set to GL_UNSIGNED_BYTE. This is where the normalized parameter comes in. As you probably know, the range of values representable by an unsigned byte is 0 to 255. However, that’s not what we want in our vertex shader. There, we want to represent colors as values between 0.0 and 1.0. If you set normalized to GL_TRUE, then OpenGL will automatically divide each component of the input by the maximum possible representable positive value, normalizing it.
color被申明为vec4,但是color的数据成员实则是3个byte。无论在构成元素的个数还是数据类型都是不一样的,OpenGL可以帮你做转换。为了让3个byte的color编程4个浮点数的vertex shader输入,我们调用 glVertexAttribFormat函数,然后设置size为3并且设置type为GL_UNSIGNED_BYTE。这就跟之前相似。一个unsigned byte的数据范围是0到255,但是我们在vertex shader里想要的不是这个。我们想要一个0.0到1.0的数来 表示颜色。如果你设置normalized为GL_TRUE,OpenGL就会帮你自动把byte数值除以它们的最大值,来标准化它。
Because two’s-complement numbers are able to represent a greater-magnitude negative number than positive number, this can place one value below −1.0 (−128 for GLbyte, −32,768 for GLshort, and −2,147,483,648 for GLint). Those most negative numbers are treated specially and are clamped to the floating-point value −1.0 during normalization. If normalized is GL_FALSE, then the value will be converted directly to floating point and presented to the vertex shader. In the case of unsigned byte data (like color), this means that the values will be between 0.0 and 255.0. Table 7.1 shows the tokens that can be used for the type parameter, their corresponding OpenGL types, and the range of values that they can represent.
有符号的数据类型可以表达的负方向的值的绝对值更大,所以刚才那个操作可能会出现比-1.0小的数值。在标准化的过程中最小的那个复数会被截断至-1.0。如果normalized传GL_FALSE,那么这个值会被直接转换成浮点数。 也就是说颜色数据会被转换成0.0到255.0之间的数。表7.1展示了type可以传入的参数清单,以及他们对应的OpenGL的数据类型和他们可以表达的数据范围。
In Table 7.1, the floating-point types (GLhalf, GLfloat, and GLdouble) don’t have ranges because they can’t be normalized. The GLfixed type is a special case. It represents fixed-point data that is made up of 32 bits with the binary point at position 16 (halfway through the number); as such, it is treated as one of the floating-point types and cannot be normalized.
在表7.1中,浮点类型的数据没有范围,因为他们不能被标准化。GLFixed数据是特殊情况。它表达的是32位的定点数。所以它也不能被标准化。
In addition to the scalar types shown in Table 7.1, glVertexAttribFormat() supports several packed data formats that use a single integer to store multiple components. The two packed data formats supported by OpenGL are GL_UNSIGNED_INT_2_10_10_10_REV and GL_INT_2_10_10_10_REV, which both represent four components packed into a single 32-bit word.
作为表7.1的补充,glVertexAttribFormat支持一些一些压缩版的数据格式,比如使用一个整型数据的内存去存储多个数据,OpenGL支持的两种压缩数据格式为GL_UNSIGNED_INT_2_10_10_10_REV和GL_INT_2_10_10_10_REV, 它们都是把四个数据打包到一个32比特的内存里去了。
The GL_UNSIGNED_INT_2_10_10_10_REV format provides 10 bits for each of the x, y, and z components of the vector and only 2 bits for the w component, which are all treated as unsigned quantities. This gives a range of 0 to 1023 for each of x, y, and z, and 0 to 3 for w. Likewise, the GL_INT_2_10_10_10_REV format provides 10 bits for x, y, and z, and 2 bits for w, but in this case each component is treated as a signed quantity. That means that while x, y, and z have a range of −512 to 511, w may range from −2 to 1. While this may not seem terribly useful, there are a number of use cases for three component vectors with more than 8 bits of precision (24 bits in total) but that do not require 16 bits of precision (48 bits in total). Even though those last 2 bits might be wasted, 10 bits of precision per component provides what is needed.
GL_UNSIGNED_INT_2_10_10_10_REV为x、y、z提供了10比特存储位置,只有2比特位置留给w分量,所有分量都是无符号数,这样一来x、y、z的范围是0~1023,w的范围是0~3。类似的 GL_INT_2_10_10_10_REV给x、y、z提供了10比特存储位置,只有2比特位置留给w分量,所有分量都是有符号数,这样一来x、y、z的范围是-512到511,w的范围是-2到1。看起来这没啥 卵用,很多情况下,我们需要精度大于8bit 的数据,但是不需要超过16比特精度。虽然我们最终会额外使用掉2比特,但是10比特的精度提供了我们需要的东西。
When one of the packed data types (GL_UNSIGNED_INT_2_10_10_10_REV or GL_INT_2_10_10_10_REV) is specified, then size must be set either to 4 or the special value GL_BGRA. The latter applies an automatic swizzle to the incoming data to reverse the order of the r, g, and b components (which are equivalent to the x, y, and zcomponents) of the incoming vectors. This provides compatibility with data stored in that order without needing to modify your shaders.
当使用压缩数据格式滴时候,size必须设置成4或者GL_BGRA。OpenGL会自动的把压缩数据变成r、g、b版的。这样一来我们在shader里就不用去特殊处理输入数据的顺序问题了。
Finally, returning to our example vertex declaration, we have the material_id field, which is an integer. In this case, because we want to pass an integer value as is to the vertex shader, we’ll use a variation on the glVertexAttribFormat(), glVertexAttribIFormat(), whose prototype is
最后,回到我们的顶点的申明,我们有material_id这个值,它是个整型。这时,因为我们想传递一个整型数据给vertex shader,我们需要使用glVertexAttribFormat的重载函数glVertexAttribIFormat,它的函数申明如下:
void glVertexAttribIFormat(GLuint attribindex,GLint size,GLenum type,GLuint relativeoffset);
Again, the attribindex, size, type, and relativeoffset parameters specify the attribute index, number of components, type of those components, and offset from the start of the vertex of the attribute that’s being set up, respectively. However, you’ll notice that the normalized parameter is missing. That’s because this version of glVertexAttribFormat() is only for integer types—type must be one of the integer types (GL_BYTE, GL_SHORT, or GL_INT; one of their unsigned counterparts; or one of the packed data formats) and integer inputs to a vertex shader are never normalized. Thus, the complete code to describe our vertex format is
attributeIndex,size,type,relativeoffset参数设置了attribute的索引,有多少个组成部分,数据类型,和数据偏移。然而,你会发现normalized的参数没有。这时因为这个API是为整型而设计的,type参数必须是 GL_BYTE, GL_SHORT, 或者GL_INT,或者他们的无符号类型的形式,或者他们对应的压缩数据的形式。整型的输入是不会被标准化的。我们完整的设置顶点属性的代码如下:
// position
glVertexAttribFormat(0, 4, GL_FLOAT, GL_FALSE, offsetof(VERTEX,
position));
// normal
glVertexAttribFormat(1, 3, GL_FLOAT, GL_FALSE, offsetof(VERTEX, normal));
// tex_coord
glVertexAttribFormat(2, 2, GL_FLOAT, GL_FALSE, offsetof(VERTEX,
texcoord));
// color[3]
glVertexAttribFormat(4, 3, GL_UNSIGNED_BYTE, GL_TRUE, offsetof(VERTEX,
color));
// material_id
glVertexAttribIFormat(5, 1, GL_INT, offsetof(VERTEX, material_id));
Now that you’ve set up the vertex attribute format, you need to tell OpenGL which buffers to read the data from. If you recall our discussion of uniform blocks and how they map to buffers, you can apply similar logic to vertex attributes. Each verex shadercan have any number of input attributes (up to an implementation-defined limit), and OpenGL can provide data for them by reading from any number of buffers (again, up to a limit). Some vertex attributes can share space in a buffer; others may reside in different buffer objects. Rather than individually specifying which buffer objects are used for each vertex shader input, we can instead group inputs together and associate groups of them with a set of buffer binding points. Then, when you change the buffer bound to one of these binding points, it will change the buffer used to supply data for all of the attributes that are mapped to that binding point.
现在,你设置好了顶点的属性配置,这些将指导OpenGL如何从你的数据中读取数据。如果你回顾一下我们的uniform blocks,想想它们是如何把数据跟缓冲区对象做数据映射的,那么他们的组织逻辑是相似的。 每个vertex shader可以有很多个输入属性,OpenGL会从缓冲区里读入数据并发给它们。有些顶点属性可以在缓冲区里共享空间。另一些可能放在别的缓冲区对象里。相比把属性都放在不同的缓冲区对象里,我们把属性放到 一个缓冲区对象里,然后通过绑定节点来把它们与vertex输入进行关联。然后当你改变绑定的节点的时候,它将会改变所有属性到绑定节点的映射关系。
To establish the mapping between vertex shader inputs and buffer binding points, you can call glVertexAttribBinding(). The first to parameter glVertexAttribBinding(), attribindex, is the index of the vertex attribute; the second parameter, bindingindex, is the buffer binding point index. In our example, we’re going to store all of the vertex attributes in a single buffer. To set this up, we simply call glVertexAttribBinding() once for each attribute and specify zero for the bindingindex parameter each time:
为了建立从vertex shader输入与缓冲区绑定节点间的映射,你可以调用glVertexAttribBinding。第一个参数是属性的索引。第二个参数是绑定节点的索引。在我们的例子中,我们把所有的数据存储在一个缓冲区里的, 我们下面的代码展示了我们是如何设置这些映射的:
glVertexAttribBinding(0, 0); // position
glVertexAttribBinding(1, 0); // normal
glVertexAttribBinding(2, 0); // tex_coord
glVertexAttribBinding(4, 0); // color
glVertexAttribBinding(5, 0); // material_id