Swift SpriteKit inverse masking doesn't work on device, but works in Simulator

736 Views Asked by At

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

2

There are 2 best solutions below

0
On

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.

13
On

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