实践项目:https://github.com/JackieLong/OpenGL/tree/main/project_depth_test
深度缓冲和颜色缓冲(Color Buffer)一样,每个片段都存储了深度值,宽度和高度也一般相同。由窗口系统自动创建,精度有16bit、24bit、32bit的float形式,一般都是24bit。
发生在逐片段操作中的模板测试(Stencil Test)之后。
点击查看【processon】
工作原理
伪代码如下:
frag_depth // 新生成的片元的Z值
frag_index // 在深度缓冲中,对应片元的索引
depthBuffer // 深度缓冲,存储了每个像素(片段)的z值
if ( 开启了深度测试 )
{
if(depthFunc(frag_depth, frag_index, depthBuffer)) // 深度测试函数:执行深度测试
{
depthBuffer[frag_index] = frag_depth; // 深度测试通过,更新到深度缓冲中
}
else
{
discard; // 测试没有通过,丢弃该片段
}
}
内建变量
如何获取片段的深度值:
#version 330 core
void main()
{
vec3 screenPos = gl_FragCoord; // 内建变量,片段的屏幕空间坐标,左下角为坐标原点
float depth = screenPos.z; // 片段的深度值,用于与深度缓冲的深度值比较。
}
开启深度测试
glEnable(GL_DEPTH_TEST); // 开启深度测试,默认是关闭的。
glDepthMask(GL_FALSE); // 对所有片段都执行深度测试并丢弃相应的片段,但不希望更新深度缓冲。
// 必须要开启深度测试才有效
while(!glfwWindowShouldClose(window))
{
glClear(GL_DEPTH_BUFFER_BIT); // 在每帧开始前清除深度缓冲,否则会使用上一次的深度值。
......;
}
深度测试函数
可以自定义深度测试逻辑。
glDepthFunc(FUNC); // 默认是GL_LESS
// FUNC的可能取值如下:(depthFrag为片段深度值,depthBuffer为深度缓冲区中的深度值)
// GL_ALWAYS 永远通过深度测试,后绘制的一律覆盖之前绘制的。
// GL_NEVER 永远不通过深度测试,一律保留最先绘制的。
// GL_LESS depthFrag < depthBuffer,通过测试,即丢弃 >= depthBuffer的片段,默认值
// GL_EQUAL depthFrag == depthBuffer,通过测试
// GL_LEQUAL depthFrag <= depthBuffer,通过测试
// GL_GREATER depthFrag > depthBuffer,通过测试
// GL_NOTEQUAL depthFrag != depthBuffer,通过测试
// GL_GEQUAL depthFrag >= depthBuffer,通过测试
Z值与深度值
这两者是完全不同的东西,值是坐标值,可以为任意的浮点数值,深度值则必须满足以下特性:
depthBuffer; // 深度缓冲区中的深度值
0.0 <= depthBuffer <= 1.0; // 深度值的取值范围
depthBuffer = 0.0; // 表示片段在平截头体的近平面上(Near Plane),即就在我们眼前。
depthBuffer = 1.0; // 表示片段在平截头体的远平面上(Far Plane)
// depthBuffer越小,越靠近我们,即“在越前面”
如何得到深度值?通过投影矩阵变换,将顶点坐标的值转换成深度值。
#version 330 core
uniform vec3 pos; // 局部空间坐标
uniform mat4 modelMatrix; // 模型变换矩阵:局部坐标 -> 世界坐标
uniform mat4 viewMatrix; // 视图变换矩阵:世界坐标 -> 摄像机坐标
uniform mat4 projectionMatrix; // 投影变换矩阵:摄像机坐标 -> 裁剪坐标
void main()
{
// 裁剪空间坐标
vec3 clipPos = vec3(projectionMatrix * viewMatrix * modelMatrix * vec4(pos, 1.0));
// 裁剪坐标,特征如下:
float w = clipPos.w; // w分量
-w <= clipPos.x <= w;
-w <= clipPos.y <= w;
-w <= clipPos.z <= w;
float depthFrag = clipPos.z / w; // 透视除法:得到了片段的深度值。
......
}
深度值精度
具体是如何做到有如上深度值特征的呢?很自然地想到线性方程:
按这种方式实现的叫做线性深度缓冲(Linear Depth Buffer)。实际当中,很少采用,原因很简单,我们应该对越近的顶点采用越高的精度,较远的顶点采用较低的精度,具体啥意思?
z_near = 0.0f; // 近平面z坐标值
z_far = 100.0f; // 远平面z坐标值
z_node; // 平截头体内部任意顶点的z坐标值。
depth_node; // 该顶点的深度值
// 深度值的范围是[0.0, 1.0]
// 当采用线性深度缓冲时:
// 当z_near < z_node < 50.0f时,depth_node分布在[0.0,0.5]区间。
// 当50.0f < z_node < z_far时,depth_node分布在[0.5,1.0]区间。
// 这两者的深度值分布区间相同,也就是深度值精度相同。
// 按照实际,1000单位远的深度值和只有1单位远的充满细节的物体使用相同的精度?这大大浪费了分配给远处顶点的精度。
// 我们应该多分配一点给近的顶点,越近的,分配的越多。
显然我们需要一个非线性方程,z值越小,增速越快,才能做到分配给近处顶点更多的精度:
深度冲突(Z-fighting)
两个三角形平行排列在一起,由于深度缓冲精度限制,无法判断哪个三角形在前面,结果导致这两个三角形不断的切换前后顺序,出现很奇怪的花纹,这就是深度冲突。
防止出现深度冲突的方法:
- 不用把两个物体摆太近,防止三角形重叠
- 即使是真的靠在一起的物体,也可以人为偏离一点点,避免三角形重叠,在视觉上几乎无法发现。
- 把近平面设置远一些
- 靠近近平面,精度高一些。
- 使用更高精度缓冲
- 一般是24bit的float,可以改成32bit的float,这样会牺牲部分性能。