这篇文章介绍的内容范围还是在顶点着色器中,将介绍变换顶点的保存和裁剪。变换顶点通过一个弹簧连接点模拟例子进行说明;裁剪同样也通过一个小例子进行展示。
变换顶点的保存
在 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] 中的内容一样。
即程序用到两个顶点数组对象,用于程序切换。五个顶点缓冲对象:位置和速度信息各两个,用于迭代;连接情况是固定的,因此只需要一个,只读。两个纹理缓冲对象,为了随机索引位置信息。
然后我们看到代码清单 1.2 占 99% 功能的用于状态模拟的顶点着色器。position_mass 是位置信息,velocity 是速度信息,connection 是连接信息。为了随机索引位置信息,tex_position 将位置信息作为纹理。这边只要知道按上述介绍的公式“一顿操作”,更新的质点位置和速度信息以顶点反馈的方式,输出到了 tf_position_mass 和 tf_velocity 中。
最后,我们看到代码清单 1.3 中的渲染部分,第 6 至 16 行就是“核心”,我们看它是如何进行迭代的:先绑定不同的顶点数组对象,以便引用到不同的输入,同时更新对应的纹理缓冲绑定;将另一组“备用”的缓冲设置为顶点反馈输出;再将输出结果作为下一次的输入,依次迭代。
还有需要说明的一点是,模拟计算前使用 glEnable(GL_RASTERIZER_DISCARD) 取消光栅化,因为这些内容不用输出(而且更新的程序压根也没有片段着色器)。另一个程序需要输出前记得恢复:glDisable(GL_RASTERIZER_DISCARD)。
裁剪
裁剪的内部实现很复杂,但是通过 OpenGL 来达到裁剪的效果很简单。OpenGL 定义了内置变量 gl_ClipDistance 指示点到裁剪平面的距离。如果点到裁剪平面的距离为负数,则会按相应逻辑被裁剪掉。
我们直接看到代码清单 2.1 的例子,例子中进行普通平面裁剪和球型平面裁剪。
统一变量 clip_plane 设置为平面的法向量,w 分量为到原点的偏移。保证 clip_plane 是单位向量的话,点积运算就是点到平面的距离。我们将面设置成移动的 y-z 面,裁剪效果如视频 1 所示。
统一变量 clip_sphere 设置为球的中心点,w 分量为球的半径。点到球平面的距离为点到球的中心减去球的半径。我们将球的中心设置到原点,逐渐增大半径,裁剪效果如视频 2 所示。
总结
至此,我们第 7 章就学习完毕了。我们学习了不同的绘图指令:索引绘图指令、实例化绘图指令和间接绘图指令。接着学习了顶点反馈的方法和裁剪。
后续就会开始第 8 章的学习,按照管线的流程,将学习曲面细分着色器和几何着色器。