Scenekit : Child node is not getting added properly to parent node - ios

I am adding plane node to the sphere node using hit test. whenever I tried to add plane node to the sphere node. it's shape is different each time. I am not getting why this is happening.
Code :
let sphere = SCNSphere(radius: 10)
sphere.segmentCount = 360
let material = textureProvider.getTextureMaterial()
material.diffuse.contents = UIImage(named: "sample")
sphere.firstMaterial = material
let sphereNode = SCNNode()
sphereNode.geometry = sphere
sceneView.scene?.rootNode.addChildNode(sphereNode)
func getTextureMaterial() -> SCNMaterial {
let material = SCNMaterial()
material.diffuse.mipFilter = .nearest
material.diffuse.magnificationFilter = .linear
// material.diffuse.contentsTransform = SCNMatrix4MakeScale(-1, 1, 1)
material.diffuse.wrapS = .repeat
material.cullMode = .front
// material.isDoubleSided = true
return material
}
#objc func handleTap(rec: UITapGestureRecognizer){
if rec.state == .ended {
let location: CGPoint = rec.location(in: sceneView)
let hits = sceneView.hitTest(location, options: nil)
if !hits.isEmpty {
let result: SCNHitTestResult = hits[0]
createPlaneNode(result: result)
}
}
}
func createPlaneNode(result : SCNHitTestResult) {
let plane = SCNPlane(width: 5, height: 5)
plane.firstMaterial?.diffuse.contents = UIColor.red
plane.firstMaterial?.isDoubleSided = true
plane.firstMaterial?.cullMode = .front
let planeNode = SCNNode()
planeNode.name = "B"
planeNode.geometry = plane
planeNode.position = result.worldCoordinates
result.node.addChildNode(planeNode)
}
Please help

Related

Move camera to tapped SCNNode

I'm using SceneKit and Swift to try and move the camera so it's 'focused' on the selected node. I understand I have the defaultCameraController enabled but I was trying to adjust the camera's position via dolly, rotate and translateInCameraSpaceBy but there was no animated transition - it just jumped to the new position.
Is there anyway for the camera to glide into position like how Google Maps slides/then zooms over to a searched location?
Any help would be greatly appreciated :)
Here's my code:
import UIKit
import SceneKit
class ViewController: UIViewController {
var gameView: SCNView!
var scene: SCNScene!
var cameraNode: SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
// Scene
scene = SCNScene()
// Camera
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 0, 10)
scene.rootNode.addChildNode(cameraNode)
// Light
/*
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = .omni
lightNode.position = SCNVector3(0, 10, 2)
scene.rootNode.addChildNode(lightNode)
*/
// Stars
//let stars = SCNParticleSystem(named: "starsParticles.scnp", inDirectory: nil)!
//scene.rootNode.addParticleSystem(stars)
// Earth
let earthNode = itemPlate()
earthNode.position = SCNVector3(0, 0, 0)
scene.rootNode.addChildNode(earthNode)
// Create orbiting moonOne
let moonNodeOne = itemPlate()
moonNodeOne.position = SCNVector3(3, 0, 0)
earthNode.addChildNode(moonNodeOne)
// Create orbiting moonOne
let moonNodeTwo = itemPlate()
moonNodeTwo.position = SCNVector3(5, 3, 2)
earthNode.addChildNode(moonNodeTwo)
// Create orbiting moonOne
let moonNodeThree = itemPlate()
moonNodeThree.position = SCNVector3(-4, -3, 5)
earthNode.addChildNode(moonNodeThree)
// Scene formation
gameView = self.view as! SCNView
gameView.scene = scene
gameView.showsStatistics = true
gameView.allowsCameraControl = true
gameView.autoenablesDefaultLighting = true
gameView.defaultCameraController.interactionMode = .fly
gameView.defaultCameraController.inertiaEnabled = true
gameView.defaultCameraController.maximumVerticalAngle = 89
gameView.defaultCameraController.minimumVerticalAngle = -89
scene.background.contents = UIImage(named: "orangeBg.jpg")
}
override var prefersStatusBarHidden: Bool {
return true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: gameView)
let hitList = gameView.hitTest(location, options: nil)
if let hitObject = hitList.first {
let node = hitObject.node
// Update camera position
//gameView.defaultCameraController.translateInCameraSpaceBy(x: node.position.x, y: node.position.y, z: node.position.z + 5)
let onScreenPoint:CGPoint = CGPoint(x: 1.0, y: 1.0)
let viewport:CGSize = CGSize(width: 50, height: 50)
gameView.defaultCameraController.dolly(by: 1.0, onScreenPoint: onScreenPoint, viewport: viewport)
//let newCameraPosition = SCNVector3Make(node.position.x, node.position.y, node.position.z + 10)
print("NODE_HIT_OBJECT_COORDS: \(node.position.x), \(node.position.y) \(node.position.y)")
//let moveToAction = SCNAction.move(by: newCameraPosition, duration: 1.0)
}
}
}
You can implement in your code a methodology like this (sorry, I used macOS project instead iOS, but it's almost the same):
func handleClick(_ gestureRecognizer: NSGestureRecognizer) {
let scnView = self.view as! SCNView
let p = gestureRecognizer.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
if hitResults.count > 0 {
let result = hitResults[0]
let nodePosition = result.node.position.z
var matrix = matrix_identity_float4x4
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.5 // duration in seconds
matrix.columns.3.z = Float(nodePosition + 5.0)
scnView.pointOfView?.position.z = CGFloat(matrix.columns.3.z)
SCNTransaction.commit()
}
}
Or, as a second logical option, you can use SceneKit's constraints:
func handleClick(_ gestureRecognizer: NSGestureRecognizer) {
let scnView = self.view as! SCNView
let p = gestureRecognizer.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
if hitResults.count > 0 {
let result = hitResults[0]
let nodePosition = result.node
let constraint1 = SCNLookAtConstraint(target: nodePosition)
let constraint2 = SCNDistanceConstraint(target: nodePosition)
constraint2.minimumDistance = 5
constraint2.maximumDistance = 9
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.5
scnView.pointOfView?.constraints = [constraint2, constraint1]
SCNTransaction.commit()
}
}
P.S. These two approaches ain't out-of-the-box solutions but rather hints on how to implement what you want to.

iOS 11 ARKit : Drag Object in 3D View

I have a node object in 3d view and i need to drag that object,
So far i have tried from here : Placing, Dragging and Removing SCNNodes in ARKit
and converted in swift
#objc func handleDragGesture(_ gestureRecognizer: UIGestureRecognizer) {
let tapPoint = gestureRecognizer.location(in: self.sceneView)
switch gestureRecognizer.state {
case .began:
print("Object began to move")
let hitResults = self.sceneView.hitTest(tapPoint, options: nil)
if hitResults.isEmpty { return }
let hitResult = hitResults.first
if let node = hitResult?.node.parent?.parent?.parent {
self.photoNode = node
}
case .changed:
print("Moving object position changed")
if let _ = self.photoNode {
let hitResults = self.sceneView.hitTest(tapPoint, types: .featurePoint)
let hitResult = hitResults.last
if let transform = hitResult?.worldTransform {
let matrix = SCNMatrix4FromMat4(transform)
let vector = SCNVector3Make(matrix.m41, matrix.m42, matrix.m43)
self.photoNode?.position = vector
}
}
case .ended:
print("Done moving object")
default:
break
}
}
but it is not working properly. what is the correct way to do?
You can do this using panGestureRecongniser... see basic swift Playground code for handling a SCNNode.
import UIKit
import ARKit
import SceneKit
import PlaygroundSupport
public var textNode : SCNNode?
// Main ARKIT ViewController
class ViewController : UIViewController, ARSCNViewDelegate, ARSessionDelegate {
var textNode: SCNNode!
var counter = 0
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
// set the views delegate
sceneView.delegate = self as! ARSCNViewDelegate
// show statistics such as fps and timing information
sceneView.showsStatistics = true
// Create a new scene
sceneView.scene.rootNode
// Add ligthing
sceneView.autoenablesDefaultLighting = true
let text = SCNText(string: "Drag Me with Pan Gesture!", extrusionDepth: 1)
// create material
let material = SCNMaterial()
material.diffuse.contents = UIColor.green
text.materials = [material]
//Create Node object
textNode = SCNNode()
textNode.name = "textNode"
textNode.scale = SCNVector3(x:0.004,y:0.004,z:0.004)
textNode.geometry = text
textNode.position = SCNVector3(x: 0, y:0.02, z: -1)
// add new node to root node
self.sceneView.scene.rootNode.addChildNode(textNode)
// Add pan gesture for dragging the textNode about
sceneView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(panGesture(_:))))
}
override func loadView() {
sceneView = ARSCNView(frame:CGRect(x: 0.0, y: 0.0, width: 500.0, height: 600.0))
// Set the view's delegate
sceneView.delegate = self
let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal
// Now we'll get messages when planes were detected...
sceneView.session.delegate = self
self.view = sceneView
sceneView.session.run(config)
}
#objc func panGesture(_ gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 1
let results = self.sceneView.hitTest(gesture.location(in: gesture.view), types: ARHitTestResult.ResultType.featurePoint)
guard let result: ARHitTestResult = results.first else {
return
}
let position = SCNVector3Make(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
textNode.position = position
}
}
PlaygroundPage.current.liveView = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true
EDIT:
The above drag function only worked if you had 1 object in the view, so it was not really necessary to hit the node to start dragging. It will just drag from where ever you tapped on the screen. If you have multiple objects in the view, and you want to drag nodes independently. You could change the panGesture function to the following, detect each node tapped first:
// drags nodes independently
#objc func panGesture(_ gesture: UIPanGestureRecognizer) {
gesture.minimumNumberOfTouches = 1
let results = self.sceneView.hitTest(gesture.location(in: gesture.view), types: ARHitTestResult.ResultType.featurePoint)
guard let result: ARHitTestResult = results.first else {
return
}
let hits = self.sceneView.hitTest(gesture.location(in: gesture.view), options: nil)
if let tappedNode = hits.first?.node {
let position = SCNVector3Make(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
tappedNode.position = position
}
}
REF: https://stackoverflow.com/a/48220751/5589073
This code works for me
private func drag(sender: UIPanGestureRecognizer) {
switch sender.state {
case .began:
let location = sender.location(in: self.sceneView)
guard let hitNodeResult = self.sceneView.hitTest(location,
options: nil).first else { return }
self.PCoordx = hitNodeResult.worldCoordinates.x
self.PCoordy = hitNodeResult.worldCoordinates.y
self.PCoordz = hitNodeResult.worldCoordinates.z
case .changed:
// when you start to pan in screen with your finger
// hittest gives new coordinates of touched location in sceneView
// coord-pcoord gives distance to move or distance paned in sceneview
let hitNode = sceneView.hitTest(sender.location(in: sceneView), options: nil)
if let coordx = hitNode.first?.worldCoordinates.x,
let coordy = hitNode.first?.worldCoordinates.y,
let coordz = hitNode.first?.worldCoordinates.z {
let action = SCNAction.moveBy(x: CGFloat(coordx - self.PCoordx),
y: CGFloat(coordy - self.PCoordy),
z: CGFloat(coordz - self.PCoordz),
duration: 0.0)
self.photoNode.runAction(action)
self.PCoordx = coordx
self.PCoordy = coordy
self.PCoordz = coordz
}
sender.setTranslation(CGPoint.zero, in: self.sceneView)
case .ended:
self.PCoordx = 0.0
self.PCoordy = 0.0
self.PCoordz = 0.0
default:
break
}
}

SceneKit: Hit test not work with billboarded quads

I made billboarded quads using SceneKit.
The cameraNode is synchronized with UIDeviceMotion, and the billboard nodes are appearing as I expected.
The thing is, I want these nodes to be called when I tap it.
For this, I used UITapGestureRecognizer with hitTest.
Here is some of my code.
// ==== in viewDidLoad
// initialize tap gesture
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(onNodeTapped))
sceneView.addGestureRecognizer(tapGesture)
// initialize scenekit.scene
let scene = SCNScene()
scene.rootNode.addChildNode(cameraNode)
scene.rootNode.addChildNode(worldNode)
sceneView.scene = scene
sceneView.autoenablesDefaultLighting = true
sceneView.pointOfView = cameraNode
And this is the tap handler
func onNodeTapped(_ gestureRecognize: UIGestureRecognizer) {
let location = gestureRecognize.location(in: sceneView) // <---- updated
let hitResults = sceneView.hitTest(location, options: nil)
for result in hitResults {
// FOR_TEST: hit test visualization
if let material = result.node.geometry?.materials.first {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
material.emission.contents = UIColor.black
SCNTransaction.commit()
}
material.emission.contents = UIColor.red
SCNTransaction.commit()
}
// target tap event handling
if let target = (result.node as? TargetNode)?.target {
if onTargetTapped(target) {
return
}
}
}
}
This code works very rarly. What I mean is the visualization part respond only 1 out of 20 times, and the onTargetTapped is called only 1 out of 100...
The weired thing is the targeting is fine, which means this is not the coordinate problem.
I found something related to SCNHitTestOption.categoryBitMask, but it didn't help at all.
Also, when I open this Scenview, this error message appears on the console.
"[SceneKit] Error: error in _C3DUnProjectPoints"
Maybe this message is related to the hitTest malfunctioning?
Updated
This code is for building billboard SCNGeometry and SCNNode
override func initializeGeometry() -> SCNGeometry {
let geometry = SCNPlane(width: width, height: height)
let material = geometry.materials.first
material?.diffuse.contents = initializeTexture()
material?.writesToDepthBuffer = false
material?.readsFromDepthBuffer = false
return geometry
}
// ==== building node
node = SCNNode()
node.geometry = initializeGeometry()
node.categoryBitMask = MyConstraints.targetNodeHitTestCategoryBitMask
node.constraints = [SCNBillboardConstraint()]
You're retrieving the tap coordinates in annotationsDisplayView, but then passing those coordinates to sceneView for the hit test lookup. Unless those views are precisely on top of each other, you'll get a mismatch.
I think you want
let location = gestureRecognize.location(in: sceneView)
instead.
I was curious whether the hit test would use the rotated version of the billboard. I confirmed that it does. Here's a working (Mac) sample.
In the SCNView subclass:
override func mouseDown(with theEvent: NSEvent) {
/* Called when a mouse click occurs */
// check what nodes are clicked
let p = self.convert(theEvent.locationInWindow, from: nil)
let hitResults = self.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
Swift.print("hit me")
}
for result in hitResults {
Swift.print(result.node.name)
// get its material
let material = result.node.geometry!.firstMaterial!
// highlight it
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
// on completion - unhighlight
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
material.emission.contents = NSColor.black
SCNTransaction.commit()
}
material.emission.contents = NSColor.yellow
SCNTransaction.commit()
}
super.mouseDown(with: theEvent)
}
And in the view controller:
override func awakeFromNib(){
super.awakeFromNib()
// create a new scene
let scene = SCNScene()
let side = CGFloat(2.0)
let quad1 = SCNNode.init(geometry: SCNPlane(width: side, height: side))
quad1.name = "green"
quad1.geometry?.firstMaterial?.diffuse.contents = NSColor.green
let quad2 = SCNNode.init(geometry: SCNPlane(width: side, height: side))
quad2.name = "red"
quad2.geometry?.firstMaterial?.diffuse.contents = NSColor.red
let quad3 = SCNNode.init(geometry: SCNPlane(width: side, height: side))
quad3.name = "blue"
quad3.geometry?.firstMaterial?.diffuse.contents = NSColor.blue
scene.rootNode.addChildNode(quad1)
scene.rootNode.addChildNode(quad2)
scene.rootNode.addChildNode(quad3)
let rotation = CGFloat(M_PI_2) * 0.9
quad1.position.y = side/2
quad1.eulerAngles.y = rotation
quad2.position.x = side/2
quad2.eulerAngles.x = rotation
// comment out these constraints to verify that they matter
quad1.constraints = [SCNBillboardConstraint()]
quad2.constraints = [SCNBillboardConstraint()]
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// create and add a light to the scene
let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light!.type = .omni
lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
scene.rootNode.addChildNode(lightNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = .ambient
ambientLightNode.light!.color = NSColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// set the scene to the view
self.gameView!.scene = scene
// allows the user to manipulate the camera
self.gameView!.allowsCameraControl = true
// show statistics such as fps and timing information
self.gameView!.showsStatistics = true
// configure the view
self.gameView!.backgroundColor = NSColor.black
}
Hit testing should work whether your node is billboard constrained or not. It does for me.
override func viewDidLoad() {
self.sceneView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap(_:))))
...
}
#objc func tap(_ recognizer: UITapGestureRecognizer) {
let loc = recognizer.location(in: self.sceneView)
if let hit = self.sceneView.hitTest(loc) {
...
}
}

SCNNode releasing children with altered transform and pivot properties

I'm dealing with children nodes with pivot and position that have been altered. I found a lot of SCNNode transformation topics, but it seems none of them represent my situation.
I have six balls : (can't post more than 2 links, image is at i.stack.imgur.com/v3Lc4.png )
And I select the top four of them, adjust the pivot, adjust the position (to counter the pivot translation effect), and rotate. This is the code I use :
//core code
let fourBalls = SCNNode()
for i in 1...4
{
let ball = scene.rootNode.childNode(withName: "b" + String(i), recursively: false)!
ball.removeFromParentNode()
fourBalls.addChildNode(ball)
}
scene.rootNode.addChildNode(fourBalls)
//adjust the pivot of the fourBalls node
fourBalls.pivot = SCNMatrix4MakeTranslation(-1.5, 0.5, -5)
//fix the position
fourBalls.position = SCNVector3Make(-1.5, 0.5, -5)
//rotate
let action = SCNAction.rotateBy(x: 0, y: 0, z: CGFloat(M_PI_2), duration: 2)
fourBalls.run(action)
It did the job well :
Now, I need to release back the fourBalls child nodes into the rootNode, I use this code which I put as completion block :
//core problem
//how to release the node with the transform?
for node in fourBalls.childNodes
{ node.transform = node.worldTransform
node.removeFromParentNode()
self.scene.rootNode.addChildNode(node)
}
And here comes the problem, I released them wrongly :
So my question is, how to release the children nodes to the rootNode with correct pivot, position, and transform properties?
Here is my full GameViewController.swift for you who want to try :
import SceneKit
class GameViewController: UIViewController {
let scene = SCNScene()
override func viewDidLoad() {
super.viewDidLoad()
let ball1 = SCNSphere(radius: 0.4)
let ball2 = SCNSphere(radius: 0.4)
let ball3 = SCNSphere(radius: 0.4)
let ball4 = SCNSphere(radius: 0.4)
let ball5 = SCNSphere(radius: 0.4)
let ball6 = SCNSphere(radius: 0.4)
ball1.firstMaterial?.diffuse.contents = UIColor.purple()
ball2.firstMaterial?.diffuse.contents = UIColor.white()
ball3.firstMaterial?.diffuse.contents = UIColor.cyan()
ball4.firstMaterial?.diffuse.contents = UIColor.green()
ball5.firstMaterial?.diffuse.contents = UIColor.black()
ball6.firstMaterial?.diffuse.contents = UIColor.blue()
let B1 = SCNNode(geometry: ball1)
B1.position = SCNVector3(x:-2,y:1,z:-5)
scene.rootNode.addChildNode(B1)
B1.name = "b1"
let B2 = SCNNode(geometry: ball2)
B2.position = SCNVector3(x:-1,y:1,z:-5)
scene.rootNode.addChildNode(B2)
B2.name = "b2"
let B3 = SCNNode(geometry: ball3)
B3.position = SCNVector3(x:-2,y:0,z:-5)
scene.rootNode.addChildNode(B3)
B3.name = "b3"
let B4 = SCNNode(geometry: ball4)
B4.position = SCNVector3(x:-1,y:0,z:-5)
scene.rootNode.addChildNode(B4)
B4.name = "b4"
let B5 = SCNNode(geometry: ball5)
B5.position = SCNVector3(x:-2,y:-1,z:-5)
scene.rootNode.addChildNode(B5)
B5.name = "b5"
let B6 = SCNNode(geometry: ball6)
B6.position = SCNVector3(x:-1,y:-1,z:-5)
scene.rootNode.addChildNode(B6)
B6.name = "b6"
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(-1.5,0,2)
scene.rootNode.addChildNode(cameraNode)
// create and add an ambient light to the scene
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = UIColor.yellow()
scene.rootNode.addChildNode(ambientLightNode)
let scnView = self.view as! SCNView
scnView.scene = scene
scnView.allowsCameraControl = false
scnView.backgroundColor = UIColor.orange()
//core code
let fourBalls = SCNNode()
for i in 1...4
{
let ball = scene.rootNode.childNode(withName: "b" + String(i), recursively: false)!
ball.removeFromParentNode()
fourBalls.addChildNode(ball)
}
scene.rootNode.addChildNode(fourBalls)
//adjust the pivot of the fourBalls node
fourBalls.pivot = SCNMatrix4MakeTranslation(-1.5, 0.5, -5)
//fix the position
fourBalls.position = SCNVector3Make(-1.5, 0.5, -5)
//rotate
let action = SCNAction.rotateBy(x: 0, y: 0, z: CGFloat(M_PI_2), duration: 2)
fourBalls.run(action, completionHandler:
{
//core problem
for node in fourBalls.childNodes
{
node.transform = node.worldTransform
node.removeFromParentNode()
self.scene.rootNode.addChildNode(node)
}
})
}
override func shouldAutorotate() -> Bool {
return true
}
override func prefersStatusBarHidden() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.current().userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
}
SCNActions update the presentation tree directly, not the model tree, this is probably best explained in the 2014 WWDC video (skip to 16:38). What this means is that throughout the animation the current transform for each of the animating nodes is only available from the presentationNode. Your code works if we get the transform from here instead.
Apologies for the Swift 2 backport...
//rotate
let action = SCNAction.rotateByX(0, y: 0, z: CGFloat(M_PI_2), duration: 2)
fourBalls.runAction(action, completionHandler: {
//core problem
for node in fourBalls.childNodes
{
node.transform = node.presentationNode.worldTransform
node.removeFromParentNode()
self.scene.rootNode.addChildNode(node)
}
})
TBH I was expecting node.worldTransform == node.presentationNode.worldTransform when it got to the completion handler.

Objects not affected by gravity field in SceneKit [duplicate]

I'm trying to use a SCNPhysicsField.linearGravityField object to affect only specific objects in my scene. The problem is, that I can't seem to get it to affect anything. Here's a sample of my code in Swift:
let downGravityCatagory = 1 << 0
let fieldDown = SCNPhysicsField.linearGravityField()
let fieldUp = SCNPhysicsField.linearGravityField()
let fieldNode = SCNNode()
let sceneView = view as! SCNView
sceneView.scene = scene
sceneView.scene!.physicsWorld.gravity = SCNVector3(x: 0, y: 0, z: 0)
fieldDown.categoryBitMask = downGravityCatagory
fieldDown.active = true
fieldDown.strength = 3
fieldNode.physicsField = fieldDown
scene.rootNode.addChildNode(fieldNode)
var dice = SCNNode()
//I then attach geometry here
dice.physicsBody = SCNPhysicsBody(type: SCNPhysicsBodyType.Dynamic, shape: SCNPhysicsShape(geometry: dice.geometry!, options: nil))
dice.physicsBody?.categoryBitMask = downGravityCatagory
scene.rootNode.addChildNode(dice)
Even though the Physics Bodies are assigned the same catagoryBitMask as the gravity field, they just float there in zero G, only affected by the physicsworld gravity.
Set the "downGravityCatagory" bit mask on the node:
dice.categoryBitMask = downGravityCatagory;
the physics's categoryBitMask is for physics collisions only.
I am trying to get my ball to fall faster. Right now it falls really slowly. I tried using this answer. I think I am close, but I do not really know. The code:
import Cocoa
import SceneKit
class AppController : NSObject {
#IBOutlet weak var _sceneView: SCNView!
#IBOutlet weak var _pushButton: NSButton!
#IBOutlet weak var _resetButton: NSButton!
private let _mySphereNode = SCNNode()
private let _gravityFieldNode = SCNNode()
private let downGravityCategory = 1 << 0
private func setupScene() {
// setup ambient light source
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = NSColor(white: 0.35, alpha: 1.0).CGColor
// add ambient light the scene
_sceneView.scene!.rootNode.addChildNode(ambientLightNode)
// setup onmidirectional light
let omniLightNode = SCNNode()
omniLightNode.light = SCNLight()
omniLightNode.light!.type = SCNLightTypeOmni
omniLightNode.light!.color = NSColor(white: 0.56, alpha: 1.0).CGColor
omniLightNode.position = SCNVector3Make(0.0, 200.0, 0.0)
_sceneView.scene!.rootNode.addChildNode(omniLightNode)
// add plane
let myPlane = SCNPlane(width: 125.0, height: 2000.0)
myPlane.widthSegmentCount = 10
myPlane.heightSegmentCount = 10
myPlane.firstMaterial!.diffuse.contents = NSColor.orangeColor().CGColor
myPlane.firstMaterial!.specular.contents = NSColor.whiteColor().CGColor
let planeNode = SCNNode()
planeNode.geometry = myPlane
// rotote -90.0 degrees about the y-axis, then rotate -90.0 about the x-axis
var rotMat = SCNMatrix4MakeRotation(-3.14/2.0, 0.0, 1.0, 0.0)
rotMat = SCNMatrix4Rotate(rotMat, -3.14/2.0, 1.0, 0.0, 0.0)
planeNode.transform = rotMat
planeNode.position = SCNVector3Make(0.0, 0.0, 0.0)
// add physcis to plane
planeNode.physicsBody = SCNPhysicsBody.staticBody()
// add plane to scene
_sceneView.scene!.rootNode.addChildNode(planeNode)
// gravity folks...
// first, set the position for field effect
let gravityField = SCNPhysicsField.linearGravityField()
gravityField.categoryBitMask = downGravityCategory
gravityField.active = true
gravityField.strength = 3.0
gravityField.exclusive = true
_gravityFieldNode.physicsField = gravityField
_sceneView.scene!.rootNode.addChildNode(_gravityFieldNode)
// attach the sphere node to the scene's root node
_mySphereNode.categoryBitMask = downGravityCategory
_sceneView.scene!.rootNode.addChildNode(_mySphereNode)
}
private func setupBall() {
let radius = 25.0
// sphere geometry
let mySphere = SCNSphere(radius: CGFloat(radius))
mySphere.geodesic = true
mySphere.segmentCount = 50
mySphere.firstMaterial!.diffuse.contents = NSColor.purpleColor().CGColor
mySphere.firstMaterial!.specular.contents = NSColor.whiteColor().CGColor
// position sphere geometry, add it to node
_mySphereNode.position = SCNVector3(0.0, CGFloat(radius), 0.0)
_mySphereNode.geometry = mySphere
// physics body and shape
_mySphereNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: mySphere, options: nil))
_mySphereNode.physicsBody!.mass = 0.125
}
private func stopBall() {
_mySphereNode.geometry = nil
_mySphereNode.physicsBody = nil
}
override func awakeFromNib() {
// assign empty scene
_sceneView.scene = SCNScene()
setupScene()
setupBall()
}
#IBAction func moveBall(sender: AnyObject) {
let forceApplied = SCNVector3Make(35.0, 0.0, 0.0)
if _mySphereNode.physicsBody?.isResting == true {
_mySphereNode.physicsBody!.applyForce(forceApplied, impulse: true)
}
else if _mySphereNode.physicsBody?.isResting == false {
print("ball not at rest...")
}
else {
print("No physics associated with the node...")
}
}
#IBAction func resetBall(sender: AnyObject) {
// remove the ball from the sphere node
stopBall()
// reset the ball
setupBall()
}
}
Is there some trick to get "real-world" type gravity?
Based on what #tedesignz suggested, here is the slightly modified working code:
import Cocoa
import SceneKit
class AppController : NSObject, SCNPhysicsContactDelegate {
#IBOutlet weak var _sceneView: SCNView!
#IBOutlet weak var _pushButton: NSButton!
#IBOutlet weak var _resetButton: NSButton!
private let _mySphereNode = SCNNode()
private let _myPlaneNode = SCNNode()
private let _gravityFieldNode = SCNNode()
private let BallType = 1
private let PlaneType = 2
private let GravityType = 3
private func setupScene() {
// setup ambient light source
let ambientLightNode = SCNNode()
ambientLightNode.light = SCNLight()
ambientLightNode.light!.type = SCNLightTypeAmbient
ambientLightNode.light!.color = NSColor(white: 0.35, alpha: 1.0).CGColor
// add ambient light the scene
_sceneView.scene!.rootNode.addChildNode(ambientLightNode)
// setup onmidirectional light
let omniLightNode = SCNNode()
omniLightNode.light = SCNLight()
omniLightNode.light!.type = SCNLightTypeOmni
omniLightNode.light!.color = NSColor(white: 0.56, alpha: 1.0).CGColor
omniLightNode.position = SCNVector3Make(0.0, 200.0, 0.0)
_sceneView.scene!.rootNode.addChildNode(omniLightNode)
// add plane
let myPlane = SCNPlane(width: 125.0, height: 150.0)
myPlane.widthSegmentCount = 10
myPlane.heightSegmentCount = 10
myPlane.firstMaterial!.diffuse.contents = NSColor.orangeColor().CGColor
myPlane.firstMaterial!.specular.contents = NSColor.whiteColor().CGColor
_myPlaneNode.geometry = myPlane
// rotote -90.0 degrees about the y-axis, then rotate -90.0 about the x-axis
var rotMat = SCNMatrix4MakeRotation(-3.14/2.0, 0.0, 1.0, 0.0)
rotMat = SCNMatrix4Rotate(rotMat, -3.14/2.0, 1.0, 0.0, 0.0)
_myPlaneNode.transform = rotMat
_myPlaneNode.position = SCNVector3Make(0.0, 0.0, 0.0)
// add physcis to plane
_myPlaneNode.physicsBody = SCNPhysicsBody(type: .Static, shape: SCNPhysicsShape(geometry: myPlane, options: nil))
// configure physics body
_myPlaneNode.physicsBody!.contactTestBitMask = BallType
_myPlaneNode.physicsBody!.collisionBitMask = BallType
_myPlaneNode.physicsBody!.categoryBitMask = PlaneType
// add plane to scene
_sceneView.scene!.rootNode.addChildNode(_myPlaneNode)
// add sphere node
_sceneView.scene!.rootNode.addChildNode(_mySphereNode)
// gravity folks...
// first, set the position for field effect
let gravityField = SCNPhysicsField.linearGravityField()
gravityField.categoryBitMask = GravityType
gravityField.active = true
gravityField.direction = SCNVector3(0.0, -9.81, 0.0)
print(gravityField.direction)
gravityField.strength = 10.0
gravityField.exclusive = true
_gravityFieldNode.physicsField = gravityField
_sceneView.scene!.rootNode.addChildNode(_gravityFieldNode)
// set the default gravity to zero vector
_sceneView.scene!.physicsWorld.gravity = SCNVector3(0.0, 0.0, 0.0)
}
private func setupBall() {
let radius = 25.0
// sphere geometry
let mySphere = SCNSphere(radius: CGFloat(radius))
mySphere.geodesic = true
mySphere.segmentCount = 50
mySphere.firstMaterial!.diffuse.contents = NSColor.purpleColor().CGColor
mySphere.firstMaterial!.specular.contents = NSColor.whiteColor().CGColor
// position sphere geometry, add it to node
_mySphereNode.position = SCNVector3(0.0, CGFloat(radius), 0.0)
_mySphereNode.geometry = mySphere
// physics body and shape
_mySphereNode.physicsBody = SCNPhysicsBody(type: .Dynamic, shape: SCNPhysicsShape(geometry: mySphere, options: nil))
_mySphereNode.physicsBody!.mass = 0.125
_mySphereNode.physicsBody!.contactTestBitMask = PlaneType
_mySphereNode.physicsBody!.collisionBitMask = PlaneType
_mySphereNode.physicsBody!.categoryBitMask = (BallType | GravityType)
}
private func stopBall() {
_mySphereNode.geometry = nil
_mySphereNode.physicsBody = nil
}
override func awakeFromNib() {
// assign empty scene
_sceneView.scene = SCNScene()
// contact delegate
_sceneView.scene!.physicsWorld.contactDelegate = self
setupScene()
setupBall()
}
#IBAction func moveBall(sender: AnyObject) {
let forceApplied = SCNVector3Make(5.0, 0.0, 0.0)
if _mySphereNode.physicsBody?.isResting == true {
_mySphereNode.physicsBody!.applyForce(forceApplied, impulse: true)
}
else if _mySphereNode.physicsBody?.isResting == false {
print("ball not at rest...")
}
else {
print("No physics associated with the node...")
}
}
#IBAction func resetBall(sender: AnyObject) {
// remove the ball from the sphere node
stopBall()
// reset the ball
setupBall()
}
/*** SCENEKIT DELEGATE METHODS ***/
func physicsWorld(world: SCNPhysicsWorld, didBeginContact contact: SCNPhysicsContact) {
print("we have contact...")
}
func physicsWorld(world: SCNPhysicsWorld, didEndContact contact: SCNPhysicsContact) {
print("No longer touching...")
}
}

Resources