I'm attempting my first SceneKit app. My goal is to simulate a view from the surface of the Earth and being able to point the device's camera in any direction and overlay information over the camera view.
To start, I'm simply trying to get the SceneKit camera view to match the device's orientation. To verify that it is working as desired, I am adding a bunch of spheres at specific latitude and longitude coordinates.
Everything is working except for one important issue. The view is mirrored left/right (east/west) from what it should be showing. I've spent hours trying different permutations of adjusting the camera.
Below is my complete test app view controller. I can't figure out the right combination of changes to get the scene to render properly. The sphere at the North Pole is correct. The line of spheres stretching from the North Pole to the equator at my own current longitude appears overhead as expected. It's the other lines of spheres that are incorrect. They are mirrored east/west from what they should be as if the mirror is at my own longitude.
If you want to test this code, create a new Game project with SceneKit. Replace the template GameViewController.swift file with the one below. You also need to add the "Privacy - Location When In Use Description" key to the Info.plist. I also suggest adjusting the for lon in ... line so the numbers either start or end with your own longitude. Then you can see whether the spheres are being drawn on the correct portion of the display. This might also require a slight adjustment to the UIColor(hue: CGFloat(lon + 104) * 2 / 255.0 color.
import UIKit
import QuartzCore
import SceneKit
import CoreLocation
import CoreMotion
let EARTH_RADIUS = 6378137.0
class GameViewController: UIViewController, CLLocationManagerDelegate {
var motionManager: CMMotionManager!
var scnCameraArm: SCNNode!
var scnCamera: SCNNode!
var locationManager: CLLocationManager!
var pitchAdjust = 1.0
var rollAdjust = -1.0
var yawAdjust = 0.0
func radians(_ degrees: Double) -> Double {
return degrees * Double.pi / 180
}
func degrees(_ radians: Double) -> Double {
return radians * 180 / Double.pi
}
func setCameraPosition(lat: Double, lon: Double, alt: Double) {
let yaw = lon
let pitch = lat
scnCameraArm.eulerAngles.y = Float(radians(yaw))
scnCameraArm.eulerAngles.x = Float(radians(pitch))
scnCameraArm.eulerAngles.z = 0
scnCamera.position = SCNVector3(x: 0.0, y: 0.0, z: Float(alt + EARTH_RADIUS))
}
func setCameraPosition(loc: CLLocation) {
setCameraPosition(lat: loc.coordinate.latitude, lon: loc.coordinate.longitude, alt: loc.altitude)
}
// MARK: - UIViewController methods
override func viewDidLoad() {
super.viewDidLoad()
// create a new scene
let scene = SCNScene()
let scnCamera = SCNNode()
let camera = SCNCamera()
camera.zFar = 2.5 * EARTH_RADIUS
scnCamera.camera = camera
scnCamera.position = SCNVector3(x: 0.0, y: 0.0, z: Float(EARTH_RADIUS))
self.scnCamera = scnCamera
let scnCameraArm = SCNNode()
scnCameraArm.position = SCNVector3(x: 0, y: 0, z: 0)
scnCameraArm.addChildNode(scnCamera)
self.scnCameraArm = scnCameraArm
scene.rootNode.addChildNode(scnCameraArm)
// 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)
// retrieve the SCNView
let scnView = self.view as! SCNView
// set the scene to the view
scnView.scene = scene
//scnView.pointOfView = scnCamera
// Draw spheres over part of the western hemisphere
for lon in stride(from: 0, through: -105, by: -15) {
for lat in stride(from: 0, through: 90, by: 15) {
let mat4 = SCNMaterial()
if lat == 90 {
mat4.diffuse.contents = UIColor.yellow
} else if lat == -90 {
mat4.diffuse.contents = UIColor.orange
} else {
//mat4.diffuse.contents = UIColor(red: CGFloat(lat + 90) / 255.0, green: CGFloat(lon + 104) * 4 / 255.0, blue: 1, alpha: 1)
mat4.diffuse.contents = UIColor(hue: CGFloat(lon + 104) * 2 / 255.0, saturation: 1, brightness: CGFloat(255 - lat * 2) / 255.0, alpha: 1)
}
let ball = SCNSphere(radius: 100000)
ball.firstMaterial = mat4
let ballNode = SCNNode(geometry: ball)
ballNode.position = SCNVector3(x: 0.0, y: 0.0, z: Float(100000 + EARTH_RADIUS))
let ballArm = SCNNode()
ballArm.position = SCNVector3(x: 0, y: 0, z: 0)
ballArm.addChildNode(ballNode)
scene.rootNode.addChildNode(ballArm)
ballArm.eulerAngles.y = Float(radians(Double(lon)))
ballArm.eulerAngles.x = Float(radians(Double(lat)))
}
}
// configure the view
scnView.backgroundColor = UIColor(red: 0, green: 191/255, blue: 255/255, alpha: 1) // sky blue
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
let auth = CLLocationManager.authorizationStatus()
switch auth {
case .authorizedWhenInUse:
locationManager.startUpdatingLocation()
case .notDetermined:
locationManager.requestWhenInUseAuthorization()
default:
break
}
motionManager = CMMotionManager()
motionManager.deviceMotionUpdateInterval = 1 / 30
motionManager.startDeviceMotionUpdates(using: .xTrueNorthZVertical, to: OperationQueue.main) { (motion, error) in
if error == nil {
if let motion = motion {
//print("pitch: \(self.degrees(motion.attitude.roll * self.pitchAdjust)), roll: \(self.degrees(motion.attitude.pitch * self.rollAdjust)), yaw: \(self.degrees(-motion.attitude.yaw))")
self.scnCamera.eulerAngles.z = Float(motion.attitude.yaw + self.yawAdjust)
self.scnCamera.eulerAngles.x = Float(motion.attitude.roll * self.pitchAdjust)
self.scnCamera.eulerAngles.y = Float(motion.attitude.pitch * self.rollAdjust)
}
}
}
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if UIApplication.shared.statusBarOrientation == .landscapeRight {
pitchAdjust = -1.0
rollAdjust = 1.0
yawAdjust = Double.pi
} else {
pitchAdjust = 1.0
rollAdjust = -1.0
yawAdjust = 0.0
}
}
override var shouldAutorotate: Bool {
return false
}
override var prefersStatusBarHidden: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscape
}
// MARK: - CLLocationManagerDelegate methods
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
if status == .authorizedWhenInUse {
manager.startUpdatingLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for loc in locations {
print(loc)
if loc.horizontalAccuracy > 0 && loc.horizontalAccuracy <= 100 {
setCameraPosition(loc: loc)
}
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
}
}
The changes probably need to be made in some combination of the setCameraPosition method and/or the motionManager.startDeviceMotionUpdates closure at the end of viewDidLoad.
I don't understand precisely what's wrong, but I believe that the way SceneKit uses pitch and yaw in eulerAngles doesn't match the way you're picturing it. I was unable to find chapter/verse that specifies which direction positive pitch, roll, and yaw are.
I was able to get it working (at least I think I've done what you were trying for) by changing the pitch/yaw setup to
ballArm.eulerAngles.x = -1 * Float(radians(Double(lat)))
I also added some debugging colors and labeling, and archived the scene to .SCN format to load it into the Xcode Scene Editor (I think Xcode 9 will do this for you in the debugger). Putting a name on each node lets you see what's what in the editor. Revised code:
// Draw spheres over part of the western hemisphere
for lon in stride(from: 0, through: -105, by: -15) {
for lat in stride(from: 0, through: 90, by: 15) {
let mat4 = SCNMaterial()
if lon == 0 {
mat4.diffuse.contents = UIColor.black
} else if lon == -105 {
mat4.diffuse.contents = UIColor.green
} else if lat == 90 {
mat4.diffuse.contents = UIColor.yellow
} else if lat == 0 {
mat4.diffuse.contents = UIColor.orange
} else {
mat4.diffuse.contents = UIColor(red: CGFloat(lat + 90) / 255.0, green: CGFloat(lon + 104) * 4 / 255.0, blue: 1, alpha: 1)
//mat4.diffuse.contents = UIColor(hue: CGFloat(lon + 104) * 2 / 255.0, saturation: 1, brightness: CGFloat(255 - lat * 2) / 255.0, alpha: 1)
//mat4.diffuse.contents = UIColor.green
}
let ball = SCNSphere(radius: 100000)
ball.firstMaterial = mat4
let ballNode = SCNNode(geometry: ball)
ballNode.position = SCNVector3(x: 0.0, y: 0.0, z: Float(100000 + EARTH_RADIUS))
let ballArm = SCNNode()
ballArm.position = SCNVector3(x: 0, y: 0, z: 0)
// debugging label
ballArm.name = "\(lat) \(lon)"
ballArm.addChildNode(ballNode)
scene.rootNode.addChildNode(ballArm)
ballArm.eulerAngles.y = Float(radians(Double(lon)))
ballArm.eulerAngles.x = -1 * Float(radians(Double(lat)))
}
}
// configure the view
scnView.backgroundColor = UIColor.cyan
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let docsFolderURL = urls[urls.count - 1]
let archiveURL = docsFolderURL.appendingPathComponent("rmaddy.scn")
let archivePath = archiveURL.path
// for copy/paste from Simulator's file system
print (archivePath)
let archiveResult = NSKeyedArchiver.archiveRootObject(scene, toFile: archivePath)
print(archiveResult)
And here's a screenshot of the scene in the Scene Editor. Note the faint orange circle around the sphere for the lat 45/lon -90 node, selected in the nodes listing.
Thanks to Hal for his very helpful suggestion for creating an "scn" file from my scene. After viewing that scene in the scene editor, I realized that everything was reversed left/right because the yaw Euler angle (Y-axis) rotation was reverse of what I thought. So when I set the node's eulerAngles.y to the desired longitude, I was rotating in the opposite direction of what I wanted. But since the camera had the same error, the spheres at my location were in the correct location but everything else was reversed left-to-right.
The solution was to negate the longitude value for both the spheres and the camera location.
scnCameraArm.eulerAngles.y = Float(radians(yaw))
needs to be:
scnCameraArm.eulerAngles.y = -Float(radians(yaw))
And:
ballArm.eulerAngles.y = Float(radians(Double(lon)))
needs to be:
ballArm.eulerAngles.y = -Float(radians(Double(lon)))
Related
I need to draw a line between two spritnodes while hand is pointing from one node to another with rendering or animation.
The line drawing is working for me as I follow this link Bobjt's answer, Animate Path Drawing in SpriteKit . I have used concept of fragment shader and vertex shader here..
I have added animatestroke.fsh file and apply the following code .. But the handplayer animation and line drawing speed is mismatching some times.
my line rendering i have given increment by
strokeLengthFloat += 0.01
And handplayer duration is 1.5:
let moveTosecondObject = SKAction.move(to: handmovepoint, duration: 1.5 )
Here is my implementation code:
variable declaration
var handplayer = SKSpriteNode()
var firstObject = SKSpriteNode()
var secondObject = SKSpriteNode()
var startTrigger: Bool = false
let strokeSizeFactor = CGFloat( 2.0 )
var strokeShader: SKShader!
var strokeLengthUniform: SKUniform!
var _strokeLengthFloat: Float = 0.0
var strokeLengthKey: String!
var strokeLengthFloat: Float {
get {
return _strokeLengthFloat
}
set( newStrokeLengthFloat ) {
_strokeLengthFloat = newStrokeLengthFloat
strokeLengthUniform.floatValue = newStrokeLengthFloat
}
}
Loading method
override func didMove(to view: SKView) {
handplayer = SKSpriteNode(imageNamed: "HandImage.png")
handplayer.size.width = handplayer.size.width * 0.5
handplayer.size.height = handplayer.size.height * 0.5
handplayer.position.x = firstObject.position
self.addChild(handplayer)
UpdatePosition()
}
func UpdatePosition(){
//Line drawing part
strokeLengthKey = "u_current_percentage"
strokeLengthUniform = SKUniform( name: strokeLengthKey, float: 0.0 )
let uniforms: [SKUniform] = [strokeLengthUniform]
strokeShader = shaderWithFilename( "animateStroke", fileExtension: "fsh", uniforms: uniforms )
strokeLengthFloat = 0.0
let cameraNode = SKCameraNode()
self.camera = cameraNode
let lineStartPosition = CGPoint(x:firstObject.frame.origin.x+firstObject.size.width/2,y:firstObject.frame.origin.y-firstObject.size.height*0.2)
let lineEndPosition = CGPoint(x:secondObject.frame.origin.x+secondObject.size.width/2,y:secondObject.frame.origin.y+secondObject.size.height*1.2)
let path = CGMutablePath()
path.move(to: CGPoint(x: lineStartPosition.x, y: lineStartPosition.y))
path.addLine(to: CGPoint(x: : lineEndPosition.x, y: lineEndPosition.y), transform: CGAffineTransform.identity)
let strokeWidth = 1.0 * strokeSizeFactor
let shapeNode = SKShapeNode( path: path )
shapeNode.lineWidth = strokeWidth
shapeNode.lineCap = .square
shapeNode.addChild( cameraNode )
shapeNode.strokeShader = strokeShader
shapeNode.calculateAccumulatedFrame()
self.addChild( shapeNode )
}
// center the camera
cameraNode.position = CGPoint( x: self.frame.size.width/2.0, y: self.frame.size.height/2.0 )
**Handplayer Rendering part**
let handmovepoint = CGPoint(x: secondObject.position.x , y: secondObject.position.y + handplayer.size.height*0.1 )
let moveTosecondObject = SKAction.move(to: handmovepoint, duration: 1.5 )
let delay = SKAction.wait(forDuration: 1)
handplayer.run(SKAction.sequence([ delay ]), completion: {() -> Void in
self.startTrigger = true
self.handplayer.run(SKAction.sequence([ moveTosecondObject ]), completion: {() -> Void in
//self.handplayer.alpha = 0.0
})
}
//line rendering part
override func update(_ currentTime: TimeInterval) {
if(startTrigger){
strokeLengthFloat += 0.01
}
if startTrigger == true{
if strokeLengthFloat > 1.0 {
//once line drawing completed
self.startTrigger = false
print("Line drawing ended")
self.handplayer.alpha = 0.0
self.handplayer.removeFromParent()
}
}
}
animatestroke.fsh class
void main()
{
if ( u_path_length == 0.0 ) {
gl_FragColor = vec4( 0.0, 0.0, 1.0, 1.0 ); // draw blue // this is an error
} else if ( v_path_distance / u_path_length <= u_current_percentage ) {
gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 ); // draw red
} else {
gl_FragColor = vec4( 0.0, 0.0, 0.0, 0.0 ); // draw nothing
}
}
How could I fix this? Or If anyone have other idea for implementhing this scenario share your Idea.
I created a 3d cube using SceneKit and added a gesture to recognize swiping right. But, I do not know how to rotate the cube to another face when swiped right. I do not want the cube to rotate continuously when swiped, only moving to another face that is on the right side. Sorry for any confusions.
Here is my code where I created cube:
import UIKit
import SceneKit
class ViewController: UIViewController {
// UI
#IBOutlet weak var geometryLabel: UILabel!
#IBOutlet weak var sceneView: SCNView!
// Geometry
var geometryNode: SCNNode = SCNNode()
// Gestures
var currentAngle: Float = 0.0
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// MARK: Scene
sceneSetup()
}
func sceneSetup() {
let scene = SCNScene()
sceneView.scene = scene
sceneView.allowsCameraControl = false
sceneView.autoenablesDefaultLighting = true
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3Make(0, 0, 25)
scene.rootNode.addChildNode(cameraNode)
var geometries = [
SCNBox(width: 8.0, height: 8.0, length: 8.0, chamferRadius: 1.0)
]
var materials = [SCNMaterial]()
for i in 1...6 {
let material = SCNMaterial()
if i == 1 { material.diffuse.contents = UIColor(red:0.02, green:0.98, blue:0.98, alpha:1.0) }
if i == 2 { material.diffuse.contents = UIColor(red:0.02, green:0.98, blue:0.98, alpha:1.0) }
if i == 3 { material.diffuse.contents = UIColor(red:0.02, green:0.98, blue:0.98, alpha:1.0) }
if i == 4 { material.diffuse.contents = UIColor(red:0.02, green:0.98, blue:0.98, alpha:1.0) }
if i == 5 { material.diffuse.contents = UIColor(red:0.02, green:0.98, blue:0.98, alpha:1.0) }
if i == 6 { material.diffuse.contents = UIColor(red:0.02, green:0.98, blue:0.98, alpha:1.0) }
materials.append(material)
}
for i in 0..<geometries.count {
let geometry = geometries[i]
let node = SCNNode(geometry: geometry)
node.geometry?.materials = materials
scene.rootNode.addChildNode(node)
}
let swipeRight: UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(respondToSwipeGesture(gesture:)))
swipeRight.direction = .right
sceneView.addGestureRecognizer(swipeRight)
}
Function for swipe right gesture when recognize:
func respondToSwipeGesture(gesture: UIGestureRecognizer) {
if let swipeGesture = gesture as? UISwipeGestureRecognizer {
switch swipeGesture.direction {
case UISwipeGestureRecognizerDirection.right:
print("Swiped Right")
default:
break
}
}
}
You need to use a SCNAction.rotate getting the current w value of the node rotation and adding the amount of angle you want, using this 2 extensions for converting to Radians and from Radians, replace my self.shipNode by your cube node, and change the axis if you need to, taking in account that the first 0 in the SCNVector4Make sentence is X axis
This is an example
extension Int {
var degreesToRadians: Double { return Double(self) * .pi / 180 }
var radiansToDegrees: Double { return Double(self) * 180 / .pi }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}
#objc func handleTap(_ gestureRecognize: UIGestureRecognizer) {
//here we rotate the ship node 30 grades in the Y axis each time
self.shipNode?.runAction(SCNAction.rotate(toAxisAngle: SCNVector4Make(0, 1, 0, (self.shipNode?.rotation.w)! + Float(30.degreesToRadians)), duration: 1))
}
To rotate down you only have to change the axis of the rotation
you can use this
- (void) handleTap:(UIGestureRecognizer*)gestureRecognize
{
[self.ship runAction:[SCNAction rotateToAxisAngle:SCNVector4Make(1, 0, 0, [self getRadiansFromAngle:[self getAngleFromRadian:self.ship.rotation.w] + 30]) duration:1]];
}
UPDATED
Using your code
You need to use SCNAction.rotate(by: #Float, around: #SCNVector3, duration: #duration) method instead, below is the full code for your requeriments
#objc func respondToSwipeGesture(gesture: UIGestureRecognizer) {
if let swipeGesture = gesture as? UISwipeGestureRecognizer {
switch swipeGesture.direction {
case UISwipeGestureRecognizerDirection.right:
print("Swiped Right")
geometryNode.runAction(SCNAction.rotate(by: CGFloat(90.degreesToRadians), around: SCNVector3Make(0, 1, 0), duration: 0.2))
case UISwipeGestureRecognizerDirection.left:
print("Swiped Left")
geometryNode.runAction(SCNAction.rotate(by: CGFloat(-90.degreesToRadians), around: SCNVector3Make(0, 1, 0), duration: 0.2))
case UISwipeGestureRecognizerDirection.down:
print("Swiped Down")
geometryNode.runAction(SCNAction.rotate(by: CGFloat(90.degreesToRadians), around: SCNVector3Make(1, 0, 0), duration: 0.2))
default:
break
}
}
}
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
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.
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...")
}
}