顶点处理与绘图命令 - 变换反馈和裁剪

这篇文章介绍的内容范围还是在顶点着色器中,将介绍变换顶点的保存和裁剪。变换顶点通过一个弹簧连接点模拟例子进行说明;裁剪同样也通过一个小例子进行展示。

变换顶点的保存

在 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_ATTRIBSGL_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 参数顺序指定,着色器中无法做显式的指定。

图1 变换反馈绑定点的关系

例子:弹簧质点系统

这个例子总的思路是使用两个程序,一个程序用于模拟计算弹簧系统各点状态,然后使用顶点反馈将结果进行输出,然后直接将输出结果交由另一个程序进行显示。

因为书上写了这是模拟布料的简单实现,所以当时多看了一会,虽然没怎么太理解😂 让我们先看一下模拟计算所需要用到的公式。

当前系统一个质点受到的合力:

\(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] 中的内容一样。

即程序用到两个顶点数组对象,用于程序切换。五个顶点缓冲对象:位置和速度信息各两个,用于迭代;连接情况是固定的,因此只需要一个,只读。两个纹理缓冲对象,为了随机索引位置信息。

代码清单 1.1 弹簧质点系统顶点设置
  1. vmath::vec4* initial_positions = new vmath::vec4[POINTS_TOTAL];
  2. vmath::vec3* initial_velocities = new vmath::vec3[POINTS_TOTAL];
  3. vmath::ivec4* connection_vectors = new vmath::ivec4[POINTS_TOTAL];
  4.  
  5. int n = 0;
  6. for (j = 0; j < POINTS_Y; j++)
  7. {
  8.     float fj = (float)j / (float)POINTS_Y;
  9.     for (i = 0; i < POINTS_X; i++)
  10.     {
  11.          float fi = (float)i / (float)POINTS_X;
  12.  
  13.          initial_positions[n] = vmath::vec4((fi - 0.5f) * (float)POINTS_X,
  14.                                            (fj - 0.5f) * (float)POINTS_Y,
  15.                                            0.6f * sinf(fi) * cosf(fj),
  16.                                            1.0f);
  17.          initial_velocities[n] = vmath::vec3(0.0f);
  18.          connection_vectors[n] = vmath::ivec4(-1);
  19.  
  20.          if (j != (POINTS_Y - 1))
  21.          {
  22.              if (i != 0)
  23.                   connection_vectors[n][0] = n - 1;
  24.              if (j != 0)
  25.                   connection_vectors[n][1] = n - POINTS_X;
  26.              if (i != (POINTS_X - 1))
  27.                   connection_vectors[n][2] = n + 1;
  28.              if (j != (POINTS_Y - 1))
  29.                   connection_vectors[n][3] = n + POINTS_X;
  30.          }
  31.  
  32.          n++;
  33.     }
  34. }
  35.  
  36. glCreateVertexArrays(2, m_vao);
  37. glCreateBuffers(5, m_vbo);
  38.  
  39. for (i = 0; i < 2; i++)
  40. {
  41.     glBindVertexArray(m_vao[i]);
  42.  
  43.     glBindBuffer(GL_ARRAY_BUFFER, m_vbo[POSITION_A + i]);
  44.     glBufferData(GL_ARRAY_BUFFER, POINTS_TOTAL * sizeof(vmath::vec4), initial_positions, GL_DYNAMIC_COPY);
  45.     glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, NULL);
  46.     glEnableVertexAttribArray(0);
  47.  
  48.     glBindBuffer(GL_ARRAY_BUFFER, m_vbo[VELOCITY_A + i]);
  49.     glBufferData(GL_ARRAY_BUFFER, POINTS_TOTAL * sizeof(vmath::vec3), initial_velocities, GL_DYNAMIC_COPY);
  50.     glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  51.     glEnableVertexAttribArray(1);
  52.  
  53.     glBindBuffer(GL_ARRAY_BUFFER, m_vbo[CONNECTION]);
  54.     glBufferData(GL_ARRAY_BUFFER, POINTS_TOTAL * sizeof(vmath::ivec4), connection_vectors, GL_STATIC_DRAW);
  55.     glVertexAttribIPointer(2, 4, GL_INT, 0, NULL);
  56.     glEnableVertexAttribArray(2);
  57. }
  58.  
  59. delete[] connection_vectors;
  60. delete[] initial_velocities;
  61. delete[] initial_positions;
  62.  
  63. glCreateTextures(GL_TEXTURE_BUFFER, 2, m_pos_tbo);
  64. glBindTexture(GL_TEXTURE_BUFFER, m_pos_tbo[0]);
  65. glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, m_vbo[POSITION_A]);
  66. glBindTexture(GL_TEXTURE_BUFFER, m_pos_tbo[1]);
  67. glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, m_vbo[POSITION_B]);
图2 弹簧质点系统中的顶点连接

然后我们看到代码清单 1.2 占 99% 功能的用于状态模拟的顶点着色器。position_mass 是位置信息,velocity 是速度信息,connection 是连接信息。为了随机索引位置信息,tex_position 将位置信息作为纹理。这边只要知道按上述介绍的公式“一顿操作”,更新的质点位置和速度信息以顶点反馈的方式,输出到了 tf_position_mass 和 tf_velocity 中。

代码清单 1.2 弹簧质点系统顶点着色器
  1. #version 410 core
  2.  
  3. // This input vector contains the vertex position in xyz, and the
  4. // mass of the vertex in w
  5. layout (location = 0) in vec4 position_mass;
  6. // This is the current velocity of the vertex
  7. layout (location = 1) in vec3 velocity;
  8. // This is our connection vector
  9. layout (location = 2) in ivec4 connection;
  10.  
  11. // This is a TBO that will be bound to the same buffer as the
  12. // position_mass input attribute
  13. uniform samplerBuffer tex_position;
  14.  
  15. // The outputs of the vertex shader are the same as the inputs
  16. out vec4 tf_position_mass;
  17. out vec3 tf_velocity;
  18.  
  19. // A uniform to hold the timestep. The application can update this
  20. uniform float t = 0.07;
  21.  
  22. // The global spring constant
  23. uniform float k = 7.1;
  24.  
  25. // Gravity
  26. const vec3 gravity = vec3(0.0, -0.08, 0.0);
  27.  
  28. // Global damping constant
  29. uniform float c = 2.8;
  30.  
  31. // Spring resting length
  32. uniform float rest_length = 0.88;
  33.  
  34. void main(void)
  35. {
  36.     vec3 p = position_mass.xyz;    // p can be our position
  37.     float m = position_mass.w;     // m is the mass of our vertex
  38.     vec3 u = velocity;             // u is the initial velocity
  39.     vec3 F = gravity * m - c * u;  // F is the force on the mass
  40.     bool fixed_node = true;        // Becomes false when force is applied
  41.  
  42.     for (int i = 0; i < 4; i++)
  43.     {
  44.          if (connection[i] != -1)
  45.          {
  46.              // q is the position of the other vertex
  47.              vec3 q = texelFetch(tex_position, connection[i]).xyz;
  48.              vec3 d = q - p;
  49.              float x = length(d);
  50.              F += -k * (rest_length - x) * normalize(d);
  51.              fixed_node = false;
  52.          }
  53.     }
  54.  
  55.     // If this is a fixed node, reset force to zero
  56.     if (fixed_node)
  57.     {
  58.          F = vec3(0.0);
  59.     }
  60.  
  61.     // Accelleration due to force
  62.     vec3 a = F / m;
  63.  
  64.     // Displacement
  65.     vec3 s = u * t + 0.5 * a * t * t;
  66.  
  67.     // Final velocity
  68.     vec3 v = u + a * t;
  69.  
  70.     // Constrain the absolute value of the displacement per step
  71.     s = clamp(s, vec3(-25.0), vec3(25.0));
  72.  
  73.     // Write the outputs
  74.     tf_position_mass = vec4(p + s, m);
  75.     tf_velocity = v;
  76. }

最后,我们看到代码清单 1.3 中的渲染部分,第 6 至 16 行就是“核心”,我们看它是如何进行迭代的:先绑定不同的顶点数组对象,以便引用到不同的输入,同时更新对应的纹理缓冲绑定;将另一组“备用”的缓冲设置为顶点反馈输出;再将输出结果作为下一次的输入,依次迭代。

还有需要说明的一点是,模拟计算前使用 glEnable(GL_RASTERIZER_DISCARD) 取消光栅化,因为这些内容不用输出(而且更新的程序压根也没有片段着色器)。另一个程序需要输出前记得恢复:glDisable(GL_RASTERIZER_DISCARD)。

代码清单 1.4 弹簧质点系统迭代循环
  1. void render(double t)
  2. {
  3.     int i;
  4.     glUseProgram(m_update_program);
  5.     glEnable(GL_RASTERIZER_DISCARD);
  6.     for (i = iterations_per_frame; i != 0; i--)
  7.     {
  8.          glBindVertexArray(m_vao[m_iteration_index & 1]);
  9.          glBindTexture(GL_TEXTURE_BUFFER, m_pos_tbo[m_iteration_index & 1]);
  10.          m_iteration_index++;
  11.          glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_vbo[POSITION_A + (m_iteration_index & 1)]);
  12.          glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 1, m_vbo[VELOCITY_A + (m_iteration_index & 1)]);
  13.          glBeginTransformFeedback(GL_POINTS);
  14.          glDrawArrays(GL_POINTS, 0, POINTS_TOTAL);
  15.          glEndTransformFeedback();
  16.     }
  17.  
  18.     glDisable(GL_RASTERIZER_DISCARD);
  19.     glClearBufferfv(GL_COLOR, 0, sb7::color::Black);
  20.  
  21.     glUseProgram(m_render_program);
  22.     if (draw_points)
  23.     {
  24.          glDrawArrays(GL_POINTS, 0, POINTS_TOTAL);
  25.     }
  26. }

裁剪

裁剪的内部实现很复杂,但是通过 OpenGL 来达到裁剪的效果很简单。OpenGL 定义了内置变量 gl_ClipDistance 指示点到裁剪平面的距离。如果点到裁剪平面的距离为负数,则会按相应逻辑被裁剪掉。

我们直接看到代码清单 2.1 的例子,例子中进行普通平面裁剪和球型平面裁剪。

代码清单 2.1 将对象裁剪成平面和球体
  1. #version 410 core
  2.  
  3. // Per-vertex inputs
  4. layout (location = 0) in vec4 position;
  5. layout (location = 1) in vec3 normal;
  6.  
  7. uniform mat4 mv_matrix;
  8. uniform mat4 proj_matrix;
  9.  
  10. // Clip plane
  11. uniform vec4 clip_plane = vec4(1.0, 1.0, 0.0, 0.85);
  12. uniform vec4 clip_sphere = vec4(0.0, 0.0, 0.0, 4.0);
  13.  
  14. void main(void)
  15. {
  16.     // Calculate view-space coordinate
  17.     vec4 P = proj_matrix * mv_matrix * position;
  18.  
  19.     // Write clip distances
  20.     gl_ClipDistance[0] = dot(P, clip_plane);
  21.     gl_ClipDistance[1] = length(P.xyz / P.w - clip_sphere.xyz) - clip_sphere.w;
  22.  
  23.     // Calculate the clip-space position of each vertex
  24.     gl_Position = P;
  25. }

统一变量 clip_plane 设置为平面的法向量,w 分量为到原点的偏移。保证 clip_plane 是单位向量的话,点积运算就是点到平面的距离。我们将面设置成移动的 y-z 面,裁剪效果如视频 1 所示。

视频 1 按面裁剪

统一变量 clip_sphere 设置为球的中心点,w 分量为球的半径。点到球平面的距离为点到球的中心减去球的半径。我们将球的中心设置到原点,逐渐增大半径,裁剪效果如视频 2 所示。

视频 2 按球面裁剪

总结

至此,我们第 7 章就学习完毕了。我们学习了不同的绘图指令:索引绘图指令、实例化绘图指令和间接绘图指令。接着学习了顶点反馈的方法和裁剪。

后续就会开始第 8 章的学习,按照管线的流程,将学习曲面细分着色器和几何着色器。