2

I want to change a single, specified colour in my SpriteKit sprite to alpha, leaving all other colours unaffected. I don't think that there is an 'easy' way to do this and that a custom shader might be required.

Does anyone know how to do this in Swift or Objective-C?

Here's the image - I design it with a black background as it's easier to see but in my program, I want the background to be transparent.

enter image description here

If I ry to edit the image with the background already set as alpha, then the package I'm using (Pixaki) show this as white-and-grey checks, so it looks like this:

enter image description here

Steve Ives
  • 7,894
  • 3
  • 24
  • 55
  • Can you update your question with images which show original sprite / desired output ? – Whirlwind Apr 21 '16 at 14:32
  • Sample image added... – Steve Ives Apr 21 '16 at 16:23
  • The way I would do this without changing the eyes is to create one that is transparent and white that is just the body. You can then set the sprites color and blendFactor. This will change the white to whatever color you want. You can then add a new layer for the eyes. Not sure if you classify that as 'easy' so I didn't post as an answer =) – Skyler Lauren Apr 21 '16 at 17:39
  • Yes, but most graphics packages display 'transparent' as grey-and-white checks, which is *really* hard to edit a white sprite against :-) – Steve Ives Apr 21 '16 at 17:50
  • 1
    You do not want to do this in code, you should set up some process that will convert the specific color to alpha if you are adamant about including it in your xcode project with the color. Here is a post that may help you. http://stackoverflow.com/questions/17516624/use-gimp-color-to-alpha-script-in-a-shell-script – Knight0fDragon Apr 21 '16 at 17:55
  • @Knight0fDragon - indeed. I've contacted the developer of the editing app I use about being able to change the way it displays transparency, which he's going to look into and he also suggested adding an extra black layer that I fade to 0% opacity before exporting the .png file, which is what I'm doing at the moment. – Steve Ives Apr 21 '16 at 18:09
  • Ha I completely misunderstood what you were trying to do. I thought you were looking to change the color of the alien not remove the black. Now I see what you meant. @Knight0fDragon is correct you don't want to do that via code this is more of a limitation of your sprite editor. – Skyler Lauren Apr 21 '16 at 20:51

2 Answers2

0
  1. You really should not perform preparing in code those assets which can be processed beforehand.

Here is sample code which does what you want though.

It is based on this answer(bascially just converted to Swift with some modifications). Credits go there.

// color - source color, which is must be replaced
// withColor - target color
// tolerance - value in range from 0 to 1
func replaceColor(color:SKColor, withColor:SKColor, image:UIImage, tolerance:CGFloat) -> UIImage{

    // This function expects to get source color(color which is supposed to be replaced) 
    // and target color in RGBA color space, hence we expect to get 4 color components: r, g, b, a
    assert(CGColorGetNumberOfComponents(color.CGColor) == 4 && CGColorGetNumberOfComponents(withColor.CGColor) == 4,
           "Must be RGBA colorspace")

    // Allocate bitmap in memory with the same width and size as source image
    let imageRef = image.CGImage
    let width = CGImageGetWidth(imageRef)
    let height = CGImageGetHeight(imageRef)

    let colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB)!

    let bytesPerPixel = 4
    let bytesPerRow = bytesPerPixel * width;
    let bitsPerComponent = 8
    let bitmapByteCount = bytesPerRow * height

    let rawData = UnsafeMutablePointer<UInt8>.alloc(bitmapByteCount)

    let context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace,
                                        CGImageAlphaInfo.PremultipliedLast.rawValue | CGBitmapInfo.ByteOrder32Big.rawValue)


    let rc = CGRect(x: 0, y: 0, width: width, height: height)

    // Draw source image on created context
    CGContextDrawImage(context, rc, imageRef)


    // Get color components from replacement color
    let withColorComponents = CGColorGetComponents(withColor.CGColor)
    let r2 = UInt8(withColorComponents[0] * 255)
    let g2 = UInt8(withColorComponents[1] * 255)
    let b2 = UInt8(withColorComponents[2] * 255)
    let a2 = UInt8(withColorComponents[3] * 255)

    // Prepare to iterate over image pixels
    var byteIndex = 0

    while byteIndex < bitmapByteCount {

        // Get color of current pixel
        let red:CGFloat = CGFloat(rawData[byteIndex + 0])/255
        let green:CGFloat = CGFloat(rawData[byteIndex + 1])/255
        let blue:CGFloat = CGFloat(rawData[byteIndex + 2])/255
        let alpha:CGFloat = CGFloat(rawData[byteIndex + 3])/255

        let currentColor = SKColor(red: red, green: green, blue: blue, alpha: alpha);

        // Compare two colors using given tolerance value
        if compareColor(color, withColor: currentColor , withTolerance: tolerance) {

            // If the're 'similar', then replace pixel color with given target color
            rawData[byteIndex + 0] = r2
            rawData[byteIndex + 1] = g2
            rawData[byteIndex + 2] = b2
            rawData[byteIndex + 3] = a2
        }

        byteIndex = byteIndex + 4;
    }

    // Retrieve image from memory context
    let imgref = CGBitmapContextCreateImage(context)
    let result = UIImage(CGImage: imgref!)

    // Clean up a bit
    rawData.destroy()

    return result
}

func compareColor(color:SKColor, withColor:SKColor, withTolerance:CGFloat) -> Bool {

    var r1: CGFloat = 0.0, g1: CGFloat = 0.0, b1: CGFloat = 0.0, a1: CGFloat = 0.0;
    var r2: CGFloat = 0.0, g2: CGFloat = 0.0, b2: CGFloat = 0.0, a2: CGFloat = 0.0;

    color.getRed(&r1, green: &g1, blue: &b1, alpha: &a1);
    withColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2);

    return fabs(r1 - r2) <= withTolerance &&
        fabs(g1 - g2) <= withTolerance &&
        fabs(b1 - b2) <= withTolerance &&
        fabs(a1 - a2) <= withTolerance;
}

You can adjust it and create command line tool or use it directly in your code.

Here is sample project: https://github.com/andrey-str/soa-replace-a-color-colour-in-a-skspritenode

Disclaimer: code may leaking, I'm newbie in Swift.

  1. And here is a simpler way :-)

    $ brew install imagemagick
    $ convert B2CkX.png -fuzz 90% -transparent black result.png
    

90% - is a tolerance value, result looks the same as with code above

Good luck!

Community
  • 1
  • 1
andrey.s
  • 789
  • 10
  • 28
  • andrey is correct, instead of attempting to special case this one specific problem, just use a more general solution that is already known to work 100% of the time. Prepare a PNG with an alpha channel on the desktop, preview and export, then simply load that into SpriteKit in the normal way. This is going to be 100 times less work, and it will actually work. – MoDJ May 29 '16 at 20:27
0

Old question, Swift 5 answer: mask the colors of an UIImage.

(Since this works on UIImage.cgImage you may think that it also works on an SKTexture.cgImage but alas no, so we need to convert to an UIImage first.

First, convert the sprite texture to an UIImage and then use:

extension UIImage {
    func remove(color: UIColor, tolerance: CGFloat = 4) -> UIImage {
        let ciColor = CIColor(color: color)
        let maskComponents: [CGFloat] = [ciColor.red, ciColor.green, ciColor.blue].flatMap { value in
            [(value * 255) - tolerance, (value * 255) + tolerance]
        }
        
        guard let masked = cgImage?.copy(maskingColorComponents: maskComponents) else { return self }
        return UIImage(cgImage: masked)
    }
}

On macOS, we need an NSImage to do the trick:

extension NSImage {
    func remove(color: NSColor, tolerance: CGFloat = 4) -> NSImage {
        if let ciColor = CIColor(color: color) {
            let maskComponents: [CGFloat] = [ciColor.red, ciColor.green, ciColor.blue].flatMap { value in
                [(value * 255) - tolerance, (value * 255) + tolerance]
            }
            
            guard let masked = cgImage(forProposedRect: nil, context: nil, hints: nil)?.copy(maskingColorComponents: maskComponents) else { return self }
            return NSImage(cgImage: masked, size: size)
        }

        return self
    }
}
hotdogsoup.nl
  • 1,078
  • 1
  • 9
  • 22