Mask SKSpriteNode - ios

I add few nodes to my sks file, and now I want to add some mask to the most lowest node SKSpriteNode. Structure is as shown below:
where
green - wordInfoHolder
red - container for Label
whiteText - label
Now I want to hide part that is shown in red. To perform such action I read that SKCropNode can be used.
I able to find all my nodes in scene file and save them to variables.
if let holder = childNode(withName: "wordInfoHolder") as? SKSpriteNode {
wordInfoHolder = holder
if let wordSwitchNode = wordInfoHolder?.childNode(withName:"wordSwitchNode") as? SKSpriteNode {
self.wordSwitchNode = wordSwitchNode
if let label = self.wordSwitchNode?.childNode(withName:"infoLabel") as? SKLabelNode {
wordSwitchLabelNode = label
}
}
}
all 3 objects are stored and correct.
Now I want to add some mask to root object.
For such purpose I prepared mask image:
and try to do something like:
guard let holder = wordInfoHolder else { return }
let positionToSet = holder.position
let mask = SKSpriteNode(imageNamed: "rectangle_mask")
let cropNode = SKCropNode()
holder.removeFromParent()
cropNode.addChild(holder)
cropNode.maskNode = mask
cropNode.position = positionToSet
self.addChild(cropNode)
But I see nothing. I expect to see green part of SKSpriteNode.
What was done wrong?

Assuming that you have set zPosition on all your objects.
I'm quite sure the issue is that because you are moving your holder object from the scene to the cropNode it is retaining it's position info from the scene (for example if it's position in the scene was 500, 500 it's position in the cropNode is now 500, 500)
I was able to recreate your issue and by setting the holder.position to zero the issue went away.
In the picture below I am using the yellow box as mask and the blue and pink boxes are test objects to ensure the the cropNode is placed between them.
if let holder = self.childNode(withName: "holder") as? SKSpriteNode {
self.holder = holder
if let switcher = holder.childNode(withName: "//switcher") as? SKSpriteNode {
self.switcher = switcher
}
}
if let mask = self.childNode(withName: "mask") as? SKSpriteNode {
mask.removeFromParent()
let positionToSet = holder.position
holder.position = CGPoint.zero
mask.position = CGPoint.zero
let cropNode = SKCropNode()
holder.removeFromParent()
cropNode.addChild(holder)
cropNode.maskNode = mask
cropNode.position = positionToSet
cropNode.zPosition = 10
self.addChild(cropNode)
}
added tidbit
holder.move(toParent: cropNode)
can be used in place of
holder.removeFromParent()
cropNode.addChild(holder)

Related

Can I use SKScene for material on an Obj file loaded in SceneKit?

My goal is to be able to tap on a specific model and color the surface. I have managed to do this with generic SCNSphere, SCNBox, etc.
I set it up like this, and it basically works for SCNSphere, etc:
let node = SCNNode(geometry: geometry)
let material = SCNMaterial()
material.specular.contents = SKColor.init(white: 0.1, alpha: 1)
material.shininess = 2.0;
material.normal.contents = "wood-normal.png"
let skScene = SKScene.init(size: CGSize(width: SPRITE_SIZE, height: SPRITE_SIZE))
skScene.backgroundColor = SKColor.orange
skScene.scaleMode = .aspectFill
material.diffuse.contents = skScene
geometry.firstMaterial = material
scnScene.rootNode.addChildNode(node)
However, when I load a .obj file and try to set the material with a SKScene I don't get anything. Here is how I set up the obj
let bundle = Bundle.main
let path = bundle.path(forResource: name, ofType: "obj")
let url = NSURL(fileURLWithPath: path!)
let asset = MDLAsset(url: url as URL)
guard let object = asset.object(at: 0) as? MDLMesh else {
print("Failed to get mesh from obj asset")
return
}
let material = SCNMaterial()
material.specular.contents = SKColor.init(white: 0.1, alpha: 1)
let skScene = SKScene.init(size: CGSize(width: SPRITE_SIZE, height: SPRITE_SIZE))
skScene.backgroundColor = SKColor.orange
skScene.scaleMode = .aspectFill
material.diffuse.contents = skScene
let geometry = SCNGeometry.init(mdlMesh: object)
let node = SCNNode(geometry: geometry)
node.geometry?.materials = [material]
scnScene.rootNode.addChildNode(node)
But as you can see, the color of the teapot is not orange
My question is, am I doing something wrong in terms of using a SKScene as a material for an obj file, or is there some limitation that I'm aware of? I've spent a few hours on this, and am open to suggestions if anyone has any ideas.
Also, in this sort of situation, how do I decide what the size of the SKScene is since I want to arbitrarily wrap the whole mesh with a SKScene?
Update 1
I sort of figured something out. My obj file doesn't have texture coordinates, only vertices. When I tried another obj file with texture coordinates, then the skScene seems to have loaded correctly. So I guess my teapot doesn't have uv coordinates, so it can't map the texture. Is there a way to add uv coordinates if the obj file doesn't have them already?

Issue with adding shadow to a SCNPlane with clear background

I am trying to add a shadow on SCNPlane, everything works fine but I cannot make SCNPlane transparent to show only the shadow not with the white background. here is the code:
let flourPlane = SCNPlane()
let groundPlane = SCNNode()
let clearMaterial = SCNMaterial()
clearMaterial.lightingModel = .constant
//clearMaterial.colorBufferWriteMask = []
clearMaterial.writesToDepthBuffer = true
clearMaterial.transparencyMode = .default
flourPlane.materials = [clearMaterial]
groundPlane.scale = SCNVector3(200, 200, 200)
groundPlane.geometry = flourPlane
groundPlane.castsShadow = false
groundPlane.eulerAngles = SCNVector3Make(-Float.pi/2, 0, 0)
groundPlane.position = SCNVector3(x: 0.0, y: shadowY, z: 0.0)
node.addChildNode(groundPlane)
// Create a ambient light
let ambientLight = SCNNode()
ambientLight.light = SCNLight()
ambientLight.light?.shadowMode = .deferred
ambientLight.light?.color = UIColor.white
ambientLight.light?.type = SCNLight.LightType.ambient
ambientLight.position = SCNVector3(x: 0,y: 5,z: 0)
// Create a directional light node with shadow
let myNode = SCNNode()
myNode.light = SCNLight()
myNode.light?.type = .directional
myNode.light?.castsShadow = true
myNode.light?.automaticallyAdjustsShadowProjection = true
myNode.light?.shadowSampleCount = 80
myNode.light?.shadowBias = 1
myNode.light?.orthographicScale = 1
myNode.light?.shadowMode = .deferred
myNode.light?.shadowMapSize = CGSize(width: 2048, height: 2048)
myNode.light?.shadowColor = UIColor.black.withAlphaComponent(0.5)
myNode.light?.shadowRadius = 10.0
myNode.eulerAngles = SCNVector3Make(-Float.pi/2, 0, 0)
node.addChildNode(ambientLight)
node.addChildNode(myNode)
When I add clearMaterial.colorBufferWriteMask = [] shadow disappears! how can create a transparent material to show only the shadow.
The white area is SCNPlane and the red is the background.
You can set materials "color" to .clear like below:
extension SCNMaterial {
convenience init(color: UIColor) {
self.init()
diffuse.contents = color
}
convenience init(image: UIImage) {
self.init()
diffuse.contents = image
}
}
let clearColor = SCNMaterial(color: .clear)
flourPlane.materials = [clearColor]
I have found a trick in another SO answer.
Adjust the floor plane's blendMode to alter how its pixels are combined with the underlying pixels.
let clearMaterial = SCNMaterial()
// alter how pixels affect underlying pixels
clearMaterial.blendMode = .multiply
// use the simplest shading that shows a shadow (so not .constant!)
clearMaterial.lightingModel = .lambert
// unobstructed parts are render pure white and thus have no effect
clearMaterial.diffuse.contents = UIColor.white
floorPlane.firstMaterial = clearMaterial
You can see the effect in this image. The main plane is invisible, yet the shadow is visible. The cube, its shadow and the ball grid have the same XZ position. Notice how the "shadow" affects underlying geometries and the scene background.

How to add button and label to the scene

I am new in Scenekit and ARKit, I wanted to add button and label inside UIView to the scenekit's scene. I am getting the world coordinates by the hit test to place the view but SCNNode don't have addSubView Method to add view.
I want to achieve following output :
My attempts to achieve this :
func addHotspot(result : SCNHitTestResult, parentNode : SCNNode? = nil) {
let view = UIView()
view.backgroundColor = .red
let material = SCNMaterial()
material.diffuse.contents = view
let plane = SCNPlane(width: 100, height: 100)
plane.materials = [material]
let node = SCNNode()
node.geometry = plane
node.position = result.worldCoordinates
parentNode?.addChildNode(node)
}
error :
Please suggest the way I can complete it.
To add a UIView object to a scene node, you can assign it to the node's geometry material diffuse content. It will be added to the node and will maintain its position as you move your device around:
DispatchQueue.main.async {
let view: UIView = getView() // this is your UIView instance that you want to add to the node
let material = SCNMaterial()
material.diffuse.contents = view
node.geometry.materials = [material]
}

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

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
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
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.

Generating positions for SKSpriteNode

I am generating Y position for SKSpriteNode. I need to generate y position, but it should not be in the position previously generated image, should not overlap. How would I be able to do this in Swift in SpriteKit?
for _ in 1...3 {
let lower : UInt32 = 100
let upper : UInt32 = UInt32(screenSize.height) - 100
let randomY = arc4random_uniform(upper - lower) + lower
obstaclesTexture = SKTexture(imageNamed: "levaPrekazka")
obstacle = SKSpriteNode(texture: obstaclesTexture)
prekazka = obstacle.copy() as! SKSpriteNode
prekazka.name = "prekazka"
prekazka.position = CGPoint(x: 0, y: Int(randomY))
prekazka.zPosition = 10;
prekazka.size = CGSize(width: screenSize.width / 7, height: screenSize.height / 7)
prekazka.physicsBody = SKPhysicsBody(texture: obstaclesTexture, size: obstacle.size)
prekazka.physicsBody?.dynamic = false
prekazka.physicsBody?.categoryBitMask = PhysicsCatagory.obstacle
prekazka.physicsBody?.contactTestBitMask = PhysicsCatagory.ball
prekazka.physicsBody?.contactTestBitMask = PhysicsCatagory.ball
self.addChild(prekazka)
}
To avoid both overlapping sprites and different positions you need to take the height of the sprite into consideration. Something like this perhaps:
let spriteHeight = 60 // or whatever
let lower = 100
let upper = Int(screenSize.height) - 100
let area = upper - lower
let ySpacing = area/spriteHeight
var allowedYs = Set<CGFloat>()
let numberOfSprites = 3
while allowedYs.count < numberOfSprites {
let random = arc4random_uniform(UInt32(spriteHeight))
let y = CGFloat(random) * CGFloat(ySpacing)
allowedYs.insert(y)
}
for y in allowedYs {
obstaclesTexture = SKTexture(imageNamed: "levaPrekazka")
obstacle = SKSpriteNode(texture: obstaclesTexture)
prekazka = obstacle.copy() as! SKSpriteNode
prekazka.name = "prekazka"
prekazka.position = CGPoint(x: 0, y: y)
// etc...
}
I would be tempted to generate the sprite's position and then loop over all existing sprites in the scene and see if their frame overlaps with the new sprites frame:
let lower : UInt32 = 100
let upper : UInt32 = UInt32(screenSize.height) - 100
obstaclesTexture = SKTexture(imageNamed: "levaPrekazka")
obstacle = SKSpriteNode(texture: obstaclesTexture)
// Generate a position for our new sprite. Repeat until the generated position cause the new sprite’s frame not to overlap with any existing sprite’s frame.
repeat {
let randomY = arc4random_uniform(upper - lower) + lower
prekazka.position = CGPoint(x: 0, y: Int(randomY))
} while checkForOverlap(prekazka) = true // If it overlaps, generate a new position and check again
//Function to see if an SkSpriteNode (passed as parameter node) overlaps with any existing node. Return True if it does, else False.
Func checkForOverlap(SKSpriteNode: node) ->> bool {
for existingNode in children { // for every existing node
if CGRectIntersectsRect(existingNode.frame, node.frame) {return True} // return True if frames overlap
}
return false //No overlap
}
(Writing this from memory, so may need a bit of fiddling to work).
Another approach would be to generate and position all the nodes with physics bodies, and set the scene up so that all nodes collide with all others. Then allow the physics engine to handle any collisions which would cause it to move the nodes around until none are overlapping. At this point, your could remove all the physics bodies.

Resources