SCNNode releasing children with altered transform and pivot properties - ios

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.

Related

How can I dynamically track SCNNodes in a scene for removal?

Obligatory: first time writing an app, relevant code is below. My code runs mostly as I want it to, but does not achieve my goal of dynamic object tracking.
I'm using Swift and Scenekit to build a simple puzzle game, similar to a 3d-version of candy crush.
I have a class Cube that extends SCNNode. On initialization, this class will randomly draw a 5x5 cube of SCNBoxes with each box being red, green, or blue (all 6 sides of a box are 1 color).
The goal of the game is to get the highest score by removing "chains" of SCNBoxes of like-colors. When a chain is removed, cubes should recognize gravity and drop to fill in the voids created by the removed-chain. This is where I need to dynamically track position. As the cubes fall into the gaps, their neighbors change.
My approach: build a struct CubeDetails that has properties var color: String and var location: SCNVector3. Next, build a dictionary masterCubeDict = [SCNNode: CubeDetails] that has all of the cubes of 1 color (the color is provided by a hittestresult).
Every time a user taps a cube, grab its color, refresh the masterCubeDict, and then use math on the SCNVector3 position to determine which cubes are neighbors.
I think my algorithm for finding 'cube neighbors' using math on the scnvector3 is where I'm off. There must be a better way for scenekit nodes to identity/find each other, right?
Also -- I would like the physics of the cubes to let them fall and have no bounce/sliding at all. They should only move straight up/down. Collisions should never happen. I thought I implemented that properly through friction, restituion, and mass of the cubes but I'm not getting the outcome I want.
class Cube
import SceneKit
class Cube : SCNNode {
let cubeWidth:Float = 0.95
let spaceBetweenCubes:Float = 0.05
var cubecolor:UIColor = UIColor.black
var masterCubeDict: [SCNNode: CubeDetails] = [:]
struct CubeDetails {
var color:String
var position:SCNVector3
}
override init() {
super.init()
let cubeOffsetDistance = self.cubeOffsetDistance()
var cubeColorString: String = ""
var xPos:Float = -cubeOffsetDistance
var yPos:Float = -cubeOffsetDistance
var zPos:Float = -cubeOffsetDistance
let xFloor:Float = -1.5
let yFloor:Float = -1.5
let zFloor:Float = -1.5
let floorGeo = SCNBox(width: 20, height: 0, length: 20, chamferRadius: 0)
let floor = SCNNode(geometry: floorGeo)
floor.position = SCNVector3(x: xFloor, y: yFloor, z: zFloor)
floor.name = "floor"
floor.opacity = 0.0
floor.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil)
floor.physicsBody?.collisionBitMask = 1
floor.physicsBody?.friction = 1.0
self.addChildNode(floor)
for _ in 0..<5 {
for _ in 0..<5 {
for _ in 0..<5 {
let cubeGeometry = SCNBox(width: CGFloat(cubeWidth), height: CGFloat(cubeWidth), length: CGFloat(cubeWidth), chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = randomColor()
//unwrap material (type any) and cast to uicolor for switch
if let unwrapColor: UIColor = material.diffuse.contents as? UIColor {
switch unwrapColor {
case UIColor.red:
cubeColorString = "red"
case UIColor.green:
cubeColorString = "green"
case UIColor.blue:
cubeColorString = "blue"
default:
cubeColorString = "black"
}
} else { print("Error unwrapping color") }
cubeGeometry.materials = [material, material, material, material, material, material]
let cube = SCNNode(geometry: cubeGeometry)
cube.name = cubeColorString
cube.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
cube.physicsBody?.restitution = 0.0
cube.physicsBody?.isAffectedByGravity = true
cube.physicsBody?.mass = 25.0
cube.physicsBody?.friction = 1.0
cube.physicsBody?.collisionBitMask = 1
cube.position = SCNVector3(x: xPos, y: yPos, z: zPos)
let details = CubeDetails(color: cubeColorString, position: cube.position)
//add cube details to the master dict
masterCubeDict[cube] = details
//print(masterCubeDict)
xPos += cubeWidth + spaceBetweenCubes
self.addChildNode(cube)
}
xPos = -cubeOffsetDistance
yPos += cubeWidth + spaceBetweenCubes
}
xPos = -cubeOffsetDistance
yPos = -cubeOffsetDistance
zPos += cubeWidth + spaceBetweenCubes
}
}
private func cubeOffsetDistance()->Float {
return (cubeWidth + spaceBetweenCubes) / 2
}
private func randomColor() -> UIColor{
var tmpColor: UIColor
let num = Int.random(in:0...2)
switch num {
case 0:
tmpColor = UIColor.red
case 1:
tmpColor = UIColor.blue
case 2:
tmpColor = UIColor.green
default:
tmpColor = UIColor.black
}
return tmpColor
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
GameViewController
import UIKit
import QuartzCore
import SceneKit
var myMasterCubeDict: [SCNNode: Cube.CubeDetails] = [:]
class GameViewController: UIViewController {
let gameCube = Cube()
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
// let scene = SCNScene(named: "art.scnassets/ship.scn")!
let scene = SCNScene()
// 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: 2, y: 0, z: 20)
// 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 = UIColor.darkGray
scene.rootNode.addChildNode(ambientLightNode)
// init cube
myMasterCubeDict = gameCube.masterCubeDict
scene.rootNode.addChildNode(gameCube)
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.black
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
#objc
func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// retrieve the SCNView
let scnView = self.view as! SCNView
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result = hitResults[0]
//get dict of same-color node
var dictOfSameColor = findAndReturnChain(boi: result.node)
// print(dictOfSameColor)
var finalNodes: [SCNNode] = [result.node]
var resFlag = 1
repeat {
var xSame: Bool = false
var ySame: Bool = false
var zSame: Bool = false
resFlag = 0
for node in finalNodes {
// var nodeX = node.position.x
for (key, value) in dictOfSameColor {
if(abs(node.position.x - value.position.x) < 0.7) {
xSame = true
}
if(abs(node.position.y - value.position.y) < 0.7) {
ySame = true
}
if(abs(node.position.z - value.position.z) < 0.7) {
zSame = true
}
//print("X-val: \(xDif) \nY-val: \(yDif) \nZ-val: \(zDif) \nColor: \(key.name) \n\n\n\n")
if (xSame && ySame ) {
if !(zSame) {
if (abs((node.position.z-value.position.z)) < 2) {
finalNodes.append(key)
dictOfSameColor.removeValue(forKey: key)
resFlag = 1
}
}
}
if (xSame && zSame) {
if !(ySame) {
if (abs((node.position.y-value.position.y)) < 2) {
finalNodes.append(key)
dictOfSameColor.removeValue(forKey: key)
resFlag = 1
}
}
}
if (ySame && zSame) {
if !(xSame) {
if (abs((node.position.x-value.position.x)) < 2) {
finalNodes.append(key)
dictOfSameColor.removeValue(forKey: key)
resFlag = 1
}
}
}
xSame = false
ySame = false
zSame = false
}
}
//print(finalNodes)
} while resFlag == 1
//print(finalNodes)
for node in finalNodes {
if node.name != "floor" {
node.removeFromParentNode()
}
}
//IMPLEMENT: Reset dicts to current state of the cube
myMasterCubeDict = updateMasterCubeDict(cube: gameCube)
dictOfSameColor.removeAll()
}
}
func findAndReturnChain(boi: SCNNode) -> [SCNNode:Cube.CubeDetails] {
var ret: [SCNNode:Cube.CubeDetails] = [:]
//find cubes with the same color
for (key, value) in myMasterCubeDict {
if value.color == boi.name {
ret[key] = value
}
}
return ret
}
func updateMasterCubeDict(cube: Cube) -> [SCNNode:Cube.CubeDetails] {
myMasterCubeDict.removeAll()
var newNode: SCNNode = SCNNode()
var newDetails = Cube.CubeDetails(color: "", position: SCNVector3Zero)
cube.enumerateChildNodes { (cube, stop) in
newNode = cube
if let newName = cube.name {
newDetails.color = newName
}
newDetails.position = cube.position
myMasterCubeDict[newNode] = newDetails
}
return myMasterCubeDict
}
override var shouldAutorotate: Bool {
return true
}
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
}
I did a game somewhat like this. You could probably get the math to work, but the way I did it was to map out each node and have an array containing its adjacent nodes. Doing it this way, I'm sure that when I remove a node and loop through its adjacent[array] nodes, then I got the right ones.
I don't subclass SCNNodes - some do, but I create the class I want that contains info about my node - I add the node to Scenekit there, that's separates the actual node from other work I may want to do with the class. Some nodes have a lot of detail that I may want to manage separately (multiple particle systems, movements, etc). Then I just keep my classes of nodes in an array and each class has direct access to it's own node.
Sorry - I don't know about the bounce, there are a lot of choices with the physics engine.

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.

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

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

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) {
...
}
}

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