Ash
Ash

Reputation: 9351

MTKView displaying CIImage goes wrong when image changes size

I'm using a fairly standard subclass of MTKView that displays a CIImage, for the purposes of rapidly updating using CIFilters. Code for the draw() function is below.

The problem I have is that only the portions of the view that are covered by the image, in this case scaledImage in the code below, are actually redrawn. This means that if the new image is smaller than the previous image, I can still see portions of the old image poking out from behind it.

Is there a way to clear the content of the drawable's texture when the size of the contained image changes, so that this does not happen?

override func draw() {
    guard let image = image,
        let targetTexture = currentDrawable?.texture,
        let commandBuffer = commandQueue.makeCommandBuffer()
        else { return }

    let bounds = CGRect(origin: CGPoint.zero, size: drawableSize)

    let scaleX = drawableSize.width / image.extent.width
    let scaleY = drawableSize.height / image.extent.height
    let scale = min(scaleX, scaleY)

    let width = image.extent.width * scale
    let height = image.extent.height * scale
    let originX = (bounds.width - width) / 2
    let originY = (bounds.height - height) / 2

    let scaledImage = image
        .transformed(by: CGAffineTransform(scaleX: scale, y: scale))
        .transformed(by: CGAffineTransform(translationX: originX, y: originY))

    ciContext.render(
        scaledImage,
        to: targetTexture,
        commandBuffer: commandBuffer,
        bounds: bounds,
        colorSpace: colorSpace
    )

    guard let drawable = currentDrawable else { return }
    commandBuffer.present(drawable)
    commandBuffer.commit()
    super.draw()
}

Upvotes: 1

Views: 1401

Answers (3)

Carlos De la Mora
Carlos De la Mora

Reputation: 195

What you can do is tell the ciContext to clear the texture before you draw to it. So before you do this,

ciContext.render(
        scaledImage,
        to: targetTexture,
        commandBuffer: commandBuffer,
        bounds: bounds,
        colorSpace: colorSpace)

clear the context by first.

let renderDestination = CIRenderDestination(
            width: Int(sizeToClear.width),
            height: Int(sizeToClear.height),
            pixelFormat: colorPixelFormat,
            commandBuffer: nil) { () -> MTLTexture in
            return drawable.texture
        }
do {
    try ciContext.startTask(toClear: renderDestination)
} catch {

}

For more info see the documentation

Upvotes: 2

Dannie P
Dannie P

Reputation: 4622

MTKView has clearColor property which you can use like so:

clearColor = MTLClearColorMake(1, 1, 1, 1)

Alternatively, if you'd like to use CIImage, you can just create one with CIColor like so:

CIImage(color: CIColor(red: 1, green: 1, blue: 1)).cropped(to: CGRect(origin: .zero, size: drawableSize))

Upvotes: 1

Ash
Ash

Reputation: 9351

I have the following solution which works, but I'm not very pleased with it as it feels hacky so please, somebody tell me a better way of doing it!

What I'm doing is tracking when the size of the image changes and setting a flag called needsRefresh. During the view's draw() function I then check to see if this flag is set and, if it is, I create a new CIImage from the view's clearColor that's the same size as the drawable and render this before continuing to the image I actually want to render.

This seems really inefficient, so better solutions are welcome!

Here's the additional drawing code:

    if needsRefresh {
        let color = UIColor(mtkColor: clearColor)
        let clearImage = CIImage(cgImage: UIImage.withColor(color, size: bounds.size)!.cgImage!)
        ciContext.render(
            clearImage,
            to: targetTexture,
            commandBuffer: commandBuffer,
            bounds: bounds,
            colorSpace: colorSpace
        )
        needsRefresh = false
    }

Here's the UIColor extension for converting the clear color:

extension UIColor {
    convenience init(mtkColor: MTLClearColor) {
        let red = CGFloat(mtkColor.red)
        let green = CGFloat(mtkColor.green)
        let blue = CGFloat(mtkColor.blue)
        let alpha = CGFloat(mtkColor.alpha)
        self.init(red: red, green: green, blue: blue, alpha: alpha)
    }
}

And finally, a small extension for creating CIImages from a given background colour:

extension UIImage {
    static func withColor(_ color: UIColor, size: CGSize = CGSize(width: 10, height: 10)) -> UIImage? {
        UIGraphicsBeginImageContext(size)
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        context.setFillColor(color.cgColor)
        context.fill(CGRect(origin: .zero, size: size))
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }   
}

Upvotes: 0

Related Questions