Fast way to blur the background behind a Sprite Node

Hi. For the past few days, I have been trying to create an SKShapeNode (a rectangle) that will be used as a menu screen moving down from the outside of the screen when a button is tapped. Everything behind the node should be blurred.


Something to this effect: https://stackoverflow.com/questions/49142867/spritekit-blurred-background-sknode


Alternatively, I may have everything in the background blurred with the exception of the SKShapeNode and its children.


Using this:


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


where self is the scene, blurs the background and the SKShapeNode. Besides, it is an awful hit on FPS.


I tried the following:


let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.light)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = self.view!.bounds
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.view!.addSubview(blurEffectView)


This is very fast, but again this blurs everything including the SKShapeNode, which is not intended.


Is there anyone who did something like that?


Thanks a lot!

Accepted Reply

moontiger is on the right track, however, it is absolutely possible to blur only a portion of your SpriteKit scene, here is how:


1. Set up a node (contentNode) within your scene that will hold all of your scene's contents, except for the blur node.


2. Add an SKEffectNode to the root of your scene, add an SKShapeNode (blurNode) as its child. Make sure that your blurNode is in front of all of the content in your contentNode by setting its zPosition.


3. Set the filter of your SKEffectNode to a CIGaussianBlur:


let blurFilter = CIFilter(name: "CIGaussianBlur", parameters: ["inputRadius": 75])


4. Set the initial fillColor of your blurNode to .white


5. Generate an SKTexture of your content node, crop this texture using the frame of your blurNode, set this cropped texture as the fillTexture of your blurNode (self is the SKScene here):


let fillTexture = self.view?.texture(from: contentNode, crop: blurNode.frame)

Replies

Hi, I don't know if this is ideal for what you want, but I was trying to do a similar thing, except the entire background would be blurred. I found the following things key to helping reduce frame rate hit...


1. I think the higher the blur inputRadius, the slower it is. 4 blurs it enough for my situation.

2. Lower resolution is quicker. My scene is only 200x200 and then scaled to fit, etc. Luckily it suits the graphics style I'm doing.

3. I rendered the entire scene to a texture (view.texture(from: myscene)) and then applied that texture to a SKSpriteNode which was a child of an SKEffectNode which has the blur filter applied.


With those things applied, I'm still seeing a frame rate of at least 60fps.

moontiger is on the right track, however, it is absolutely possible to blur only a portion of your SpriteKit scene, here is how:


1. Set up a node (contentNode) within your scene that will hold all of your scene's contents, except for the blur node.


2. Add an SKEffectNode to the root of your scene, add an SKShapeNode (blurNode) as its child. Make sure that your blurNode is in front of all of the content in your contentNode by setting its zPosition.


3. Set the filter of your SKEffectNode to a CIGaussianBlur:


let blurFilter = CIFilter(name: "CIGaussianBlur", parameters: ["inputRadius": 75])


4. Set the initial fillColor of your blurNode to .white


5. Generate an SKTexture of your content node, crop this texture using the frame of your blurNode, set this cropped texture as the fillTexture of your blurNode (self is the SKScene here):


let fillTexture = self.view?.texture(from: contentNode, crop: blurNode.frame)

Thank you, gchiste.


I tried to follow your approach, but unfortunately it is not working, nothing is added to the scene. This is my code:



class ShowCardsView: SKShapeNode
{
    /// Button Exit
    var btnExit:BtnIntroScene!
   
    required init(coder: NSCoder)
    {
        fatalError("NSCoding not supported")
    }
   
    init(name:String)
    {
        super.init()
       
        self.name = name
       
        self.fillColor = UIColor(red: 120/255, green: 0/255, blue: 0/255, alpha: 0.95)
        self.strokeColor = UIColor(red: 105/255, green: 0/255, blue: 0/255, alpha: 1)
        self.lineWidth = 10
       
        self.path = UIBezierPath(roundedRect: CGRect(x: -128, y: -128, width: 500, height: 500), cornerRadius: 32).cgPath
    }
}


class GameScene: SKScene,ConnectionManagerDelegate
{
func showPeerCardsTemp()
    {
        print("<<<<<<<< EFFECT >>>>>>>>")
  
        if let effectNode = SKEffectNode(fileNamed: "ShowCardsEffectNode")
        {
            self.addChild(effectNode)
      
            let showCardsView = ShowCardsView(name: "ShowCardsView")
            showCardsView.position = CGPoint(x: frame.midX, y: frame.midY)
            showCardsView.zPosition = CardLevel.peerName.rawValue
            effectNode.addChild(showCardsView)
      
            let blurFilter = CIFilter(name: "CIGaussianBlur", parameters: ["inputRadius": 75])
      
            effectNode.filter = blurFilter
      
            effectNode.shouldRasterize = true
            effectNode.shouldEnableEffects = true
      
            showCardsView.fillColor = .white
      
            let fillTexture = self.view?.texture(from: background, crop: showCardsView.frame)
      
            showCardsView.fillTexture = fillTexture
      
            self.addChild(effectNode)
        }
    }

}


Thank you for your help!


Edit:

I am actually happy with my second code in the original post. However, as I said, it also blurs all nodes added to the scene. Is there a way to add a node without blurring it (so that only everything below this node is blurred)?

Hello,


Both of the scenarios that you described in your original post are possible. If you are still having trouble with this issue I recommend that you Request Technical Support for this issue.

OK. Looks like I've nailed it:


import SpriteKit
import GameplayKit

class GameScene: SKScene {
   
    private var label : SKLabelNode!
    private var rectangle : SKShapeNode!
    private var background : SKSpriteNode!
    private var rectangle1 : SKShapeNode!
   
    override func didMove(to view: SKView)
    {
        self.label = SKLabelNode(text: "HELLO WORLD")
        self.label.fontName = "Noteworthy-Bold"
        self.label.fontSize = 100
        self.label.alpha = 1.0
        self.label.color = .green
        self.label.position = CGPoint(x: 10, y: 400)
        addChild(self.label)
       
        let w = (self.size.width + self.size.height) * 0.05
       
        self.rectangle = SKShapeNode.init(rectOf: CGSize.init(width: w*5, height: w*5), cornerRadius: w * 0.3)
        rectangle.fillColor = UIColor.red
        addChild(rectangle)
       
        let blurEffect = UIBlurEffect(style: UIBlurEffect.Style.regular)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        blurEffectView.frame = self.view!.bounds
        blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        self.view!.addSubview(blurEffectView)
       
        // https://www.hackingwithswift.com/example-code/media/how-to-render-a-uiview-to-a-uiimage
        let renderer = UIGraphicsImageRenderer(size: self.view!.bounds.size)
        let textureImage = renderer.image { ctx in
            view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
        }
       
        let texture = SKTexture(image: textureImage)
        let textureSizeWidth = textureImage.size.width * self.view!.contentScaleFactor
        let textureSizeHeight = textureImage.size.height * self.view!.contentScaleFactor
        let textureSize = CGSize(width: textureSizeWidth, height: textureSizeHeight)

        self.background = SKSpriteNode.init(texture: texture, size: textureSize)
        addChild(background)

        self.rectangle1 = SKShapeNode.init(rectOf: CGSize.init(width: w*3, height: w*3), cornerRadius: w * 0.3)
        self.rectangle1.fillColor = UIColor.blue
        self.rectangle1.zPosition = 10
        self.background!.addChild(rectangle1)
           
        blurEffectView.removeFromSuperview()

    }
   
}


Any comments?

This seems like a *very* roundabout way of achieving the desired effect. There are definitely better ways which exclusively use SpriteKit (see my original post in this thread), no UIViews or UIGraphicsImageRenderers required.

gchiste.


Thank you. No doubt there are better ways to do it. Unfortunately, no one seems to know them. I tried your approach but to no avail. It would be great to come up with something more direct. Cheers.


EDIT:


I tried the following:


class GameScene: SKScene {
   
    private var label : SKLabelNode!
    private var rectangle : SKShapeNode!
    private var background : SKSpriteNode!
    private var rectangle1 : SKShapeNode!
   
    override func didMove(to view: SKView)
    {
        self.label = SKLabelNode(text: "HELLO WORLD")
        self.label.fontName = "Noteworthy-Bold"
        self.label.fontSize = 100
        self.label.alpha = 1.0
        self.label.color = .green
        self.label.position = CGPoint(x: 10, y: 400)
        addChild(self.label)
       
        // Create shape node to use during mouse interaction
        let w = (self.size.width + self.size.height) * 0.05
       
        self.rectangle = SKShapeNode.init(rectOf: CGSize.init(width: w*5, height: w*5), cornerRadius: w * 0.3)
        rectangle.fillColor = UIColor.red
        addChild(rectangle)
       
        let effectNode = SKEffectNode()
        let blurFilter = CIFilter(name: "CIGaussianBlur", parameters: ["inputRadius": 75])
        effectNode.filter = blurFilter
        effectNode.shouldRasterize = true
        effectNode.shouldEnableEffects = true
        self.addChild(effectNode)

        background = SKSpriteNode()
        background.position = CGPoint(x: frame.midX, y: frame.midY)
        background.size = CGSize(width: frame.width, height: frame.height)
        background.zPosition = 5
        let fillTexture = self.view?.texture(from: self.scene!)
        background.normalTexture = fillTexture
        effectNode.addChild(background)

        self.rectangle1 = SKShapeNode.init(rectOf: CGSize.init(width: w*3, height: w*3), cornerRadius: w * 0.3)
        self.rectangle1.fillColor = UIColor.blue
        self.rectangle1.zPosition = 10
        self.background!.addChild(rectangle1)
    }
}


What I get is a blurred blue rectangle1, whereas I need to blue the label and the red rectange. I could have added the label and the red rectangle to background. However, they have already been added to the scene and, sure enough, the application crashes when I add them again. So, I can't do that.


Thoughts?

Thanks to gchiste, I have achieved it by doing the following:


import SpriteKit
import GameplayKit

class GameScene: SKScene {
   
    private var label : SKLabelNode!
    private var rectangle : SKShapeNode!
    private var container : SKSpriteNode!
    private var rectangle1 : SKShapeNode!
    private var button : SKSpriteNode!
    private var effectNode : SKEffectNode!
    private var blurNode : SKShapeNode!
   
    override func didMove(to view: SKView)
    {
        effectNode = SKEffectNode()
        let blurFilter = CIFilter(name: "CIGaussianBlur", parameters: ["inputRadius": 20])
        effectNode.filter = blurFilter
        effectNode.zPosition = 500
        addChild(effectNode)
       
        blurNode = SKShapeNode(rect: self.frame)
        blurNode.fillColor = .white
        blurNode.isHidden = true
        effectNode.addChild(blurNode)
       
       
        self.container = SKSpriteNode()
        //self.container.position = CGPoint(x: frame.width/2, y: frame.height/2)
        self.container.size = CGSize(width: frame.width, height: frame.height)
        self.container.color = UIColor(red: 0/255, green: 100/255, blue: 0/255, alpha: 1)
        addChild(container)
       

        self.label = SKLabelNode(text: "HELLO WORLD")
        self.label.fontName = "Noteworthy-Bold"
        self.label.fontSize = 100
        self.label.alpha = 1.0
        self.label.color = .red
        self.label.zPosition = 2
        self.label.position = self.convert(CGPoint(x: 5, y: 400),to: container)
        self.container.addChild(self.label)
           
           
        self.rectangle = SKShapeNode.init(rectOf: CGSize.init(width: 500, height: 500),
                                          cornerRadius: 30)
        self.rectangle.fillColor = UIColor.green
        self.rectangle.strokeColor = .clear
        self.rectangle.position = self.convert(self.rectangle.position, to: container)
        self.container.addChild(self.rectangle)
       
       
       
        self.button = SKSpriteNode(color: .blue, size: CGSize(width: 300, height: 100))
        self.button.position = self.convert(CGPoint(x: 0, y: -400), to: container)
        self.button.zPosition = 2
        self.button.name = "button"
        self.container.addChild(self.button)
       
    }
   
    override func touchesEnded(_ touches: Set, with event: UIEvent?)
    {
        let touch = touches.first
        let positionInScene = touch!.location(in: self)
        let allNodes = nodes(at: positionInScene)
        for node in allNodes
        {
            let nodePositionConverted = self.convert(node.position, from: node)
            let nodeFrameConverted = CGRect(origin: CGPoint(x:nodePositionConverted.x-node.frame.maxX,
                                                            y:nodePositionConverted.y-node.frame.maxY),
                                            size:node.frame.size)
            if nodeFrameConverted.contains(positionInScene)
            {
                if(node == button)
                {
                    addMenu()
                }
                else if(node == rectangle1)
                {
                    hideMenu()
                }
            }
        }
    }
   
   
    func addMenu()
    {
        print("POINT A")
        blurNode.isHidden = false
        let fillTexture = self.view?.texture(from: container, crop: blurNode.frame)
        print("POINT B")
        blurNode.fillTexture = fillTexture
       
        self.rectangle1 = SKShapeNode.init(rectOf: CGSize.init(width: 300, height: 300), cornerRadius: 30)
        self.rectangle1.fillColor = UIColor.blue
        self.rectangle1.zPosition = 600
        addChild(rectangle1)
    }
   
   
    func hideMenu()
    {
        blurNode.isHidden = true
        blurNode.fillTexture = nil
        rectangle1.removeFromParent()
    }
}



The only thing that bothers me is that it takes about a second or two before the blurred image appears. Does it take so long to create fillTexture from the container? Is there a way to do it more efficiently? Can it be done in the background thread?


Thanks!


EDIT 1: Seems like the delay is obvious in Simulator and not as much on an actual device...


EDIT 2: One more issue. The blurring effect does not cover the whole scene. The edges of the scene are not blurred. Any idea why? Thanks!

At the end, I abandoned this approach. Takes so much memory to the extent that my daughter's old iPad would crash with an out of memory warning...

There appears to be currently (as of iOS 13.x) some memory leak of IOSurface stuff when using an SKEffectNode wth CIFilters. We were using that functionality initially for blurring the playing area when a game was paused. But repeated pausing and unpausing (i.e., toggling shouldEnableEffects) would gradually pump up the number of retained IOSurfaces and memory as much as desired. We switched the code from using a CIFilter to a custom shader on the effect node; that fixed it.

Thanks, bg2b. Interesting approach. Is there more information on how to use a custom shader to blur the background that you could recommend?


Cheers.

The source of our app is available on GitHub, so you can take a look there. Here's the relevant file:


https://github.com/bg2b/RockRats/blob/master/Asteroids/scenes/BasicScene.swift


There's a discussion of the memory leak issue around line 500 onwards. We ultimately used a kind of pixelated filter for the game's pause effect, but did try a Gaussian blur as well, and that blur shader is still sitting in the code (starting around line 575). It's one pass and not the classic two-pass Gaussian blur since we didn't want to make an intermediate effect node, but you could do that too if you need a large blur.


The comments there also mention an alternative approach. You can render the blurred part of the scene into an SKTexture using the view's texture(from:) or texture(from:crop:) methods, then run that texture through a CGImage -> CIFilter -> CGImage -> SKTexture -> SKSpriteNode sequence to get a regular sprite node that shows the desired effect. It's a little slow in general (probably around 1/10 of a second, enough to be noticed when you pressed the pause button in our game). For the blur case, you can avoid that lag by scaling the part to be blurred down, doing the CIFiltering, and then upscaling the result. It's blurry anyway, so you won't notice the difference. We didn't go with that method because we need a different pause effect in certain circumstances where the scaling wasn't feasible.