Since a while I am looking around to find answers to my Issue, but cannot find anything that really helps, or explains what is happening. I also over and over checked everything I found on SO, but couldn't find the answer.
The Issue is happening continuously when displaying Objects in the AR World. iEx I place an object to a Plane on the floor, which is my invisible Shadow Plane. Then it depends all on the viewing angle from the device. To clarify, I added to images, which has just a slightly different viewing angle. Have a look what is happening to the shadows:
I would like to have a good shadow all the time and not such artefacts as you can see.
Note: I already played around using the shadowSampleCount, the Bias, and all the other options, that should help to get a proper, low rendering cost shadow.
Here are is the extract of the relevant code for Lighting and Plane, Material, etc
For the SCNLight:
class func directionalLight() -> SCNLight {
let light = SCNLight()
light.type = .directional
light.castsShadow = true
light.color = UIColor.white
light.shadowMode = .deferred
light.shadowSampleCount = 8
light.shadowRadius = 1
// light.automaticallyAdjustsShadowProjection = false
light.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)
light.categoryBitMask = -1
return light
}
and how I add it:
func setupLights() {
lightDirectionNode.light = Lighting.directionalLight()
// lightDirectionNode.eulerAngles = SCNVector3(-66.degreesToRadians, 0, 0)
lightDirectionNode.eulerAngles = SCNVector3(0, 90.degreesToRadians, 45.degreesToRadians)
sceneView.scene.rootNode.addChildNode(lightDirectionNode)
}
For the SCNPlane:
class func shadowPlane() -> SCNNode {
let objectShape = SCNPlane(width: 200, height: 200)
objectShape.heightSegmentCount = 2
objectShape.widthSegmentCount = 2
objectShape.cornerRadius = 100
objectShape.cornerSegmentCount = 16
let objectNode = SCNNode(geometry: objectShape)
objectNode.geometry?.firstMaterial?.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
objectNode.geometry?.firstMaterial?.colorBufferWriteMask = SCNColorMask(rawValue: 0)
objectNode.physicsBody = Physics.floorPhysicsBody(shape: objectShape)
objectNode.name = "floor"
objectNode.renderingOrder = -10 // renderingOrder // 0
return objectNode
}
and how I add it:
func setupShadowPlane() {
let shadowPlane = NodeFactory.shadowPlane()
// Set the Node's properties
shadowPlane.position = SCNVector3(x: (focusSquare.lastPosition?.x)!, y: (focusSquare.lastPosition?.y)!, z: (focusSquare.lastPosition?.z)!)
shadowPlane.eulerAngles = SCNVector3(-90.degreesToRadians, 0.0, 0.0)
sceneView.scene.rootNode.addChildNode(shadowPlane)
}
What am I doing wrong? Can anyone help?
There are 3 more instance properties to take into consideration:
var shadowRadius: CGFloat { get set }
var shadowCascadeCount: Int { get set }
var shadowCascadeSplittingFactor: CGFloat { get set }
If you don't setup these ones they definitely cause rendering artifacts.
let lightNode = SCNNode()
lightNode.light = SCNLight()
// POSITION OF DIRECTIONAL LIGHT ISN'T IMPORTANT.
// ONLY DIRECTION IS CRUCIAL FOR DIRECTIONAL LIGHTS.
lightNode.rotation = SCNVector4(x: 0, y: 0, z: 0, w: 1)
lightNode.light!.type = .directional
lightNode.light!.castsShadow = true
lightNode.light?.shadowMode = .deferred
/* THREE INSTANCE PROPERTIES TO SETUP */
lightNode.light?.shadowRadius = 3.25
lightNode.light?.shadowCascadeCount = 3
lightNode.light?.shadowCascadeSplittingFactor = 0.09
lightNode.light?.shadowColor = UIColor(white: 0, alpha: 0.75)
scene.rootNode.addChildNode(lightNode)
And one more thing – when Auto Adjust is off:
Light, like a camera, has near and far clipping planes for setup.
lightNode.light?.zNear = 0
lightNode.light?.zFar = 1000000 // Far Clipping Plane is important
Hope this helps.
Related
Using Swift 5.5, iOS 14
Trying to create a simple 3D bar chart and I immediately find myself in trouble.
I wrote this code...
var virgin = true
for i in stride(from: 0, to: 6, by: 0.5) {
let rnd = CGFloat.random(in: 1.0...4.0)
let targetGeometry = SCNBox(width: 0.5,
height: rnd,
length: 0.5,
chamferRadius: 0.2)
targetGeometry.firstMaterial?.fillMode = .lines
targetGeometry.firstMaterial?.diffuse.contents = UIColor.blue
let box = SCNNode(geometry: targetGeometry)
box.simdPosition = SIMD3(x: 0, y: 0, z: 0)
coreNode.addChildNode(box)
}
This works well, but all the bars a centred around their centre. But how can I ask SceneKit to change the alignment?
Almost got this working with this code...
box.simdPosition = SIMD3(x: Float(i) - 3,
y: Float(rnd * 0.5),
z: 0)
But the result isn't right... I want/need the bar to grow from the base.
https://youtu.be/KJgvdBFBfyc
How can I make this grow from the base?
--Updated with working solution--
Tried Andy Jazz suggestion replacing simdposition with the following formulae.
box.simdPosition = SIMD3(x:box.simdPosition.x, y:0, z:0)
box.simdPivot.columns.3.y = box.boundingBox.max.y - Float(rnd * 0.5)
Worked well, to which I added some animation! Thanks Andy.
changer = changeling.sink(receiveValue: { [self] _ in
var rnds:[CGFloat] = []
for i in 0..<12 {
let rnd = CGFloat.random(in: 1.0...2.0)
let targetGeometry = SCNBox(width: 0.45, height: rnd, length: 0.45, chamferRadius: 0.01)
let newNode = SCNNode(geometry: targetGeometry)
sourceNodes[i].simdPivot.columns.3.y = newNode.boundingBox.min.y
sourceNodes[i].simdPosition = SIMD3(x: sourceNodes[i].simdPosition.x, y: 0, z: 0)
rnds.append(rnd)
}
for k in 0..<12 {
if virgin {
coreNode.addChildNode(sourceNodes[k])
}
let targetGeometry = SCNBox(width: 0.45, height: rnds[k], length: 0.45, chamferRadius: 0.01)
targetGeometry.firstMaterial?.fillMode = .lines
targetGeometry.firstMaterial?.diffuse.contents = UIColor.blue
let morpher = SCNMorpher()
morpher.targets = [targetGeometry]
sourceNodes[k].morpher = morpher
let animation = CABasicAnimation(keyPath: "morpher.weights[0]")
animation.toValue = 1.0
animation.repeatCount = 0.0
animation.duration = 1.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
sourceNodes[k].addAnimation(animation, forKey: nil)
}
virgin = false
})
You have to position a pivot point of each bar to its base:
boxNode.simdPivot.columns.3.y = someFloatNumber
To reposition a pivot to bar's base use bounding box property:
boxNode.simdPivot.columns.3.y += (boxNode.boundingBox.min.y as? simd_float1)!
After pivot's offset, reposition boxNode towards negative direction of Y-axis.
boxNode.position.y = 0
I have a SpriteKit scene with a sprite. The sprite has a physics body derived from the texture's alpha to get an accurate physics shape like so:
let texture_bottle = SKTexture(imageNamed:"Bottle")
let sprite_bottle = SKSpriteNode(texture: texture_bottle)
physicsBody_bottle = SKPhysicsBody(texture: texture_bottle, size: size)
physicsBody_bottle.affectedByGravity = false
sprite_bottle.physicsBody = physicsBody_bottle
root.addChild(sprite_bottle)
....
func touchesBegan(_ touches: Set<UITouch>?, with event: UIEvent?, touchLocation: CGPoint!) {
let hitNodes = self.nodes(at: touchLocation)
}
When a user taps the screen, how can I detect if they actually touched within the physics body shape (not the sprite's rect)?
You "can't" (Not easily)
UITouch commands are based on CGRects, so let hitNodes = self.nodes(at: touchLocation) is going to be filled with any node who's frame intersects with that touch.
This can't be avoided, so the next step is to determine pixel accuracy from the nodes that registered as "hit". The first thing you should do is convert the touch position to local coordinates to your sprite.
for node in hitNodes
{
//assuming touchLocation is based on screen coordinates
let localLocation = node.convertPoint(touchLocation,from:scene)
}
Then from this point you need to figure out which method you want to use.
If you need speed, then I would recommend creating a 2D boolean array that behaves as a mask, and fill this array with false for transparent areas and true for opaque areas. Then you can use localLocation to point to a certain index of the array (Remember to add anchorPoint * width and height to your x and y values then cast to int)
func isHit(node: SKNode,mask: [[Boolean]],position:CGPoint) -> Boolean
{
return mask[Int(node.size.height * node.anchorPoint.y + position.y)][Int(node.size.width * node.anchorPoint.x + position.x)]
}
If speed is not of concern, then you can create a CGContext, fill your texture into this context, and then check if the point in the context is transparent or not.
Something like this would help you out:
How do I get the RGB Value of a pixel using CGContext?
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
extension CALayer {
func colorOfPoint(point:CGPoint) -> UIColor
{
var pixel:[CUnsignedChar] = [0,0,0,0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue)
let context = CGBitmapContextCreate(&pixel, 1, 1, 8, 4, colorSpace,bitmapInfo.rawValue)
CGContextTranslateCTM(context, -point.x, -point.y)
self.renderInContext(context!)
let red:CGFloat = CGFloat(pixel[0])/255.0
let green:CGFloat = CGFloat(pixel[1])/255.0
let blue:CGFloat = CGFloat(pixel[2])/255.0
let alpha:CGFloat = CGFloat(pixel[3])/255.0
//println("point color - red:\(red) green:\(green) blue:\(blue)")
let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)
return color
}
}
extension UIColor {
var components:(red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
var r:CGFloat = 0
var g:CGFloat = 0
var b:CGFloat = 0
var a:CGFloat = 0
getRed(&r, green: &g, blue: &b, alpha: &a)
return (r,g,b,a)
}
}
//get an image we can work on
var imageFromURL = UIImage(data: NSData(contentsOfURL: NSURL(string:"https://www.gravatar.com/avatar/ba4178644a33a51e928ffd820269347c?s=328&d=identicon&r=PG&f=1")!)!)
//only use a small area of that image - 50 x 50 square
let imageSliceArea = CGRectMake(0, 0, 50, 50);
let imageSlice = CGImageCreateWithImageInRect(imageFromURL?.CGImage, imageSliceArea);
//we'll work on this image
var image = UIImage(CGImage: imageSlice!)
let imageView = UIImageView(image: image)
//test out the extension above on the point (0,0) - returns r 0.541 g 0.78 b 0.227 a 1.0
var pointColor = imageView.layer.colorOfPoint(CGPoint(x: 0, y: 0))
let imageRect = CGRectMake(0, 0, image.size.width, image.size.height)
UIGraphicsBeginImageContext(image.size)
let context = UIGraphicsGetCurrentContext()
CGContextSaveGState(context)
CGContextDrawImage(context, imageRect, image.CGImage)
for x in 0...Int(image.size.width) {
for y in 0...Int(image.size.height) {
var pointColor = imageView.layer.colorOfPoint(CGPoint(x: x, y: y))
//I used my own creativity here - change this to whatever logic you want
if y % 2 == 0 {
CGContextSetRGBFillColor(context, pointColor.components.red , 0.5, 0.5, 1)
}
else {
CGContextSetRGBFillColor(context, 255, 0.5, 0.5, 1)
}
CGContextFillRect(context, CGRectMake(CGFloat(x), CGFloat(y), 1, 1))
}
}
CGContextRestoreGState(context)
image = UIGraphicsGetImageFromCurrentImageContext()
where you would eventually call colorOfPoint(point:localLocation).cgColor.alpha > 0 to determine if you are touching a node or not.
Now I would recommend you make colorOfPoint an extension of SKSpriteNode, so be creative with the code posted above.
func isHit(node: SKSpriteNode,position:CGPoint) -> Boolean
{
return node.colorOfPoint(point:localLocation).cgColor.alpha > 0
}
Your final code would look something like this:
hitNodes = hitNodes.filter
{
node in
//assuming touchLocation is based on screen coordinates
let localLocation = node.convertPoint(touchLocation,from:node.scene)
return isHit(node:node,mask:mask,position:localLocation)
}
OR
hitNodes = hitNodes.filter
{
node in
//assuming touchLocation is based on screen coordinates
let localLocation = node.convertPoint(touchLocation,from:node.scene)
return isHit(node:node,position:localLocation)
}
which is basically filtering out all nodes that were in the detected by the frame comparison, leaving you pixel perfect touched nodes.
Note: The code from the separate SO link may need to be converted to Swift 4.
Aside from the fact that the question asked here : Draw a hole in a rectangle with SpriteKit? has not satisfactorily been answered in its own right, the most significant difference between the two is that this question requires an absence of workarounds and specifically asks about reversing the functionality of SKCropNodes.
The primary concerns in this question cannot be addressed by the type of hacky ways the above question could be answered due to the randomness of the holes, the number of holes, and the variation in objects to which the holes are to be applied.
Hence the Swiss Cheese example:
Imagine a bunch of randomly sized rectangular SKSpriteNodes filled with various shades of cheese-like colours.
How to make Swiss Cheese out of each by cutting random circles out of each slice of cheese?
If SKCropNodes are the circles, they only leave circular pieces of cheese rather than cutting holes out of the pieces of cheese. Is there a way to invert the behaviour of SKCropNodes so they cut holes instead?
In my total honesty, I'm not really sure to understand deeply what do you want to achieve, but I can try to answer to this question:
..cutting random circles out of each slice of cheese?
In this project I've try to re-build a typical rectangle (the piece of cheese) with random holes, then I've extract these holes and collect them to an array.
import SpriteKit
class GameScene: SKScene {
struct Cheese
{
static let color1 = SKColor(red: 255/255, green: 241/255, blue: 173/255, alpha: 1)
static let color2 = SKColor(red: 255/255, green: 212/255, blue: 0/255, alpha: 1)
static let color3 = SKColor(red: 204/255, green: 170/255, blue: 0/255, alpha: 1)
static let color4 = SKColor(red: 140/255, green: 116/255, blue: 0/255, alpha: 1)
}
let cheeseColor = [Cheese.color1,Cheese.color2,Cheese.color3,Cheese.color4]
override func didMove(to view: SKView) {
let totHoles = randomNumber(range:4...8)
let color = randomNumber(range:0...3)
let cheeseCropNode = makeCheese(size: CGSize(width:400,height:200),color: cheeseColor[color], totHoles:totHoles)
cheeseCropNode.position = CGPoint(x:0,y:-50)
addChild(cheeseCropNode)
// Start to collect and show holes
var holes = [SKNode]()
var counter = 1
let _ = cheeseCropNode.enumerateChildNodes(withName: "//hole*", using:{ node, stop in
// node is the hole
let pos = self.convert(node.position, from: cheeseCropNode)
let sprite = SKSpriteNode.init(color: .red, size: node.frame.size)
sprite.position = pos
//Remove these shapes, it's just to debug
let shape = SKShapeNode.init(rect: sprite.frame)
shape.strokeColor = .red
self.addChild(shape)
// -- end to remove
let holeTxt = SKView().texture(from: cheeseCropNode, crop: sprite.frame)
let hole = SKSpriteNode.init(texture: holeTxt)
hole.position = CGPoint(x:-(self.frame.maxX)+(100*CGFloat(counter)),y:150)
hole.name = node.name
self.addChild(hole)
holes.append(hole)
counter += 1
})
}
func randomNumber(range: ClosedRange<Int> = 1...6) -> Int {
let min = range.lowerBound
let max = range.upperBound
return Int(arc4random_uniform(UInt32(1 + max - min))) + min
}
func randomCGFloat(min: CGFloat, max: CGFloat) -> CGFloat {
return (CGFloat(arc4random()) / CGFloat(UINT32_MAX)) * (max - min) + min
}
func makeCheese(size:CGSize , color:SKColor, totHoles:Int)->SKCropNode {
let cropNode = SKCropNode()
let cheese = SKSpriteNode.init(color: color, size: size)
for i in 0..<totHoles {
let radius = randomCGFloat(min:20.0, max:50.0)
let circle = SKShapeNode(circleOfRadius: radius)
circle.position = CGPoint(x:randomCGFloat(min:-size.width/2, max:size.width/2),y:randomCGFloat(min:-size.height/2, max:size.height/2))
circle.fillColor = color
circle.blendMode = .subtract
circle.name = "hole\(i)"
cheese.addChild(circle)
}
cropNode.addChild(cheese)
cropNode.maskNode = cheese
return cropNode
}
}
Result:
P.S. Don't pay attention to red rectangles, it's just to show you the holes:
If you want the exactly reversed hole, (the negative image), you could use SKCropNode with the hole.blendMode, for example:
Substitute this part of the code:
// -- end to remove
let holeTxt = SKView().texture(from: cheeseCropNode, crop: sprite.frame)
let hole = SKSpriteNode.init(texture: holeTxt)
hole.position = CGPoint(x:-(self.frame.maxX)+(100*CGFloat(counter)),y:150)
hole.name = node.name
self.addChild(hole)
holes.append(hole)
counter += 1
with this part:
// -- end to remove
let holeTxt = SKView().texture(from: cheeseCropNode, crop: sprite.frame)
let hole = SKSpriteNode.init(texture: holeTxt)
hole.position = CGPoint(x:-(self.frame.maxX)+(100*CGFloat(counter)),y:150)
hole.name = node.name
let negativeCropHole = SKCropNode()
let shadow = SKShapeNode.init(rect: hole.frame)
shadow.fillColor = (node as! SKShapeNode).fillColor
shadow.strokeColor = SKColor.clear
hole.blendMode = .subtract
negativeCropHole.addChild(shadow)
negativeCropHole.maskNode = shadow
negativeCropHole.addChild(hole)
negativeCropHole.name = hole.name
self.addChild(negativeCropHole)
holes.append(negativeCropHole)
counter += 1
Result (another example):
Hope these example and this code help you to obtain your objectives, I've used rectangles to make masks but your could create CGPaths if you need.
I have 2 spheres in my project at the same location. The firstSphere is smaller and the tempSphere is larger.
EDIT:
The firstShape will grow larger than the tempShape. When I pause the firstShape I am trying to test like 'if firstShape.scale == tempShape.scale'
//Create Shape
let firstShapeGeo = SCNSphere(radius: shapeRadius)
firstShape.geometry = firstShapeGeo
let shapeMaterial = SCNMaterial()
shapeMaterial.diffuse.contents = UIColor(colorLiteralRed: 0.2, green: 0.8, blue: 0.9, alpha: 1.0)
firstShapeGeo.materials = [shapeMaterial]
firstShape.position = SCNVector3Make(0, 0, 0)
scene.rootNode.addChildNode(firstShape)
firstShape.name = "\(shapeNumber)"
// Create Temp Shape
tempShapeRadius = shapeRadius + 1.0
let tempShapeGeo = SCNSphere(radius: tempShapeRadius)
tempShape.geometry = tempShapeGeo
let tempShapeMaterial = SCNMaterial()
tempShapeMaterial.diffuse.contents = UIColor(colorLiteralRed: 0.2, green: 0.8, blue: 0.9, alpha: 0.5)
tempShapeGeo.materials = [tempShapeMaterial]
firstShape.position = SCNVector3Make(0, 0, 0)
scene.rootNode.addChildNode(tempShape)
This is how I am growing the firstSphere
let grow = SCNAction.scale(to: tempShapeRadius * 2 + 1, duration: 1)
let shrink = SCNAction.scale(to: tempShapeRadius, duration: 1)
let sequence = SCNAction.sequence([grow, shrink])
firstShape.run(SCNAction.repeatForever(sequence))
The smaller sphere is growing larger by using SCNAction.scale so I do not believe the radius is actually changing. Not sure if this is something you would need to know.
Any help would be much appreciated! Thanks!
Assign the same radius to both spheres. Set the scale of your tempShape to whatever value makes it the desired amount larger that the initial value of firstShape. Now you need only compare scale.
Let's suppose you want the tempShape sphere to be 3 times the size of the adjustable sphere.
let tempScale: CGFloat = 3.0
tempShape.scale = SCNVector3(tempScale, tempScale, tempScale)
// grow from 1 to tempScale, then back to 1
let grow = SCNAction.scaleTo(tempScale, duration: 1)
let shrink = SCNAction.scaleTo(1, duration: 1)
let sequence = SCNAction.sequence([grow, shrink])
firstShape.run(SCNAction.repeatForever(sequence))
I am making a IOS game in Swift using Spritekit, the player has a triangular mesh, that I create with this code:
let viewpath = CGPathCreateMutable();
CGPathMoveToPoint(viewpath, nil, 200, 0);
CGPathAddLineToPoint(viewpath, nil, 0, 400);
CGPathAddLineToPoint(viewpath, nil, 400, 400);
CGPathAddLineToPoint(viewpath, nil, 200, 0);
let playerviewshape = SKShapeNode(path: viewpath);
playerviewshape.fillColor = SKColor(red: 1, green: 1, blue: 1, alpha: 0.1)
playerviewshape.strokeColor = SKColor(red: 1, green: 1, blue: 1, alpha: 0.2)
playerview = SKSpriteNode(texture: view.textureFromNode(playerviewshape));
playerview.zPosition = 0;
playerview.position = CGPoint(x: 0, y: 200);
player.addChild(playerview);
I want to know if any enemies are inside the triangle, So in the update loop I use if(enemy.intersectsNode(playerview)) { however this only checks if it is inside the node's frame, Which is a rectangle around the triangle. I have tried using the triangle as a SKShapeNode and I have also tried using containsPoint instead of intersectsNode.
How can I change the frame/the way I detect intersections, so it will only detect the sprites if they are inside the triangle?
I write some code that can be useful to starting to set your physics:
(P.S. I dont know which type was player so I use playerview just to make an example..)
enum CollisionTypes: UInt32 {
case Playerview = 1 // my hero
case WarField = 2 // the warfield
case Enemy1 = 4 // simple enemies type
case Enemy2 = 8 // 2 level enemies type
case Enemy3 = 16 // boss enemies type
case Enemy4 = 32 // boss of the boss enemies type
}
playerview.physicsBody = SKPhysicsBody(texture: texture, size: texture.size())
playerview.physicsBody!.categoryBitMask = CollisionTypes.Playerview.rawValue
playerview.physicsBody!.contactTestBitMask = CollisionTypes.Enemy1.rawValue | CollisionTypes.Enemy2.rawValue | CollisionTypes.Enemy3.rawValue | CollisionTypes.Enemy4.rawValue
playerview.physicsBody!.collisionBitMask = 0
func didBeginContact(contact: SKPhysicsContact) {
if (contact.bodyA.categoryBitMask == CollisionTypes.Playerview.rawValue &&
contact.bodyB.categoryBitMask == CollisionTypes.Enemy1.rawValue) {
print("The collision was between the playerview and a enemy1")
}
...
}