PAYATHON 2026

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

数据阿明

给出的 render pipeline 没有启用 alpha blending,需要先加上这个配置。fragment shader 返回带 alpha 的颜色,不会自动与 render target 中已有的颜色混合。清屏颜色也应在创建 render command encoder 之前设置。

纹理加载代码没有提供,目前无法确定纹理中的 alpha 是否保留、RGB 是否已经预乘 alpha,也不能仅凭描述断定 iOS 和 macOS 的表现差异来自同一个设置。

1. 在 render pipeline 中启用 alpha blending

如果 fragment shader 输出的是非预乘颜色,也就是 RGB 尚未乘以 alpha,可以这样修改现有方法:

func makePipeline() {
    let library = device.makeDefaultLibrary()
    let pipelineDesc = MTLRenderPipelineDescriptor()

    pipelineDesc.vertexFunction = library?.makeFunction(name: "vertex_main")
    pipelineDesc.fragmentFunction = library?.makeFunction(name: "fragment_main")

    let attachment = pipelineDesc.colorAttachments[0]!
    attachment.pixelFormat = .bgra8Unorm
    attachment.isBlendingEnabled = true

    attachment.rgbBlendOperation = .add
    attachment.sourceRGBBlendFactor = .sourceAlpha
    attachment.destinationRGBBlendFactor = .oneMinusSourceAlpha

    attachment.alphaBlendOperation = .add
    attachment.sourceAlphaBlendFactor = .one
    attachment.destinationAlphaBlendFactor = .oneMinusSourceAlpha

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

这里保留了原来的 .bgra8Unorm。它必须与 MTKView.colorPixelFormat 一致;如果视图使用其他格式,需要同步调整 pipeline 的格式。

上述配置对应的混合公式为:

RGB_out = RGB_src × A_src + RGB_dst × (1 − A_src)
A_out   = A_src + A_dst × (1 − A_src)

src 是当前 fragment shader 的输出,dst 是 render target 中已有的内容。启用混合后,部分透明的像素才能按比例与黑色背景或先前绘制的图案混合。

alpha 通道的 source factor 使用 .one,避免把源 alpha 再乘一次自身。

2. 根据实际纹理数据选择预乘 alpha 配置

PNG 或 TIFF 文件带有 alpha channel,不能说明加载到 GPU 后的 RGB 是否已经预乘 alpha。需要检查实际的图像解码和纹理上传路径。

两种数据表示的区别如下:

数据表示半透明红色示例sourceRGBBlendFactor
非预乘 alpha(1.0, 0.0, 0.0, 0.5).sourceAlpha
预乘 alpha(0.5, 0.0, 0.0, 0.5).one

如果纹理已经预乘 alpha,且 shader 直接返回采样结果,应把上面配置中的一行改为:

attachment.sourceRGBBlendFactor = .one

其余混合设置保持一致。此时 RGB 的公式变为:

RGB_out = RGB_src + RGB_dst × (1 − A_src)

已经预乘的 RGB 如果再使用 .sourceAlpha,会重复乘 alpha,半透明区域容易发暗。非预乘的 RGB 如果使用 .one,则可能出现过亮或透明边缘带颜色的问题。

应根据 fragment shader 最终输出的数据表示选择混合方式。如果 shader 修改了 RGB 或 alpha,还要确保修改后的数据仍符合所选方式。

3. 在创建 encoder 之前设置清屏参数

现有代码先创建 encoder,再修改 descriptor.colorAttachments[0].clearColor。这个修改不能用于配置已经开始的 render pass,应移到创建 encoder 之前。

draw 方法开头可以调整为:

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

    let attachment = descriptor.colorAttachments[0]!
    attachment.loadAction = .clear
    attachment.clearColor = MTLClearColor(
        red: 0.0,
        green: 0.0,
        blue: 0.0,
        alpha: 1.0
    )

    guard let encoder = commandBuffer.makeRenderCommandEncoder(
        descriptor: descriptor
    ) else {
        return
    }

    encoder.setRenderPipelineState(pipeline)
    encoder.setFragmentTexture(texture, index: 0)

    // 在这里保留原有的顶点、fragment 参数设置和绘制循环。

    encoder.endEncoding()
    commandBuffer.present(drawable)
    commandBuffer.commit()
}

在示例标注的位置放回原有绘制循环。如果后续还使用其他 render pass,应按实际渲染流程安排提交操作。

清屏 alpha 为 1.0 表示背景本身不透明。使用前述混合公式后,最终 framebuffer 的 alpha 为 1.0 是正常结果;纹理的部分透明度仍会通过 RGB 与黑色背景的混合表现出来。

4. shader 保留采样结果的 alpha 即可

原来的纹理分支已经直接返回 tex.sample(s, in.texCoord),没有把采样 alpha 强制改为 1.0。可以先保留这个行为,删去最后那条不会执行到的 return:

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 textureColor = {1, 2, 3, 4};

    if (all(color == textureColor)) {
        return tex.sample(s, in.texCoord);
    }

    return color;
}

这里的 (1, 2, 3, 4) 是选择纹理分支的标记,没有作为该分支的输出颜色,其第四个分量也不会覆盖纹理 alpha。不过,使用单独的模式参数区分纯色和纹理,比使用特殊颜色值更容易维护。

5. 如果仍然异常,先确认 shader 实际采样到的 alpha

可以临时把 fragment shader 改成下面的形式,将 alpha 显示为灰度:

fragment float4 fragment_main(
    VertexOut in [[stage_in]],
    texture2d<float> tex [[texture(0)]]
) {
    constexpr sampler s(address::repeat, filter::linear);
    float alpha = tex.sample(s, in.texCoord).a;
    return float4(alpha, alpha, alpha, 1.0);
}

黑色表示 alpha 为 0,白色表示 1,中间灰度表示部分透明。

  • 如果预期半透明的位置出现灰度,说明 shader 能采样到中间 alpha 值,应继续检查实际使用的 pipeline、混合因子和后续渲染过程。
  • 如果已知透明区域也全部显示白色,应检查加载和上传过程是否把 alpha 填成了 1,以及纹理是否采用了保留 alpha 的格式。
  • 如果仅出现黑白,应结合源图和采样位置检查 alpha 数据,不能只凭最终显示效果判断 Metal 把 alpha 二值化了。

目前六个三角形组成的是同一个六边形,图像区域并没有相互叠加。之后绘制翻转后的重叠图案时,仍需启用 blending,并注意普通 alpha blending 的结果与绘制顺序有关。

如果还希望看到 MTKView 后面的 SwiftUI 内容,需要另行检查视图及底层 layer 的透明合成配置,并使用透明清屏背景。如果只要求半透明纹理正确叠加在黑色背景上,先完成上述 pipeline 和 alpha 数据检查即可。

备注:内容仅供参考。