概述
在图形基本图元相互重叠情况下,Metal 按照画家算法来绘制图元,即按照图元提交的顺序来渲染它们。这种绘制方式一定可以得到正确的绘制结果,但是这种方法不足以渲染复杂的 3D 场景。 为了解决这个问题, 需要使用 Metal 提供的深度测试来确定每个片段在渲染时的可见性,消除隐藏的面。
深度是从观察位置到特定像素的距离的度量。使用深度测试时,需要将深度纹理(有时称为深度缓冲区)添加到渲染通道中。深度纹理为每个像素存储深度值,存储方式与颜色纹理保存颜色值相同。每个片段的深度值是通过硬件插值每个顶点的深度来实现的。 GPU 测试新的片段以查看它们是否比存储在深度纹理中的当前值更接近查看位置,如果片段距离较远,GPU 会丢弃该片段;否则,它会更新像素数据,包括新的深度值。因为 GPU 会测试所有片段的深度,所以即使三角形被部分遮挡,它也能正确渲染三角形。
本文示例通过更改一个三角形每个顶点的深度值来演示深度测试。每个片段的深度由三角形顶点的深度插值计算而来, GPU 依据上述规则来进行深度测试。每次执行渲染通道时,都会清除深度纹理的数据,然后在中间点渲染一个灰色方块,最后再渲染三角形。只有比灰色方块更接近观察者的片段才会被渲染出来。
创建深度纹理
默认情况下,MTKView 不会创建深度纹理。需要设置 depthStencilPixelFormat 属性,让它的值跟想要的深度纹理的数据格式一致。视图会自动创建和管理它们。
mtkView.depthStencilPixelFormat = MTLPixelFormatDepth32Float;
本示例对每个像素使用 32 位浮点深度值。
在渲染管线中指定深度格式
要启用渲染管道的深度测试,需要在创建渲染管道时,在描述符上设置 depthAttachmentPixelFormat 属性,如下所示:
pipelineStateDescriptor.depthAttachmentPixelFormat = mtkView.depthStencilPixelFormat;
与颜色格式一样,渲染管道需要有关深度纹理格式的信息,以便它可以在纹理中读取或写入值。添加深度纹理时,深度纹理的格式需要与视图的深度格式相同,Metal 会在渲染管线上其他阶段使用相同的深度值:
配置深度测试
在 Metal 中,渲染管道和深度测试是独立配置的,可以将渲染管道与深度测试混合搭配使用。
和使用渲染管道一样,在初始化 App 时,使用 MTLDepthStencilState 对象来指定深度测试,并在需要执行该测试时保留对它的引用。
与上述流程图中所描述的一样,当新的深度值小于深度纹理中目标像素的现有值时,就可以通过深度测试,这表明该片段比之前渲染的任何内容都更接近观看者。当深度测试通过时,片段的颜色值被写入颜色渲染附件中,同时新的深度值被写入深度附件中。如下代码描述了如何配置深度测试:
``` MTLDepthStencilDescriptor *depthDescriptor = [MTLDepthStencilDescriptor new];
depthDescriptor.depthCompareFunction = MTLCompareFunctionLessEqual;
depthDescriptor.depthWriteEnabled = YES;
depthState = [device newDepthStencilStateWithDescriptor:depthDescriptor]; ```
``` 注释: Metal 将深度测试与模板测试相结合,执行类似的测试,通过存储每个像素的计数来表示片段通过深度测试的次数。
模板操作对于实现某些 3D 算法很有用。默认情况下,模板测试是禁用的,本示例未启用它。 ```
在着色器中生成深度值
初始化步骤完成后,可以编写顶点着色器代码。在 《 Metal 框架之渲染管线渲染图元 》中,了解到 Metal 的归一化设备坐标 (NDC) 系统使用四维坐标,在此坐标系中顶点着色器必须为每个顶点提供一个位置。该示例忽略了 z 坐标,但要实现深度测试,需要为 z 坐标提供一个值。
因此本示例提供了用户界面来配置 z 值,并将这些值传递给顶点着色器,然后着色器获取输入数据上的 z 值并将它们传递到输出的 z 组件。
out.clipSpacePosition.z = vertices[vertexID].position.z;
当光栅化器计算要发送到片段着色器的数据时,它会在这些 z 值之间进行插值:
片元函数可以根据需要读取、忽略或修改 z 值。在不修改光栅化器计算的值情况下,GPU 有时可以执行额外的优化。例如,它能够在运行片段着色器之前执行 z 测试,这样就不会为隐藏的片段运行片段着色器。当更改片段着色器中的深度值, GPU 必须先执行片段着色器,这可能会导致性能下降。
清除深度纹理
渲染通道拥有一个目标纹理列表,其中包括(可选)深度纹理。使用深度测试时,需要配置渲染通道的深度纹理附件。将视图配置为包含深度纹理,当访问视图的渲染通道描述符时,会自动配置描述符的深度渲染目标指向深度纹理,还配置渲染通道以清除每帧开始处的深度纹理。需要做的就是为深度纹理提供起始深度值。
mtkView.clearDepth = 1.0;
当渲染通道开始时,深度纹理中的每个像素都被初始化为 1.0 。
编码绘图命令
与其他 Metal 渲染示例一样,此示例创建了一个渲染命令编码器,然后对一系列绘制命令进行编码。
[renderEncoder setDepthStencilState:_depthState];
设置参数和编码绘制命令的其余代码与您在使用渲染管线渲染基元中已经看到的类似。
此示例使用着色器对两个绘制命令进行编码。首先,它在视图中渲染一个深度值为 0.5四边形。由于四边形的所有顶点深度值都小于默认值,所以四边形总是被绘制到渲染目标中,并且深度值总是被更新。然后该示例使用用户界面设置的深度值来渲染一个三角形。如果将三角形任意顶点的深度值增加到 0.5 以上,则三角形会部分消失,因为一些片段在四边形后面,并且无法通过深度测试。
尝试使用滑块,看看结果如何变化。
总结
本文介绍了 Metal 下的深度测试,示例通过更改一个三角形每个顶点的深度值来演示深度测试。每个片段的深度由三角形顶点的深度插值计算而来, GPU 依据上述规则来进行深度测试。