iossprite-kitcore-imagecifilterskeffectnode

On iOS, can you add multiple CIFilters to a SpriteKit node?


On iOS, can you add more than one CIFilter to a SKEffectsNode?

CIFilterGenerator seems like what I want but it isn't available on iOS.

I know you can use multiple filters on an image by passing the output of one as the input of the next, but that's not helpful if you want to affect non-image nodes.

Does this mean I have to create an artificial hierarchy of SKEffectNode and add a filter to each of them, with my actual content at the very bottom? Is there a better way?


Solution

  • Where it's difficult or impossible to "chain" together multiple CIFilter calls to achieve the desired effect - maybe due to a class that has a single property, one way to overcome this is to do the following:

    For example, let's say you wish to combine a gaussian blur and then add a monochrome red tone to an image. At it's most basic you can do this:

    class BlurThenColor:CIFilter {
    
        let blurFilter = CIFilter(name: "CIGaussianBlur")
    
        override public var attributes: [String : Any] {
            return [
                kCIAttributeFilterDisplayName: "Blur then Color",
    
                "inputImage": [kCIAttributeIdentity: 0,
                               kCIAttributeClass: "CIImage",
                               kCIAttributeDisplayName: "Image",
                               kCIAttributeType: kCIAttributeTypeImage]
            ]
        }
        override init() {
            super.init()
        }
        @available(*, unavailable) required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
        override public func setValue(_ value: Any?, forKey key: String) {
            switch key {
            case "inputImage":
                blurFilter?.setValue(inputImage, forKey: "inputImage")
            default:
                break
            }
        }
        override public var  outputImage: CIImage {
            return (blurFilter?.outputImage)! .applyingFilter("CIColorMonochrome", parameters: ["inputColor": CIColor(red: 1.0, green: 0.0, blue: 0.0)])
        }
    }
    

    If you wish to expose more attributes, you can simply add them to the attributes and setValue(forKey:) overrides along wist adding variables and setDefaults. Here I'm simply using the defaults.

    Now that you've chained your effect together into one custom filter, you can register it and use it:

    let CustomFilterCategory = "CustomFilter"
    
    public class CustomFilterConstructor: NSObject, CIFilterConstructor {
        static public func registerFilter() {
            CIFilter.registerName(
                "BlurThenColor",
                constructor: CustomFilterConstructor(),
                classAttributes: [
                    kCIAttributeFilterCategories: [CustomFilterCategory]
                ])
        }
        public func filter(withName name: String) -> CIFilter? {
            switch name {
            case "BlurThenColor":
                return BlurThenColor()
            default:
                return nil
            }
        }
    }
    

    To use this, just be sure to register the filter (I tend to put mine in AppDelegate if possible):

    CustomFilterConstructor.registerFilter()
    

    From there, you can use BlurThenColor just like any other CIFilter. Instantiate it, use setValue, and call outputImage.

    Please note, this code will crash because of force-unwrapping of inputImage and/or typos. I'm sure you can make this more safe - but rest assured that I've tested this and it works. (I created this custom filter and replaced it in an app where the force-unwraps don't happen.)