如何在SpriteKit中模糊一个场景?

25

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

我想要的效果类似于这样: 高斯暂停菜单


你可能会发现这个链接很有用:http://eppz.eu/blog/create-ios-7-blur-effect/ - JKallio
不,我不想导入任何东西,我希望它全部都是SKScene,我不能使用UIView中的任何内容。 - Zane Helton
7个回答

35
你需要的是一个 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类参考 更新:
请看下面Markus的评论。他指出,SKScene实际上是SKEffectNode的一个子类,因此你真的应该能够在场景本身上调用所有这些方法,而不是在节点树中任意插入一个效果节点。

3
SKScene也是SKEffectNode,你不能直接将滤镜添加到SKScene中吗? - Markus Rautopuro
@MarkusRautopuro 哦,我的天啊,你说得对!虽然我还没有尝试过,但我想不出任何理由直接将其应用于SKScene会有问题。 - jemmons
3
如果您将特效应用于整个场景,我的猜测是“已暂停”标签和任何恢复游戏的按钮也会变模糊。这就是为什么我将SKEffectNode作为所有游戏内容的“画布”,并且它是场景的子节点。 - Nicolas Miari
3
这应该是被采纳的答案。它使用了已经在SpriteKit中可用的功能(无需第三方代码、UIKit、CoreImage等)。 - Nicolas Miari
3
请注意,shouldRasterize = YES仅在特效节点的子节点不需要重新绘制时才会防止重新绘制。对于暂停屏幕,您可能希望在应用模糊之前“暂停”这些节点(或其共同的父节点),以便您不会尝试以60fps进行全屏模糊并使GPU崩溃。 - rickster
显示剩余2条评论

12

使用 @Bendegúz 的答案和来自 http://www.bytearray.org/?p=5360 的代码,我能够在我的 IOS 8 Swift 游戏项目中让它工作。返回的是 SKSpriteNode 而不是 UIImage,做法与其他人略有不同,当前场景调用 view 属性时给出的是弱 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)

}

2
查克,标签显示为Objective-C。 - Juan Boero

10

这是我的暂停屏幕解决方案。 它会拍摄屏幕截图,模糊化处理后用动画显示。 如果您不想浪费太多帧率,我认为您应该这样做。

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

2
有点晚了,但我会保存下来以备将来参考 :)非常感谢! - Zane Helton

4

Swift 4:

如果您想模糊场景中的所有内容,请将以下代码添加到游戏场景中:

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

当你想使用它时,请将self.shouldEnableEffects = true更改为true。


3

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)

}

2
这是在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")

}))

}


1
我正在尝试在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属性(请参见代码末尾附近的大写字母注释),或许可以强制刷新,结果确实如此。

完整代码:

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


我希望这能帮到某些人!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接