引言
Hi, 大家好,我是一牛,今天继续带来Metal 技术分享。大家有没有思考过,当我们使用 Metal 绘制多个物体时,且它们位置重叠时,它们可见性是什么样? Metal 使用的是画家算法,后绘制的物体会遮盖先绘制的物体。但是当我们绘制 3D 场景时,这种算法很低效,我们需要能够独立调整物体的可见性。为此,Metal 提供了一种深度测试技术,可以很方便的调整物体的可见性。接下来我们将深入探讨这种技术。
深度测试原理
当我们为Metal 开启深度测试时,它会在渲染管线中增加一个额外步骤,发生在片元着色之后。深度测试依赖于深度缓冲(Depth Buffer), 它存储了当前缓冲区中的每个像素的深度值。
工作流程
- 初始化时,我们将深度缓冲的所有值设置为1.0 (最大深度值)
- 在深度测试阶段时,比较片元的深度值和深度缓冲区的深度值
- 如果片元的深度值通过检测(如 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);
}
效果
当我们更改顶点深度值时,哪吒逐渐显示出来
结语
深度测试可以动态的改变物体的深度值,调整物体的可见性,这在3D渲染时发挥着不可或缺的作用。
欢迎点赞、收藏。谢谢大家!