How can you create a glow around a sprite via SKEffectNode
Asked Answered
G

3

23

I have a SKSpriteNode that I'd like to have a blue glow around it's edges for highlighting purposes. I am guessing that I would need to make my sprite a child of a SKEffectNode and then create/apply a filter of some sort.

UPDATE : I have investigated this quite a it with the chosen answer's approach, and discovered that SKEffectNode has a sizeable hit on performance even if you have it set to shouldRasterize and 'no filter' defined. My conclusion is that if your game requires more than 10 moving objects at one time, they can't involve a SKEffectNode even if rasterized.

My solution will likely involve pre-rendered glow images/animations, as SKEffectNode is not going to cut it for my requirements.

If someone has insight as to anything I am missing, I'd appreciate hearing whatever you know!

I am accepting an answer because it does achieve what I asked for, but wanted to add these notes to anyone looking to go this route, so you can be aware of some of the issues with using SKEffectNode.

Googol answered 6/10, 2013 at 21:13 Comment(1)
I tried a few things, but haven't come up with solution - Cannot get EffectNode to "stroke" an image.Ravenravening
W
28

@rickster's answer is great. Since I have low rep, I'm apparently not allowed to add this code as a comment to his. I hope this doesn't break stackoverflow rules of propriety. I'm not trying to userp his rep in any way.

Here's code that does what he's describing in his answer:

Header:

//  ENHGlowFilter.h
#import <CoreImage/CoreImage.h>

@interface ENHGlowFilter : CIFilter

@property (strong, nonatomic) UIColor *glowColor;
@property (strong, nonatomic) CIImage *inputImage;
@property (strong, nonatomic) NSNumber *inputRadius;
@property (strong, nonatomic) CIVector *inputCenter;

@end

//Based on ASCGLowFilter from Apple

Implementation:

#import "ENHGlowFilter.h"

@implementation ENHGlowFilter

-(id)init
{
    self = [super init];
    if (self)
    {
        _glowColor = [UIColor whiteColor];
    }
    return self;
}

- (NSArray *)attributeKeys {
    return @[@"inputRadius", @"inputCenter"];
}

- (CIImage *)outputImage {
    CIImage *inputImage = [self valueForKey:@"inputImage"];
    if (!inputImage)
        return nil;

    // Monochrome
    CIFilter *monochromeFilter = [CIFilter filterWithName:@"CIColorMatrix"];
    CGFloat red = 0.0;
    CGFloat green = 0.0;
    CGFloat blue = 0.0;
    CGFloat alpha = 0.0;
    [self.glowColor getRed:&red green:&green blue:&blue alpha:&alpha];
    [monochromeFilter setDefaults];
    [monochromeFilter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:red] forKey:@"inputRVector"];
    [monochromeFilter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:green] forKey:@"inputGVector"];
    [monochromeFilter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:blue] forKey:@"inputBVector"];
    [monochromeFilter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:alpha] forKey:@"inputAVector"];
    [monochromeFilter setValue:inputImage forKey:@"inputImage"];
    CIImage *glowImage = [monochromeFilter valueForKey:@"outputImage"];

    // Scale
    float centerX = [self.inputCenter X];
    float centerY = [self.inputCenter Y];
    if (centerX > 0) {
        CGAffineTransform transform = CGAffineTransformIdentity;
        transform = CGAffineTransformTranslate(transform, centerX, centerY);
        transform = CGAffineTransformScale(transform, 1.2, 1.2);
        transform = CGAffineTransformTranslate(transform, -centerX, -centerY);

        CIFilter *affineTransformFilter = [CIFilter filterWithName:@"CIAffineTransform"];
        [affineTransformFilter setDefaults];
        [affineTransformFilter setValue:[NSValue valueWithCGAffineTransform:transform] forKey:@"inputTransform"];
        [affineTransformFilter setValue:glowImage forKey:@"inputImage"];
        glowImage = [affineTransformFilter valueForKey:@"outputImage"];
    }

    // Blur
    CIFilter *gaussianBlurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
    [gaussianBlurFilter setDefaults];
    [gaussianBlurFilter setValue:glowImage forKey:@"inputImage"];
    [gaussianBlurFilter setValue:self.inputRadius ?: @10.0 forKey:@"inputRadius"];
    glowImage = [gaussianBlurFilter valueForKey:@"outputImage"];

    // Blend
    CIFilter *blendFilter = [CIFilter filterWithName:@"CISourceOverCompositing"];
    [blendFilter setDefaults];
    [blendFilter setValue:glowImage forKey:@"inputBackgroundImage"];
    [blendFilter setValue:inputImage forKey:@"inputImage"];
    glowImage = [blendFilter valueForKey:@"outputImage"];

    return glowImage;
}


@end

In use:

@implementation ENHMyScene //SKScene subclass

-(id)initWithSize:(CGSize)size {    
    if (self = [super initWithSize:size]) {
        /* Setup your scene here */
        [self setAnchorPoint:(CGPoint){0.5, 0.5}];
        self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];

        SKEffectNode *effectNode = [[SKEffectNode alloc] init];
        ENHGlowFilter *glowFilter = [[ENHGlowFilter alloc] init];
        [glowFilter setGlowColor:[[UIColor redColor] colorWithAlphaComponent:0.5]];
        [effectNode setShouldRasterize:YES];
        [effectNode setFilter:glowFilter];
        [self addChild:effectNode];
        _effectNode = effectNode;
    }
    return self;
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    /* Called when a touch begins */

    for (UITouch *touch in touches) {
        CGPoint location = [touch locationInNode:self];
        SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
        sprite.position = location;
        [self.effectNode addChild:sprite];
    }
}
Watson answered 5/2, 2014 at 19:16 Comment(1)
In case anyone else is interested, I threw together a Swift 2.1 translation of this to try it out in my own app. gist.github.com/cruinh/df5a2f59210863d201a2Brisson
L
25

You can create a glow effect in Core Image by creating a CIFilter subclass that composes multiple built-in filters. Such a filter would involve steps like these:

  1. Create an image to be used as the blue glow. There's probably a few decent ways to do this; one is to use CIColorMatrix to create a monochrome version of the input image.
  2. Scale up and blur the glow image (CIAffineTransform + CIGaussianBlur).
  3. Composite the original input image over the glow image (CISourceOverCompositing).

Once you have a CIFilter subclass that does all that, you can use it with a SKEffectNode to get a realtime glow around the effect node's children. Here it is running in the "Sprite Kit Game" Xcode template on an iPad 4:

Glowing spaceship in Sprite Kit

I got this up and running in a few minutes by cribbing the custom filter class used for a similar effect in the Scene Kit presentation from WWDC 2013 -- grab it from the WWDC Sample Code package at developer.apple.com/downloads, and look for the ASCGlowFilter class. (If you want to use that code on iOS, you'll need to change the NSAffineTransform part to use CGAffineTransform instead. I also replaced the centerX and centerY properties with an inputCenter parameter of type CIVector so Sprite Kit can automatically center the effect on the sprite.)

Did I say "realtime" glow? Yup! That's short for "really eats CPU time". Notice in the screenshot it's no longer pegged at 60 fps, even with only one spaceship -- and with the software OpenGL ES renderer on the iOS Simulator, it runs at slideshow speed. If you're on the Mac, you probably have silicon to spare... but if you want to do this in your game, keep some things in mind:

  • There are probably some ways to get better performance out of the filter itself. Play with different CI filters and you might see some improvement (there are several blur filters in Core Image, some of which will certainly be faster than Gaussian). Also note blur effects tend to be fragment-shader bound, so the smaller the image and the smaller the glow radius the better.
  • If you want to have multiple glows in a scene, consider making all the glowing sprites children of the same effect node -- that'll render them all into one image, then apply the filter once.
  • If the sprites to be glowed don't change much (e.g. if our spaceship wasn't rotating), setting shouldRasterize to YES on the effect node should help a lot. (Actually, in this case, you might get some improvement by rotating the effect node instead of the sprite within it.)
  • Do you really need realtime glow? As with many spiffy graphical effects in games, you'll get much better performance if you fake it. Make a blurry, bluey spaceship in your favorite graphics editor and put it in the scene as another sprite.
Lite answered 8/10, 2013 at 0:58 Comment(5)
Yep, this is very close to what I want. I intend to rasterize, not the realtime rendering each frame. I'll try this out.Googol
@rickster, can you post the code of how you did the 3 steps. Great answer by the way.Ravenravening
I used a filter from Apple sample code with minor changes -- see the download link above.Lite
Wow, in testing out this direction I discovered that using a SKEffectNode, even if shouldRasterize is set to YES and no realtime rendering, framerate takes a pretty significant hit (20fps) if I have even 10 objects moving at the same time(with NO filter defined). I can see that I simply need to handle this kind of thing as a pre-rendered image/animation.Googol
I am accepting this answer because it does achieve a glow effect with a SKEffectNode, and it is very thorough on the caveats involved.Googol
D
4

You could use a SKShapeNode behind the sprite and define a glow using it's glowWidth and strokeColor properties. If you size and position it right, this should give you the appearance of a glow. This doesn't give you many options for customization, but I imagine it's much easier than using a CIFilter with an SKEffectNode which is likely the other logical option you have for this.

Deprivation answered 7/10, 2013 at 12:34 Comment(5)
How would I get the effect with a SKShapeNode ? You mean creating a shape that matches the outline of the SKspriteNode and then making that glow and putting it behind the SKSpriteNode ? The sprite is not square, is there a way to create a SKShapeNode matching a sprite ? Does the physics engine do that for collison ?Googol
As rickster mentioned, this is basically faking it, but saving a ton of performance in the process. You would create the path of your sprite and use behind the sprite in the SKShapeNode. I would attach it with a fixed joint or something similar, and turn off its physics so it just follows the sprite basically - let the sprite handle the physics/collisions.Deprivation
I'm asking if there is a way to create a SKShapeNode path of a SKSpriteNode. Would be ideal to have a way to create a SKShapeNode path that follows the outline of the SKSpriteNode.Googol
No, there's no automatic way. There are some external tools that render to CGPath's, but nothing build into SpriteKit sadly.Deprivation
@juggleware SKShapeNode wrapped in a SKEffectNode with shouldRasterize property set to YES, is very performance friendly.Googol

© 2022 - 2024 — McMap. All rights reserved.