Created
August 21, 2020 18:06
-
-
Save jungchris/64eb028ff419da62350b828814706702 to your computer and use it in GitHub Desktop.
Animations and Interactions of "Solar System" in ARKIt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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