3

The code to accomplish this is pretty straightforward:

var cropNode = SKCropNode()
var shape = SKShapeNode(rectOf: CGSize(width:100,height:100))
shape.fillColor = SKColor.orange

var shape2 = SKShapeNode(rectOf: CGSize(width:25,height:25))
shape2.fillColor = SKColor.red
shape2.blendMode = .subtract
shape.addChild(shape2)

cropNode.addChild(shape)
cropNode.position = CGPoint(x:150,y:170)
cropNode.maskNode=shape
container.addChild(cropNode)

Same code, same iOS, different results = no bueno

image

OhMyGuin
  • 33
  • 3
  • I'm amazed the simulator "worked". And pleasantly surprised someone else expected/desired inverse masking. It's not currently possible in SpriteKit, unfortunately. See here: https://stackoverflow.com/questions/41004925/how-do-i-cut-a-hole-in-a-sprite-image-or-texture-to-show-what-is-behind-it-using/41007482#41007482 – Confused Dec 12 '17 at 04:24
  • Thanks for the comments. Check out my answer below. It's a hack, but hopefully it helps others running into the same problem. – OhMyGuin Dec 12 '17 at 08:08

2 Answers2

3

Here is a method that will generate a maskNode for you using shaders:

func generateMaskNode(from mask:SKNode) -> SKNode
{
    var returningNode : SKNode!
    autoreleasepool
    {
        let view = SKView()
        //First let's flatten the node
        let texture = view.texture(from: mask) 
        let node = SKSpriteNode(texture:texture) 
        //Next apply the shader to the flattened node to allow for color swapping
        node.shader = SKShader(fileNamed: "shader.fsh")
        let texture2 = view.texture(from: node)
        returningNode = SKSpriteNode(texture:texture2)

    }
    return returningNode
}

It requires you to create a file called shader.fsh, the code inside looks like this:

void main() {

    // Find the pixel at the coordinate of the actual texture
    vec4 val = texture2D(u_texture, v_tex_coord);

    // If the color value of that pixel is 0,0,0
    if (val.r == 0.0 && val.g == 0.0 && val.b == 0.0) {
        // Turn the pixel off
        gl_FragColor  = vec4(0.0,0.0,0.0,0.0);
    } 
    else {
        // Otherwise, keep the original color
        gl_FragColor = val;
    }
}

To use it, it requires that you have black pixels instead of alpha as the means of determining what gets cropped, so here is what your code now should look like:

var cropNode = SKCropNode()
var shape = SKShapeNode(rectOf: CGSize(width:100,height:100))
shape.fillColor = SKColor.orange

var shape2 = SKShapeNode(rectOf: CGSize(width:25,height:25))
shape2.fillColor = SKColor.orange
shape2.blendMode = .subtract
shape.addChild(shape2)

let mask = generateMaskNode(from:shape)

cropNode.addChild(shape)
cropNode.position = CGPoint(x:150,y:170)
cropNode.maskNode=mask
container.addChild(cropNode)

The reason why subtract works on the simulator and not the device is because simulator subtracts the alpha channel, where as the device does not. The device is actually behaving correctly, since alpha is not suppose to be subtracted, it is suppose to be ignored.

Do note, you do not have to choose black to be your crop color, you can change the shader to allow for any color of your choosing, just change the line:

if (val.r == 0.0 && val.g == 0.0 && val.b == 0.0) 

to a color you desire. (Like in your case. you can say r = 0 g = 1 b = 0 to crop only on green)

Result of above code on a device

Edit: I wanted to note that subtract blending is not necessary, this would also work:

var cropNode = SKCropNode()
var shape = SKShapeNode(rectOf: CGSize(width:100,height:100))
shape.fillColor = SKColor.orange

var shape2 = SKShapeNode(rectOf: CGSize(width:25,height:25))
shape2.fillColor = SKColor.black
shape2.blendMode = .replace
shape.addChild(shape2)

let mask = generateMaskNode(from:shape)

cropNode.addChild(shape)
cropNode.position = CGPoint(x:150,y:170)
cropNode.maskNode=mask
container.addChild(cropNode)

Which begs the question now that I cannot test, is my function even needed. The following code in theory should work, since it is replacing the underlying pixels with the one above, so in theory the alpha should transfer over. If anybody could test this, please let me know if it works.

var cropNode = SKCropNode()
var shape = SKShapeNode(rectOf: CGSize(width:100,height:100))
shape.fillColor = SKColor.orange

var shape2 = SKShapeNode(rectOf: CGSize(width:25,height:25))
shape2.fillColor = SKColor(red:0,green:0,blue:0,alpha:0)
shape2.blendMode = .replace
shape.addChild(shape2)

cropNode.addChild(shape)
cropNode.position = CGPoint(x:150,y:170)
cropNode.maskNode= shape.copy() as! SKNode
container.addChild(cropNode)

replace only replaces color not alpha

OhMyGuin
  • 33
  • 3
Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44
  • Does this work? And do inverse masking? And... if so, how did you become so proficient with shaders? and... WELL DONE! – Confused Dec 12 '17 at 13:09
  • 1
    It doesn’t do inverse masking, all it does is swap a color and make it alpha 0. Can’t do blending via shaders from what I can find – Knight0fDragon Dec 12 '17 at 13:34
  • Doh. I'm still hoping Apple will (one day) give Sprite Kit some love. And inverse masking. And some more abstraction so it's more like a game engine than a framework for those that love coding more than doing. – Confused Dec 12 '17 at 13:36
  • 1
    They need to stop breaking stuff first, I wouldn’t even trust them with inverse masking. It is time they open source sprite kit since they obviously do not care about it anymore – Knight0fDragon Dec 12 '17 at 13:41
  • Tested this out and it works on the simulator, but not on the device :( – OhMyGuin Dec 14 '17 at 07:07
  • What did you test? I know my function works on device – Knight0fDragon Dec 14 '17 at 09:50
  • Okay retested and noticed that shader.fsh wasn't actually loading (I guess shader files don't automatically get added to Copy Bundle Resources). I've added and retested and here are the results: 1) with .subtract and shader, it works on simulator and device, but on device only leaves a black border around shape2 (looks like 2px on the bottom and left and 1px on the top and right). 2) with .replace, shape.copy and black fill on shape2, it doesn't work on simulator or device, but it does make just the border of shape2 transparent (the fill displays as orange). – OhMyGuin Dec 14 '17 at 17:10
  • What do you mean it only leaves a black border around shape2, I am guessing you are referring to the stroke color that you aren't setting, the default is white, so when you subtract from orange you are left with blue. Set the stroke to clear – Knight0fDragon Dec 14 '17 at 17:12
  • Even if you set the strokeColor to clear, the 1px black border remains on the bottom and left of shape2 (only on device). Not a big deal, just odd. Thanks for your help. – OhMyGuin Dec 15 '17 at 04:40
  • probably antialias needs to be turned off – Knight0fDragon Dec 15 '17 at 23:21
  • I tried turning off antialiasing, but same result. I added a screenshot to your answer if you are interested in seeing what I'm talking about. It's not really a problem, just interesting to me because I don't understand it. – OhMyGuin Dec 16 '17 at 03:40
  • Something else must be doing that.... something is causing that line to not be black – Knight0fDragon Dec 16 '17 at 03:44
  • Yeah, just weird because it doesn't show up on the simulator. – OhMyGuin Dec 16 '17 at 03:55
0

Since inverse masking doesn't seem to be inherently available with SpriteKit (in a way that works on devices), I think the following is the closest thing to an answer:

let background = SKSpriteNode(imageNamed:"stocksnap")
background.position = CGPoint(x:65, y:background.size.height/2)
addChild(background)

let container = SKNode()
let cropNode = SKCropNode()

let bgCopy = SKSpriteNode(imageNamed:"stocksnap")
bgCopy.position = background.position
cropNode.addChild(bgCopy)

let cover = SKShapeNode(rect: CGRect(x:0,y:0,width:200,height:200))
cover.position = CGPoint(x:80,y:150)
cover.fillColor = SKColor.orange
container.addChild(cover)

let highlight = SKShapeNode(rectOf: CGSize(width:100,height:100))
highlight.position = CGPoint(x:cover.position.x+cover.frame.size.width/2,y:cover.position.y+cover.frame.size.height/2)
highlight.fillColor = SKColor.red

cropNode.maskNode = highlight
container.addChild(cropNode)

addChild(container)

Here is a screenshot from a device using above technique

This just uses a duplicate of the background, masks it, and overlays it in the same position to create the inverse masking effect. In situations where you are wanting to duplicate whatever is on the screen, you could use something like this:

func captureScreen() -> SKSpriteNode {
    var image = UIImage()
    if let view = self.view {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, UIScreen.main.scale)

        view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)

        if let imageFromContext = UIGraphicsGetImageFromCurrentImageContext() {
            image = imageFromContext
        }
        UIGraphicsEndImageContext()
    }
    let texture = SKTexture(image:image)
    let sprite = SKSpriteNode(texture:texture)
    //scale is applicable if using fixed screen sizes that aren't the actual width and height
    sprite.scale(to: CGSize(width:size.width,height:size.height))
    sprite.anchorPoint = CGPoint(x:0,y:0)
    return sprite
}

Hopefully someone finds a better way or an update is made to SpriteKit to support inverse masking, but in the meantime this works fine for my use case.

OhMyGuin
  • 33
  • 3