原文地址http://www.arcsynthesis.org/gltut/Basics/Tut02%20Vertex%20Attributes.html
Vertex Attributes
Using the fragment position in a fragment shader is quite useful, but it is far from the best tool for controlling the color of triangles. A much more useful tool is to give each vertex a color explicitly. The Vertex Colors tutorial implements this; the main file for this tutorial isVertexColors.cpp
.
We want to affect the data being passed through the system. The sequence of events we want to happen is as follows.
-
For every position that we pass to a vertex shader, we want to pass a corresponding color value.
-
For every output position in the vertex shader, we also want to output a color value that is the same as the input color value the vertex shader received.
-
In the fragment shader, we want to receive an input color from the vertex shader and use that as the output color of that fragment.
You most likely have some serious questions about that sequence of events, notably about how steps 2 and 3 could possibly work. We'll get to that. We will follow the revised flow of data through the OpenGL pipeline.
In order to accomplish the first step, we need to change our vertex array data. That data now looks like this:
Example 2.2. New Vertex Array Data
const float vertexData[] = { 0.0f, 0.5f, 0.0f, 1.0f, 0.5f, -0.366f, 0.0f, 1.0f, -0.5f, -0.366f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, };
First, we need to understand what arrays of data look like at the lowest level. A single byte is the smallest addressible data in C/C++. A byte represents 8 bits (a bit can be 0 or 1), and it is a number on the range [0, 255]. A value of type float requires 4 bytes worth of storage. Any floatvalue is stored in 4 consecutive bytes of memory.
A sequence of 4 floats, in GLSL parlance a vec4, is exactly that: a sequence of four floating-point values. Therefore, a vec4
takes up 16 bytes, 4 floats times the size of a float.
The vertexData
variable is one large array of floats. The way we want to use it however is as two arrays. Each 4 floats is a single vec4, and the first three vec4s represents the positions. The next 3 are the colors for the corresponding vertices.
vertexData是一个float型数组.我们想使用它来当做两个数组使用.每4个float是一个vec4,第一个3组vec4代码表位置,接下来三个代表对应的颜色.
In memory, the vertexData
array looks like this:
The top two show the layout of the basic data types, and each box is a byte. The bottom diagram shows the layout of the entire array, and each box is a float. The left half of the box represents the positions and the right half represents the colors.
The first 3 sets of values are the three positions of the triangle, and the next 3 sets of values are the three colors at these vertices. What we really have is two arrays that just happen to be adjacent to one another in memory. One starts at the memory address &vertexData[0]
, and the other starts at the memory address &vertexData[12]
上面两个图表示基本的数据类型,每一个方框是一个byte.底下的表示一整个数组每个方框是一个float.左面的半个方框表示顶点位置右面半个表示颜色.
第一个三组数值是三角形的三个位置,接下来三个数值是这些顶点对应的颜色.我们真正拥有的是两个相邻存储的数组.一个从地址 &vertexData[0]
开始,另一个从
&vertexData[12]
开始
.
As with all vertex data, this is put into a buffer object. We have seen this code before:
Example 2.3. Buffer Object Initialization
void InitializeVertexBuffer() { glGenBuffers(1, &vertexBufferObject); glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject); glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); }
The code has not changed, because the sizes of the array is computed by the compiler with the sizeof
directive. Since we added a few elements to the buffer, the computed size naturally grows bigger.
Also, you may notice that the positions are different from prior tutorials. The original triangle, and the one that was used in the Fragment Position code, was a right triangle (one of the angles of the triangle is 90 degrees) that was isosceles (two of its three sides are the same length). This new triangle is an equilateral triangle (all three sides are the same length) centered at the origin.
Recall from above that we are sending two pieces of data per-vertex: a position and a color. We have two arrays, one for each piece of data. They may happen to be adjacent to one another in memory, but this changes nothing; there are two arrays of data. We need to tell OpenGL how to get each of these pieces of data.
我们给每个vertex传送两块数据:位置和颜色.我们有两个数组,对应两块数据.他们或许相邻存储,但这不影响什么;有两个数据的数组.我们需要告诉opengl如何获得这两块数据.
This is done as follows:
Example 2.4. Rendering the Scene
void display() { glClearColor(0.0f, 0.0f, 0.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(theProgram); glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject); glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0); glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, (void*)48); glDrawArrays(GL_TRIANGLES, 0, 3); glDisableVertexAttribArray(0); glDisableVertexAttribArray(1); glUseProgram(0); glutSwapBuffers(); glutPostRedisplay(); }
Since we have two pieces of data, we have two vertex attributes. For each attribute, we must call glEnableVertexAttribArray
to enable that particular attribute. The first parameter is the attribute location set by the layout(location)
field for that attribute in the vertex shader.
我们有两块数据,两个vertex attributes.每个attributes我们都要调用函数glEnableVertexAttribArray来开启特殊的attribute.唯一的参数是attribute location,它通过顶点shader中layout设置的 field来标识attribute.
Then, we call glVertexAttribPointer
for each of the attribute arrays we want to use. The only difference in the two calls are which attribute location to send the data to and the last parameter. The last parameter is the byte offset into the buffer of where the data for this attribute starts. This offset, in this case, is 4 (the size of a float) * 4 (the number of floats in a vec4) * 3 (the number of vec4's in the position data).
然后对每个我们想要使用的attribute数组调用glVertexAttribPointer函数.这个函数被调用两次,两次的唯一区别是哪个attribute location 的数据会被传送和最后一个参数.最后一个参数是标识想要被传送的数据的起始位置.这个例子中,该参数为: 4 (sizeof (float) )* 4 ( vec4中floats的数量) * 3 ( 顶点的位置数据中vec4's 的数量 ).
Note
If you're wondering why it is (void*)48
and not just 48
, that is because of some legacy API cruft. The reason why the function name is glVertexAttrib“Pointer” is because the last parameter is technically a pointer to client memory. Or at least, it could be in the past. So we must explicitly cast the integer value 48 to a pointer type.
最后一个参数是一个指向cllient内存的pointer.所以我们需要将值48转换为pointer类型
After this, we use glDrawArrays
to render, then disable the arrays with glDisableVertexAttribArray.
In the last tutorial, we skimmed over the details of what exactly glDrawArrays
does. Let us take a closer look now.
The various attribute array functions set up arrays for OpenGL to read from when rendering. In our case here, we have two arrays. Each array has a buffer object and an offset into that buffer where the array begins, but the arrays do not have an explicit size. If we look at everything as C++ pseudo-code, what we have is this:
如果我们按C++伪码的写法,有如下:
Example 2.5. Vertex Arrays
GLbyte *bufferObject = (void*){0.0f, 0.5f, 0.0f, 1.0f, 0.5f, -0.366f, ...}; float *positionAttribArray[4] = (float *[4])(&(bufferObject + 0)); float *colorAttribArray[4] = (float *[4])(&(bufferObject + 48));
Each element of the positionAttribArray
contains 4 components, the size of our input to the vertex shader (vec4). This is the case because the second parameter of glVertexAttribPointer
is 4. Each component is a floating-point number; similarly because the third parameter is GL_FLOAT
. The array takes its data from bufferObject
because this was the buffer object that was bound at the time thatglVertexAttribPointer
was called. And the offset from the beginning of the buffer object is 0 because that is the last parameter ofglVertexAttribPointer
.
我们input到顶点shader的每个positionAttribArray的元素有四个分量.这是由于glVertexAttribPointer
的第二个参数是4.每个分量是一个浮点型,这是由于第三个参数是 GL_FLOAT
. AttributArray从bufferObject取出数据而不是别的buffer是因为 在调用glVertexAttribPointer函数时bind了bufferObject.最后一个参数为0.
函数小结:
-------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------
void glVertexAttribPointer(GLuint index,GLint size,GLenum type,GLboolean normalized,GLsizei stride,const GLvoid * pointer)
-------------------------------------------------------------------------------------------------------------
index:你需要绑定到vertex shader的in变量对应的索引;这个例子的shader中,position变量为0,color变量绑定为1
size: 你需要绑定到vertex shader的in变量的类型所包含的CPU数据类型的个数;这个例子中position为vec4,有4个float,同理,color也为vec4,也有4个float
type:你引用的CPU数据的类型:这个例子中数据类型为float,在这里用GL_FLOAT表示.
normalized:点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE).
stride:指定连续顶点属性之间的空隙大小。此例子中为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。
pointer:标识想要被传送的数据的起始位置.此例子中:position在数组中起始位置为0,而color的起始位置紧跟着position最后一个数据,所以起始位置为 4 (sizeof (float)的返回值 )* 4 ( vec4中floats的数量) * 3 ( 顶点的position数据中有多少个vec4).
-------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------
The same goes for colorAttribArray
, except for the offset value, which is 48 bytes.
Using the above pseudo-code representation of the vertex array data, glDrawArrays
would be implemented as follows:
使用前面的伪码表示的顶点数据,glDrawArrays
函数按如下方法实现:
Example 2.6. Draw Arrays Implementation
void glDrawArrays(GLenum type, GLint start, GLint count) { for(GLint element = start; element < start + count; element++) { VertexShader(positionAttribArray[element], colorAttribArray[element]); } }
This means that the vertex shader will be executed count
times, and it will be given data beginning with the start
-th element and continuing for count
elements. So the first time the vertex shader gets run, it takes the position attribute from bufferObject[0 + (0 * 4 * sizeof(float))]
and the color attribute from bufferObject[48 + (0 * 4 * sizeof(float))]
. The second time pulls the position from bufferObject[0 + (1 * 4 * sizeof(float))]
and color from bufferObject[48 + (1 * 4 * sizeof(float))]
. And so on.
这表示顶点shader会执行count次,从第start个开始,执行count次.所以顶点shader第一次运行时,它从bufferObject[0 + (0 * 4 * sizeof(float))]位置取出 position attribute,从bufferObject[48 + (0 * 4 * sizeof(float))]位置取出 color attribute.第二次执行时position attribute为bufferObject[0 + (1 * 4 * sizeof(float))],color attribute为bufferObject[48 + (1 * 4 * sizeof(float))].
The data flow from the buffer object to the vertex shaders looks like this now:
As before, every 3 vertices process is transformed into a triangle.
Our new vertex shader looks like this:
Example 2.7. Multi-input Vertex Shader
#version 330 layout (location = 0) in vec4 position; layout (location = 1) in vec4 color; smooth out vec4 theColor; void main() { gl_Position = position; theColor = color; }
There are three new lines here. Let us take them one at a time.
The declaration of the global color
defines a new input for the vertex shader. So this shader, in addition to taking an input named position
also takes a second input named color
. As with the position
input, this tutorial assigns each attribute to an attribute index. position
is assigned the attribute index 0, while color
is assigned 1.
color
为顶点shader定义了一个新的input.在这个shader中,同样定义了一个position.本篇文章中我们给每个 attribute 一个索引,position是0,color
是1.
That much only gets the data into the vertex shader. We want to pass this data out of the vertex shader. To do this, we must define an output variable. This is done using the out
keyword. In this case, the output variable is called theColor
and is of type vec4.
以上只是将数据送入顶点shader,我们还需要将数据送出顶点shader.这样的话我们要定义一个output variable.这通过使用out关键词来实现,这个例子中我们使用vec4型的theColor
.
The smooth
keyword is an interpolation qualifier. We will see what this means in a bit later.
Of course, this simply defines the output variable. In main
, we actually write to it, assigning to it the value of color
that was given as a vertex attribute. This being shader code, we could have used some other heuristic or arbitrary algorithm to compute the color. But for the purpose of this tutorial, it is simply passing the value of an attribute passed to the vertex shader.
当然,这只是简单的定义了输出变量.在顶点shader的main()中我们需要向它写入,使用被定义为vertex attribute的 color
的值给它赋值.这是shader代码,我们需要使用其他的heuristic或arbitrary算法来计算
color
的值.但是本文中简单将attribute 的值传递给顶点shader.
Technically, the built-in variable gl_Position
is defined as out vec4 gl_Position
. So it is an output variable as well. It is a special output because this value is directly used by the system, rather than used only by shaders. User-defined outputs, like theColor
above, have no intrinsic meaning to the system. They only have an effect in so far as other shader stages use them, as we will see next.
内置变量 gl_Position
被定义为out vec4 gl_Position.所以它同样是个output 变量.但它是个系统直接可以使用它的值的特殊的output,当然,只是shader可以使用.用户自定义的outputs,像theColor
,对于系统来说没有内在的含义.唯一的功效同使用它的其他shader一样,接下来会看到.
The new fragment shader looks like this:
Example 2.8. Fragment Shader with Input
#version 330 smooth in vec4 theColor; out vec4 outputColor; void main() { outputColor = theColor; }
This fragment shader defines an input variable. It is no coincidence that this input variable is named and typed the same as the output variable from the vertex shader. We are trying to feed information from the vertex shader to the fragment shader. To do this, OpenGL requires that the output from the previous stage have the same name and type as the input to the next stage. It also must use the same interpolation qualifier; if the vertex shader used smooth
, the fragment shader must do the same.
这个 fragment shader定义了一个input变量.这个input变量的名称和类型都与vertex shader中的output变量相同,这不是巧合.我们正在努力将信息从vertex shader传递到fragment shader.为了达到这样的目的,OpenGL需要使前一阶段中的output变量的类型和名称与下一阶段中的input变量类型和名称相同.当然也需要使用相同的interpolation qualifier;如果vertex shader使用了 smooth
,那么fragment shader 也同样需要使用.
This is a good part of the reason why OpenGL requires vertex and fragment shaders to be linked together; if the names, types, or interpolation qualifiers do not match, then OpenGL will raise an error when the program is linked.
这也是为什么OpenGL需要将两个shader连接起来的部分原因.如果名字类型或者interpolation qualifiers不同,OpenGL将会在program链接时返回错误.
So the fragment shader receives the value output from the vertex shader. The shader simply takes this value and copies it to the output. Thus, the color of each fragment will simply be whatever the vertex shader passed along.
Now we come to the elephant in the room, so to speak. There is a basic communication problem.
See, our vertex shader is run only 3 times. This execution produces 3 output positions (gl_Position
) and 3 output colors (theColor
). The 3 positions are used to construct and rasterize a triangle, producing a number of fragments.
See,我们的vertex shader只执行3次.这样的结果是产生3个output positions (gl_Position
)和3个output colors (theColor
).3个positions用来构造和光栅化一个三角形,产生一系列的fragmens.
The fragment shader is not run 3 times. It is run once for every fragment produced by the rasterizer for this triangle. The number of fragments produced by a triangle depends on the viewing resolution and how much area of the screen the triangle covers. An equilateral triangle the length of who's sides are 1 has an area of ~0.433. The total screen area (on the range [-1, 1] in X and Y) is 4, so our triangle covers approximately one-tenth of the screen. The window's natural resolution is 500x500 pixels. 500*500 is 250,000 pixels; one-tenth of this is 25,000. So our fragment shader gets executed about 25,000 times.
fragment shader不是运行3次.它对于三角形光栅化产生的每个fragment都运行一次.三角形光栅化产生的fragment的数量取决于viewing resolution(分辨率?) 和三角形在屏幕中所占的面积.边长为1的等边三角形面积为0.433.整个屏幕面积为4a (X,Y的范围 [-1, 1] ),这样三角形大概占1/10的屏幕面积.窗口的自然分辨率为500*500像素.500*500是250000像素,十分之一就是25000.所以fragment shader执行大概25000次.
There's a slight disparity here. If the vertex shader is directly communicating with the fragment shader, and the vertex shader is outputting only 3 total color values, where do the other 24,997 values come from?
有个轻微的差距在这里.如果vertex shader直接与fragment shader进行通信,vertex只输出了3个颜色值,那剩下的25000-3=24997个值从哪里来?
The answer is fragment interpolation.
答案就是fragment interpolation(片段插值).
By using the interpolation qualifier smooth
when defining the vertex output and fragment input, we are telling OpenGL to do something special with this value. Instead of each fragment receiving one value from a single vertex, what each fragment gets is a blend between the three output values over the surface of the triangle. The closer the fragment is to one vertex, the more that vertex's output contributes to the value that the fragment program receives.
通过在vertex output 和fragment input使用 interpolation qualifier smooth
,我们告诉OpenGL使用这个value做一些特殊的事情.不是每个fragment从一个vertex接受到一个值,二十,fragment获得一个blend,它在三个输出值和三角形的surface 之间.一个fragment离一个vertex越近,该vertex的输出值对于fragment接受值影响越多.
Because such interpolation is by far the most common mode for communicating between the vertex shader and the fragment shader, if you do not provide an interpolation keyword, smooth
will be used by default. There are two other alternatives: noperspective
and flat
.
smooth为默认插值模式,还有其他两种 noperspective
和
flat
模式.
If you were to modify the tutorial and change smooth
to noperspective
, you would see no change. That does not mean a change did not happen; our example is just too simple for there to actually be a change. The difference between smooth
and noperspective
is somewhat subtle, and only matters once we start using more concrete examples. We will discuss this difference later.
如果你修改本文中的 smooth为
noperspective
,不会发生变化.那不是表示没有变化发生,我们例子中发生的变化太简单了.smooth和
noperspective
的区别是非常微妙的,只有在具体例子中我们才能看出差别.接下来讨论.
The flat
interpolation actually turns interpolation off. It essentially says that, rather than interpolating between three values, each fragment of the triangle will simply get the first of the three vertex shader output variables. The fragment shader gets a flat value across the surface of the triangle, hence the term “flat
.”
flat
插值模式是不使用插值.本质上来说,不是在三个值之间插值,而是三角形的每个fragment将会使用从vertex shader输出的三个output变量中的第一个. 使用flat以后fragment shader 从三角形的surface获得一个flat值.
Each rasterized triangle has its own set of 3 outputs that are interpolated to compute the value for the fragments created by that triangle. So if you render 2 triangles, the interpolated values from one triangle do not directly affect the interpolated values from another triangle. Thus, each triangle can be taken independently from the rest.
每个光栅化三角形有它自己的3个用来给fragment插值的output.所以如果你render两个三角形,一个三角形的插值不会直接影响另一个.因此每个三角形可以从rest中独立取出来.
It is possible, and highly desirable in many cases, to build multiple triangles from shared vertices and vertex data. But we will discuss this at a later time.