I would like to retrieve the screencoordinates from a SCNNode so I can use these coordinates in a SpriteKit-overlay.
The node is a child node.
When I rotate the parent node's pivot projectPoint returns garbled results.
SCNScene:
class ScenekitScene:SCNScene {
override init() {
super.init()
let nodeCamera = SCNNode()
nodeCamera.camera = SCNCamera()
rootNode.addChildNode(nodeCamera)
let geoPyramid = SCNPyramid(width: 0.2, height: 0.5, length: 0.2)
let nodeCenterOfUniverse = SCNNode(geometry: geoPyramid)
nodeCenterOfUniverse.position = SCNVector3(0, 0, -5)
nodeCenterOfUniverse.pivot = SCNMatrix4MakeRotation(CGFloat.pi/4, 0.0, 0.0, 1.0)
rootNode.addChildNode(nodeCenterOfUniverse)
let geoSphere = SCNSphere(radius: 0.3)
let nodePlanet = SCNNode(geometry: geoSphere)
nodePlanet.name = "planet"
nodePlanet.position = SCNVector3(2, 0, 0)
nodeCenterOfUniverse.addChildNode(nodePlanet)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
SCNSceneRenderDelegate:
extension ViewController:SCNSceneRendererDelegate {
func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
if let planet = scene.rootNode.childNode(withName: "planet", recursively: true) {
let position = planet.convertPosition(planet.position, to: scene.rootNode)
let screencoords = renderer.projectPoint(position)
print (screencoords)
# result:
# SCNVector3(x: 461.913848876953, y: 2.58612823486328, z: 0.808080852031708)
# y should clearly be more to the center of the screen somewhere between 100-200
}
}
}
Does anyone know how to get the correct screencoordinates?
It looks like the planet position in world coordinates is not correct.
When I manually calculate the planet position I get different results and projectPoints works as advertised. [I might post a different question regarding the worldTransform].
// using SIMD
let pivCou = float4x4(nodeCenterOfUniverse.pivot)
let posCou = float3(nodeCenterOfUniverse.position)
let posCou4 = float4(posCou.x, posCou.y, posCou.z, 1.0) // convert float3 to float4
let posPlanet = float3(nodePlanet.position)
let posPlanet4 = float4(posPlanet.x, posPlanet.y, posPlanet.z, 1.0)
let worldcoordinatesPlanet = posCou4 + posPlanet4 * pivCou
Related
I want to create app where you can set points in real world in detected surface and after you have 4 or more points it creates plane/polygon between them with texture. Basic controller configuration:
#IBOutlet weak var sceneView: ARSCNView!
let configuration = ARWorldTrackingConfiguration()
var vectors: [SCNVector3] = []
var mostBottomYAxis: Float? {
didSet {
for (index, _) in vectors.enumerated() {
vectors[index].y = self.mostBottomYAxis ?? vectors[index].y
}
}
}
var identifier: UUID?
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
self.sceneView.showsStatistics = true
self.configuration.worldAlignment = .gravity // Y axis is Up and Down
self.configuration.planeDetection = .horizontal // Detect horizontal surfaces
self.sceneView.session.run(configuration)
self.sceneView.autoenablesDefaultLighting = true
self.sceneView.delegate = self
self.registerGestureRecognizers()
}
Tap gesture method for adding vertex to surface:
#objc func handleTap(sender: UITapGestureRecognizer) {
let sceneView = sender.view as! ARSCNView
let tapLocation = sender.location(in: sceneView)
if let raycast = sceneView.raycastQuery(from: tapLocation, allowing: .estimatedPlane, alignment: .horizontal),
let result = sceneView.session.raycast(raycast).first {
self.addItem(raycastResult: result)
}
}
func addItem(raycastResult: ARRaycastResult) {
let transform = raycastResult.worldTransform
let thirdColumn = transform.columns.3
let vector = SCNVector3(x: thirdColumn.x, y: mostBottomYAxis ?? thirdColumn.y, z: thirdColumn.z)
self.vectors.append(vector)
self.addItem(toPosition: vector)
}
and methods for drawing polygon:
func getGeometry(forVectors vectors: [SCNVector3]) -> SCNGeometry {
let polygonIndices = Array(0...vectors.count).map({ Int32($0) })
let indices: [Int32] = [Int32(vectors.count)] + /* We have a polygon with this count of points */
polygonIndices /* The indices for our polygon */
let source = SCNGeometrySource(vertices: vectors)
let indexData = Data(bytes: indices,
count: indices.count * MemoryLayout<Int32>.size)
let element = SCNGeometryElement(data: indexData,
primitiveType: .polygon,
primitiveCount: 1,
bytesPerIndex: MemoryLayout<Int32>.size)
// MARK: - Texture
let textureCoordinates = [
CGPoint(x: 0, y: 0),
CGPoint(x: 1, y: 0),
CGPoint(x: 0, y: 1),
CGPoint(x: 1, y: 1)
]
let uvSource = SCNGeometrySource(textureCoordinates: textureCoordinates)
let geometry = SCNGeometry(sources: [source, uvSource],
elements: [element])
return geometry
}
func getNode(forVectors vectors: [SCNVector3]) -> SCNNode {
let polyDraw = getGeometry(forVectors: vectors)
let material = SCNMaterial()
material.isDoubleSided = true
material.diffuse.contents = UIImage(named: "Altezo_colormix_brilant")
material.diffuse.wrapS = .repeat
material.diffuse.wrapT = .repeat
polyDraw.materials = [material]
let node = SCNNode(geometry: polyDraw)
return node
}
extension PlacePointsForPlaneViewController: ARSCNViewDelegate {
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
let y = planeAnchor.transform.columns.3.y
if mostBottomYAxis == nil {
mostBottomYAxis = y
identifier = planeAnchor.identifier
}
if mostBottomYAxis! > y {
mostBottomYAxis = y
identifier = planeAnchor.identifier
}
// Remove existing plane nodes
node.enumerateChildNodes { (childNode, _) in
childNode.removeFromParentNode()
}
if planeAnchor.identifier == self.identifier {
if self.vectors.count > 3 {
let modelNode = getNode(forVectors: self.vectors)
node.addChildNode(modelNode)
}
}
}
}
I hope it's mostly self explained. I have vectors which contains manually added points. I want to be working only with one surface that is most bottom (thats what do mostBottomYAxis and identifier).
What am I doing wrong? Why polygon is not drawn exactly between points? When I tried to draw line between two points it's working.
One more problem. How to set texture coordinates to correctly draw texture in polygon and to get it working not only for 4 verticies but for more (dynamically as user add more points and can change their positions)?
Thanks for help
Edit: After I post question I tried to different nodes to add my custom polygon and it's working fine if I added to sceneView rootNode:
sceneView.scene.rootNode.addChildNode(modelNode)
So this helps with position of polygon. But still I have problems with texture. How to set texture offset/transform to get it working? To have texture fill this custom geometry and repeat texture to edges.
I need some help to implement a custom node into my ARKit scene. I am using ARSCNFaceGeometry for a mask node to move around. I need some help to implement a custom image or a node rather than using Apple's mask node. Thank you for your help.
var maskNode: Mask?
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.backgroundColor = .clear
self.sceneView.scene = SCNScene()
self.sceneView.rendersContinuously = true
if let device = MTLCreateSystemDefaultDevice(),
let geo = ARSCNFaceGeometry(device: device) {
self.maskNode = Mask(geometry: geo)
self.sceneView.scene?.rootNode.addChildNode(self.maskNode!)
self.maskNode?.position = SCNVector3(x: 0.0, y: 0.0, z: 0.0)
self.maskNode?.isHidden = true
}
let config = ARFaceTrackingConfiguration()
config.worldAlignment = .gravity
session.delegate = self
session.run(config, options: [])
self.updateUI()
}
Mask:
class Mask: SCNNode, VirtualFaceContent {
init(geometry: ARSCNFaceGeometry) {
let material = SCNMaterial()
material.diffuse.contents = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
geometry.firstMaterial = material
super.init()
self.geometry = geometry
}
required init?(coder aDecoder: NSCoder) {
fatalError("\(#function) has not been implemented")
}
// MARK: VirtualFaceContent
/// - Tag: SCNFaceGeometryUpdate
func update(withFaceAnchor anchor: ARFaceAnchor) {
let faceGeometry = geometry as! ARSCNFaceGeometry
faceGeometry.update(from: anchor.geometry)
}
}
VirtualFaceContent:
protocol VirtualFaceContent {
func update(withFaceAnchor: ARFaceAnchor)
}
typealias VirtualFaceNode = VirtualFaceContent & SCNNode
// MARK: Loading Content
func loadedContentForAsset(named resourceName: String) -> SCNNode {
let url = Bundle.main.url(forResource: resourceName, withExtension: "scn", subdirectory: "Models.scnassets")!
let node = SCNReferenceNode(url: url)!
node.load()
return node
}
You can use renderer(node: for anchor:). Guard check if the anchor is an ARFaceAnchor and then assign your custom geometry to that node. You can also just add your custom node to the node ARKit added for the anchor, up to you.
I have a SCNPlane that is added to the scene when a sufficient area is detected for a horizontal surface. The plane appears to be placed in a correct spot, according to the floor/table it's being placed on. The problem is when I drop a SCNNode(this has been consistent whether it was a box, pyramid, 3D-model, etc.) onto the plane, it will eventually find a spot to land and 99% start jiggling all crazy. Very few times has it just landed and not moved at all. I also think this may be cause by the node being dropped and landing slightly below the plane surface. It is not "on top" neither "below" the plane. Maybe the node is freaking out because it's kind of teetering between both levels?
Here is a video of what's going on, you can see at the beginning that the box is below and above the plane and the orange box does stop when it collides with the dark blue box, but does go back to its jiggling ways when the green box collides with it at the end:
The code is here on github
I will also show some of the relevant parts embedded in code:
I just create a Plane class to add to the scene when I need to
class Plane: SCNNode {
var anchor :ARPlaneAnchor
var planeGeometry :SCNPlane!
init(anchor :ARPlaneAnchor) {
self.anchor = anchor
super.init()
setup()
}
func update(anchor: ARPlaneAnchor) {
self.planeGeometry.width = CGFloat(anchor.extent.x)
self.planeGeometry.height = CGFloat(anchor.extent.z)
self.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)
let planeNode = self.childNodes.first!
planeNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: self.planeGeometry, options: nil))
}
private func setup() {
//plane dimensions
self.planeGeometry = SCNPlane(width: CGFloat(self.anchor.extent.x), height: CGFloat(self.anchor.extent.z))
//plane material
let material = SCNMaterial()
material.diffuse.contents = UIImage(named: "tronGrid.png")
self.planeGeometry.materials = [material]
//plane geometry and physics
let planeNode = SCNNode(geometry: self.planeGeometry)
planeNode.physicsBody = SCNPhysicsBody(type: .static, shape: SCNPhysicsShape(geometry: self.planeGeometry, options: nil))
planeNode.physicsBody?.categoryBitMask = BodyType.plane.rawValue
planeNode.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z)
planeNode.transform = SCNMatrix4MakeRotation(Float(-Double.pi / 2.0), 1, 0, 0)
//add plane node
self.addChildNode(planeNode)
}
This is the ViewController
enum BodyType: Int {
case box = 1
case pyramid = 2
case plane = 3
}
class ViewController: UIViewController, ARSCNViewDelegate, SCNPhysicsContactDelegate {
//outlets
#IBOutlet var sceneView: ARSCNView!
//globals
var planes = [Plane]()
var boxes = [SCNNode]()
//life cycle
override func viewDidLoad() {
super.viewDidLoad()
//set sceneView's frame
self.sceneView = ARSCNView(frame: self.view.frame)
//add debugging option for sceneView (show x, y , z coords)
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
//give lighting to the scene
self.sceneView.autoenablesDefaultLighting = true
//add subview to scene
self.view.addSubview(self.sceneView)
// Set the view's delegate
sceneView.delegate = self
//subscribe to physics contact delegate
self.sceneView.scene.physicsWorld.contactDelegate = self
//show statistics such as fps and timing information
sceneView.showsStatistics = true
//create new scene
let scene = SCNScene()
//set scene to view
sceneView.scene = scene
//setup recognizer to add scooter to scene
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapped))
sceneView.addGestureRecognizer(tapGestureRecognizer)
}
//MARK: helper funcs
#objc func tapped(recognizer: UIGestureRecognizer) {
let scnView = recognizer.view as! ARSCNView
let touchLocation = recognizer.location(in: scnView)
let touch = scnView.hitTest(touchLocation, types: .existingPlaneUsingExtent)
//take action if user touches box
if !touch.isEmpty {
guard let hitResult = touch.first else { return }
addBox(hitResult: hitResult)
}
}
private func addBox(hitResult: ARHitTestResult) {
let boxGeometry = SCNBox(width: 0.1,
height: 0.1,
length: 0.1,
chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor(red: .random(),
green: .random(),
blue: .random(),
alpha: 1.0)
boxGeometry.materials = [material]
let boxNode = SCNNode(geometry: boxGeometry)
//adding physics body, a box already has a shape, so nil is fine
boxNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
//set bitMask on boxNode, enabling objects with diff categoryBitMasks to collide w/ each other
boxNode.physicsBody?.categoryBitMask = BodyType.plane.rawValue | BodyType.box.rawValue
boxNode.position = SCNVector3(hitResult.worldTransform.columns.3.x,
hitResult.worldTransform.columns.3.y + 0.3,
hitResult.worldTransform.columns.3.z)
self.sceneView.scene.rootNode.addChildNode(boxNode)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
//track objects in ARWorld and start session
sceneView.session.run(configuration)
}
//MARK: - ARSCNViewDelegate
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
//if no anchor found, don't render anything!
if !(anchor is ARPlaneAnchor) {
return
}
DispatchQueue.main.async {
//add plane to scene
let plane = Plane(anchor: anchor as! ARPlaneAnchor)
self.planes.append(plane)
node.addChildNode(plane)
//add initial scene object
let pyramidGeometry = SCNPyramid(width: CGFloat(plane.planeGeometry.width / 8), height: plane.planeGeometry.height / 8, length: plane.planeGeometry.height / 8)
pyramidGeometry.firstMaterial?.diffuse.contents = UIColor.white
let pyramidNode = SCNNode(geometry: pyramidGeometry)
pyramidNode.name = "pyramid"
pyramidNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil)
pyramidNode.physicsBody?.categoryBitMask = BodyType.pyramid.rawValue | BodyType.plane.rawValue
pyramidNode.physicsBody?.contactTestBitMask = BodyType.box.rawValue
pyramidNode.position = SCNVector3(-(plane.planeGeometry.width) / 3, 0, plane.planeGeometry.height / 3)
node.addChildNode(pyramidNode)
}
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
let plane = self.planes.filter {
plane in return plane.anchor.identifier == anchor.identifier
}.first
if plane == nil {
return
}
plane?.update(anchor: anchor as! ARPlaneAnchor)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
//pause session
sceneView.session.pause()
}
}
I think i followed the same tutorial. I also had same result. Reason is because when the cube drops from higher place, it accelerates and doesnot exactly hit on the plane but passes through. If you scale down the cube to '1 mm' you can see box completely passes through plane and continue falling below plane. You can try droping cube from nearer to the plane, box drops slower and this 'jiggling' will not occur. Or you can try with box with small height instead of plane.
I had the same problem i found out one solution.I was initializing the ARSCNView programmatically.I just removed those code and just added a ARSCNView in the storyboard joined it in my UIViewcontroller class using IBOutlet it worked like a charm.
Hope it helps anyone who is going through this problem.
The same code is below.
#IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints,ARSCNDebugOptions.showWorldOrigin]
sceneView.delegate = self
sceneView.showsStatistics = true
let scene = SCNScene()
sceneView.scene = scene
}
The "jiggling" is probably caused by an incorrect gravity vector. Try experimenting with setting the gravity of your scene.
For example, add this to your viewDidLoad function:
sceneView.scene.physicsWorld.gravity = SCNVector3Make(0.0, -1.0, 0.0)
I found that setting the gravity - either through code, or by loading an empty scene - resolves this issue.
I'm having an issue with the physics of cones and pyramids in SceneKit where the physics body seems to hover above the ground. If I replace it with say a SCNBox then there is no issue. It is more noticeable at smaller scales where the space above the ground is much larger relative to the size of the node. It's almost like there is a fixed offset. It happens whether the cone has the flat side or point facing the floor.
Code and screenshots below (linked), sample created and runs in a newly created Xcode SceneKit project.
(Xcode 9, swift 4 but was having the issue in prior version of Xcode and swift 3 as well).
image with geometry
image with just physics
import UIKit
import QuartzCore
import SceneKit
var scnView: SCNView!
var scnScene: SCNScene!
var cameraNode: SCNNode!
let universalScale: Float = 1 / 40
enter image description hereclass GameViewController: UIViewController, SCNSceneRendererDelegate {
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupScene()
setupCamera()
spawnFloor()
spawnShape()
}
func setupView() {
scnView = self.view as! SCNView
scnView.showsStatistics = false
scnView.allowsCameraControl = true
scnView.autoenablesDefaultLighting = true
scnView.debugOptions = .showPhysicsShapes
scnView.showsStatistics = true
scnView.delegate = self
}
func setupScene() {
scnScene = SCNScene()
scnView.scene = scnScene
}
func setupCamera() {
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0 * universalScale, y: 50 * universalScale, z: 50 * universalScale)
cameraNode.rotation = SCNVector4Make(1, 0, 0, -Float(Double.pi)/4)
scnScene.rootNode.addChildNode(cameraNode)
}
func spawnFloor() {
let floor = SCNFloor()
floor.reflectivity = 0.5
let material = floor.firstMaterial
material?.diffuse.contents = UIColor.gray
let floorNode = SCNNode(geometry: floor)
floorNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
scnScene.rootNode.addChildNode(floorNode)
}
var shapeNode: SCNNode?
func spawnShape(){
let cone = SCNCone(topRadius: 0, bottomRadius:CGFloat(1 * universalScale), height: CGFloat(universalScale * 2))
let coneShape = SCNPhysicsShape(geometry: cone, options: nil)
let coneNode = SCNNode(geometry: cone)
coneNode.physicsBody = SCNPhysicsBody(type: .dynamic, shape: coneShape)
coneNode.position = SCNVector3(0, -coneNode.boundingBox.min.y + 20*universalScale, 0)
scnScene.rootNode.addChildNode(coneNode)
shapeNode = coneNode
}
func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
if shapeNode != nil{
print(shapeNode!.presentation.position)
print(shapeNode!.boundingBox.min)
}
}
}
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.