NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模
有时我们只想渲染屏幕的一部分,屏蔽掉其余的当前颜色缓冲区的任何更改。在 OpenGL 中有两种方法,Scissor Regions 和Stencil Buffer。本教程将解释如何在 OpenGL 应用程序中使用它们。
1、剪裁区域(Scissor Region)
在渲染时用于屏蔽后台缓冲区部分的两种方法中,较简单的是剪裁区域。剪裁区域是缓冲区的一个矩形部分,渲染在其中正常发生 - 任何在剪裁区域之外渲染的尝试都将被忽略丢弃。剪裁区域锁定所有缓冲区以防止写入 - 因此无法写入颜色和活动剪裁区域之外的深度缓冲区。
与其他 OpenGL 状态一样,当启用glEnable 渲染时,使用 GL SCISSOR TEST 符号常量对剪裁区域进行测试。一次只能激活一个剪裁区域,这个剪裁区域覆盖的区域由 glScissor OpenGL 函数设置。这需要4 个参数 - 剪裁区域的 x 和 y 轴起始位置,以及 x 和 y 轴区域的大小。与 glViewport 一样,这些参数位于屏幕空间中,因此以
像素为单位。值得注意的是,剪裁测试也会影响 glClear - 如果启用了剪裁测试,它只会清除剪裁区域内部的内容。
2、模板缓冲区(Stencil Buffer)
屏蔽部分屏幕的更高级方法是使用模板缓冲区。这是一个屏幕大小的缓冲区,就像深度缓冲区、前缓冲区和后缓冲区一样。与剪裁区域限制渲染到屏幕的单个矩形部分不同,模板缓冲区可以像颜色缓冲区一样被写入,然后在绘制几何图形时进行测试,就像你使用的深度缓冲区一样。
例如,可以在模板缓冲区中画一个圆圈,只有该圆圈内的片元被渲染 - 对于做狙击手的瞄准镜很方便!或者也许 在绘制游戏世界之前画HUD,将其遮盖以保存永远不会看到的渲染片段。
在模板缓冲区中为每个像素留出多少位是可变的,尽管在现代图形硬件上通常可以保证每个像素8 位。写入模板缓冲区的值 渲染期间是可编程的 - 写入可以递增、递减、覆盖或执行布尔值 现有模板值上的运算符。与深度缓冲区一样,我们可以决定允许或丢弃绘制到包含模板值的像素中的片段。
可以绘制多个对象到模板缓冲区中,每个缓冲区都会增加模板缓冲区中的现有值 - 然后只允许在模板缓冲区值大于 8 的像素上进一步绘制到颜色缓冲区中, 或者正好是 4,如果你真的想的话! 可以使用 GL STENCIL TEST 符号启用测试和写入模板缓冲区常量,并使用两个 OpenGL 函数进行控制 - glStencilFunc 和 glStencilOp。
glStencilFunc 控制如何测试模板缓冲区,并具有三个参数,func、ref 和 mask。模板测试的工作方式与前面介绍的深度测试相同 - 你可以检查现有值是否大于、小于或等于某个值,除其他外,设置 使用 func 参数。深度测试使用的值是眼睛空间 z 坐标,但模板 testing 使用 ref 参数提供的值。参考参数和现有的参数,与掩码参数的值进行与运算以进行比较。这允许我们为许多并发测试使用单个模板缓冲区。以下是使用模板的一些示例功能:
- glStencilFunc(GL ALWAYS, 1, 0) - 如果我们启用模板缓冲区,并使用此模板函数来确定进入模板缓冲区的内容,模板测试将始终通过(由于 GL 始终运行)。
- glStencilFunc(GL GREATER, 1, ∼0) - 在这种情况下,参考值(在这种情况下为 1)和模板缓冲区中的现有值将与掩码值〜0一起进行与运算,这将执行按位 NOT 运算。这会将值 0 反转为全 1 - 意味着掩码将保持值不受影响(与全为 1 的东西进行逻辑与操作得到原始值)。模板函数是 GL GREATER,则模具将只允许大于现有值的值通过 - 所以在在这种情况下,如果模板缓冲区中有 0,则此模板函数将通过(1 大于 0!),否则 stencil func 将失败,并且不会进行绘图。
- glStencilFunc(GL EQUAL, 255, 8) - 如果模板缓冲区中的值为 1,则此测试将失败,因为我们正在测试相等性 - 参考值 255 与 8 进行AND操作(只有第 4 位 mask 已启用)为 8,而 1 与 8 与的缓冲区值为 0。0 和 8 显然不相等,所以当前处理的片段被丢弃。但是,如果现有的模板缓冲区值确实有 第 4 位设置,则模板测试将通过。
当模板测试通过或失败时会发生什么由 glStencilOp 函数确定。 这个函数有 3 个参数,它们控制当模板测试失败时会发生什么,当模板测试通过但随后片元未通过深度测试,以及当模板和深度测试都通过时(或者模板测试通过并且深度测试被禁用)。然后我们可以保留、重置、 增加或替换当前模板缓冲区值。这允许你针对模板缓冲区进行测试,无需实际更新其内容。更多例子!
- glStencilOp(GL ZERO, GL KEEP, GL KEEP) - 如果模板测试失败,这将设置 该测试位置的模板缓冲区为零。如果模板测试通过,即使深度测试失败,那么 模板缓冲区被单独留下。
- glStencilOp(GL KEEP, GL KEEP, GL REPLACE) - 在这个例子中,如果模板测试 失败,或者当深度测试失败时通过,什么都没有发生。然而,如果模板和深度测试 通过,模板缓冲区中的值被模板函数的当前参考值替换。
- glStencilOp(GL KEEP, GL REPLACE, GL KEEP) - 在这里,如果模板测试通过,但是深度测试失败,模板缓冲区中的值将被替换。这可以用来确定其中一个对象与另一个对象相交,因为对象 A 与对象 B 相交的部分深度测试将失败 。
3、示例程序
本教程示例程序将做两件事 - 使用剪裁测试来限制渲染 一个三角形到屏幕中间的一个矩形区域,使用模板测试来限制渲染通过 alpha 映射纹理到屏幕上的棋盘图案,以显示几何图形的绘制方式 进入模板缓冲区会影响后续渲染。
我们将重用之前的顶点着色器 教程系列,但是要编写一个新的片元着色器 - 所以添加一个名为 StencilFragment.glsl 的新文本文件 到 Shaders 文件夹,然后添加一个 Renderer 类和一个 Tutorial5.cpp 文件到 Tutorial5 项目。
4、渲染器头文件
在我们的 Renderer 类头文件中,我们有两个新的公共函数 - ToggleScissor 和 ToggleStencil, 它控制几个受保护的成员变量,usingScissor 和 usingStencil。我们还声明 两个网格。
# pragma once
# include "./ nclgl / OGLRenderer .h"
class Renderer : public OGLRenderer {
public :
Renderer ( Window & parent );
virtual ~ Renderer ( void );
virtual void RenderScene ();
void ToggleScissor ();
void ToggleStencil ();
protected :
Mesh * triangle ;
Mesh * quad ;
bool usingScissor ;
bool usingStencil ;
};
5、渲染器类文件
像往常一样,我们使用构造函数启动 Renderer 类。我们用它来初始化我们的两个网格, 加载着色器,并加载我们将在本教程中使用的两个纹理。
注意着色器 使用我们在教程 3 中写的 TexturedVertex.glsl 顶点着色器和 我们很快就会写出来的StencilFragment.glsl 文件。我们创建的所有东西当然都必须删除,所以我们的析构函数删除两个网格,这反过来又会删除我们的纹理。
现在我们将摆脱无聊的切换功能。与以前的教程一样,他们使用 NOT 布尔运算符来翻转我们的布尔值,但这次我们不直接启用或禁用 相关的 OpenGL 状态 - 为什么会很快解释!
void Renderer :: ToggleScissor () {
usingScissor = ! usingScissor ;
}
void Renderer :: ToggleStencil () {
usingStencil = ! usingStencil ;
}
这次的 RenderScene 函数有点复杂。像往常一样,我们清除 缓冲区——这一次,使用 多个GL STENCIL BUFFER BIT 符号常量的或(OR) ,者会将模板缓冲区清除为全 1 的值。然后,如果选择了剪裁测试,我们 将glEnable scissor 测试,并使用 glScissor 将进一步的场景渲染限制为大致在 屏幕中间——记住,glScissor 直接在屏幕坐标中工作,所以我们使用了 屏幕宽度和高度 OGLRenderer 成员变量来定义区域。另外,我们等到 在调用 glClear 之后 - 请记住,glScissor 会影响 glClear!这就是为什么我们不直接启用 或在我们的切换功能中禁用剪刀测试。
void Renderer :: RenderScene () {
glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT| GL_STENCIL_BUFFER_BIT );
if( usingScissor ) {
glEnable ( GL_SCISSOR_TEST );
glScissor (( float ) width / 2.5 f , ( float ) height / 2.5 f ,
( float ) width / 5.0 f , ( float ) height / 5.0 f );
}
然后,我们绑定着色器,并像往常一样更新它的矩阵。在纹理教程中,我们也 将着色器的diffuseTex 纹理采样器绑定到纹理单元0。
glUseProgram ( currentShader - > GetProgram ());
UpdateShaderMatrices ();
glUniform1i ( glGetUniformLocation ( currentShader - > GetProgram () , " diffuseTex ") , 0);
这是有趣的一点!如果我们启用了模板测试,我们将绘制一个屏幕大小的 屏幕上方的四边形,其中应用了棋盘纹理。黑色棋盘格将 将值 2 写入模板缓冲区,然后我们将其设置为仅允许后续绘制 如果该片段处的模板缓冲区等于,则通过。
但是,我们实际上并不希望四边形渲染 到屏幕,只到模板缓冲区,所以我们使用一个你以前可能没有遇到过的函数: glColorMask。此功能可让你关闭每个通道的颜色写入 - 有 4 个参数,1 每个代表红色、绿色、蓝色和 alpha,我们将它们都设置为 false。
即使这将禁用所有 任何被写入活动缓冲区的东西,所有几何图形仍将通过渲染管道, 所以它仍然会影响模板缓冲区。 因此,我们启用模板缓冲区,关闭颜色写入,使用模板函数始终写入 值 2 到模板缓冲区中,然后在屏幕上绘制我们的棋盘纹理四边形。我们 然后重新打开颜色写入,并将模板缓冲区设置为只允许片元传递部分 模板缓冲区的值为 2 ;我们也不希望有其他任何东西改变模板缓冲区,所以 我们使用 glStencilOp 来保留模板缓冲区,因为无论它包含什么。
if( usingStencil ) {
glEnable ( GL_STENCIL_TEST );
glColorMask ( GL_FALSE , GL_FALSE , GL_FALSE , GL_FALSE );
glStencilFunc ( GL_ALWAYS , 2 , ~0);
glStencilOp ( GL_REPLACE , GL_REPLACE , GL_REPLACE );
quad - > Draw ();
glColorMask ( GL_TRUE , GL_TRUE , GL_TRUE , GL_TRUE );
glStencilFunc ( GL_EQUAL , 2 , ~0);
glStencilOp ( GL_KEEP , GL_KEEP , GL_KEEP );
}
最后,绘制三角形。我们还 glDisable scissor 和 stencil 测试,为下一帧的测试做好准备 渲染。
triangle - > Draw ();
glUseProgram (0);
glDisable ( GL_SCISSOR_TEST );
glDisable ( GL_STENCIL_TEST );
SwapBuffers ();
}
6、主文件
很好很简单,我们从第一个主文件中改变的只是几个键盘检查来切换剪裁和模板测试。
# include "./ nclgl / window .h"
2 # include " Renderer .h"
3 # pragma comment ( lib , " nclgl .lib ")
4
int main () {
Window w (" Index Buffers !", 800 ,600 , false );
if (! w . HasInitialised ()) {
return -1;
}
Renderer renderer ( w );
if (! renderer . HasInitialised ()) {
return -1;
}
while ( w . UpdateWindow () &&
! Window :: GetKeyboard () - > KeyDown ( KEYBOARD_ESCAPE )){
if( Window :: GetKeyboard () - > KeyTriggered ( KEYBOARD_1 )) {
renderer . ToggleScissor ();
}
if( Window :: GetKeyboard () - > KeyTriggered ( KEYBOARD_2 )) {
renderer . ToggleStencil ();
}
renderer . RenderScene ();
}
return 0;
}
7、片元着色器
你可能想知道棋盘纹理如何选择性地写入模板缓冲区。 好吧,纹理的白色瓦片的 alpha 值为 0 - 我们可以在片段中检查 着色器!你可能已经写过类似上一个教程的东西,但如果没有,这里是如何 根据 alpha 丢弃片段,这样深度或模板缓冲区都不会更新。 我们做一个简单的 if 语句来检查传入的 alpha 值是否为 0.0,如果是,则使用 GLSL 关键字丢弃。请注意,我们实际上从未在 OpenGL 中启用 alpha 混合——能够 从纹理中采样 alpha 值完全独立于 alpha 混合过程。
# version 150 core
uniform sampler2D diffuseTex ;
in Vertex {
vec2 texCoord ;
} IN ;
out vec4 fragColour ;
void main ( void ) {
vec4 value = texture ( diffuseTex , IN . texCoord ). rgba ;
if( value . a == 0.0) {
discard ;
}
fragColour = value ;
}
8、运行程序
如果一切正常,运行这个程序时你会在屏幕上看到一个带纹理的三角形——不是 很有意思!
按 1 键将启用剪刀测试,并限制三角形的绘制 到屏幕中间的一个小盒子。按 2 键将启用模板测试,结果是 棋盘式的绘图限制。这是由于我们在四边形上使用的棋盘纹理 如果启用了模板测试,我们会在屏幕上绘制——“白”板的 alpha 为 0.0,所以 只有“黑色”板件写入模板缓冲区。
当我们禁用颜色写入时,颜色缓冲区 不是由我们的棋盘四边形写入,但模板缓冲区已更新 - 然后我们对其进行测试 画三角形的时候。模板缓冲区和剪裁测试不是相互排斥的——我们可以启用, 如果我们愿意的话!
如果你仍然不太了解模板缓冲区的工作原理,请尝试注释掉 第 55 行并再次运行它,它应该更清楚棋盘纹理是如何选择性地选择的 禁用颜色写入屏幕的各个部分。
9、教程总结
完成这个简单的教程后,你应该对如何使用这两个模板有一个很好的了解:游戏渲染中的缓冲区和剪裁区域。剪裁区域没什么好说的, 但是模板缓冲区可能更复杂一些 - 可以添加多个模板区域 一起在模板缓冲区中,使用 glStencilFunc 的掩码变量;但现在你应该有 对模板缓冲区有足够的了解,可以在你的游戏中做一些有趣的效果。在接下来的几 教程,我们将开始扩展我们在教程 1 中创建的 Mesh 类,以支持更多 高效的渲染方法称为索引缓冲区。它还将向你展示如何使用 高度图 - 最后,不再有简单的四边形和三角形!我们还将研究如何组织你的游戏 使用称为场景图的对象。
BimAnt翻译整理,转载请标明出处