Metal 进阶:深度测试

449 阅读4分钟

引言

Hi, 大家好,我是一牛,今天继续带来Metal 技术分享。大家有没有思考过,当我们使用 Metal 绘制多个物体时,且它们位置重叠时,它们可见性是什么样? Metal 使用的是画家算法,后绘制的物体会遮盖先绘制的物体。但是当我们绘制 3D 场景时,这种算法很低效,我们需要能够独立调整物体的可见性。为此,Metal 提供了一种深度测试技术,可以很方便的调整物体的可见性。接下来我们将深入探讨这种技术。

深度测试原理

当我们为Metal 开启深度测试时,它会在渲染管线中增加一个额外步骤,发生在片元着色之后。深度测试依赖于深度缓冲(Depth Buffer), 它存储了当前缓冲区中的每个像素的深度值。

Screenshot 2025-02-26 at 11.56.50.png

工作流程
  1. 初始化时,我们将深度缓冲的所有值设置为1.0 (最大深度值)
  2. 在深度测试阶段时,比较片元的深度值和深度缓冲区的深度值
  3. 如果片元的深度值通过检测(如 lessEqual),则更新深度缓冲区并渲染片元,否则丢弃该片元

应用

创建深度缓冲 Metal 默认关闭深度缓冲。

mtkView.depthStencilPixelFormat = .depth32Float

初始化深度缓冲。设置为最大缓冲值。

mtkView.clearDepth = 1.0

开启深度测试。我们将测试方法设置为.lessEqual,表示当片元深度值小于等于当前缓冲区的深度值,则更新深度缓冲区并渲染片元,否则丢弃该片元。

let depthDescriptor = MTLDepthStencilDescriptor()
depthDescriptor.depthCompareFunction = .lessEqual
depthDescriptor.isDepthWriteEnabled = true
depthState = device.makeDepthStencilState(descriptor: depthDescriptor)

创建渲染管线

// Create render pipeline
let pipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = vertextFunc
pipelineDescriptor.fragmentFunction = fragmentFunc
pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
pipelineDescriptor.depthAttachmentPixelFormat = mtkView.depthStencilPixelFormat
do {
    pipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
} catch {
    fatalError("fail to create pipelineState")
}

绘制图像。我们需要给渲染通道设置深度测试setDepthStencilState。我们先绘制了敖丙的纹理,然后绘制哪吒的纹理,根据画家算法,哪吒会完全遮挡敖丙。由于我们开启了深度测试,敖丙的顶点深度值是0.5,哪吒的深度值1.0, 所以我们看到的是敖丙。哪吒的顶点深度值是可变的(0到1之间浮动),当三角形顶点的深度值不一致,片元的深度值会被进行硬件插值,本例是线性插值。当绘制哪吒纹理的所有顶点全部变为0时,敖丙完全可见。

guard let renderPassDesriptor = view.currentRenderPassDescriptor else {
    return
}
let commanderBuffer = commandQueue.makeCommandBuffer()
let commanderEncoder = commanderBuffer?.makeRenderCommandEncoder(descriptor: renderPassDesriptor)
commanderEncoder?.setRenderPipelineState(pipelineState)
commanderEncoder?.setDepthStencilState(depthState)
commanderEncoder?.setVertexBytes(&viewPortSize, length: MemoryLayout<vector_int2>.stride, index: 1)
if let texture1 {
    let vertices = [
        // 左上角
        Vertex(position: [-256,  256, 0.5], textureCoordinate: [0.0, 0.0]),
        // 左下角
        Vertex(position: [-256, -256, 0.5], textureCoordinate: [0.0, 1.0]),
        // 右下角
        Vertex(position: [ 256, -256, 0.5], textureCoordinate: [1.0, 1.0]),
        // 左上角
        Vertex(position: [-256,  256, 0.5], textureCoordinate: [0.0, 0.0]),
        // 右下角
        Vertex(position: [ 256, -256, 0.5], textureCoordinate: [1.0, 1.0]),
        // 右上角
        Vertex(position: [ 256,  256, 0.5], textureCoordinate: [1.0, 0.0]),
    ]

    let vertexBuffer = device.makeBuffer(bytes: vertices, length: MemoryLayout<Vertex>.stride * vertices.count)
    commanderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)

    commanderEncoder?.setFragmentTexture(texture1, index: 0)
    commanderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
}

if let texture2 {
    let vertices = [
        // 左上角
        Vertex(position: [-256,  256, topLeftDepth], textureCoordinate: [0.0, 0.0]),
        // 左下角
        Vertex(position: [-256, -256, bottomLeftDepth], textureCoordinate: [0.0, 1.0]),
        // 右下角
        Vertex(position: [ 256, -256, bottomRightDepth], textureCoordinate: [1.0, 1.0]),
        // 左上角
        Vertex(position: [-256,  256, topLeftDepth], textureCoordinate: [0.0, 0.0]),
        // 右下角
        Vertex(position: [ 256, -256, bottomRightDepth], textureCoordinate: [1.0, 1.0]),
        // 右上角
        Vertex(position: [ 256,  256, topRightDepth], textureCoordinate: [1.0, 0.0]),
    ]

    let vertexBuffer = device.makeBuffer(bytes: vertices, length: MemoryLayout<Vertex>.stride * vertices.count)
    commanderEncoder?.setVertexBuffer(vertexBuffer, offset: 0, index: 0)

    commanderEncoder?.setFragmentTexture(texture2, index: 0)
    commanderEncoder?.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
}

commanderEncoder?.endEncoding()

guard let drawable = view.currentDrawable else {
    return
}
commanderBuffer?.present(drawable)
commanderBuffer?.commit()

着色器函数。在顶点着色阶段,这里我们使用了顶点缓冲的深度值。在光栅化阶段,Metal 会插值计算片元的深度值。

struct RasterizerData {
    float4 position [[position]];
    float2 textureCoordinate;
};

struct Vertex {
    float3 pixelPosition;
    float2 textureCoordinate;
};


vertex RasterizerData vertexShader(uint vertexID [[vertex_id]],
                           constant Vertex *vertices [[buffer(0)]],
                                   constant uint2 *viewportSizePointer [[buffer(1)]]
                                   ) {
    float2 pixelPosition = vertices[vertexID].pixelPosition.xy;
    float2 viewportSize = float2(*viewportSizePointer);
    RasterizerData out;
    out.position = float4(0.0, 0.0, 0.0, 1.0);
    out.position.xy = pixelPosition / (viewportSize / 2.0);
    out.position.z = vertices[vertexID].pixelPosition.z;
    out.textureCoordinate = vertices[vertexID].textureCoordinate;
    return out;
}

fragment float4 fragmentShader(RasterizerData in [[stage_in]],
                               texture2d<half> colorTexture [[texture(0)]]
                               ) {
    constexpr sampler textureSampler (mag_filter::linear,
                                      min_filter::linear);
    const half4 colorSample = colorTexture.sample(textureSampler, in.textureCoordinate);
    return float4(colorSample);
}

效果

当我们更改顶点深度值时,哪吒逐渐显示出来

ezgif-6c8c48e552ca60.gif

结语

深度测试可以动态的改变物体的深度值,调整物体的可见性,这在3D渲染时发挥着不可或缺的作用。

欢迎点赞、收藏。谢谢大家!

本项目已开源