# Moon And Earth Simulation on iPhone

Updated: Oct 14, 2021

Learn how to make a rough estimate of the moon's orbit around the earth and draw it using __iOS Scenekit__ to make a cool demo App for iPhone!

In this project we are using an **Apple iPhone 8.**

**Watch this video clip to see what you will learn to make in this tutorial:**

In this app we can simulate what the earth looks like from the perspective of the moon, view the moon's orbit from afar in space, and get a closeup of the rotating earth -- and in the process we will touch on the topic of parametric equations.

Project Setup

Create a new Xcode Game Project in Swift.

Remove the contents of the **art.scnassets** directory, and replace with the contents you can download here:

The folder contains the main scene that will be used for the app, and also .jpg textures available for free at__ https://www.solarsystemscope.com/Textures__.

Contents:

**MainScene.scn**: This is the main scene for the project. The screenshot to the right shows that the scene has already been configured for you in Xcode editor, which is where we assign the background image.**earth.jpg:**Earth image**milky_way.jpg:**Background image of stars**moon.jpg:**Moon image

This is what the file structure looks like in this project. We will be referring to these files in the steps below.

In addition to the files in the art.scnassets directory, there 2 other files we will be editing:

**1. AppDelegate.swift - **Will contain basic boilerplate code required by Swift, with nothing specific to our project.

**2. GameViewController.swift - **This is where we will be implementing all of our code specific to this project. Chunks of code will be added in steps as you follow along with the tutorial. If you'd like, you can skip to the final code at the bottom.

First, here is some boilerplate code that needs to go into the file named **AppDelegate.swift:**

```
// AppDelegate.swift
// MoonOrbit
```**import** UIKit
**@UIApplicationMain**
**class** AppDelegate: UIResponder, UIApplicationDelegate {
**var** window: UIWindow?
**func** application(**_** application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: **Any**]?) -> Bool {
**return** **true**
}
}

Create a Background of Stars and Set up the Camera

Go to **GameViewController.swift **and add *SCNSceneRendererDelegate *to the class definition. This will allow us to add code to the SceneKit rendering loop later on:

**class** GameViewController: UIViewController, **SCNSceneRendererDelegate** { .. }

Next, add these three variables right underneath for the scene and camera:

` `**var** sceneView: SCNView!
**var** scene: SCNScene!
**var** cameraNode: SCNNode!

Update the function *viewDidLoad() *to the following:

**override** **func** viewDidLoad() {
**super**.viewDidLoad()
setupScene()
setupCamera(xLoc: 0.0, yLoc: 4.0, zLoc:10.0, radius: 1.0)
}

Now we have to actually implement the functions setupScene() and setupCamera().

**func** setupScene() {
sceneView = **self**.view **as**? SCNView
sceneView.delegate = **self** **as** SCNSceneRendererDelegate
scene = SCNScene(named: "art.scnassets/MainScene.scn")
sceneView.scene = scene
**let** tap = UITapGestureRecognizer(target: **self**, action: **#****selector**(**self**.handleTap(**_**:)))
sceneView.addGestureRecognizer(tap)
view.isUserInteractionEnabled = **true**
}

**setupScene() **does a few boilerplate things, but most importantly refers to "art.scnassets/MainScene.scn", which you may recognize as one of the files you downloaded. This is the main scene, thought of as the canvas, where we are going to do our actual rendering. If you click on *MainScene.scn* in the project explorer on the left, the editor will appear in Xcode all the way on the right. The **Background** dropdown refers to our "milky_way.jpg" image we've also placed in the same directory (refer to image above in the *Project Setup* section of this article).

Next, implement the setupCamera() function so we have a way of actually seeing our scene:

**func** setupCamera(xLoc: Float, yLoc: Float, zLoc:Float, radius: Float) {
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: xLoc, y: yLoc, z: zLoc)
cameraNode.camera?.zFar = 500
cameraNode.eulerAngles = SCNVector3(x: (-20.0 * (Float.pi / 180)), y: 0.0, z: 0.0)
scene.rootNode.addChildNode(cameraNode)
}

**Now hit play and give it a run in the simulator. This is what we get:**

Great! It's empty space. Now since this isn't 4.5 billion years ago, which is prior to when scientists believe the earth was formed, let's place an actual earth there on the canvas.

Creating the Earth

Declare an SCNNode for the earth right under the cameraNode:

` `**var** earthNode: SCNNode!

Update viewDidLoad with:

**override** **func** viewDidLoad() {
**super**.viewDidLoad()
setupScene()
setupCamera(xLoc: 0.0, yLoc: 4.0, zLoc:10.0, radius: 1.0)
createEarth(xLoc: 0.0, yLoc: 0.4, zLoc:0.0, radius: 3.66)
}

Implement the createEarth() function:

**func** createEarth(xLoc: Float, yLoc: Float, zLoc:Float, radius: Float) {
// Define sphere
**var** geometry:SCNGeometry
geometry = SCNSphere(radius:CGFloat(radius))
earthNode = SCNNode(geometry: geometry)
// Cover sphere with earth.jpg
geometry.materials.first?.diffuse.contents = ("art.scnassets/earth.jpg")
// Assign earth position
earthNode.position = SCNVector3(x: xLoc, y: yLoc, z:zLoc)
// Add earth to scene
scene.rootNode.addChildNode(earthNode)
// Rotate earth along the y-axis at rate of 0.5 radians every 15 seconds
earthNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0.0 , y: 0.5, z: 0, duration: 15)))
}

The function above is commented throughout, where each step is pretty self explanatory. Basically we are drawing the Earth and telling SceneKit to make it rotate......forever.

**We have a rotating earth!**

Creating the Moon

Now let's add the moon. Declare an SCNNode for the moon right under the declaration for the cameraNode:

**var** moonNode: SCNNode!

Where should we place the moon? According to NASA the moon is an average of __238,855 miles__ (384,400 kilometers) away from earth with a radius of 1,080 miles, and the earth's radius is __3,959 miles__ (6,371 km). This means that the distance between the them in terms of earth's radius is 238,855 / 3,959 = 60.332 earth radiuses. Also, the earth's radius is 3,959 / 1080 = 3.66 times as larges as the moon's.

Based on the ratios computed above, If we set the moon's radius in our simulation to be a value of 1.0, then the Earth's radius is 3.66. And if we place our Earth at the origin along the z-axis, then the moon should be place along the z-axis at a distance of 60.332 earth radius, which is 3.66 X 60 = 221.0. The values are reflected in the arguments below the functions createEarth() and createMoon(), which we also need to define in our class.

Update viewDidLoad with:

**override** **func** viewDidLoad() {
**super**.viewDidLoad()
setupScene()
setupCamera(xLoc: 0.0, yLoc: 4.0, zLoc:226.0, radius: 1.0)
createEarth(xLoc: 0.0, yLoc: 0.4, zLoc:0.0, radius: 3.66)
createMoon(xLoc: 0.0, yLoc: 0.4, zLoc:221.0, radius: 1.0)
}

Implement the createMoon() function:

**func** createMoon(xLoc: Float, yLoc: Float, zLoc:Float, radius: Float) {
// moonExists = true
// Define sphere
**var** geometry:SCNGeometry
geometry = SCNSphere(radius:CGFloat(radius))
moonNode = SCNNode(geometry: geometry)
// Cover sphere with moon.jpg
geometry.materials.first?.diffuse.contents = ("art.scnassets/moon.jpg")
// Assign moon position
moonNode.position = SCNVector3(x: xLoc, y: yLoc, z:zLoc)
// Add moon to scene
scene.rootNode.addChildNode(moonNode)
// Rotate moon along the y-axis
moonNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0.0 , y: 0.5, z: 0, duration: 15)))
}

The createMoon() function does the same thing as createEarth(), only for the moon instead. Note that in the last line where we tell the moon to rotate forever, it is set to the same rate as the earth. This is because the moon spins synchronously with its orbit, so that the same side is more or less always facing the earth.

**Now hit run in the Simulator and we have a view from the moon:**

Now we want to put these celestial objects in motion. This will require a little bit of math.

Modeling the Moon's motion as a circle

The orbital motion of the moon around the earth contains a certain amount of eccentricity, in that it is not a perfect circle. There are other orbital elements to factor when creating a simulation as well, such as the semi-major axis, inclination, longitude of the ascending node, argument of perigee, and right ascension at epoch --- these elements are referred to and explained in the book *Fundamentals of Astrodynamics** *beginning on page 324. This text was developed at the U.S. Airforce academy and I think it pretty cool to have as reference for some more advanced reading.

Although the moon's orbit around the earth is elliptical, for the sake of simplicity we are going to approximate it as a perfect circle along the same plane as the earth. This means that for every rendering frame in SceneKit we need to update the Moon's position to follow a circular path with the earth at its center. Mathematically, we know that the standard equation for a circle with a radius of 3, for example, and centered at the origin of a 2-dimensional coordinate system is:

But we're interested in calculating the coordinates of the Moon's circular motion as a function of time, *t. *For this we will use parametric equations instead of the more standard equation shown in the example above.

A Brief Aside on Parametric Equations

These are the parametric equations of a circle for computing the values of *x and y* with radius *r *as a functions of *t:*

*x = r cos(t)*

*y = r sin(t)*

As examples, to the right is a 2-dimensional plot of 2 different parametrically defined circles.

(graphed using __Maple__)

The outer circle, in blue, represents the equations ** x = 3cos(t)**

*and*

*y = 3sin(t)**.*This circle has a

**radius of 3**.

The inner circle, in red, represents the equations ** x = 2cos(t)**

*and*

*y = 2sin(t)**.*This circle has a

**radius of 2**.

I think it's natural to use parametric equations instead of the regular equations for this kind of a simulation for the moon's orbit, since we are dealing with an animation where the position of the moon and sun varies according to time, *t.*

In this case I keep referring to the variable *t *as time, but mathematically it is actually representing the moon's position along the circle by it's *angle *from its center. This is why when we implement these equations in the code so we can pre-compute the Moon's coordinates, it's in the range * 0 <= t <= 360, *so that it makes a complete circle by computing coordinates along all 360 degrees of the circle.

Here is the code where we implement the equations * x=221cos(t-90) *and

*for*

**y=-221sin(t-90)***t*in the range

*0 <= t <= 360.*We need to subtract 90 degrees from the starting point of

*t*because we want to begin at the bottom of our circle at -90 degrees, placing the moon right in front of the camera with the earth behind it at the center of the circle.

**x=221cos(t-90)**

**let** xCoord = radius * cos(Float(degree - 90) * Float.pi / 180)

**y=-221sin(t-90)**** **

**let** yCoord = -1.0 * (radius * sin(Float(degree - 90) * Float.pi / 180))

Place these statements above into a function so we can store the computed orbital values in a data structure moonCoords for later use, then display the computed values in the console so we can see what's going on. Please note that the trigonometric functions *cosine* and *sine *expect arguments in *radians*, not degrees. This is why we have to multiply our *degree *variable by *pi / 180 *to convert from degrees to radians.

**func** setupMoonCoordinates() {
**let** radius = Float(221.0)
**for** degree **in** 1...360 {
// Convert degrees to radians with pi/180
**let** xCoord = radius * cos(Float(degree - 90) * Float.pi / 180)
**let** yCoord = -1.0 * (radius * sin(Float(degree - 90) * Float.pi / 180))
moonCoords.append( SCNVector3(x:Float(xCoord), y:Float(0.04), z:Float(yCoord)))
}
// Display computed values of the orbit
print("XYZ Coordinates of Moon Orbit")
**var** position = 1;
**for** coord **in** moonCoords{
print("\(position): X: \(coord.x) Y: \(coord.y) Z: \(coord.z)")
position += 1
}
}

Now add this line in the viewDidLoad() function to invoke the function:

` setupMoonCoordinates()`

If you run the app after these latest changes, there will be no difference in the display because we haven't implemented the code to actually update the moon's location based off the coordinates stored in moonCoords. Rather, we have only told our app to print out values to the XCode console:

The console will print out the x, y, and z coordinates computed by the setupMoonCoordinates() function. Since we are computing the coordinates of a 2-dimensional circle but then rendering it in 3-dimensional space, we actually want the orbit to go into and out of the screen, which is the z-direction. So our computed values for *y *are assigned to the z coordinates, and a constant value of 0.04 is assigned for *y* for every location because this puts it in the same plane as the earth's which is also at y = 0.04.

Making the Moon move along its predefined orbital path

In order to actually see the moon move we need to add logic to the iPhone's rendering loop. But first, here are some helper variables.

Add these to the top of the file right with the other existing class variables:

**var** frameRate:TimeInterval = 0
**var** moonExists:Bool = **false**

Add this code to the bottom of the file, after the GamViewController class definition:

**extension** GameViewController {
**func** renderer(**_** renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
**if** (moonExists == **true**) {
**if** time > frameRate{
**if** (degreeIndex >= 360) {
degreeIndex = 1
}
**let** newLocationMoon = moonCoords[degreeIndex]
**let** moonAction = SCNAction.move(to: newLocationMoon, duration: 0.60)
moonNode.runAction(moonAction)
frameRate = time + TimeInterval(Float(0.60))
degreeIndex+=1
}
}
}
}

Lastly, let's temporarily update our camera position and angle:

`setupCamera(xLoc: 0.0, yLoc: 4.0, zLoc:260.0, radius: 1.0)`

Then in the setupCamera function, update this line with a new y-angle:

```
cameraNode.eulerAngles = SCNVector3(x: (-20.0 * (Float.pi / 180)), y: (-25.0 * (Float.pi / 180)), z: 0.0)
```

**Give the app a run and you should see this below:**

Putting it all together for a final app

Here is the complete code for the final version of the App and a summary of items which have changed:

**@objc****func**handleTap(**_**sender: UITapGestureRecognizer) - This function handles user input this way we can alter the game state. This is how the user will toggle between the view of the moon versus the view of the earth.**enum**GameStateType {..} - We define an enum data type to handle the different game states.**func**setupCameraCoordinates() - This function does the same thing as setupMoonCoordinates(). If we want to track the circular movement of the moon, we must defines a slight larger circle for our camera so that our camera will move in tandum with the moon.

Final Version of** GameViewController.swift**

```
// GameViewController.swift
// MoonOrbitTutorial
```**import** UIKit
**import** QuartzCore
**import** SceneKit
**class** GameViewController: UIViewController, SCNSceneRendererDelegate {
**var** sceneView: SCNView!
**var** scene: SCNScene!
**var** cameraNode: SCNNode!
**var** frameRate:TimeInterval = 0
**var** earthNode: SCNNode!
**var** moonNode: SCNNode!
**var** moonCoords = [SCNVector3]()
**var** cameraCoords = [SCNVector3]()
**var** degreeIndex:Int!
**var** moonExists:Bool = **false**
**var** newLocationCamera = SCNVector3(0.0,0.0,0.0)
// Game state
**enum** GameStateType {
**case** moonView
**case** earthView
}
**var** state = GameStateType.moonView
**override** **func** viewDidLoad() {
**super**.viewDidLoad()
setupScene()
setupCamera(xLoc: 0.0, yLoc: 8.0, zLoc:235.0, radius: 1.0)
degreeIndex = 1
setupMoonCoordinates()
setupCameraCoordinates()
createEarth(xLoc: 0.0, yLoc: 0.4, zLoc:0.0, radius: 3.66)
createMoon(xLoc: 0.0, yLoc: 0.4, zLoc:221.0, radius: 1.0)
}
**func** setupScene() {
sceneView = **self**.view **as**? SCNView
seneView.delegate = **self** **as** SCNSceneRendererDelegate
scene = SCNScene(named: "art.scnassets/MainScene.scn")
sceneView.scene = scene
**let** tap = UITapGestureRecognizer(target: **self**, action: **#****selector**(**self**.handleTap(**_**:)))
sceneView.addGestureRecognizer(tap)
view.isUserInteractionEnabled = **true**
}
**func** setupCamera(xLoc: Float, yLoc: Float, zLoc:Float, radius: Float) {
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: xLoc, y: yLoc, z: zLoc)
cameraNode.camera?.zFar = 500
cameraNode.eulerAngles = SCNVector3(x: (-20.0 * (Float.pi / 180)), y: 0.0, z: 0.0)
scene.rootNode.addChildNode(cameraNode)
}
**func** createEarth(xLoc: Float, yLoc: Float, zLoc:Float, radius: Float) {
// Define sphere
**var** geometry:SCNGeometry
geometry = SCNSphere(radius:CGFloat(radius))
earthNode = SCNNode(geometry: geometry)
// Cover sphere with earth.jpg
geometry.materials.first?.diffuse.contents = ("art.scnassets/earth.jpg")
// Assign earth position
earthNode.position = SCNVector3(x: xLoc, y: yLoc, z:zLoc)
// Add earth to scene
scene.rootNode.addChildNode(earthNode)
// Rotate earth along the y-axis
earthNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0.0 , y: 0.5, z: 0, duration: 15)))
}
**func** createMoon(xLoc: Float, yLoc: Float, zLoc:Float, radius: Float) {
// Define sphere
**var** geometry:SCNGeometry
geometry = SCNSphere(radius:CGFloat(radius))
moonNode = SCNNode(geometry: geometry)
// Cover sphere with moon.jpg
geometry.materials.first?.diffuse.contents = ("art.scnassets/moon.jpg")
// Assign moon position
moonNode.position = SCNVector3(x: xLoc, y: yLoc, z:zLoc)
// Add moon to scene
scene.rootNode.addChildNode(moonNode)
// Rotate moon along the y-axis
moonNode.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0.0 , y: 0.5, z: 0, duration: 15)))
moonExists = **true**
}
**func** setupMoonCoordinates() {
**let** radius = Float(221.0)
**for** degree **in** 1...360 {
// Convert degrees to radians with pi/180
**let** xCoord = radius * cos(Float(degree - 90) * Float.pi / 180)
**let** yCoord = -1.0 * (radius * sin(Float(degree - 90) * Float.pi / 180))
moonCoords.append( SCNVector3(x:Float(xCoord), y:Float(0.04), z:Float(yCoord)))
}
}
**func** setupCameraCoordinates() {
**let** radius = Float(223.0)
**for** degree **in** 1...360 {
// Convert degrees to radians with pi/180
**let** xCoord = radius * cos(Float(degree - 90) * Float.pi / 180)
**let** yCoord = (-1.0 * (radius * sin(Float(degree - 90) * Float.pi / 180)))
cameraCoords.append( SCNVector3(x:Float(xCoord), y:Float(1.5), z:Float(yCoord)))
}
}
**@objc** **func** handleTap(**_** sender: UITapGestureRecognizer) {
// Toggle GameState
**if** (state == GameStateType.moonView) {
state = GameStateType.earthView
**var** cameraMoveAction:SCNAction!
**let** camLocation = SCNVector3(0.0, 3.5, 9.0)
cameraMoveAction = SCNAction.move(to: camLocation, duration: 3.0)
cameraNode.runAction(cameraMoveAction)
cameraNode.eulerAngles = SCNVector3(x: (-20.0 * (Float.pi / 180)), y: 0.0, z: 0.0)
} **else** **if** (state == GameStateType.earthView){
state = GameStateType.moonView
cameraNode.position = newLocationCamera
cameraNode.eulerAngles = SCNVector3(x: (-20.0 * (Float.pi / 180)), y: moonNode.eulerAngles.y, z: 0.0)
}
}
**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
}
}
}
**extension** GameViewController {
**func** renderer(**_** renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
**if** (moonExists == **true**) {
**if** time > frameRate{
**if** (degreeIndex >= 360) {
degreeIndex = 1
}
**let** newLocationMoon = moonCoords[degreeIndex]
**var** cameraAction:SCNAction!
**var** cameraRotateAction:SCNAction!
newLocationCamera = cameraCoords[degreeIndex]
**if** (state == GameStateType.moonView) {
cameraAction = SCNAction.move(to: newLocationCamera, duration: 0.60)
cameraRotateAction = SCNAction.rotate(by: CGFloat(1.0 * (Float.pi / 180)), around: SCNVector3(x: 0.0 , y: 0.01, z: 0), duration: 0.50)
cameraNode.runAction(cameraRotateAction)
cameraNode.runAction(cameraAction)
}
**let** moonAction = SCNAction.move(to: newLocationMoon, duration: 0.60)
moonNode.runAction(moonAction)
frameRate = time + TimeInterval(Float(0.60))
degreeIndex+=1
}
}
}
}