Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Animations and Interactions of "Solar System" in ARKIt
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
@IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
sceneView.addGestureRecognizer(tapGestureRecognizer)
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
// sceneView.showsStatistics = true
// light source
sceneView.autoenablesDefaultLighting = true
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// show world origin
// sceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]
// Run the view's session
sceneView.session.run(configuration)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// sun
let sun = SCNNode(geometry: SCNSphere(radius: 0.33))
sun.name = "sun"
sun.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "starSunDiffuse")
sun.position = SCNVector3Make(0.0, 0.0, -1.5)
sceneView.scene.rootNode.addChildNode(sun)
// rotate the sun around itself
let rotationAction = SCNAction.rotateBy(x: 0.0, y: CGFloat(360.degreesToRadians), z: 0.0, duration: 60.0)
let rotationPeriod = SCNAction.repeatForever(rotationAction)
sun.runAction(rotationPeriod)
// create parent nodes of planets so they can have their own motions
let earthParentNode = SCNNode()
let moonParentNode = SCNNode()
earthParentNode.name = "earthParent"
moonParentNode.name = "moonParent"
earthParentNode.position = SCNVector3Make(0.0, 0.0, -1.5)
moonParentNode.position = SCNVector3Make(0.0, 0.0, 0.0)
sceneView.scene.rootNode.addChildNode(earthParentNode)
// earth appearance & position
let earth = astralBody(geometry: SCNSphere(radius: 0.2), diffuse: UIImage(named: "planetEarth")!, specular: UIImage(named: "planetEarthSpecular")!, emission: UIImage(named: "planetEarthEmissive")!, normal: UIImage(named: "planetEarthNormal")!, position: SCNVector3Make(1.2, 0.0, 0.0))
// moon appearance & position
let moon = astralBody(geometry: SCNSphere(radius: 0.14), diffuse: UIImage(named: "moonLunaDiffuse")!, specular: nil, emission: nil, normal: nil, position: SCNVector3Make(0.5, 0.0, 0.0))
// name them in case we want to interact
earth.name = "earth"
moon.name = "moon"
// add actual bodies to their parent nodes
earthParentNode.addChildNode(earth)
moonParentNode.addChildNode(moon)
earth.addChildNode(moonParentNode)
// earth rotation (spin on own axis)
let earthRotationPeriod = nodeRotation(time: 12.0, x: 0.0, y: -CGFloat(360.degreesToRadians), z: 0.0)
earth.runAction(earthRotationPeriod)
// earth orbit
let earthParentRotationPeriod = nodeRotation(time: 30.0, x: 0.0, y: CGFloat(360.degreesToRadians), z: 0.0)
earthParentNode.runAction(earthParentRotationPeriod)
// moon spin
let moonRotationPeriod = nodeRotation(time: 2.0, x: 0.0, y: CGFloat(360.degreesToRadians), z: 0.0)
moon.runAction(moonRotationPeriod)
// moon orbit
let moonParentRotationPeriod = nodeRotation(time: 8.0, x: 0.0, y: CGFloat(360.degreesToRadians), z: 0.0)
moonParentNode.runAction(moonParentRotationPeriod)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
// used for setting spin and orbits
func nodeRotation(time: TimeInterval, x: CGFloat, y: CGFloat, z: CGFloat) -> SCNAction {
let rotationAction = SCNAction.rotateBy(x: x, y: y, z: z, duration: time)
let rotationPeriod = SCNAction.repeatForever(rotationAction)
return rotationPeriod
}
// used to create a sperical body and set its appearance
func astralBody(geometry: SCNGeometry, diffuse: UIImage?, specular: UIImage?, emission: UIImage?, normal: UIImage?, position: SCNVector3) -> SCNNode {
let body = SCNNode(geometry: geometry)
body.geometry?.firstMaterial?.diffuse.contents = diffuse
body.geometry?.firstMaterial?.specular.contents = specular
body.geometry?.firstMaterial?.emission.contents = emission
body.geometry?.firstMaterial?.normal.contents = normal
body.position = position
return body
}
@objc func handleTap(sender: UITapGestureRecognizer) {
let sceneViewTappedOn = sender.view as! SCNView
let touchCoordinates = sender.location(in: sceneViewTappedOn)
let hitTest = sceneViewTappedOn.hitTest(touchCoordinates)
if hitTest.isEmpty {
print("outside of scene")
} else {
let results = hitTest.first!
let geometry = results.node.geometry
let nameOfHit = results.node.name
print(geometry ?? "default value")
print(nameOfHit ?? "unnamed")
if (nameOfHit == "sun") {
animateNode(node: sceneView.scene.rootNode.childNode(withName: "sun", recursively: false)!)
} else if (nameOfHit == "earth") {
reverseNode(node: sceneView.scene.rootNode.childNode(withName: "earthParent", recursively: false)!)
} else if (nameOfHit == "moon") {
messWithTexture(node: sceneView.scene.rootNode.childNode(withName: "moon", recursively: true)!)
}
}
}
func animateNode(node: SCNNode) {
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 1
animation.toValue = 0
let animationX = CABasicAnimation(keyPath: "transform.scale.x")
let animationY = CABasicAnimation(keyPath: "transform.scale.y")
let animationZ = CABasicAnimation(keyPath: "transform.scale.z")
animationX.fromValue = 1
animationX.toValue = 2
animationY.fromValue = 1
animationY.toValue = 2
animationZ.fromValue = 1
animationZ.toValue = 2
node.addAnimation(animationX, forKey: "transform.scale.x")
node.addAnimation(animationY, forKey: "transform.scale.y")
node.addAnimation(animationZ, forKey: "transform.scale.z")
node.addAnimation(animation, forKey: "opacity")
}
func reverseNode(node: SCNNode) {
let orbitPeriod = nodeRotation(time: 10.00, x: 0.0, y: -CGFloat(360.degreesToRadians), z: 0.0)
node.runAction(orbitPeriod)
}
func messWithTexture(node: SCNNode) {
node.geometry?.firstMaterial?.diffuse.contents = UIImage(named: "planetVenus")
}
// MARK: - ARSCNViewDelegate
func session(_ session: ARSession, didFailWithError error: Error) {
// Present an error message to the user
}
func sessionWasInterrupted(_ session: ARSession) {
// Inform the user that the session has been interrupted, for example, by presenting an overlay
}
func sessionInterruptionEnded(_ session: ARSession) {
// Reset tracking and/or remove existing anchors if consistent tracking is required
}
}
extension Int {
var degreesToRadians: Double { return Double(self) * .pi/180 }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment