25

How would I add a gaussian blur to all nodes (there's no fixed number of nodes) in an SKScene in SpriteKit? A label will be added on top of the scene later, this will be my pause menu. Almost anything would help!

Something like this is what I'm going for: Gaussian pause menu

Community
  • 1
  • 1
Zane Helton
  • 1,044
  • 1
  • 14
  • 34
  • You might find this link useful: [http://eppz.eu/blog/create-ios-7-blur-effect/](http://eppz.eu/blog/create-ios-7-blur-effect/) – JKallio Mar 18 '14 at 21:42
  • No, I don't want to have to import anything, and I'd like it to be all SKScene, I can't use anything from UIView – Zane Helton Mar 18 '14 at 23:09

7 Answers7

35

What you're looking for is an SKEffectNode. It applies a CoreImage filter to itself (and thus all subnodes). Just make it the root view of your scene, give it one of CoreImage's blur filters, and you're set.

For example, I set up an SKScene with an SKEffectNode as it's first child node and a property, root that holds a weak reference to it:

-(void)createLayers{
  SKEffectNode *node = [SKEffectNode node];
  [node setShouldEnableEffects:NO];
  CIFilter *blur = [CIFilter filterWithName:@"CIGaussianBlur" keysAndValues:@"inputRadius", @1.0f, nil];
  [node setFilter:blur];
  [self setRoot:node];
}

And here's the method I use to (animate!) the blur of my scene:

-(void)blurWithCompletion:(void (^)())handler{
  CGFloat duration = 0.5f;
  [[self root] setShouldRasterize:YES];
  [[self root] setShouldEnableEffects:YES];
  [[self root] runAction:[SKAction customActionWithDuration:duration actionBlock:^(SKNode *node, CGFloat elapsedTime){
    NSNumber *radius = [NSNumber numberWithFloat:(elapsedTime/duration) * 10.0];
    [[(SKEffectNode *)node filter] setValue:radius forKey:@"inputRadius"];
  }] completion:handler];
}

Note that, like you, I'm using this as a pause screen, so I rasterize the scene. If you want your scene to animate while blurred, you should probably setShouldResterize: to NO.

And if you're not interested in animating the transition to the blur, you could always just set the filter to an initial radius of 10.0f or so and do a simple setShouldEnableEffects:YES when you want to switch it on.

See also: SKEffectNode class reference

UPDATE:
See Markus's comment below. He points out that SKScene is, in fact, a subclass of SKEffectNode, so you really ought to be able to call all of this on the scene itself rather than arbitrarily inserting an effect node in your node tree.

jemmons
  • 18,605
  • 8
  • 55
  • 84
  • How did you add the node as child? [self addChild:node]; // it drops an error => (lldb) Have the example typed to ViewController? – Bendegúz May 15 '14 at 12:05
  • I really don't understand where is the mistake. Here is my code, http://postimg.org/image/bg8qb70rp/ It doesn't show any error, but it doesn't show any action too. – Bendegúz May 16 '14 at 13:11
  • 3
    But isn't SKScene also an SKEffectNode? Couldn't you add the filter directly to the SKScene? – Markus Rautopuro Sep 27 '14 at 07:09
  • @MarkusRautopuro Oh my goodness, you're right! I haven't tried it yet, but I can't think of any reason applying it directly to the `SKScene` wouldn't work. – jemmons Sep 29 '14 at 21:20
  • 3
    If you apply the effect to the whole scene, my bet is that the "PAUSED" label and any button to resume the game will be blurred as well. That is why I have an `SKEffectNode` be the "canvas" of all my game content, and that one is a child of the scene. – Nicolas Miari Oct 22 '14 at 03:06
  • 3
    Also, this should be the accepted answer. It uses functionality already available in SpriteKit (no need for third party code, UIKit, CoreImage, etc). – Nicolas Miari Oct 22 '14 at 03:08
  • 3
    Note that `shouldRasterize = YES` prevents redrawing only if the children of the effect node don't need redrawing. For your pause screen, you probably want to `pause` those nodes (or a common parent thereof) before applying the blur so that you're not trying to do fullscreen blur at 60fps and melt your GPU. – rickster Feb 17 '15 at 21:32
12

To add to this by using @Bendegúz's answer and code from http://www.bytearray.org/?p=5360

I was able to get this to work in my current game project that's being done in IOS 8 Swift. Done a bit differently by returning an SKSpriteNode instead of a UIImage. Also note that my unwrapped currentScene.view! call is to a weak GameScene reference but should work with self.view.frame based on where you are calling these methods. My pause screen is called in a separate HUD class hence why this is the case.

I would imagine this could be done more elegantly, maybe more like @jemmons's answer. Just wanted to possibly help out anyone else trying to do this in SpriteKit projects written in all or some Swift code.

func getBluredScreenshot() -> SKSpriteNode{

    create the graphics context
    UIGraphicsBeginImageContextWithOptions(CGSize(width: currentScene.view!.frame.size.width, height: currentScene.view!.frame.size.height), true, 1)

    currentScene.view!.drawViewHierarchyInRect(currentScene.view!.frame, afterScreenUpdates: true)

    // retrieve graphics context
    let context = UIGraphicsGetCurrentContext()

    // query image from it
    let image = UIGraphicsGetImageFromCurrentImageContext()

    // create Core Image context
    let ciContext = CIContext(options: nil)
    // create a CIImage, think of a CIImage as image data for processing, nothing is displayed or can be displayed at this point
    let coreImage = CIImage(image: image)
    // pick the filter we want
    let filter = CIFilter(name: "CIGaussianBlur")
    // pass our image as input
    filter.setValue(coreImage, forKey: kCIInputImageKey)

    //edit the amount of blur
    filter.setValue(3, forKey: kCIInputRadiusKey)

    //retrieve the processed image
    let filteredImageData = filter.valueForKey(kCIOutputImageKey) as CIImage
    // return a Quartz image from the Core Image context
    let filteredImageRef = ciContext.createCGImage(filteredImageData, fromRect: filteredImageData.extent())
    // final UIImage
    let filteredImage = UIImage(CGImage: filteredImageRef)

    // create a texture, pass the UIImage
    let texture = SKTexture(image: filteredImage!)
    // wrap it inside a sprite node
    let sprite = SKSpriteNode(texture:texture)

    // make image the position in the center
    sprite.position = CGPointMake(CGRectGetMidX(currentScene.frame), CGRectGetMidY(currentScene.frame))

    var scale:CGFloat = UIScreen.mainScreen().scale

    sprite.size.width  *= scale

    sprite.size.height *= scale

    return sprite


}


func loadPauseBGScreen(){

    let duration = 1.0

    let pauseBG:SKSpriteNode = self.getBluredScreenshot()

    //pauseBG.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
    pauseBG.alpha = 0
    pauseBG.zPosition = self.zPosition + 1
    pauseBG.runAction(SKAction.fadeAlphaTo(1, duration: duration))

    self.addChild(pauseBG)

}
Chuck Gaffney
  • 256
  • 3
  • 9
10

This is my solution for the pause screen. It will take a screenshot, blur it and after that show it with animation. I think you should do it if you don't wanna waste to much fps.

-(void)pause {
    SKSpriteNode *pauseBG = [SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImage:[self getBluredScreenshot]]];
    pauseBG.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
    pauseBG.alpha = 0;
    pauseBG.zPosition = 2;
    [pauseBG runAction:[SKAction fadeAlphaTo:1 duration:duration / 2]];
    [self addChild:pauseBG];
}

And this is the helper method:

- (UIImage *)getBluredScreenshot {
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 1);
    [self.view drawViewHierarchyInRect:self.view.frame afterScreenUpdates:YES];
    UIImage *ss = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    CIFilter *gaussianBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
    [gaussianBlurFilter setDefaults];
    [gaussianBlurFilter setValue:[CIImage imageWithCGImage:[ss CGImage]] forKey:kCIInputImageKey];
    [gaussianBlurFilter setValue:@10 forKey:kCIInputRadiusKey];

    CIImage *outputImage = [gaussianBlurFilter outputImage];
    CIContext *context   = [CIContext contextWithOptions:nil];
    CGRect rect          = [outputImage extent];
    rect.origin.x        += (rect.size.width  - ss.size.width ) / 2;
    rect.origin.y        += (rect.size.height - ss.size.height) / 2;
    rect.size            = ss.size;
    CGImageRef cgimg     = [context createCGImage:outputImage fromRect:rect];
    UIImage *image       = [UIImage imageWithCGImage:cgimg];
    CGImageRelease(cgimg);
    return image;
}
Bendegúz
  • 758
  • 7
  • 15
4

Swift 4:

add this to your gameScene if you want to blur everything in the scene:

let  blur = CIFilter(name:"CIGaussianBlur",withInputParameters: ["inputRadius": 10.0])
        self.filter = blur
        self.shouldRasterize = true
        self.shouldEnableEffects = false

change self.shouldEnableEffects = true when you want to use it.

Alex Bailey
  • 793
  • 9
  • 20
3

Swift 3 Update: This is @Chuck Gaffney's answer updated for Swift 3. I know this question is tagged objective-c, but this page ranked 2nd in Google for "swift spritekit blur". I changed currentScene to self.

    func getBluredScreenshot() -> SKSpriteNode{

    //create the graphics context
    UIGraphicsBeginImageContextWithOptions(CGSize(width: self.view!.frame.size.width, height: self.view!.frame.size.height), true, 1)

    self.view!.drawHierarchy(in: self.view!.frame, afterScreenUpdates: true)

    // retrieve graphics context
    _ = UIGraphicsGetCurrentContext()

    // query image from it
    let image = UIGraphicsGetImageFromCurrentImageContext()

    // create Core Image context
    let ciContext = CIContext(options: nil)
    // create a CIImage, think of a CIImage as image data for processing, nothing is displayed or can be displayed at this point
    let coreImage = CIImage(image: image!)
    // pick the filter we want
    let filter = CIFilter(name: "CIGaussianBlur")
    // pass our image as input
    filter?.setValue(coreImage, forKey: kCIInputImageKey)

    //edit the amount of blur
    filter?.setValue(3, forKey: kCIInputRadiusKey)

    //retrieve the processed image
    let filteredImageData = filter?.value(forKey: kCIOutputImageKey) as! CIImage
    // return a Quartz image from the Core Image context
    let filteredImageRef = ciContext.createCGImage(filteredImageData, from: filteredImageData.extent)
    // final UIImage
    let filteredImage = UIImage(cgImage: filteredImageRef!)

    // create a texture, pass the UIImage
    let texture = SKTexture(image: filteredImage)
    // wrap it inside a sprite node
    let sprite = SKSpriteNode(texture:texture)

    // make image the position in the center
    sprite.position = CGPoint(x: self.frame.midX, y: self.frame.midY)

    let scale:CGFloat = UIScreen.main.scale

    sprite.size.width  *= scale

    sprite.size.height *= scale

    return sprite


}

func loadPauseBGScreen(){

    let duration = 1.0

    let pauseBG:SKSpriteNode = self.getBluredScreenshot()

    pauseBG.alpha = 0
    pauseBG.zPosition = self.zPosition + 1
    pauseBG.run(SKAction.fadeAlpha(to: 1, duration: duration))

    self.addChild(pauseBG)

}
riot
  • 307
  • 3
  • 7
2

This is another example of getting this done in swift 2 without the layers:

func blurWithCompletion() {
let duration: CGFloat = 0.5
let filter: CIFilter = CIFilter(name: "CIGaussianBlur", withInputParameters: ["inputRadius" : NSNumber(double:1.0)])!
scene!.filter = filter
scene!.shouldRasterize = true
scene!.shouldEnableEffects = true
scene!.runAction(SKAction.customActionWithDuration(0.5, actionBlock: { (node: SKNode, elapsedTime: CGFloat) in
    let radius = (elapsedTime/duration)*10.0
    (node as? SKEffectNode)!.filter!.setValue(radius, forKey: "inputRadius")

}))

}

Victor --------
  • 512
  • 1
  • 11
  • 29
1

I was trying to do this same thing now, in May 2020 (Xcode 11 and iOS 13.x), but wasn't unable to 'animate' the blur radius. In my case, I start with the scene fully blurred, and then 'unblur' it gradually (set inputRadius to 0).

Somehow, the new input radius value set in the custom action block wasn't reflected in the rendered scene. My code was as follows:

    private func unblur() {
        run(SKAction.customAction(withDuration: unblurDuration, actionBlock: { [weak self] (_, elapsed) in
            guard let this = self else { return }
            let ratio = (TimeInterval(elapsed) / this.unblurDuration)
            let radius = this.maxBlurRadius * (1 - ratio) // goes to 0 as ratio goes to 1
            this.filter?.setValue(radius, forKey: kCIInputRadiusKey)
        }))
    }

I even tried updating the value manually using SKScene.update(_:) and some variables for time book-keeping, but the same result.

It occurred to me that perhaps I could force the refresh if I "re-assingned" the blur filter to the .filter property of my SKScene (see comments in ALL CAPS near the end of the code), to the same effect, and it worked.

The full code:

class MyScene: SKScene {

    private let maxBlurRadius: Double = 50
    private let unblurDuration: TimeInterval = 5

    init(size: CGSize) {
        super.init(size: size)

        let filter = CIFilter(name: "CIGaussianBlur")
        filter?.setValue(maxBlurRadius, forKey: kCIInputRadiusKey)
        self.filter = filter
        self.shouldEnableEffects = true
        self.shouldRasterize = false

        // (...rest of the child nodes, etc...)

    }

    override func didMove(to view: SKView) {
        super.didMove(to: view)
        self.unblur()
    }

    private func unblur() {
        run(SKAction.customAction(withDuration: unblurDuration, actionBlock: { [weak self] (_, elapsed) in
            guard let this = self else { return }
            let ratio = (TimeInterval(elapsed) / this.unblurDuration)
            let radius = this.maxBlurRadius * (1 - ratio) // goes to 0 as ratio goes to 1

            // OBTAIN THE FILTER
            let filter = this.filter

            // MODIFY ATTRIBUTE 
            filter?.setValue(radius, forKey: kCIInputRadiusKey)

            // RE=ASSIGN TO SCENE
            this.filter = filter
        }))
    }
}

I hope this helps someone!

Nicolas Miari
  • 16,006
  • 8
  • 81
  • 189