SpriteKit - SKLightNode shadow blend mode - ios

I have 3 SKLightNodes each one with a light color: Red, Green and Blue. The effect that I want is the shadow generated by the SKLightNodes to have a blend mode.
Xcode Simulator
I did some photoshop examples.
This is the shadow current behavior:
This is the shadow behavior desired:
Is this possible to do in SpriteKit?

Color space
Hi, first of all, please note that there are different Color Spaces.
The result of mixing 2 colors depends on the Color Space you want to use.
RGB Color Space
With this space color you can represent the intersection of the 2 raylights.
In this case, when 2 colors are mixed, the intersection is brighter.
You can get this effect in SpriteKit using blendMode = .add.
Here's the full code.
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
let red = SKShapeNode(circleOfRadius: 100)
red.fillColor = .red
red.blendMode = .add
red.position = CGPoint(x: 0, y: 0)
addChild(red)
let green = SKShapeNode(circleOfRadius: 100)
green.fillColor = .green
green.blendMode = .add
green.position = CGPoint(x: 0, y: 100)
addChild(green)
let blue = SKShapeNode(circleOfRadius: 100)
blue.fillColor = .blue
blue.blendMode = .add
blue.position = CGPoint(x: 87, y: 50)
addChild(blue)
}
}
And the result
CMY Colorspace
This color space represents the real world scenario of mixing 2 fluids.
Now mixing to colors produce a new darker color.
You can get this effect in SpriteKit simply using blendMode = .multiply (and IO suggest a white background).
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
self.backgroundColor = .white
let yellow = SKShapeNode(circleOfRadius: 100)
yellow.fillColor = .yellow
yellow.blendMode = .multiply
yellow.position = CGPoint(x: 0, y: 0)
addChild(yellow)
let cyan = SKShapeNode(circleOfRadius: 100)
cyan.fillColor = .cyan
cyan.blendMode = .multiply
cyan.position = CGPoint(x: 0, y: 100)
addChild(cyan)
let magenta = SKShapeNode(circleOfRadius: 100)
magenta.fillColor = .magenta
magenta.blendMode = .multiply
magenta.position = CGPoint(x: 87, y: 50)
addChild(magenta)
}
}

Related

Gradient progress bar with rounded corners SpriteKit Swift

I'm trying to build a gradient progress bar with rounded corners in SpriteKit, but I'm completely stuck at this point. I've tried different combinations of SKCropNode, SKShapeNodes etc. but I can't seem to get it to work.
Any help is appreciated, kind regards!
It's about SKCropNode + its maskNode property. From the docs:
SKCropNode is a container node that you use to crop other nodes in the
scene. You add other nodes to a crop node and set the crop node's
maskNode property. For example, here are some ways you might specify a
mask:
An untextured sprite that limits content to a rectangular portion of
the scene.
A textured sprite that works as a precise per-pixel mask.
A collection of child nodes that form a unique shape.
You can animate the shape or contents of the mask to implement
interesting effects such as hiding or revealing.
So, a simple example would be like this:
class GameScene: SKScene {
override func sceneDidLoad() {
super.sceneDidLoad()
createProgressBar()
}
private func createProgressBar(){
let barFrame = CGRect(x: 0, y: 0, width: 300, height: 15)
if let cgImage = createImage(frame: barFrame) {
let texture = SKTexture(cgImage: cgImage)
let sprite = SKSpriteNode(texture: texture)
let cropNode = SKCropNode()
let mask = SKSpriteNode(color: .gray, size: barFrame.size)
cropNode.addChild(sprite)
cropNode.maskNode = mask
sprite.anchorPoint = CGPoint(x: 0.0, y: 0.5)
mask.anchorPoint = CGPoint(x: 0.0, y: 0.5)
var counter:Double = 0
let action = SKAction.run {[weak self, sprite] in
guard let `self` = self, counter < 100 else {
sprite?.removeAction(forKey: "loop")
return
}
counter += 1
let newWidth = self.getWidth(percents: counter, spriteWidth: barFrame.width)
print("Bar width \(newWidth), percentage \(counter)")
mask.size = CGSize(width: newWidth, height: barFrame.height)
}
let wait = SKAction.wait(forDuration: 0.05)
let sequence = SKAction.sequence([wait, action])
let loop = SKAction.repeatForever(sequence)
addChild(cropNode)
cropNode.position = CGPoint(x: self.frame.width / 2.0, y: self.frame.height / 2.0)
sprite.run(loop, withKey: "loop")
}
}
private func getWidth(percents:Double, spriteWidth:Double)->Double{
let onePercent = spriteWidth / 100.0
return onePercent * percents
}
private func createImage(frame barFrame:CGRect) -> CGImage?{
if let ciFilter = CIFilter(name: "CILinearGradient"){
let ciContext = CIContext()
ciFilter.setDefaults()
let startColor = CIColor(red: 0.75, green: 0.35, blue: 0.45, alpha: 1)
let endColor = CIColor(red: 0.45, green: 0.35, blue: 0.75, alpha: 1)
let startVector = CIVector(x: 0, y: 0)
let endVector = CIVector(x: barFrame.width, y: 0)
ciFilter.setValue(startColor, forKey: "inputColor0")
ciFilter.setValue(endColor, forKey: "inputColor1")
ciFilter.setValue(startVector, forKey: "inputPoint0")
ciFilter.setValue(endVector, forKey: "inputPoint1")
if let outputImage = ciFilter.outputImage {
let cgImage = ciContext.createCGImage(outputImage, from: CGRect(x: 0, y: 0, width: barFrame.width, height: barFrame.height))
return cgImage
}
}
return nil
}
}
Now cause this is just an example I won't go all the way to implement this right, but You can maybe make a class of it with designable and inspectable properties, optimize code, make it reusable etc. But the general idea is shown here.
You use SKCropNode to add progress bar in it, and use maskNode property to reveal progress bar as percentage increases. Also I gave a method to create texture programatically, but You can use just a .png file instead.
Crop node is here used only cause of a gradient (cause we don't wan't to scale image, but rather to show it part by part). Obviously, crop node is not needed if a progress bar had only one color.
Here is final result:

SKSpriteNode shadow rendering incorrectly?

I'm using Xcode 11.4.1 and Swift 5.2.2, and I am trying cast a shadow using an SKLightNode onto a circular SKSpriteNode.
However, the shadow is cast over the rectangular frame of the SpriteNode rather than the circle image png that I am using.
This is the desired effect
This is what happens
In the first image, I am using a 611x611px circle png while in the second I am using a 612x612 png. I have found that this "corner shadow" happens only when using specific image dimensions to create the SpriteNode.
Specifically, through my testing, square images with size <688px and >601px display no corner shadow, except sizes exactly 612 and 608. I am testing this in a playground but the same problem occurs in a full xcodeproj.
What have I done wrong here? I doubt my code is the issue but here it is:
import PlaygroundSupport
import SpriteKit
class GameScene: SKScene {
override func didMove(to view: SKView) {
let background = SKSpriteNode(color: UIColor.blue, size: frame.size)
background.zPosition = 0
background.position = CGPoint(x: frame.midX, y: frame.midY)
background.lightingBitMask = 1
addChild(background)
let light = SKLightNode()
light.zPosition = 100
light.categoryBitMask = 1
light.falloff = 0.5
light.lightColor = UIColor.white
light.position = CGPoint(x: frame.midX, y: frame.midY)
addChild(light)
let sprite = SKSpriteNode(imageNamed: "612.png")
sprite.color = UIColor.yellow
sprite.colorBlendFactor = 1
sprite.zPosition = 3
sprite.position = CGPoint(x: frame.midX, y: frame.midY + 100)
sprite.size = CGSize(width: 100, height: 100)
sprite.physicsBody = SKPhysicsBody(circleOfRadius: 50)
sprite.physicsBody?.categoryBitMask = 2
sprite.shadowCastBitMask = 1
sprite.lightingBitMask = 1
sprite.physicsBody?.isDynamic = false
addChild(sprite)
}
}
let sceneView = SKView(frame: CGRect(x:0 , y:0, width: 640, height: 480))
let scene = GameScene()
scene.scaleMode = .aspectFill
scene.size = sceneView.frame.size
sceneView.presentScene(scene)
PlaygroundSupport.PlaygroundPage.current.liveView = sceneView

Cannot Display Sprites Above SK3DNode

I'm displaying some basic 3D geometry within my SpriteKit scene using an instance of SK3DNode to display the contents of a SceneKit scene, as explained in Apple's article here.
I have been able to position the node and 3D contents as I want using SceneKit node transforms and the position/viewport size of the SK3DNode.
Next, I want to display some other sprites in my SpriteKit scene overlaid on top of the 3D content, but I am unable to do so: The contents of the SK3DNode are always drawn on top.
I have tried specifying the zPosition property of both the SK3DNode and the SKSpriteNode, to no avail.
From Apple's documentation on SK3DNode:
Use SK3DNode objects to incorporate 3D SceneKit content into a
SpriteKit-based game. When SpriteKit renders the node, the SceneKit
scene is animated and rendered first. Then this rendered image is
composited into the SpriteKit scene. Use the scnScene property to
specify the SceneKit scene to be rendered.
(emphasis mine)
It is a bit ambiguous withv regard to z-order (it only seems to mention the temporal order in which rendering takes place).
I have put together a minimal demo project on GitHub; the relevant code is:
1. SceneKit Scene
import SceneKit
class SceneKitScene: SCNScene {
override init() {
super.init()
let box = SCNBox(width: 10, height: 10, length: 10, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.green
box.materials = [material]
let boxNode = SCNNode(geometry: box)
boxNode.transform = SCNMatrix4MakeRotation(.pi/2, 1, 1, 1)
self.rootNode.addChildNode(boxNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
2. SpriteKit Scene
import SpriteKit
class SpriteKitScene: SKScene {
override init(size: CGSize) {
super.init(size: size)
// Scene Background
self.backgroundColor = .red
// 3D Node
let objectNode = SK3DNode(viewportSize: size)
objectNode.scnScene = SceneKitScene()
addChild(objectNode)
objectNode.position = CGPoint(x: size.width/2, y: size.height/2)
let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
objectNode.pointOfView = cameraNode
objectNode.pointOfView?.position = SCNVector3(x: 0, y: 0, z: 60)
objectNode.zPosition = -100
// 2D Sprite
let sprite = SKSpriteNode(color: .yellow, size: CGSize(width: 250, height: 60))
sprite.position = objectNode.position
sprite.zPosition = +100
addChild(sprite)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
...And the rendered result is:
(I want the yellow rectangle above the green box)
I made a Technical Support Incident with Apple about this and they just got back to me. The solution is actually very very simple.
If you want 2D sprites to render on top of SK3DNodes, you need to stop the contents of the SK3DNodes from writing to the depth buffer.
To do this, you just need to set writesToDepthBuffer to false on the SCNMaterial.
...
let material = SCNMaterial()
material.diffuse.contents = UIColor.green
material.writesToDepthBuffer = false
...
Boom. Works.
Please note that this is just something I stumbled upon. I have no idea why it works and I probably wouldn't trust it without further understanding, but maybe it'll help find an explanation or a real solution.
It seems that having an SKShapeNode (with a fill) alongside an SK3DNode (either as a sibling, part of a sibling tree, or child), draws it in proper order. The SKShapeNode doesn't seem to need to intersect with the SK3DNode either.
The fill is important, as having a transparent fill makes this not work. Stroke doesn't seem to have any effect.
An SKShapeNode of extremely small size and almost zero alpha fill works too.
Here's my playground:
import PlaygroundSupport
import SceneKit
import SpriteKit
let viewSize = CGSize(width: 300, height: 150)
let viewportSize: CGFloat = viewSize.height * 0.75
func skview(color: UIColor, index: Int) -> SKView {
let scene = SKScene(size: viewSize)
scene.backgroundColor = color
let view = SKView(
frame: CGRect(
origin: CGPoint(x: 0, y: CGFloat(index) * viewSize.height),
size: viewSize
)
)
view.presentScene(scene)
view.showsDrawCount = true
// Draw the box of the 3d node view port
let viewport = SKSpriteNode(color: .orange, size: CGSize(width: viewportSize, height: viewportSize))
viewport.position = CGPoint(x: viewSize.width / 2, y: viewSize.height / 2)
scene.addChild(viewport)
return view
}
func cube() -> SK3DNode {
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.green
let box = SCNBox(width: viewportSize, height: viewportSize, length: viewportSize, chamferRadius: 0)
box.firstMaterial = mat
let boxNode3d = SCNNode(geometry: box)
boxNode3d.runAction(.repeatForever(.rotateBy(x: 10, y: 10, z: 10, duration: 10)))
let scene = SCNScene()
scene.rootNode.addChildNode(boxNode3d)
let boxNode2d = SK3DNode(viewportSize: CGSize(width: viewportSize, height: viewportSize))
boxNode2d.position = CGPoint(x: viewSize.width / 2, y: viewSize.height / 2)
boxNode2d.scnScene = scene
return boxNode2d
}
func shape() -> SKShapeNode {
let shape = SKShapeNode(rectOf: CGSize(width: viewSize.height / 4, height: viewSize.height / 4))
shape.strokeColor = .clear
shape.fillColor = .purple
return shape
}
func rect(_ color: UIColor) -> SKSpriteNode {
let sp = SKSpriteNode(texture: nil, color: color, size: CGSize(width: 200, height: viewSize.height / 4))
sp.position = CGPoint(x: viewSize.width / 2, y: viewSize.height / 2)
return sp
}
// The original issue, untouched.
func v1() -> SKView {
let v = skview(color: .red, index: 0)
v.scene?.addChild(cube())
v.scene?.addChild(rect(.yellow))
return v
}
// Shape added as sibling after the 3d node. Notice that it doesn't overlap the SK3DNode.
func v2() -> SKView {
let v = skview(color: .blue, index: 1)
v.scene?.addChild(cube())
v.scene?.addChild(shape())
v.scene?.addChild(rect(.yellow))
return v
}
// Shape added to the 3d node.
func v3() -> SKView {
let v = skview(color: .magenta, index: 2)
let box = cube()
box.addChild(shape())
v.scene?.addChild(box)
v.scene?.addChild(rect(.yellow))
return v
}
// 3d node added after, but zPos set to -1.
func v4() -> SKView {
let v = skview(color: .cyan, index: 3)
v.scene?.addChild(shape())
v.scene?.addChild(rect(.yellow))
let box = cube()
box.zPosition = -1
v.scene?.addChild(box)
return v
}
// Shape added after the 3d node, but not as a sibling.
func v5() -> SKView {
let v = skview(color: .green, index: 4)
let parent = SKNode()
parent.addChild(cube())
parent.addChild(rect(.yellow))
v.scene?.addChild(parent)
v.scene?.addChild(shape())
return v
}
let container = UIView(frame: CGRect(origin: .zero, size: CGSize(width: viewSize.width, height: viewSize.height * 5)))
container.addSubview(v1())
container.addSubview(v2())
container.addSubview(v3())
container.addSubview(v4())
container.addSubview(v5())
PlaygroundPage.current.liveView = container
TL;DR
In your code, try:
...
let shape = SKShapeNode(rectOf: CGSize(width: 0.01, height: 0.01))
shape.strokeColor = .clear
shape.fillColor = UIColor.black.withAlphaComponent(0.01)
// 3D Node
let objectNode = SK3DNode(viewportSize: size)
objectNode.addChild(shape)
...

SpriteKit: A gap between SKSpriteNodes

When the blue square falls down on the red one a gap will remain. But when I make the blue square fall from a lower position there is no gap. Also when I give the blue square a higher mass the gap (on the ground) under the red square disappears and when the mass is too high the red square is gone.
I also made the squares smaller because the amount of pixels didn't equal the pixels of the png file which led to an even bigger gap. And when I set "showsPhysics" to true there is also a gap to see and even bigger when the squares are not scaled down.
Gap between SKSpriteNodes (picture)
//: Playground - noun: a place where people can play
import UIKit
import SpriteKit
import PlaygroundSupport
class Scene: SKScene {
var square = SKSpriteNode(texture: SKTexture(imageNamed: "red"), size: SKTexture(imageNamed: "red").size())
var square2 = SKSpriteNode(texture: SKTexture(imageNamed: "blue"), size: SKTexture(imageNamed: "blue").size())
var label = SKLabelNode(fontNamed: "Chalkduster")
override func didMove(to view: SKView) {
self.addChild(square)
self.addChild(square2)
self.addChild(label)
}
override func didChangeSize(_ oldSize: CGSize) {
// Add Scene Physics
self.physicsBody?.usesPreciseCollisionDetection = true
self.physicsBody = SKPhysicsBody(edgeLoopFrom: CGRect(x: -1, y: -1, width: self.size.width + 2, height: self.size.height + 2))
self.backgroundColor = UIColor.white
self.physicsBody?.usesPreciseCollisionDetection = true
//Add square
square.texture?.usesMipmaps = true
square.zPosition = 11
square.physicsBody = SKPhysicsBody(rectangleOf: square.size)
square.physicsBody?.usesPreciseCollisionDetection = true
square.physicsBody?.mass = 0.1
square.physicsBody?.allowsRotation = false
square.physicsBody?.usesPreciseCollisionDetection = true
square.setScale(0.25)
square.position = CGPoint(x: self.frame.width / 2, y: self.square.size.height)
square.name = "square"
//Add square2
square2.texture?.usesMipmaps = true
square2.zPosition = 11
square2.physicsBody = SKPhysicsBody(rectangleOf: square2.size)
square2.physicsBody?.usesPreciseCollisionDetection = true
square2.physicsBody?.mass = 0.1
square2.physicsBody?.allowsRotation = false
square2.physicsBody?.usesPreciseCollisionDetection = true
square2.setScale(0.25)
square2.position = CGPoint(x: self.frame.width / 2, y: self.square.size.height * 6)
square2.name = "square2"
//Add label
label.fontSize = 30
label.text = "w: \(self.frame.size.width) h: \(self.frame.size.height)"
label.position = CGPoint(x: self.frame.size.width / 2, y: self.frame.size.height - 30)
label.fontColor = UIColor.black
label.zPosition = 100
}
}
class ViewController: UIViewController {
let scene = Scene()
var viewSize = CGSize(width: 0, height: 0)
override func loadView() {
self.view = SKView()
}
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
view.frame = CGRect(origin: CGPoint(x: -1, y: -1), size: viewSize)
view.presentScene(scene)
view.showsPhysics = false
view.showsFPS = true
view.showsNodeCount = true
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
scene.size = CGSize(width: self.view.frame.size.width, height: self.view.frame.size.height)
}
}
PlaygroundPage.current.liveView = ViewController()
As proof from the print commands, there is no actual gap between the sprite nodes, and with second proof from a super-zoomed-in-screenshot:
The playground you provided does not replicate an issue, which makes me wonder... what is going on with your project?
pg #2:
The scene's width and height has to be multiplied by 2. So the scene has to be 4 times bigger than the ViewControllers view. It's also like that in the game template that Apple provides for Spritekit.
scene.size = CGSize(width: self.view.frame.size.width * 2, height: self.view.frame.size.height * 2)
https://github.com/simon2204/Playground

Programmatically create constrained region of physics, SpriteKit

I would like two regions, as shown in the image below, where the yellow region is to contain sprites. For example I'd like to have balls in the yellow region bouncing and reflecting off the boundaries of the yellow region. How can I programmatically do this without using an sks file?
You create an edge based physics body using the +bodyWithEdgeLoopFromRect:
override func didMoveToView(view: SKView) {
//Setup scene's physics body (setup the walls)
physicsBody = SKPhysicsBody(edgeLoopFromRect: frame)
let yellowSprite = SKSpriteNode(color: .yellowColor(), size: CGSize(width: 300, height: 300))
yellowSprite.position = CGPoint(x: frame.midX, y: frame.midY)
//Create the rectangle which will represent physics body.
let rect = CGRect(origin: CGPoint(x: -yellowSprite.size.width/2, y: -yellowSprite.size.height/2), size: yellowSprite.size)
yellowSprite.physicsBody = SKPhysicsBody(edgeLoopFromRect: rect)
addChild(yellowSprite)
//Add Red ball "inside" the yellow sprite
let red = SKShapeNode(circleOfRadius: 20)
red.fillColor = .redColor()
red.strokeColor = .clearColor()
red.position = yellowSprite.position
red.physicsBody = SKPhysicsBody(circleOfRadius: 20)
red.physicsBody?.restitution = 1
red.physicsBody?.friction = 0
red.physicsBody?.affectedByGravity = false
addChild(red)
red.physicsBody?.applyImpulse(CGVector(dx: 20, dy: 15))
}
About rect parameter:
The rectangle that defines the edges. The rectangle is specified
relative to the owning node’s origin.
Hope this helps!

Resources