I'd like to add an scnfloor where the real floor is, but it appears above and when adding a childNode, the object doesn't lay on the floor, but looks like flying.
When logging the position of the added node or plane anchor I always get y = 0, no matter if I'm scanning the floor or a table.
What am I missing? Or is it impossible to change the y of a scnfloor? But in the docs is written
"...of its local coordinate space"
, so I thought it was possible.
Thanks
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else {return}
let anchorX = planeAnchor.center.x
let anchorY = planeAnchor.center.y
let anchorZ = planeAnchor.center.z
print("planeAnchor.center %#", planeAnchor.center) //y always 0
print("node.position %#", node.position) //y always 0
print("convert %#", node.convertPosition(SCNVector3(anchorX, anchorY, anchorZ), to: nil)) //y always 0
let floor = SCNFloor()
let floorNode = SCNNode(geometry: floor)
floorNode.position = SCNVector3(anchorX, anchorY, anchorZ)
let floorShape = SCNPhysicsShape(geometry: floor, options: nil)
floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: floorShape)
node.addChildNode(floorNode)
let childNode = createChildNode()
floorNode.addChildNode(childNode)
}
The center.y value of the anchor will always be zero, there is some more information here: https://developer.apple.com/documentation/arkit/arplaneanchor/2882056-center
Basically the anchors transform will give the correct position of the detected plane instead. Because of this you can just add your SCNFloor instance as a child of the passed in node and it will render in the correct position, there is no need to set the position explicitly as you are doing above.
You will need to update your geometry size as the ARPlaneAnchor's extent changes as ARKit refines the bounds of the plane. Some more information is here: https://developer.apple.com/documentation/arkit/tracking_and_visualizing_planes
For example, the code below works for me, rendering a SCNFloor at the initially detected size of the plane and renders a box that will sit exactly on top of it. The plane is accurately placed on the floor (or whatever horizontal surface you detect):
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else {
return
}
let floor = SCNFloor()
let floorNode = SCNNode(geometry: floor)
floor.length = CGFloat(planeAnchor.extent.z)
floor.width = CGFloat(planeAnchor.extent.x)
node.addChildNode(floorNode)
let box = SCNBox(width: 0.05, height: 0.05, length: 0.05, chamferRadius: 0)
box.materials[0].diffuse.contents = UIColor.red
let boxNode = SCNNode(geometry: box)
boxNode.position = SCNVector3(0, 0.025, 0)
node.addChildNode(boxNode)
}
Related
Hi I'm trying to have a glowing effect around a node.
I used the SCNNode filters property and set to an array of CIFilter.
It works and renders only when the node has no node behind it which I don't understand. I tried to set the rendering order and the readDepth options without success. I'm really stuck at this point and would appreciate your input!
Please see the screenshot for an example and the code sample.
func addBloom() -> [CIFilter]? {
let bloomFilter = CIFilter(name:"CIBloom")!
bloomFilter.setValue(10.0, forKey: "inputIntensity")
bloomFilter.setValue(30.0, forKey: "inputRadius")
return [bloomFilter]
}
Calling this using:
myNode.filters = addBloom()
A final note, I noticed that for CIFilter to work with Metal the antiAliasing needs to be set to .none
arSceneView.antialiasingMode = .none
Thanks a lot!
Adrien
Have you tried setting the writesToDepthBuffer to false for those nodes which you aren't apply the filters to?
For your information writesToDepthBuffer refers to:
SceneKit’s rendering process uses a depth buffer to determine the
ordering of rendered surfaces relative to the viewer. The default
value of this property is YES, specifying that SceneKit saves depth
information for each rendered pixel for use by later rendering passes.
Typically, you disable writing to the depth buffer when rendering
semitransparent objects, because later stages of the rendering process
may require depth information about the opaque objects behind them.
This example seems to be working fine:
/// Generates An SCNPlane & A Red & Green SCNSphere
func generateNodes(){
let planeNode = SCNNode(geometry: SCNPlane(width: 1, height: 0.5))
planeNode.geometry?.firstMaterial?.diffuse.contents = UIColor.black
planeNode.position = SCNVector3(0, 0, -1)
let redSphereNode = SCNNode(geometry: SCNSphere(radius: 0.1))
redSphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
redSphereNode.position = SCNVector3(-0.3, 0, -1)
redSphereNode.filters = addBloom()
let greenSphereNode = SCNNode(geometry: SCNSphere(radius: 0.1))
greenSphereNode.geometry?.firstMaterial?.diffuse.contents = UIColor.green
greenSphereNode.position = SCNVector3(0.3, 0, -1)
greenSphereNode.filters = addBloom()
self.augmentedRealityView.scene.rootNode.addChildNode(planeNode)
self.augmentedRealityView.scene.rootNode.addChildNode(redSphereNode)
self.augmentedRealityView.scene.rootNode.addChildNode(greenSphereNode)
planeNode.geometry?.firstMaterial?.writesToDepthBuffer = false
}
/// Creates An Array Of CIBloom Filters
///
/// - Returns: [CIFilter]?
func addBloom() -> [CIFilter]? {
let bloomFilter = CIFilter(name:"CIBloom")!
bloomFilter.setValue(10.0, forKey: "inputIntensity")
bloomFilter.setValue(30.0, forKey: "inputRadius")
return [bloomFilter]
}
One thing to note however, which I did notice was that if I used an image with a transparent background for the contents of the SCNPlane it didn't work, although with another image it was fine.
Hope it points you in the right direction...
I'm having issues with getting ALL elements of an array to fall using the Gravity module. I have managed to get the LAST element in the array to fall and then the remaining elements just stay at the top of the screen during testing. Upon debugging
I am using UIKit and want to understand this language thoroughly before using other various engines such as SpriteKit and GameplayKit.
func mainGame()
{
let cars = ["car5", "car1", "car6", "car3", "car2", "car4"]
var random2 = Int(arc4random_uniform(UInt32(cars.count))) + 1
for i in 1...random2
{
let image = UIImage(named: cars[i - 1])
let carView = UIImageView(image: image!)
carView.frame = CGRect(x:i * 52, y:0 , width: 40, height: 50)
view.addSubview(carView)
dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
gravityBehavior = UIDynamicItemBehavior(items: [carView]) //cars falling
dynamicAnimator.addBehavior(gravityBehavior)
collisionBehavior = UICollisionBehavior(items: [carView, mainCar]) //collide
collisionBehavior.translatesReferenceBoundsIntoBoundary = false
gravityBehavior.addLinearVelocity(CGPoint(x: 0, y: 200), for: carView)
dynamicAnimator.addBehavior(collisionBehavior)
}
collisionBehavior.addBoundary(withIdentifier: "Barrier" as NSCopying, for: UIBezierPath(rect: mainCar.frame))
collisionBehavior.removeAllBoundaries()
}
With the game so far the last car in the array falls and the main player car that I control has collision behaviour, which is a big step for me!
You are creating a new UIDynamicAnimator with each iteration of the loop and assigning it to dynamicAnimator. That is why only the last element is working, because it is the last one assigned to that variable.
To fix it, just move this line to somewhere that would only be called once.
dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
viewDidLoad is a possible place that should work.
UIKitDynamics is backwards of most similar frameworks. You don't animate the object. You have an animator and attach objects to it. As Clever Error notes, you only want one animator in this case.
The key point is that you don't attach gravity to cars; you attach cars to behaviors (gravity), and then behaviors to the animator. Yes, that's bizarre and backwards.
I haven't tested this, but the correct code would be closer to this:
func mainGame()
{
let cars = ["car5", "car1", "car6", "car3", "car2", "car4"]
var random2 = Int(arc4random_uniform(UInt32(cars.count))) + 1
var carViews: [UIImageView] = []
dynamicAnimator = UIDynamicAnimator(referenceView: self.view)
// First create all the views
for i in 1...random2
{
let image = UIImage(named: cars[i - 1])
let carView = UIImageView(image: image!)
carView.frame = CGRect(x:i * 52, y:0 , width: 40, height: 50)
view.addSubview(carView)
carViews.append(carView)
}
// and then attach those to behaviors:
gravityBehavior = UIGravityBehavior(items: carViews) //cars falling
dynamicAnimator.addBehavior(gravityBehavior)
collisionBehavior = UICollisionBehavior(items: carView + mainCar) //collide
collisionBehavior.translatesReferenceBoundsIntoBoundary = false
dynamicAnimator.addBehavior(collisionBehavior)
collisionBehavior.addBoundary(withIdentifier: "Barrier" as NSCopying, for: UIBezierPath(rect: mainCar.frame))
collisionBehavior.removeAllBoundaries()
// You don't need this; it's built into Gravity
// gravityBehavior.addLinearVelocity(CGPoint(x: 0, y: 200), for: carView)
}
The main way that UIKitDynamics is different than most animation frameworks is that things that are animated don't know they're being animated. You can't ask a car what behaviors it has, because it doesn't have any. A UIDynamicAnimator basically is a timing loop that updates the center and transform of its targets. There's really not anything fancy about it (in contrast to something like Core Animation which has many fancy things going on). With a little iOS experience, you could probably implement all of UIKitDynamics by hand with a single GCD queue (it probably doesn't even need that, since it runs everything on main....)
I would like to create a new Array of 3 random values pulled from this list to be used in two other view controllers. Not sure how to assign the Array so those values are accessible.
class WorkoutDataSource {
var allWorkouts:[Workout]
init() {
allWorkouts = []
let bh1 = Workout(title: "Figure 8s - Clockwise", workoutText: "Clockwise around each leg.", color: UIColor.flatNavyBlueColorDark())
allWorkouts.append(bh1)
let bh2 = Workout(title: "Figure 8s - Counter Clockwise", workoutText: "Counter Clockwise around each leg.", color: UIColor.flatNavyBlue())
allWorkouts.append(bh2)
let bh3 = Workout(title: "Dribble Left Handed", workoutText: "Low and Powerfull.", color: UIColor.flatTealColorDark())
allWorkouts.append(bh3)
let bh4 = Workout(title: "Dribble Right Handed", workoutText: "Low and powerfull", color: UIColor.flatTeal())
allWorkouts.append(bh4)
let bh5 = Workout(title: "Around Both Feet", workoutText: "With feet together, circles around ankles-waist-head, work up & down, the full length of body.", color: UIColor.flatSkyBlueColorDark())
allWorkouts.append(bh5)
let bh6 = Workout(title: "Spider Dribble", workoutText: "feet apart, 1 dribble with each hand in front, then 1 dribble with each hand in back. ", color: UIColor.flatSkyBlue())
allWorkouts.append(bh6)
let bh7 = Workout(title: "Helicopter", workoutText: "1 hand in front, 1 hand in back – move your hands from front to back while catching the ball between your legs.", color: UIColor.flatGreenColorDark())
allWorkouts.append(bh7)
let bh8 = Workout(title: "Scissors - Legs Not Moving", workoutText: "One leg forward and one leg back. Dribble between your legs low and fast.", color: UIColor.flatGreen())
allWorkouts.append(bh8)
let bh9 = Workout(title: "Behind the Back", workoutText: ".",color: UIColor.flatGray())
allWorkouts.append(bh9)
let bh10 = Workout(title: "Fingertip Squeeze", workoutText: ".",color: UIColor.flatPink())
allWorkouts.append(bh10)
}
func getWorkOuts() -> [Workout]{
return allWorkouts
}
}
Thank you
If you simply want to get a random value from the allWorkouts array, then you can do something like this:
let ndx = Int(arc4random_uniform(UInt32(allWorkouts.count)))
let workout = allWorkouts[ndx]
If you want to get three workouts at random, and you don't care if they are unique or not, then you can do something like this:
var workouts = [Workout]()
for i in 0 ..< 3 {
let ndx = Int(arc4random_uniform(UInt32(allWorkouts.count)))
let workout = allWorkouts[ndx]
workouts.append(workout)
}
The workouts array will have the three workouts at that point.
If you want the random items to be unique, then you should use a Set instead of an array and ensure that you run till you get three items, like this:
var workouts = Set<Workout>()
while workouts.count < 3 {
let ndx = Int(arc4random_uniform(UInt32(allWorkouts.count)))
let workout = allWorkouts[ndx]
workouts.insert(workout)
}
I've designed my program so that each time the user touches the screen, the sprite's image and position changes. I want to be able to make an array of SKSpriteNodes. I've seen a similar post, but they used a for-in loop. Is it possible to create a SKSpriteNode at initialization?
GameScene: SKScene {
// make an array of images that you will possibly change in the future
// calls the image
let dad = SKSpriteNode(imageNamed: "dad0")
var imageName = "dad"
let moveLeft = SKAction.moveByX(-10, y:0 , duration: 0.01)
...assuming that I already placed dad sprite node onto screen...
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
// when index is zero, sprite is dad0.
// Change image to dad1 (lifted leg), don't change position
if index == 0{
index += 1 //image names are dad0, dad1
imageName += "\(index)"
print(imageName)
dad.texture = SKTexture(imageNamed:imageName)
}
else{
index += 1
imageName += "\(index)"
print(imageName)
dad.texture = SKTexture(imageNamed:imageName)
//moves dad
dad.runAction(moveLeft) // moves image
index = 0
}
//change the image name back to dad
imageName = "dad"
}
}
An array of SKSpriteNode could be simply: [SKSpriteNode] (Array in swift composed by elements of SKSpriteNode type)
So , everytime you want to add a new SKSpriteNode you can do it with:
var arraySprites :[SKSpriteNode] = [SKSpriteNode]()
let dad : SKSpriteNode!
dad = SKSpriteNode(imageNamed: "dad0")
arraySprites.append(dad)
I've been looking for a little while on how to create a circle in Swift, but it seems quite complicated. I want to create a circle at run time, with a specified x and y, and width and height. I then want to add this circle to an array where I can create more circles and add to it.
How do I do this?
Edit: This is what I've tried so far:
var center : CGPoint = touchLocation
var myContext : CGContextRef = UIGraphicsGetCurrentContext()
let color : [CGFloat] = [0, 0, 1, 0.5]
CGContextSetStrokeColor (myContext, color);
CGContextStrokeEllipseInRect (myContext, CGRectMake(touchLocation.x, touchLocation.y, 20, 20));
touchLocation is the location of the users finger. This crashes on execution on this line:
var myContext : CGContextRef = UIGraphicsGetCurrentContext()
The error says "Unexpectedly found nil while unwrapping an Optional value
Also, this doesn't allow me to add the circle to an array, because I don't know what variable type it is.
There are many ways to draw a circle, here is a snippet that I have been hacking with:
func circleWithCenter(c:CGPoint, radius r:CGFloat,
strokeColor sc: UIColor = UIColor.blackColor(),
fillColor fc: UIColor = UIColor.clearColor()) -> CAShapeLayer {
var circle = CAShapeLayer()
circle.frame = CGRect(center:c, size:CGSize(width:2*r, height:2*r))
circle.path = UIBezierPath(ovalInRect:circle.bounds).CGPath
circle.fillColor = fc.CGColor
circle.strokeColor = sc.CGColor
circle.fillColor = fc == UIColor.clearColor() ? nil : fc.CGColor
return circle
}
Note that I extended CGRect (using Swift specific features) to add a initializer that takes a center, but that is not material to your question.
Your code is not "creating" a circle as an object, it is attempting to draw one in the graphics context. What you need to do is to create a bezier path object, draw into that and save the path in your array. Something like:
var circleArray = [CGMutablePathRef]()
// ...
let path: CGMutablePathRef = CGPathCreateMutable()
CGPathAddArc(path, nil, touchLocation.x, touchLocation.y, radius, 0, M_PI * 2, true)
circleArray += [path]
You then need to stroke the path(s) in your draw routine.