Search code examples
core-graphicsmetalcgcontextantialiasing

CGContext antialiasing works not as expected when drawing on transparent background


Example was taken from https://medium.com/@s1ddok/combine-the-power-of-coregraphics-and-metal-by-sharing-resource-memory-eabb4c1be615. Just changed color space from r8unorm to rgba8unorm.

// CGContext created with aligned data, which shared with MTLTexture
let cgContext = textureContext.cgContext
        let clearRect = CGRect(
                origin: .zero,
                size: CGSize(width: cgContext.width, height: cgContext.height)
        )
        cgContext.setAllowsAntialiasing(true)
        cgContext.setShouldAntialias(true)
        cgContext.clear(clearRect)

        cgContext.addPath(path)
        cgContext.setFillColor(fillColor ?? UIColor.clear.cgColor)
        cgContext.fillPath(using: fillRule.cgFillRuleValue())

Rendered path on shared data chunk then used as texture for quad primitive that rendered on MTKView's texture. But there is problem - antialiasing will mix path fill color with clear(black) color on edges like this(hope if difference between this pics is noticeable) - Dirty color on smooth edges

This is normal overlap, when I prefill context area with red color Correct overlap

If antialiasing is switched off for context - edges will clearly white, but sharpened

Is there are way to overlap two textures with clear and smooth edges without rendering an appropriate chunk of overlapped texture to overlapping texture before drawing path?

UPD: How context is created

func createCGMTLContext(withSize size: CGSize) -> CGMTLContext? {
        let width = Int(size.width)
        let height = Int(size.height)

        let pixelRowAlignment = device.minimumTextureBufferAlignment(for: .rgba8Unorm)
        let bytesPerRow = alignUp(size: width, align: pixelRowAlignment) * 4

        let pagesize = Int(getpagesize())
        let allocationSize = alignUp(size: bytesPerRow * height, align: pagesize)
        var data: UnsafeMutableRawPointer? = nil
        let result = posix_memalign(&data, pagesize, allocationSize)
        if result != noErr {
            fatalError("Error during memory allocation")
        }

        let context = CGContext(data: data,
                width: width,
                height: height,
                bitsPerComponent: 8,
                bytesPerRow: bytesPerRow,
                space: CGColorSpaceCreateDeviceRGB(),
                bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!

        context.scaleBy(x: 1.0, y: -1.0)
        context.translateBy(x: 0, y: -CGFloat(context.height))

        let buffer = device.makeBuffer(
                bytesNoCopy: context.data!,
                length: allocationSize,
                options: .storageModeShared,
                deallocator: { pointer, length in free(data) }
        )!

        let textureDescriptor = MTLTextureDescriptor()
        textureDescriptor.pixelFormat = .rgba8Unorm
        textureDescriptor.width = context.width
        textureDescriptor.height = context.height
        textureDescriptor.storageMode = buffer.storageMode
        textureDescriptor.usage = .shaderRead

        let texture = buffer.makeTexture(descriptor: textureDescriptor,
                offset: 0,
                bytesPerRow: context.bytesPerRow)

        guard let requiredTexture = texture else {
            return nil
        }

        let cgmtlContext = CGMTLContext(
                cgContext: context,
                buffer: buffer,
                texture: requiredTexture
        )

        return cgmtlContext
    }

There is how metal blending setted up

pipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
            pipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
            pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
            pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
            pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
            pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha

Solution

  • After reading https://metalbyexample.com/translucency-and-transparency/ I noticed a comment from Warren Moore:

    Since you’re loading your textures with CoreGraphics, you’re getting premultiplied alpha (i.e., the color channels are already multiplied by their respective alpha values). The consequence of using premultiplied alpha with the SourceAlpha, OneMinusSourceAlpha blend factors is that the source alpha value is redundantly multiplied, causing areas with a < 1.0 to become unnaturally dark. If instead you use the One, OneMinusSourceAlpha blend factors, you get a much more pleasing result.Incidentally, this is the main reason I used an opaque texture and translucent vertex colors when demonstrating blending in the article.

    Changing sourceAlpha to One helped. Unfortunately, alpha premultiplying in CGContext can't be turned off