Как размыть сцену в SpriteKit?
Как бы добавить размытие по Гауссу ко всем узлам (нет фиксированного числа узлов) в SKScene в SpriteKit? Метка будет добавлена поверх сцены позже, это будет мое меню паузы. Почти все поможет!
Примерно так я и собираюсь:
6 ответов
То, что вы ищете, это 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
ОБНОВИТЬ:
Смотрите комментарий Маркуса ниже. Он указывает, что SKScene
фактически является подклассом SKEffectNode
Таким образом, вы действительно должны иметь возможность вызывать все это на самой сцене, а не произвольно вставлять узел эффекта в дерево узлов.
Чтобы добавить к этому, используя ответ @Bendegúz и код от http://www.bytearray.org/?p=5360
Я смог заставить это работать в моем текущем игровом проекте, который делается в IOS 8 Swift. Сделано немного по-другому, возвращая SKSpriteNode вместо UIImage. Также обратите внимание, что мой развернутый currentScene.view! Вызов для слабой ссылки GameScene, но должен работать с self.view.frame в зависимости от того, где вы вызываете эти методы. Мой экран паузы вызывается в отдельном классе HUD, следовательно, почему это так.
Я полагаю, что это можно сделать более элегантно, может быть, больше похоже на ответ @jemmons. Просто хотел помочь кому-то еще, пытающемуся сделать это в проектах SpriteKit, написанных во всем или некотором коде Swift.
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)
}
Это мое решение для экрана паузы. Он сделает снимок экрана, размыт его и после этого покажет анимацию. Я думаю, что вы должны сделать это, если вы не хотите тратить много кадров в секунду.
-(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:
добавьте это в свою игровую сцену, если вы хотите размыть все на сцене:
let blur = CIFilter(name:"CIGaussianBlur",withInputParameters: ["inputRadius": 10.0])
self.filter = blur
self.shouldRasterize = true
self.shouldEnableEffects = false
изменить self.shouldEnableEffects = true, если вы хотите его использовать.
Обновление Swift 3: Это ответ @Chuck Gaffney, обновленный для Swift 3. Я знаю, что этот вопрос помечен как target-c, но эта страница заняла 2-е место в Google по категории "быстрое размытие спрайтекитов". Я изменил currentScene на себя.
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)
}
Это еще один пример выполнения этого в 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")
}))
}
Я пытался сделать то же самое сейчас, в мае 2020 года (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(_:)
и некоторые переменные для учета времени, но результат тот же.
Мне пришло в голову, что, возможно, я смогу принудительно выполнить обновление, если "перенастрою" фильтр размытия на .filter
свойство моего SKScene (см. комментарии ВСЕМИ ЗАГЛАВНЫМИ БУКВАМИ в конце кода) с тем же эффектом, и это сработало.
Полный код:
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
}))
}
}
Я надеюсь, что это помогает кому-то!