Metal not rendering color with low saturation and alpha

I'm using MTKView to render some triangles. Everything works fine until I decrease the color's saturation and opacity value which makes the triangle completely transparent. Creating SwiftUI's Color with the same values shows correctly. This only happens for colors with "low" saturation, if the color has 100% saturation (like #FF0000), it still renders fine even with just 1% opacity.

I noticed if I change the colorPixelFormat of the MTKView, the result will change. So not sure if I only need to change the colorPixelFormat to fix this, in that case, I don't know which one either as I have limited knowledge about graphics. Here is an example for color #FF8888:

  • bgra8Unorm: minimum 55% opacity for it to render
  • bgra8Unorm_srgb: minimum 77% opacity for it to render and the color is a lot lighter than what it should be.

In Swift, I store the colors as [Float], in MSL, it will be converted to float4*. Nothing fancy with the vertex and fragment functions, just returning the input. This is not very likely where the issue lies as other colors work. Some code to show my setup:

    // MTKView's setup
    clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
    isOpaque = false
    layer.magnificationFilter = .nearest
    layer.minificationFilter = .nearest
    // State setup
    let pipelineDescriptor = MTLRenderPipelineDescriptor()
    pipelineDescriptor.vertexFunction = vertexFunction
    pipelineDescriptor.fragmentFunction = fragmentFunction
    pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm

    pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor)
    // draw method setup
    guard let vertexBuffer = vertexBuffer,
          let indexBuffer = indexBuffer,
          let indexCount = indexCount,
          let colorBuffer = colorBuffer,
          let pipelineState = pipelineState,
          let discriptor = view.currentRenderPassDescriptor,
          let commandBuffer = commandQueue.makeCommandBuffer(),
          let commandEncoder = commandBuffer.makeRenderCommandEncoder(
            descriptor: discriptor
          ),
          let drawable = view.currentDrawable else {
      return
    }

    commandEncoder.setRenderPipelineState(pipelineState)
    commandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
    commandEncoder.setVertexBuffer(colorBuffer, offset: 0, index: 1)
    commandEncoder.drawIndexedPrimitives(
      type: .triangle,
      indexCount: indexCount,
      indexType: .uint32,
      indexBuffer: indexBuffer,
      indexBufferOffset: 0
    )

    commandEncoder.endEncoding()
    commandBuffer.present(drawable)
    commandBuffer.commit()
Answered by Son Seo in 740897022

Found this answer: https://stackoverflow.com/a/51414692/8355412

So basically, I only need to premultiply alpha to each of my rgb component

What do you see when using the debugger on your fragment shader? https://developer.apple.com/documentation/metal/developing_and_debugging_metal_shaders

I tried that too, it shows the color normally on a black background. Digging into the vertices, color data looks correct as well

Strange thing, I tried setting my container view (just a UIView) background color from white to black, and walaa, it can render the color. This got me thinking if Metal or iOS in general does some sort of color snapping? so when the background is white and the color is "light" enough, it "snaps" to white? Any idea?

Accepted Answer

Found this answer: https://stackoverflow.com/a/51414692/8355412

So basically, I only need to premultiply alpha to each of my rgb component

Metal not rendering color with low saturation and alpha
 
 
Q