[TOC]
- 完成深度测试学习,并提交Git
- 完成模板测试学习,并提交Git
- 完成混合学习,并提交Git
- 完成面剔除学习,并提交Git
- 完成帧缓冲学习,并提交Git
- 完成立方体贴图学习,并提交Git
- 完成高级数据学习,并提交Git
- 完成高级GLSL学习,并提交Git
- 完成几何着色器学习,并提交Git
- 完成实例化学习,并提交Git
- 完成抗锯齿学习,并提交Git
深度测试 教程页
深度缓冲(或z缓冲(z-buffer))中的深度值(Depth Value)与颜色缓冲一样,有着一样的宽度和高度.
深度缓冲是由窗口系统自动创建的,以多种float形式存储他的值,大部分系统中,深度缓冲的精度为24位.
深度测试是在片段着色器运行之后(以及模板测试(Stencil Testing)运行之后),在屏幕空间中运行的;可以使用GLSL内建变量GL_FragCoord从片段着色器中直接访问;其中的x,y表示的片段的屏幕空间坐标,而z分量包含了片段真正的深度值,深度测试对比的就是z分量.
特性/提示
大部分GPU提供一个叫提前深度测试(Early Depth Testing)的硬件特性,允许深度测试在片段着色器之前运行,但前提是我们非常确定这个片段永远是不可见的.
片段着色器通常开销很大,应尽量避免其运行;当使用提前深度测试时,片段着色器会限制你不能写入片段的深度值,片段着色器对深度值进行写入,提前深度测试是不可能的,OpenGL不能提前知道深度值.
深度测试默认是禁用的
glEnable(GL_DEPTH_TEST);
深度测试启用时,片段的深度测试通过,OpenGL会丢弃原本存储的z值;未通过时,会丢弃该片段.启用深度测试,应该在每个渲染迭代前使用GL_DEPTH_BUFFER_BIT来清除深度缓冲,否则深度值会一直为上一次渲染迭代的值.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
使用深度掩码可以设置深度缓冲的可读性,例如我们希望开启深度测试,但不希望深度缓冲更新,让它成为一个只读(Read-only).
glDepthMask(GL_FALSE);
OpenGL允许我们修改深度测试中使用的比较运算符(也就是让我们自己决定什么条件下更新深度缓冲),调用glDepthFunc函数来修改.
glDepthFunc(GL_LESS);
函数 | 描述 |
---|---|
GL_ALWAYS | 永远通过深度测试 |
GL_NEVER | 永远不通过深度测试 |
GL_LESS | 在片段深度值小于缓冲深度时通过测试 |
GL_EQUAL | 在片段深度值等于缓冲深度时通过测试 |
GL_LEQUAL | 在片段深度值等于小于缓冲深度时通过测试 |
GL_GEQUAL | 在片段深度值大于等于缓冲深度时通过测试 |
GL_GREATER | 在片段深度值大于缓冲深度时通过测试 |
GL_NOEQUAL | 在片段深度值不等于缓冲深度时通过测试 |
默认使用GL_LESS,它会丢弃深度值大于等于当前深度值的片段.
深度缓冲区的缓冲值介于0.0和1.0之间,其值将会与平截头体内所有的对象的z缓冲进行比较;视图空间中的这些z值可以是平截头体near和far中的任何值;我们需要线性转化他们,使z缓冲位于[0.0,1.0],以下是转化线性方程
$$F_{depth} = {z - near \over Far - near}$$
实践中,线性深度缓冲区几乎从未使用过.
特性/提示
所有的方程都会将非常近的物体的z缓冲设置为接近0.0的值,而物体非常接近远平面时,他的z缓冲将会非常接近1.0.
为了有正确的投影性质1,需要使用一个非线性方程,他与1/z成正比;他能做到的就是在z值很小的时候提供非常高的精度,而在z值很大的时候提供更少的精度.
$$F_{depth} = {1/z - 1/near\over 1/far - 1-near}$$
这个非线性方程与1/z成正比,在1.0和2.0之间的z值会变换到1.0到0.5之间的深度值,float为我们提供了一半的精度(z值小精度大),50.0到100.0之间的z值只会占用2%的float精度
只需记住z缓冲的值在屏幕空间中并不是现行的(在透视矩阵应用之前在观察空间之中是线性的).
我们可以使用内建gl_FragCoord向量的z值来完成深度缓冲的可视化.
void main()
{
FragColor = vec4(vec3(gl_FragCoord.z),1.0f);
}
由于非线性方程的缘故,为了观察到我们希望的黑白渐变图像,应该拉近摄像机,使片元接近进平面.
该图很清晰的展示了深度值的非线性特质.
两个面非常紧密排列在一起会发生一个常见的视觉错误,z缓冲没有足够的精度来决定那个在前面.结果就是两个面不断地切换先后顺序,这个现象叫做深度冲突.
深度冲突是很常见的问题,尤其是在远处的物体(z值在较大的时候拥有很小的精度).
- 永远不要把多个物体摆的太近,以至于他们的一些三角形会重叠.
- 尽可能将进平面设置的远一些.
- 牺牲一些性能,使用精度更高的深度缓冲. 目前有很多抗深度冲突的技术,但都不能完全解决深度冲突.
模板测试 教程页
当片段着色器处理完一个片段之后,模板测试(Stencil Test)会开始执行,与深度测试相同的是它也会丢弃片段,被保留的片段进入深度测试;模板测试是根据模板缓冲(Stencil Buffer)来进行的.
一个模板缓冲中,通常每个模板值(Stencil Value)是8位的.所以每个像素可以有256钟模板值.我们可以自行设置,当某一个片段为某一个模板值时,我们可以决定是否丢弃它.
特性/提示 每个窗口库都需要为你配置一个模板缓冲;GLFW自动做了这件事,但其他的窗口库不一定会.
模板缓冲案例
模板缓冲首先被清除为0,然后填充一个空心的1,场景中的片段只有模板值为1时才被渲染.
模板缓冲允许我们在渲染片段时将模板缓冲设定为一个特定的值;通过在渲染时修改模板缓冲的内容,我们写入了模板缓冲.在同一个(或接下来)渲染迭代中,这些值我们可以读取,来决定是否丢弃.
大体步骤:
- 启用模板缓冲的写入.
- 渲染物体,更新模板缓冲.
- 禁用模板缓冲的写入.
- 渲染(其他)物体,这次根据模板缓冲的内容丢弃特定的片段.
使用GL_STENCIL_TEST来启用模板测试
glEnable(GL_STENCIL_TEST);
与颜色和深度缓冲一样,每次渲染迭代之前都应该清除缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
和深度测试的glDepthMask函数一样,模板测试也有类似的函数.glStencilMask允许我们设置一个位掩码(BitMask),它会与将要写入缓冲的模板值进行与(AND)运算.默认情况下设置的所有位掩码都为1,不影响输出,但如果我们将其设置为0x00,写入缓冲的模板值最后都为0,这与深度测试中glDepthMask(GL_FALSE)是等价的.
glStencilMask(0xFF); // 每一位写入模板缓冲时都保持原样
glStencilMask(0x00); // 每一位在写入模板缓冲时都会被改为0(禁止写入)
大部分情况下只会使用0xFF与0x00作为模板掩码.
和深度测试一样,模板测试应该通过还是失败,以及他应该如何影响模板缓冲,是由两个函数能够用来配置的; glStencilFunc和glStencilOp
glStencilFunc(GLenum func, GLint ref, GLuint mask);
- func: 设置模板测试函数(Stencil Test Function).这个测试函数将会应用到已储存的模板值上和ref上;可用的选项有GL_NEVER,GL_LESS,GL_LEQUAL,GL_GREATER,GL_GEQUAL,GL_EQUAL,GL_NOTEQUAL,GL_ALWAYS;语义与深度缓冲类似.
- ref: 设置了模板测试的参考值(Rederence Value).模板缓冲的内容将会于这个值进行比较.
- mask: 设置一个掩码,他将会与参考值和存储的模板值在测试比较他们之前进行与(AND)运算,初始情况下所有值都为1.
上述模板测试案例中,函数被设置为:
glStencilFunc(GL_EQUAL, 1, 0xFF);
这会告诉OpenGL,只要一个片段的模板值等于(GL_EQUAL)参考值1,片段将会通过测试并被绘制,否则会被丢弃.
但是glStencilFunc仅仅描述了OpenGL应该对模板缓冲内容做什么,但不知道应该如何更新缓冲;需要glStencilOp函数.
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass);
- sfail: 模板测试失败时采取的行为.
- defail: 模板测试通过,但深度测试失败采取的行为.
- dppass: 模板深度测试均通过时采取的行为.
每个选项都可以选用以下的其中一种行为:
行为 | 描述 |
---|---|
GL_KEEP | 保持当前储存的模板值 |
GL_ZERO | 将模板值设置为0 |
GL_REPLACE | 将模板值设置为glStencilFun的ref值 |
GL_LNCR | 如果模板值小于最大值将模板值加1 |
GL_INCR_WRAP | 与GL_INCR一样,但如果模板值超过了最大值则归零 |
GL_DECR | 如果模板值大于最小值则将模板值减1 |
GL_DECR_WRAP | 与GL_DECR一样,但如果模板值小于0则将其设置为最大值 |
GL_INVERT | 按位翻转当前的模板缓冲值 |
默认情况下glStencilOp是设置为(GL_KEEP,GL_KEEP,GL_KEEP)的,所以不论测试结果如何,模板缓冲都会保留他的值.
为每个,或一个物体在他周围创建一个有色的边框;步骤如下:
- 在绘制(需要添加轮廓)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将这个片段模板缓冲更新为1.
- 渲染物体.
- 禁用模板写入以及深度测试.
- 将物体缩放一点点.
- 使用一个不同的片段着色器,输出一个单独的(边框)颜色.
- 再次绘制物体,但只在他们片段的模板值不等于1时才绘制.
- 再次启用模板写入和深度测试.
个人理解:
- 第一遍渲染物体,物体所在的屏幕空间区域模板值为1其余为0.
- 禁用模板写入和深度测试
- 再次绘制物体(边框),只有模板值不为1,且在物体(边框)所在的屏幕区域绘制.
- 再次启用模板写入和深度测试。
轮廓线添加流程
glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glStencilMask(0x00); // 记得保证我们在绘制地板的时候不会更新模板缓冲
normalShader.use();
DrawFloor()
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
DrawTwoContainers();
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
DrawTwoScaledUpContainers();
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);
混合 教程链接
OpenGL中,混合(Blending)通常实现的是物体的透明度;最好的混合案例解释通过有色玻璃观察物体.
透明的物体可以是完全透明,或是半透明的;一个物体的透明度由其alpha值来决定;1.0
和0.0
分别代表不透明和透明,0.5
代表颜色50%来自自身,其余50%来自其后方的物体.
一些材质会有一个内嵌的alpha通道,对每个纹素(Texel)2都包含了一个alpha值.
有些材质不需要半透明,只需要根据颜色的值,显示一部分,丢弃一部分.比如游戏中的常客草.
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D texture1;
void main()
{
vec4 texColor = texture(texture1, TexCoords);
if(texColor.a < 0.1) // 丢弃alpha值小于0.1的片段
discard;
FragColor = texColor;
}
!!!warning
注意,当采样纹理的边缘的时候,OpenGL会对边缘的值和纹理下一个重复的值进行插值(因为我们将它的环绕方式设置为了GL_REPEAT。这通常是没问题的,但是由于我们使用了透明值,纹理图像的顶部将会与底部边缘的纯色值进行插值。这样的结果是一个半透明的有色边框,你可能会看见它环绕着你的纹理四边形。要想避免这个,每当你alpha纹理的时候,请将纹理的环绕方式设置为GL_CLAMP_TO_EDGE
glsl glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
启用混合来实现多个透明度级别的图像;它与大多数OpenGL的功能一样,我们可以使用GL_BLEND
来启用它.
glEnable(GL_BLEND);
启用之后,我们要告诉OpenGL他应该如何进行混合.
OpenGL中混合的实现方程:
$$\bar{C}{\text {result }}=\bar{{\color{Green} C} }{\text {source }} * {\color{Green} F} {\text {source }}+\bar{{\color{Blue} C} }{\text {destination }} * {\color{Blue} F} _{\text {destination }}$$
-
$\bar{{\color{Green} C} }_{\text {source }}$ : 源颜色向量.这是源自纹理的颜色向量.(人话:最前面的物体的颜色). -
${\color{Green} F} _{\text {source }}$ : 源因子值3.指定了alpha值对源颜色的影响.
-
$\bar{{\color{Blue} C} }_{\text {destination }}$ : 目标颜色向量,这是当前储存在颜色缓冲中的颜色向量. -
${\color{Blue} F} _{\text {destination }}$ : 目标因子值4.指定了alpha值对目标颜色的影响.
片段着色器运行完成后,并且所有的测试都通过之后,这个混合方程(Blend Equation)才会应用到片段颜色输出与当前颜色缓冲中的值(当前片段之前储存的之前片段的颜色)上。源颜色和目标颜色将会由OpenGL自动设定,但源因子和目标因子的值可以由我们来决定.
案例:
我们有两个方形,我们希望将这个半透明的绿色方形绘制在红色方形之上.红色的方形将会是目标颜色(所以它应该先在颜色缓冲中),我们将要在这个红色方形之上绘制这个绿色方形.
问题来了:我们将因子值设置为什么?嘛,我们至少想让绿色方形乘以它的alpha值,所以我们想要将 ${\color{Green} F} {\text {source }}$ 设置为源颜色向量的alpha值,也就是0.6。接下来就应该清楚了,目标方形的贡献应该为剩下的alpha值。如果绿色方形对最终颜色贡献了60%,那么红色方块应该对最终颜色贡献了40%,即1.0 - 0.6
。所以我们将${\color{Blue} F}{\text {destination }}$设置为1减去源颜色向量的alpha值。这个方程变成了:
结果就是重叠方形的片段包含了一个60%绿色,40%红色的一种脏兮兮的颜色:
最终的颜色将会被储存到颜色缓冲中,替代之前的颜色.
使用glBlendFunc可以决定我们如何使用因子.
glBlendFunc(GLenum sfactor,GLenum dfactor)
函数可以接收两个参数,来设置源和目标因子.OpenGL为我们定义了很多个选项;颜色向量可以通过$\bar {C}_{\text {constant }}$可以通过glBlendColor
函数来另外设置.
选项 | 值 |
---|---|
GL_ZERO |
因子等于0 |
GL_ONE |
因子等于1 |
GL_SRC_COLOR |
因子等于源颜色向量$\bar{{\color{Green} C} }_{\text {source }}$ |
GL_ONE_MINUS_SRC_COLOR |
因子等于 1 - |
GL_DST_COLOR |
因子等于目标颜色向量$\bar{{\color{Blue} C} }_{\text {destination }}$ |
GL_ONE_MINUS_DST_COLOR |
因子等于 1 - |
GL_SRC_ALPHA |
因子等于$\bar{{\color{Green} C} }_{\text {source }}$的alpha分量 |
GL_ONE_MINUS_SRC_ALPHA |
因子等于 1 - |
GL_DST_ALPHA |
因子等于$\bar{{\color{Blue} C} }_{\text {destination }}$的alpha分量 |
GL_ONE_MINUS_DST_ALPHA |
因子等于 1 − |
GL_CONSTANT_COLOR |
因子等于常数颜色向量$\bar {C}_{\text {constant }}$ |
GL_ONE_MINUS_CONSTANT_COLOR |
因子等于1 − |
GL_CONSTANT_ALPHA |
因子等于$\bar {C}_{\text {constant }}$的alpha分量 |
GL_ONE_MINUS_CONSTANT_ALPHA |
因子等于1 − |
也可以使用glBlendFuncSeparate
为RGB和alpha通道分别设置不同的选项:
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
OpenGL甚至给了我们更多的灵活性,允许我们改变方程中源和目标部分的运算符。当前源和目标是相加的,但如果愿意的话,我们也可以让它们相减。glBlendEquation(GLenum mode)
允许我们设置运算符,它提供了三个选项:
- GL_FUNC_ADD:默认选项,将两个分量相加
- GL_FUNC_SUBTRACT:将两个分量相
- GL_FUNC_REVERSE_SUBTRACT:将两个分量相减,但顺序相反
glBlendEquation(GLenum mode)
通常可以忽略,除非你想搞些新花样.
首先,启用混合,并设定相关的混合函数.
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
!!!tip
混合是不需要在GLClear
中清除缓冲的.
每当OpenGL渲染了一个片段时,他都会将当前的颜色和当前颜色缓冲中的片段颜色根据alpha值来进行混合.由于窗户纹理的玻璃部分是半透明的,我们可以透过窗户看到后面的背景了.
很明显有错误的地方,产生错误的原因是深度测试与混合测试同时启用,当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会和其他值一样写入到深度缓冲当中.结果就是窗户不论是否透明都会进行深度测试,即使透明的部分应该显示后面的窗户,深度测试仍然会丢弃它们.
!!!tip 渲染草时不会出现这种问题是因为我们直接丢弃了片段.
想让混合正确的工作,我们需要最先绘制最远的物体,普通不需要混合的物体仍然可以使用深度缓冲正常绘制,所以他们不需要排序.(人话:半透明物体由远及近绘制)但我们仍要保证他们在绘制(排序的)透明物体之前已经绘制完毕了.
当绘制有一个存在半透明的物体的场景时,答题的原则如下:
- 先绘制所有不透明的物体
- 对所有的半透明物体排序
- 按照由远及近的顺序绘制所有的不透明物体
排序透明物体的一种方法是,从观察者(摄像机)视角获取物体的距离.这可以通过摄像机位置向量和物体的位置向量之间的距离获得.接下来我们使用stl
库的map
数据结构来对应存储.map
会根据键的值来对他的值进行排序.
std::map<float,glm::vec3> sorted;
for(unsigned int i = 0; i < windows.size(); i++)
{
float distance = glm::length(camera.position - window[i]);
sorted[distance] = window[i];
}
结果就是一个排序后的容器对象,他根据distance
键的值由低到高进行排序,存储了每个窗户的位置.
渲染时,我们获取其逆序,以正确的顺序进行渲染:
for(std::map<float, glm::vec3>::reverse_iterator it = sorted.rbegin(); it != sorted.rend(); ++it)
{
model = glm::mat4(1.0f);
model = glm::translate(model, it->second);
shader.setMat4("model",model);
glDrawArrays(GL_TRIIANGLES, 0, 6);
}
我们使用了map的一个反向迭代器(Reverse iterator),反向遍历其中的条目,并将每个窗户移动到对用的位置.
虽然当前看起来可以正常工作,但是它没有考虑旋转,缩放或者其他变换,奇怪形状的物体需要一个不同的计量,而不仅仅是一个位置向量.
在场景中排序物体是一个很困难的技术,它需要很多的额外处理能力,完整渲染一个包含不透明和透明物体的场景很难.更高级的技术还有次序无关透明度(Order Independent Transparency, OIT).
面剔除 教程地址
绘制立方体时我们可以想象,我们最多可同时看见立方体的三个面,那么对其他的面的渲染是多余的;并且立方体的内部我们是不关心的,也看不到的,如果不去渲染这些面,我们将省去很大的开销!
我们只渲染面向观察者的面,如何绘制只面向观察者的面正式面剔除(face culling)所做的!
OpenGL能够检查所有面向(front facing)观察者的面,并渲染他们,而丢弃那些背向的面,节省很多片段着色器的调用(它们开销很大).但我们需要告诉OpenGL那些面是正向面,而那些不是.
OpenGL使用的技巧是顶点数据的环绕顺序;当我们定义三角形顶点时,可能是逆时针的,可能是顺时针的;我们需要为这三个顶点设置环绕顺序.
下面代码展示了这点:
float vertices[] = {
// 顺时针
vertices[0], // 顶点1
vertices[1], // 顶点2
vertices[2], // 顶点3
// 逆时针
vertices[0], // 顶点1
vertices[2], // 顶点3
vertices[1] // 顶点2
};
在渲染图元的过程中,这些信息将决定这个三角形是背对的还是正对的.
面剔除默认是禁用的.开启他的方法与之前的深度测试等等的开启方式是相同的;开启后我们就可以使用面剔除选项的了.
glEnable(CL_CULL_FACE);
但是,上一节的草出现了问题(地面和窗户也是),内面全部被丢弃了,如果我们想渲染草这样的非封闭物体,我们就必须关闭面剔除.
OpenGL允许我们改变需要剔除的面的类型.如果我们想剔除的是正面而不是背面我们可以通过调用glCullFace(GL_FRONT);
来定义这一行为.
glCullFace
函数有三个可用的选项.
GL_BACK
: 只剔除背向面.GL_FRONT
: 只剔除正向面.GL_FRONT_AND_BACK
: 剔除正向面和背向面.
glFrontFace
: 告诉OpenGL如何定义正向面.
GL_CCW
: 逆时针GL_CW
: 顺时针
!!!note 这课好少,对比之前的也简单,莫名感动!!!