图形学系列专栏
- 序章 初探图形编程
- 第1章 你的第一个三角形
- 第2章 变换
- 顶点变换
- 视图矩阵 & 帧速率
- 第3章 纹理映射
- 第4章 透明度和深度
- 第5章 裁剪区域和模板缓冲区
- 第6章 场景图
- 第7章 场景管理
- 第8章 索引缓冲区
- 第9章 骨骼动画
- 第10章 后处理
- 第11章 实时光照(一)
- 第12章 实时光照(二)
- 第13章 立方体贴图
- 第14章 阴影贴图
- 第15章 延迟渲染
- 第16章 高级缓冲区
文章目录
- 图形学系列专栏
- 前言
- 裁剪区域
- 模板缓冲区
- 示例程序
- Renderer头文件
- Renderer类文件
- 主文件
- 片段着色器
- 总结
- 课后作业
前言
在某些情况下,我们可能希望只渲染屏幕的一部分,将当前颜色缓冲区的其余部分屏蔽起来,防止其发生任何变化。在 OpenGL 中有两种方法可以做到这一点,即裁剪区域和模板缓冲区。本教程将解释如何在你的 OpenGL 应用程序中使用这两种方法。
裁剪区域
在渲染时用于屏蔽后置缓冲区部分区域的两种方法中,较简单的一种是裁剪区域。裁剪区域是缓冲区的一个矩形部分,在其中渲染正常进行——任何在裁剪区域之外进行渲染的尝试都会被忽略——被丢弃。裁剪区域将所有缓冲区锁定,防止在活动裁剪区域之外进行写入——所以在活动裁剪区域之外不可能写入颜色缓冲区和深度缓冲区。
像其他 OpenGL 状态一样,在渲染时针对裁剪区域进行测试是通过使用glEnable
并传入GL_SCISSOR_TEST
符号常量来启用的。在同一时间只能有一个裁剪区域处于活动状态,并且这个裁剪区域所覆盖的区域是由 OpenGL 函数glScissor
设置的。这个函数接受四个参数——裁剪区域的 x 轴和 y 轴起始位置,以及该区域的 x 轴和 y 轴大小。像glViewport
一样,这些参数是在屏幕空间中,因此是以像素为单位进行测量的。值得注意的是,裁剪测试也会影响glClear
——如果裁剪测试被启用,它将只清除裁剪区域内的内容。
模板缓冲区
一种更高级的屏蔽屏幕部分区域的方法是使用模板缓冲区。这是一个与屏幕大小相同的缓冲区,就像深度缓冲区、前置缓冲区和后置缓冲区一样。与裁剪区域不同,裁剪区域将渲染限制在屏幕的单个矩形区域内,而模板缓冲区可以像渲染颜色缓冲区一样进行渲染;然后在绘制几何图形时进行测试,就像上一个教程中介绍的深度缓冲区一样。例如,你可以在模板缓冲区上绘制一个圆形,这样只有在那个圆形内的片段才会被渲染——这对于狙击镜很有用!或者在绘制游戏世界之前先绘制你的 HUD(抬头显示),然后将其屏蔽掉,以节省渲染那些永远不会被看到的片段。
虽然在现代图形硬件上通常可以保证每个像素有 8 位用于模板缓冲区,但为模板缓冲区中的每个像素预留的位数是可变的。在渲染期间写入模板缓冲区的值是可编程的——写入可以递增、递减、覆盖或对现有模板值执行布尔运算。与深度缓冲区一样,我们可以决定允许或丢弃绘制到包含模板值的像素中的片段。你可以将多个对象绘制到模板缓冲区中,每个对象都会递增模板缓冲区中的现有值——然后仅在模板缓冲区值大于 8 或确切为 4 的像素上允许进一步绘制到颜色缓冲区中,如果你真的想这样做的话!
可以使用GL_STENCIL_TEST
符号常量启用对模板缓冲区的测试和写入,并使用两个 OpenGL 函数glStencilFunc
和glStencilOp
进行控制。glStencilFunc
控制如何对模板缓冲区进行测试,它有三个参数:func、ref 和 mask。模板测试的工作方式与本教程系列前面介绍的深度测试相同——你可以使用 func 参数设置检查现有值是否大于、小于或等于某个值等。深度测试使用的是眼空间 z 坐标的值,但模板测试使用由 ref 参数提供的值。参考参数和现有参数都与 mask 参数的值进行与运算以进行比较。这允许我们使用单个模板缓冲区进行许多并发测试。以下是一些使用模板函数的示例:
glStencilFunc(GL_ALWAYS, 1, 0)
:如果我们启用模板缓冲区,并使用这个模板函数来确定写入模板缓冲区的内容,模板测试将总是通过(由于GL_ALWAYS
操作)。
glStencilFunc(GL_GREATER, 1, ~0)
:在这种情况下,参考值(这里是 1)和模板缓冲区中的现有值将与掩码值~0
进行按位与运算,这执行了按位取反操作。这将把值 0 反转成全为 1——意味着掩码将不影响值(与全为 1 的值进行与运算将得到原始值)。由于模板函数是GL_GREATER
,模板将只允许大于现有值的值通过——所以在这种情况下,如果模板缓冲区中有 0,这个模板函数将通过(1 大于 0!),否则模板函数将失败,并且不会进行绘制。
glStencilFunc(GL_EQUAL, 255, 8)
:如果模板缓冲区中的值是 1,这个测试将失败,因为我们在测试相等性——参考值 255 与 8 进行与运算(只有掩码的第四位被启用)是 8,而缓冲区值 1 与 8 进行与运算是 0。0 和 8 显然不相等,所以当前处理的片段将被丢弃。然而,如果现有的模板缓冲区值确实设置了第四位,那么模板测试将通过。
当模板测试通过或失败时会发生什么是由glStencilOp
函数决定的。这个函数接受三个参数,分别控制当模板测试失败时、当模板测试通过但片段在深度测试中失败时以及当模板和深度测试都通过(或者模板测试通过且深度测试被禁用)时会发生什么。然后我们可以保持、重置、递增或替换当前模板缓冲区的值。这允许你在需要时对模板缓冲区进行测试而不实际更新其内容。更多示例!
glStencilOp(GL_ZERO, GL_KEEP, GL_KEEP)
:如果模板测试失败,这将把该测试位置的模板缓冲区设置为零。如果模板测试通过,即使深度测试失败,那么模板缓冲区将保持不变。
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE)
:在这个例子中,如果模板测试失败,或者在深度测试失败时通过,什么都不会发生。然而,如果模板和深度测试都通过,模板缓冲区中的值将被模板函数的当前参考值替换。
glStencilOp(GL_KEEP, GL_REPLACE, GL_KEEP)
:在这里,如果模板测试通过,但深度测试失败,模板缓冲区中的值将被替换。这可以用于确定一个对象与另一个对象相交的位置,因为对象 A 与对象 B 相交的部分将在深度测试中失败。
示例程序
本教程的示例程序将做两件事——使用裁剪测试将三角形的渲染限制在屏幕中间的一个矩形区域内,并使用模板测试通过一个 alpha 映射纹理将渲染限制在屏幕上的棋盘图案,以展示绘制到模板缓冲区中的几何图形如何影响后续的渲染。我们将重用本教程系列前面的一个顶点着色器,但编写一个新的片段着色器——所以在你的“…/Shaders/”文件夹中添加一个名为“StencilFragment.glsl”的新文本文件,并向“Tutorial5”项目中添加一个“Renderer”类和一个“Tutorial5.cpp”文件。
Renderer头文件
在我们的渲染器类头文件中,有两个新的公共函数 ——ToggleScissor
和 ToggleStencil
,它们使用保护成员变量 usingScissor
和 usingStencil
来进行控制。我们还声明了两个网格和几个纹理,这些将用于创建一个 “剪切” 效果。
#pragma once
#include "../nclgl/OGLRenderer.h"
class Renderer : public OGLRenderer
{
public:
Renderer(Window& parent);
~Renderer(void);
void RenderScene() override;
void ToggleScissor();
void ToggleStencil();
protected:
Shader* shader;
Mesh* meshes[2];
GLuint textures[2];
bool usingScissor;
bool usingStencil;
};
Renderer类文件
像往常一样,我们从渲染器类的构造函数开始。它的布局与上一个教程中的类似:我们加载两个网格和两个纹理。这次我们还将把它们设置为重复,原因你很快就会看到!注意,这个着色器使用我们在第 3 章中编写的TexturedVertex.glsl顶点着色器和我们稍后将编写的StencilFragment.glsl文件。我们创建的所有东西当然都必须删除,所以我们的析构函数会删除我们的两个网格、着色器和纹理。
#include "Renderer.h"
Renderer::Renderer(Window& parent) : OGLRenderer(parent) {
meshes[0] = Mesh::GenerateTriangle();
meshes[1] = Mesh::GenerateQuad();
textures[0] = SOIL_load_OGL_texture(TEXTUREDIR "brick.tga", SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, 0);
textures[1] = SOIL_load_OGL_texture(TEXTUREDIR "star.png", SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, 0);
if (!textures[0] || !textures[1]) {
return;
}
SetTextureRepeating(textures[0], true);
SetTextureRepeating(textures[1], true);
shader = new Shader("TexturedVertex.glsl", "StencilFragment.glsl");
if (!shader->LoadSuccess()) {
return;
}
usingScissor = false;
usingStencil = false;
init = true;
}
Renderer ::~Renderer(void) {
delete meshes[0];
delete meshes[1];
glDeleteTextures(2, textures);
delete shader;
}
现在我们先处理那些简单的切换方法。和之前的教程一样,它们使用逻辑非运算符来翻转我们的布尔值,但这次我们不直接启用或禁用相关的 OpenGL 状态——原因稍后会解释!
void Renderer::ToggleScissor()
{
usingScissor = !usingScissor;
}
void Renderer::ToggleStencil()
{
usingStencil = !usingStencil;
}
这次RenderScene
方法稍微复杂一些。像往常一样,我们清除缓冲区——这次,使用按位或运算符加上GL_STENCIL_BUFFER_BIT
符号常量,这将把模板缓冲区清除为全为 1 的值。然后,如果裁剪测试被选中,我们使用glEnable
启用裁剪测试,并使用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.5f, (float)height / 2.5f, (float)width / 5.0f, (float)height / 5.0f);
}
然后,像往常一样,我们绑定我们的着色器,并更新它的矩阵。为了创建剪切效果,我们需要重复纹理,所以纹理矩阵被设置为缩放我们对象的坐标。和在纹理教程中一样,我们也将我们的着色器的diffuseTex
纹理采样器绑定到纹理单元 0。
BindShader(shader);
textureMatrix = Matrix4::Scale(Vector3(6, 6, 6));
UpdateShaderMatrices();
glUniform1i(glGetUniformLocation(shader->GetProgram(), "diffuseTex"), 0);
这里是有趣的部分!如果我们启用了模板测试,我们将在屏幕上绘制一个全屏大小的四边形,它将把一个星形形状渲染到模板缓冲区中。用于渲染四边形的着色器将丢弃任何 alpha 通道为 0 的片段,只留下纹理的星形形状被写入,在那里它将把值 2 写入模板缓冲区。然后,模板测试被设置为只通过模板值为 2 的片段,迫使之后渲染的几何图形呈现出模板缓冲区内容的形状。我们不想实际看到全屏四边形,所以在第 61 行我们使用一个新函数“glColorMask”。这个函数允许你按通道关闭颜色写入——有四个参数,分别对应红色、绿色、蓝色和 alpha,我们将它们都设置为 false。这只禁用了颜色写入,但是模板缓冲区仍然会被写入。
为了防止任何其他东西写入模板缓冲区,设置模板函数为无论片段通过还是失败都保持内容不变。
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);
glBindTexture(GL_TEXTURE_2D, textures[1]);
meshes[1]->Draw();
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glStencilFunc(GL_EQUAL, 2, ~0);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
}
最后,绘制三角形。我们也禁用裁剪测试和模板测试,为下一帧的渲染做好准备。
glBindTexture(GL_TEXTURE_2D, textures[0]);
meshes[0]->Draw();
glDisable(GL_SCISSOR_TEST);
glDisable(GL_STENCIL_TEST);
}
主文件
非常简单,我们与第一个主文件相比所做的改变只是一些键盘检查来切换裁剪测试和模板测试。
#include "../nclGL/window.h"
#include "Renderer.h"
#include <string>
using std::string;
int main() {
Window w("Scissors and Stencils!", 800, 600, false); //This is all boring win32 window creation stuff!
if(!w.HasInitialised()) {
return -1;
}
Renderer renderer(w); //This handles all the boring OGL 3.2 initialisation stuff, and sets up our tutorial!
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();
renderer.SwapBuffers();
}
return 0;
}
片段着色器
你可能一直在想星形纹理是如何有选择地写入模板缓冲区的。嗯,大部分纹理的 alpha 值为 0——我们可以在片段着色器中检查这个!你可能在上一个教程中已经写过类似的东西,但如果没有,下面是如何根据片段的 alpha 值丢弃一个片段,这样深度缓冲区和模板缓冲区都不会被更新。我们做一个简单的“if”语句来检查输入的 alpha 值是否为 0.0,如果是,就使用 GLSL 关键字“discard”。注意我们实际上在 OpenGL 中从未启用 alpha 混合——能够从纹理中采样 alpha 值与 alpha 混合过程是完全分开的。
#version 330 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;
}
运行程序
如果一切正常,在运行这个程序时你会在屏幕上看到一个有纹理的三角形——不是很有趣!按数字键 1 将启用裁剪测试,并将三角形的绘制限制在屏幕中间的一个小方框内。按数字键 2 将启用模板测试,导致绘制受到类似棋盘的限制。这是由于如果启用了模板测试,我们在屏幕上绘制的四边形上使用的棋盘纹理——“白色”棋盘格的 alpha 值为 0.0,所以只有“黑色”棋盘格写入模板缓冲区。由于我们禁用了颜色写入,我们的棋盘四边形不会写入颜色缓冲区,但模板缓冲区会被更新——然后在绘制三角形时我们对其进行测试。模板缓冲区和裁剪测试不是相互排斥的——如果我们愿意,我们可以同时启用它们!
左边的图像与中间图像的模板区域相结合,形成了右边的图像。
总结
在完成这个简单的教程后,你应该对如何在游戏渲染中使用模板缓冲区和裁剪区域有了很好的了解。关于裁剪区域没有太多要说的了,但是模板缓冲区可能会更复杂一些——可以使用glStencilFunc
的掩码变量在模板缓冲区中添加多个模板区域;但现在你应该对模板缓冲区有足够的了解,可以在你的游戏中实现一些有趣的效果。在接下来的几个教程中,我们将开始扩展在教程 1 中创建的Mesh类,以支持一种更有效的渲染方法,称为索引缓冲区。它还将向你展示如何使用高度图创建地形——终于,不再是简单的四边形和三角形了!我们还将看看如何使用一种叫做场景图的东西来组织你的游戏对象。
课后作业
- 目前程序设置为对黑色棋盘格进行模板处理 —— 如果要对白色棋盘格进行模板处理,需要进行哪些更改?
- 就像颜色缓冲区及其
glClearColor
函数一样,可以设置模板缓冲区的清除值。研究glClearStencil
函数。 - 可以通过关闭对单个位的写入进一步控制对模板缓冲区的写入。研究 OpenGL 函数
glStencilMask
。
欢迎大家踊跃尝试,期待同学们的留言!!