在Metal中如何加载并绘制带有透明度的纹理

移动开发 2026-07-11

背景


我终于开始把这款非常古老的Mac万花筒应用ScopeWorks,从用OpenGL和 Objective-C编写,改造成在SwiftUI和 Metal上跨平台运行的应用。

我使用MetalKit的 MTKView类,并通过SwiftUI将其封装为NSViewRepresentable或 UIViewRepresentable。随后提供一个MTKViewDelegate,里面实现draw方法。draw方法会获取当前的渲染通道描述符,创建命令缓冲区,设置渲染管线,并执行绘制。

我的渲染器中的makePipeline方法看起来是这样的:

func makePipeline() {
    let library = device.makeDefaultLibrary()
    let pipelineDesc = MTLRenderPipelineDescriptor()
    pipelineDesc.vertexFunction = library?.makeFunction(name: "vertex_main")
    pipelineDesc.fragmentFunction = library?.makeFunction(name: "fragment_main")
    pipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
    pipeline = try! device.makeRenderPipelineState(descriptor: pipelineDesc)
}

而我的着色器看起来是这样的:

struct VertexOut {
    float4 position [[position]];
    float2 texCoord;
};

vertex VertexOut vertex_main(const device float2* position [[buffer(0)]],
                             uint vid [[vertex_id]]) {
    VertexOut out;
    float2 pos = position[vid];
    out.position = float4(pos, 0, 1);
    out.texCoord = pos * 0.5 + 0.5; // basic mapping
    return out;
}

fragment float4 fragment_main(VertexOut in [[stage_in]],
                              texture2d<float> tex [[texture(0)]],
                              constant float4& color [[buffer(1)]]) {
    constexpr sampler s(address::repeat, filter::linear);
    //    float4 texColor = tex.sample(s, in.texCoord);
    //    return texColor * color;
    float4 textureColor = {1, 2, 3, 4};
    if (all(color == textureColor)) {
        return tex.sample(s, in.texCoord);
    } else {
        return color;
    }

    // Sample the texture directly — no color tint applied
    return tex.sample(s, in.texCoord);
}

MTKViewDelegate的 draw方法的前半部分看起来像这样:

    func draw(in view: MTKView) {
        guard let drawable = view.currentDrawable,
              let descriptor = view.currentRenderPassDescriptor,
              let pipeline = pipeline,
              let texture = texture else { return }

        let commandBuffer = commandQueue.makeCommandBuffer()!
        let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)!
        encoder.setRenderPipelineState(pipeline)
        encoder.setFragmentTexture(texture, index: 0)
        descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0, blue: 0, alpha: 1.0)

        // Draw six equilateral triangles forming the hexagon
        let radius: Float = 0.6
        for i in 0..<6 {
            let angle = Float(i) * (.pi / 3)
            let cosA = cos(angle)
            let sinA = sin(angle)
            let nextA = Float(i+1) * (.pi / 3)
            let cosB = cos(nextA)
            let sinB = sin(nextA)
            let verts: [simd_float2] = [
                simd_float2(0, 0),
                simd_float2(radius * cosA, radius * sinA),
                simd_float2(radius * cosB, radius * sinB)
            ]
            encoder.setVertexBytes(verts, length: MemoryLayout<simd_float2>.stride * 3, index: 0)

            // Tell the fragment shader to use the texture color.
            var textureColor: simd_float4 = simd_float4(1, 2, 3, 4)
            encoder.setFragmentBytes(&textureColor, length: MemoryLayout<SIMD4<Float>>.stride, index: 1)

            encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)

现有应用的一个功能是加载带有Alpha通道的PNG或 TIFF图像,然后把图像的部分区域翻转叠加在自身之上,从而在最终的万花筒中形成有趣的摩尔条纹。

目前我只使用一个样本图像,将其加载到Metal的纹理中,然后把它渲染成六边形,并为构成六边形的三角形画出边线。(现在我把顶点坐标当作纹理坐标,因此生成的是纹理的六边形部分,而不是经三角剖分后得到的六边形中的单个三角形部分。等会再改。)

在iOS和 macOS上,我都在draw函数开始时将清除颜色设置为黑色。

问题:


源图像大部分是透明的,但有大量半透明像素。下面是在Photoshop中的效果,你可以看到透明部分以棋盘格模式呈现:

在 Photoshop 中的示意图

(我尝试裁剪原始图像以展示大致在六边形中渲染的部分,但并非完全精确。请在不同的图像中寻找相同的形状来对比。)

当我在iOS版本的应用里在Metal视图中渲染我的六边形时,似乎把每个像素强制为完全不透明或完全透明:

在 iOS 版本中的示意图

而在应用的macOS版本中,似乎把所有像素都强制为不透明:

在 macOS 版本中的示意图

我没有展示全部的设置代码,因为内容挺多的。是不是有某些渲染模式的设置我没有做,导致它无法按照像素的不透明度(包括半透明部分)输出到最终结果?

解决方案

好吧,我找到了原因。我把下面这段代码添加到了我的初始化设置中:

    func makePipeline() {
        let library = device.makeDefaultLibrary()
        let pipelineDesc = MTLRenderPipelineDescriptor()
        pipelineDesc.vertexFunction = library?.makeFunction(name: "vertex_main")
        pipelineDesc.fragmentFunction = library?.makeFunction(name: "fragment_main")
        pipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm

        // ---- New changes
        // Enable blending for transparent drawing
        pipelineDesc.colorAttachments[0].isBlendingEnabled = true
        pipelineDesc.colorAttachments[0].rgbBlendOperation = .add
        pipelineDesc.colorAttachments[0].alphaBlendOperation = .add
        pipelineDesc.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
        pipelineDesc.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
        pipelineDesc.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
        pipelineDesc.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
        // ---- end of new changes

        pipeline = try! device.makeRenderPipelineState(descriptor: pipelineDesc)
    }

这在两个平台上都解决了混合的问题。对黑色背景来说有点不太容易分辨,但当我在绘制前把Metal视图清空为白色时,就能看到图像中部分透明的区域。

结果如下:

在示意图中

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章