顶点处理与绘图命令 - 变换反馈和裁剪
这篇文章介绍的内容范围还是在顶点着色器中,将介绍变换顶点的保存和裁剪。变换顶点通过一个弹簧连接点模拟例子进行说明;裁剪同样也通过一个小例子进行展示。
变换顶点的保存
在 OpenGL 中,可将顶点、曲面细分评估或几何着色器的结果保存在一个或多个缓存对象中,这种特征也叫做顶点反馈。自己理解的更简单点,就是以上着色器阶段的输出内容,是可以保存在自定义的缓冲的,以供之后使用。
比如以下两个在顶点着色器中定义的变量,就是我们需要保存的变换顶点:
- out vec4 tf_position_mass;
- out vec3 tf_velocity;
为此,在链接程序之前需要使用 glTransformFeedbackVaryings() 函数进行指定,其原型为:
- void glTransformFeedbackVaryings(GLuint program,
- GLsizei count,
- const GLchar *const*varyings,
- GLenum bufferMode);
其参数都好理解,program 就是我们需要编译链接的程序对象;count 指定待输出的变换顶点个数;varyings 即是包含 count 个变换顶点变量名称的数组。最后一个参数 bufferMode,有两个值,GL_SEPARATE_ATTRIBS 和 GL_INTERLEAVED_ATTRIBS。GL_SEPARATE_ATTRIBS 将输出内容按变量个数逐一记录在单个缓存中,而 GL_INTERLEAVED_ATTRIBS 则统一记录在一起。
glTransformFeedbackVaryings() 需要在链接前指定。更换了想要捕获的变量需要重新链接。
进行以上设置之后,程序就可以保存我们想要的顶点数据了。现在我们需要了解如何获取这些缓存数据,获取和操作的步骤和缓冲上的操作一致,比如通过
- glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer);
绑定反馈顶点缓存,只不过是绑定目标变成了 GL_TRANSFORM_FEEDBACK_BUFFER。GL_SEPARATE_ATTRIBS 指定时,需要特别说明一下,使用 glBindBufferBase 进行绑定:
- void glBindBufferBase(GLenum target, GLuint index, GLuint buffer);
参数多了一个参数指定绑定点的下标,如图 1 所示。这个下标为 glTransformFeedbackVaryings() 中的 varyings 参数顺序指定,着色器中无法做显式的指定。

例子:弹簧质点系统
这个例子总的思路是使用两个程序,一个程序用于模拟计算弹簧系统各点状态,然后使用顶点反馈将结果进行输出,然后直接将输出结果交由另一个程序进行显示。
因为书上写了这是模拟布料的简单实现,所以当时多看了一会,虽然没怎么太理解😂 让我们先看一下模拟计算所需要用到的公式。
当前系统一个质点受到的合力:
\(F_{total}=G-\vec{d}kx-c\vec{v}\)
其中,\(\vec{d}kx\) 是胡克定律定义的弹簧弹力,包含力的方向。\(\vec{c}v\) 是摩擦损失,\(c\) 为阻尼系数。
求得合力之后,就可以使用牛顿第二定律求得质点的加速度:
\(F=m\vec{a}\)
\(\vec{a}=\frac{\vec{F}}{m}\)
进而可以根据初始速度 \(\vec{u}\) 和固定时间 \(t\) 求得最终的速度值和移动距离:
\(\vec{v}=\vec{u}+\vec{a}t\)
\(\vec{s}=\vec{u}+\frac{\vec{a}t^2}{2}\)
之后的过程,有点类似“微积分”迭代,只要 \(t\) 给的越小,迭代显示间隔越小,显示的效果就越好。
我们首先看到代码清单 1.1 中关于缓冲的设置,总共有两个顶点数组对象,记录在 m_vao[0]、m_vao[1] 中,分别对应两个着色程序。位置信息记录在 m_vbo[0]、m_vbo[1] 顶点缓冲中,一个用于输入,一个用于顶点变换输出,第四个分量存储质点重量;速度信息记录在 m_vbo[2]、m_vbo[3] 顶点缓冲中,同样一个用于输入,一个用于顶点变换输出。
m_vbo[4] 顶点缓冲存储质点连接状况。如图 2 所示,一个质点最多有四个连接点,所以用 ivec4 存储。存储内容按图 2 中的下标定义质点,-1 代表没有点连接。
由于连接状况,需要按下标随机索引质点信息(位置和质量),所以将位置信息内容绑定到纹理缓冲中,纹理缓冲满足随机访问的特性。强调说明一下,仅仅是缓冲的绑定,缓冲中的内容和原先的位置 m_vao[0]、m_vao[1] 中的内容一样。
即程序用到两个顶点数组对象,用于程序切换。五个顶点缓冲对象:位置和速度信息各两个,用于迭代;连接情况是固定的,因此只需要一个,只读。两个纹理缓冲对象,为了随机索引位置信息。
- vmath::vec4* initial_positions = new vmath::vec4[POINTS_TOTAL];
- vmath::vec3* initial_velocities = new vmath::vec3[POINTS_TOTAL];
- vmath::ivec4* connection_vectors = new vmath::ivec4[POINTS_TOTAL];
- int n = 0;
- for (j = 0; j < POINTS_Y; j++)
- {
- float fj = (float)j / (float)POINTS_Y;
- for (i = 0; i < POINTS_X; i++)
- {
- float fi = (float)i / (float)POINTS_X;
- initial_positions[n] = vmath::vec4((fi - 0.5f) * (float)POINTS_X,
- (fj - 0.5f) * (float)POINTS_Y,
- 0.6f * sinf(fi) * cosf(fj),
- 1.0f);
- initial_velocities[n] = vmath::vec3(0.0f);
- connection_vectors[n] = vmath::ivec4(-1);
- if (j != (POINTS_Y - 1))
- {
- if (i != 0)
- connection_vectors[n][0] = n - 1;
- if (j != 0)
- connection_vectors[n][1] = n - POINTS_X;
- if (i != (POINTS_X - 1))
- connection_vectors[n][2] = n + 1;
- if (j != (POINTS_Y - 1))
- connection_vectors[n][3] = n + POINTS_X;
- }
- n++;
- }
- }
- glCreateVertexArrays(2, m_vao);
- glCreateBuffers(5, m_vbo);
- for (i = 0; i < 2; i++)
- {
- glBindVertexArray(m_vao[i]);
- glBindBuffer(GL_ARRAY_BUFFER, m_vbo[POSITION_A + i]);
- glBufferData(GL_ARRAY_BUFFER, POINTS_TOTAL * sizeof(vmath::vec4), initial_positions, GL_DYNAMIC_COPY);
- glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, NULL);
- glEnableVertexAttribArray(0);
- glBindBuffer(GL_ARRAY_BUFFER, m_vbo[VELOCITY_A + i]);
- glBufferData(GL_ARRAY_BUFFER, POINTS_TOTAL * sizeof(vmath::vec3), initial_velocities, GL_DYNAMIC_COPY);
- glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);
- glEnableVertexAttribArray(1);
- glBindBuffer(GL_ARRAY_BUFFER, m_vbo[CONNECTION]);
- glBufferData(GL_ARRAY_BUFFER, POINTS_TOTAL * sizeof(vmath::ivec4), connection_vectors, GL_STATIC_DRAW);
- glVertexAttribIPointer(2, 4, GL_INT, 0, NULL);
- glEnableVertexAttribArray(2);
- }
- delete[] connection_vectors;
- delete[] initial_velocities;
- delete[] initial_positions;
- glCreateTextures(GL_TEXTURE_BUFFER, 2, m_pos_tbo);
- glBindTexture(GL_TEXTURE_BUFFER, m_pos_tbo[0]);
- glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, m_vbo[POSITION_A]);
- glBindTexture(GL_TEXTURE_BUFFER, m_pos_tbo[1]);
- glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, m_vbo[POSITION_B]);

然后我们看到代码清单 1.2 占 99% 功能的用于状态模拟的顶点着色器。position_mass 是位置信息,velocity 是速度信息,connection 是连接信息。为了随机索引位置信息,tex_position 将位置信息作为纹理。这边只要知道按上述介绍的公式“一顿操作”,更新的质点位置和速度信息以顶点反馈的方式,输出到了 tf_position_mass 和 tf_velocity 中。
- #version 410 core
- // This input vector contains the vertex position in xyz, and the
- // mass of the vertex in w
- layout (location = 0) in vec4 position_mass;
- // This is the current velocity of the vertex
- layout (location = 1) in vec3 velocity;
- // This is our connection vector
- layout (location = 2) in ivec4 connection;
- // This is a TBO that will be bound to the same buffer as the
- // position_mass input attribute
- uniform samplerBuffer tex_position;
- // The outputs of the vertex shader are the same as the inputs
- out vec4 tf_position_mass;
- out vec3 tf_velocity;
- // A uniform to hold the timestep. The application can update this
- uniform float t = 0.07;
- // The global spring constant
- uniform float k = 7.1;
- // Gravity
- const vec3 gravity = vec3(0.0, -0.08, 0.0);
- // Global damping constant
- uniform float c = 2.8;
- // Spring resting length
- uniform float rest_length = 0.88;
- void main(void)
- {
- vec3 p = position_mass.xyz; // p can be our position
- float m = position_mass.w; // m is the mass of our vertex
- vec3 u = velocity; // u is the initial velocity
- vec3 F = gravity * m - c * u; // F is the force on the mass
- bool fixed_node = true; // Becomes false when force is applied
- for (int i = 0; i < 4; i++)
- {
- if (connection[i] != -1)
- {
- // q is the position of the other vertex
- vec3 q = texelFetch(tex_position, connection[i]).xyz;
- vec3 d = q - p;
- float x = length(d);
- F += -k * (rest_length - x) * normalize(d);
- fixed_node = false;
- }
- }
- // If this is a fixed node, reset force to zero
- if (fixed_node)
- {
- F = vec3(0.0);
- }
- // Accelleration due to force
- vec3 a = F / m;
- // Displacement
- vec3 s = u * t + 0.5 * a * t * t;
- // Final velocity
- vec3 v = u + a * t;
- // Constrain the absolute value of the displacement per step
- s = clamp(s, vec3(-25.0), vec3(25.0));
- // Write the outputs
- tf_position_mass = vec4(p + s, m);
- tf_velocity = v;
- }
最后,我们看到代码清单 1.3 中的渲染部分,第 6 至 16 行就是“核心”,我们看它是如何进行迭代的:先绑定不同的顶点数组对象,以便引用到不同的输入,同时更新对应的纹理缓冲绑定;将另一组“备用”的缓冲设置为顶点反馈输出;再将输出结果作为下一次的输入,依次迭代。
还有需要说明的一点是,模拟计算前使用 glEnable(GL_RASTERIZER_DISCARD) 取消光栅化,因为这些内容不用输出(而且更新的程序压根也没有片段着色器)。另一个程序需要输出前记得恢复:glDisable(GL_RASTERIZER_DISCARD)。
- void render(double t)
- {
- int i;
- glUseProgram(m_update_program);
- glEnable(GL_RASTERIZER_DISCARD);
- for (i = iterations_per_frame; i != 0; i--)
- {
- glBindVertexArray(m_vao[m_iteration_index & 1]);
- glBindTexture(GL_TEXTURE_BUFFER, m_pos_tbo[m_iteration_index & 1]);
- m_iteration_index++;
- glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_vbo[POSITION_A + (m_iteration_index & 1)]);
- glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, m_vbo[VELOCITY_A + (m_iteration_index & 1)]);
- glBeginTransformFeedback(GL_POINTS);
- glDrawArrays(GL_POINTS, 0, POINTS_TOTAL);
- glEndTransformFeedback();
- }
- glDisable(GL_RASTERIZER_DISCARD);
- glClearBufferfv(GL_COLOR, 0, sb7::color::Black);
- glUseProgram(m_render_program);
- if (draw_points)
- {
- glDrawArrays(GL_POINTS, 0, POINTS_TOTAL);
- }
- }
裁剪
裁剪的内部实现很复杂,但是通过 OpenGL 来达到裁剪的效果很简单。OpenGL 定义了内置变量 gl_ClipDistance 指示点到裁剪平面的距离。如果点到裁剪平面的距离为负数,则会按相应逻辑被裁剪掉。
我们直接看到代码清单 2.1 的例子,例子中进行普通平面裁剪和球型平面裁剪。
- #version 410 core
- // Per-vertex inputs
- layout (location = 0) in vec4 position;
- layout (location = 1) in vec3 normal;
- uniform mat4 mv_matrix;
- uniform mat4 proj_matrix;
- // Clip plane
- uniform vec4 clip_plane = vec4(1.0, 1.0, 0.0, 0.85);
- uniform vec4 clip_sphere = vec4(0.0, 0.0, 0.0, 4.0);
- void main(void)
- {
- // Calculate view-space coordinate
- vec4 P = proj_matrix * mv_matrix * position;
- // Write clip distances
- gl_ClipDistance[0] = dot(P, clip_plane);
- gl_ClipDistance[1] = length(P.xyz / P.w - clip_sphere.xyz) - clip_sphere.w;
- // Calculate the clip-space position of each vertex
- gl_Position = P;
- }
统一变量 clip_plane 设置为平面的法向量,w 分量为到原点的偏移。保证 clip_plane 是单位向量的话,点积运算就是点到平面的距离。我们将面设置成移动的 y-z 面,裁剪效果如视频 1 所示。
统一变量 clip_sphere 设置为球的中心点,w 分量为球的半径。点到球平面的距离为点到球的中心减去球的半径。我们将球的中心设置到原点,逐渐增大半径,裁剪效果如视频 2 所示。
总结
至此,我们第 7 章就学习完毕了。我们学习了不同的绘图指令:索引绘图指令、实例化绘图指令和间接绘图指令。接着学习了顶点反馈的方法和裁剪。
后续就会开始第 8 章的学习,按照管线的流程,将学习曲面细分着色器和几何着色器。