如何在 SpriteKit 中模糊场景?

Posted

技术标签:

【中文标题】如何在 SpriteKit 中模糊场景?【英文标题】:How Do I Blur a Scene in SpriteKit? 【发布时间】:2014-04-24 19:26:20 【问题描述】:

如何在 SpriteKit 的 SKScene 中向所有节点(没有固定数量的节点)添加高斯模糊?稍后将在场景顶部添加一个标签,这将是我的暂停菜单。 几乎任何事情都会有所帮助!

这样的事情就是我想要的:

【问题讨论】:

您可能会发现此链接很有用:http://eppz.eu/blog/create-ios-7-blur-effect/ 不,我不想导入任何东西,我希望它都是 SKScene,我不能使用 UIView 中的任何东西 【参考方案1】:

您正在寻找的是SKEffectNode。它将 CoreImage 过滤器应用于自身(以及所有子节点)。只需将其设为场景的根视图,为其添加 CoreImage 的模糊滤镜之一,即可完成设置。

例如,我设置了一个 SKScene 和一个 SKEffectNode 作为它的第一个子节点和一个属性,root 持有对它的弱引用:

-(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];

这是我用来(动画!)我的场景模糊的方法:

-(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];

请注意,和您一样,我将其用作暂停屏幕,因此我将场景栅格化。如果您希望您的场景在模糊的情况下进行动画处理,您可能应该 setShouldResterize:NO

如果您对动画过渡到模糊不感兴趣,您可以随时将滤镜设置为10.0f 左右的初始半径,并在您想要打开它时执行简单的setShouldEnableEffects:YES

另请参阅:SKEffectNode class reference

更新: 请参阅下面的马库斯评论。他指出SKScene实际上是SKEffectNode的子类,所以你真的应该能够在场景本身上调用所有这些,而不是在你的节点树中任意插入一个效果节点。

【讨论】:

您是如何将节点添加为子节点的? [自我addChild:节点]; // 它会抛出一个错误 => (lldb) 示例是否已输入到 ViewController? 但是SKScene不也是SKEffectNode吗?您不能直接将过滤器添加到 SKScene 吗? 如果将效果应用到整个场景,我敢打赌“暂停”标签和任何恢复游戏的按钮也会变得模糊。这就是为什么我有一个 SKEffectNode 作为我所有游戏内容的“画布”,并且那个是场景的孩子。 另外,这应该是公认的答案。它使用 SpriteKit 中已有的功能(不需要第三方代码、UIKit、CoreImage 等)。 请注意,shouldRasterize = YES 仅在效果节点的子节点不需要重绘时才阻止重绘。对于您的暂停屏幕,您可能希望在应用模糊之前pause 这些节点(或其共同父节点),这样您就不会尝试以 60fps 进行全屏模糊并融化您的 GPU。【参考方案2】:

使用@Bendegúz 的答案和来自http://www.bytearray.org/?p=5360 的代码来添加此内容

我能够在我当前的游戏项目中使用它,该项目是在 IOS 8 Swift 中完成的。通过返回一个 SKSpriteNode 而不是 UIImage 来做一些不同的事情。另请注意,我展开的 currentScene.view! call 是一个弱 GameScene 引用,但应该根据您调用这些方法的位置与 self.view.frame 一起使用。我的暂停屏幕在单独的 HUD 类中调用,因此会出现这种情况。

我想这可以做得更优雅,也许更像@jemmons 的回答。只是想在使用全部或部分 Swift 代码编写的 SpriteKit 项目中帮助其他尝试这样做的人。

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,标签上写着 Objective-c。【参考方案3】:

这是我对暂停屏幕的解决方案。 它会截取屏幕截图,模糊它,然后用动画显示它。 如果您不想浪费太多 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];

这是辅助方法:

- (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;

【讨论】:

这个项目有点晚了,但我会保留它以供将来参考:) 非常感谢!【参考方案4】:

斯威夫特 4:

如果您想模糊场景中的所有内容,请将其添加到您的 gameScene:

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

当你想使用它时,改变 self.shouldEnableEffects = true。

【讨论】:

【参考方案5】:

这是在没有层的情况下在 swift 2 中完成此操作的另一个示例:

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")

))

【讨论】:

【参考方案6】:

Swift 3 更新:这是 @Chuck Gaffney 为 Swift 3 更新的答案。我知道这个问题被标记为 objective-c,但这个页面在 Google 中的“swift spritekit blur”排名第二。我将 currentScene 更改为 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)


【讨论】:

【参考方案7】:

我在 2020 年 5 月(Xcode 11 和 iOS 13.x)现在尝试做同样的事情,但无法“动画化”模糊半径。就我而言,我从场景完全模糊开始,然后逐渐“取消模糊”(将inputRadius 设置为 0)。

不知何故,自定义动作块中设置的新输入半径值没有反映在渲染场景中。我的代码如下:

    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)
        ))
    

我什至尝试使用SKScene.update(_:) 和一些用于记时的变量手动更新值,但结果相同。

我突然想到,如果我将模糊过滤器“重新分配”到我的 SKScene 的 .filter 属性(参见代码末尾附近的所有大写字母中的 cmets),也许我可以强制刷新,相同效果,而且奏效了。

完整代码:

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
        ))
    


我希望这对某人有所帮助!

【讨论】:

以上是关于如何在 SpriteKit 中模糊场景?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SpriteKit 中呈现场景后立即接收触摸事件?

如何在不同的 SpriteKit 场景中显示 iAd 横幅

SpriteKit中的磨砂玻璃效果?

SpriteKit:你如何突出场景的一部分,就像在教程中一样?

如果使用编辑器中的场景加载,如何在 SpriteKit 中检测触摸

如何在另一个视图控制器中访问 SpriteKit 场景的变量?