-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEarthEntity.swift
More file actions
274 lines (230 loc) · 10.6 KB
/
EarthEntity.swift
File metadata and controls
274 lines (230 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
/*
See the LICENSE.txt file for this sample’s licensing information.
Abstract:
An entity that represents the Earth and all its moving parts.
*/
import RealityKit
import SwiftUI
import WorldAssets
/// An entity that represents the Earth and all its moving parts.
class EarthEntity: Entity {
// MARK: - Sub-entities
/// The model that draws the Earth's surface features.
private var earth: Entity = Entity()
/// An entity that rotates 23.5° to create axial tilt.
private let equatorialPlane = Entity()
/// An entity that provides a configurable rotation,
/// separate from the day/night cycle.
private let rotator = Entity()
/// A physical representation of the Earth's north and south poles.
private var pole: Entity = Entity()
/// The Earth's one natural satellite.
private var moon: SatelliteEntity = SatelliteEntity()
/// A container for artificial satellites.
private let satellites = Entity()
// MARK: - Internal state
/// Keep track of solar intensity and only update when it changes.
private var currentSunIntensity: Float? = nil
// MARK: - Initializers
/// Creates a new blank earth entity.
@MainActor required init() {
super.init()
}
/// Creates a new earth entity with the specified configuration.
///
/// - Parameters:
/// - configuration: Information about how to configure the Earth.
/// - satelliteConfiguration: An array of configuration structures, one
/// for each artificial satellite. The initializer creates one
/// satellite model for each element of the array. Pass an empty
/// array to avoid creating any artificial satellites.
/// - moonConfiguration: A satellite configuration structure that's
/// specifically for the Moon. Set to `nil` to avoid creating a
/// Moon entity.
init(
configuration: Configuration,
satelliteConfiguration: [SatelliteEntity.Configuration],
moonConfiguration: SatelliteEntity.Configuration?
) async {
super.init()
// Load the earth and pole models.
guard let earth = await WorldAssets.entity(named: configuration.isCloudy ? "Earth" : "Globe"),
let pole = await WorldAssets.entity(named: "Pole") else { return }
self.earth = earth
self.pole = pole
// Create satellites.
for configuration in satelliteConfiguration {
await satellites.addChild(SatelliteEntity(configuration))
}
// Attach to the Earth to a set of entities that enable axial
// tilt and a configured amount of rotation around the axis.
self.addChild(equatorialPlane)
equatorialPlane.addChild(rotator)
rotator.addChild(earth)
// Attach the pole to the Earth to ensure that it
// moves, tilts, rotates, and scales with the Earth.
earth.addChild(pole)
// The Moon's orbit isn't affected by the tilt of the Earth, so attach
// the Moon to the root entity.
moon = await SatelliteEntity(.orbitMoonDefault)
self.addChild(moon)
// The inclination of artificial satellite orbits is measured relative
// to the Earth's equator, so attach the satellite container to the
// equatorial plane entity.
equatorialPlane.addChild(satellites)
// Configure everything for the first time.
update(
configuration: configuration,
satelliteConfiguration: satelliteConfiguration,
moonConfiguration: moonConfiguration,
animateUpdates: false)
}
// MARK: - Updates
/// Updates all the entity's configurable elements.
///
/// - Parameters:
/// - configuration: Information about how to configure the Earth.
/// - satelliteConfiguration: An array of configuration structures, one
/// for each artificial satellite.
/// - moonConfiguration: A satellite configuration structure that's
/// specifically for the Moon.
/// - animateUpdates: A Boolean that indicates whether changes to certain
/// configuration values should be animated.
func update(
configuration: Configuration,
satelliteConfiguration: [SatelliteEntity.Configuration],
moonConfiguration: SatelliteEntity.Configuration?,
animateUpdates: Bool
) {
// Indicate the position of the sun for use in turning the ground
// lights on and off.
earth.sunPositionComponent = SunPositionComponent(Float(configuration.sunAngle.radians))
// Set a static rotation of the tilted Earth, driven from the configuration.
rotator.orientation = configuration.rotation
// Set the speed of the Earth's automatic rotation on it's axis.
if var rotation: RotationComponent = earth.components[RotationComponent.self] {
rotation.speed = configuration.currentSpeed
earth.components[RotationComponent.self] = rotation
} else {
earth.components.set(RotationComponent(speed: configuration.currentSpeed))
}
// Update the Moon.
moon.update(
configuration: moonConfiguration ?? .disabledMoon,
speed: configuration.currentSpeed,
traceAnchor: self)
// Update the artificial satellites.
for satellite in satellites.children {
guard let satelliteConfiguration = satelliteConfiguration.first(where: { $0.name == satellite.name }) else { continue }
(satellite as? SatelliteEntity)?.update(
configuration: satelliteConfiguration,
speed: configuration.currentSpeed,
traceAnchor: earth)
}
// Configure the poles.
pole.isEnabled = configuration.showPoles
pole.scale = [
configuration.poleThickness,
configuration.poleLength,
configuration.poleThickness]
// Set the sunlight, if corresponding controls have changed.
if configuration.currentSunIntensity != currentSunIntensity {
setSunlight(intensity: configuration.currentSunIntensity)
currentSunIntensity = configuration.currentSunIntensity
}
// Tilt the axis according to a date. For this to be meaningful,
// locate the sun along the positive x-axis. Animate this move for
// changes that the user makes when the globe appears in the volume.
var planeTransform = equatorialPlane.transform
planeTransform.rotation = tilt(date: configuration.date)
if animateUpdates {
equatorialPlane.move(to: planeTransform, relativeTo: self, duration: 0.25)
} else {
equatorialPlane.move(to: planeTransform, relativeTo: self)
}
// Scale and position the entire entity.
move(
to: Transform(
scale: SIMD3(repeating: configuration.scale),
rotation: orientation,
translation: configuration.position),
relativeTo: parent)
// Set an accessibility component on the entity.
components.set(makeAxComponent(
configuration: configuration,
satelliteConfiguration: satelliteConfiguration,
moonConfiguration: moonConfiguration))
}
/// Create an accessibility component suitable for the Earth entity.
///
/// - Parameters:
/// - configuration: Information about how to configure the Earth.
/// - satelliteConfiguration: An array of configuration structures, one
/// for each artificial satellite.
/// - moonConfiguration: A satellite configuration structure that's
/// specifically for the Moon.
/// - Returns: A new accessibility component.
private func makeAxComponent(
configuration: Configuration,
satelliteConfiguration: [SatelliteEntity.Configuration],
moonConfiguration: SatelliteEntity.Configuration?
) -> AccessibilityComponent {
// Create an accessibility component.
var axComponent = AccessibilityComponent()
axComponent.isAccessibilityElement = true
// Add a label.
axComponent.label = "Earth model"
// Add a value that describes the model's current state.
var axValue = configuration.currentSpeed != 0 ? "Rotating, " : "Not rotating, "
axValue.append(configuration.showSun ? "with the sun shining, " : "with the sun not shining, ")
if configuration.axDescribeTilt {
if let dateString = configuration.date?.formatted(.dateTime.day().month(.wide)) {
axValue.append("and tilted for the date \(dateString)")
} else {
axValue.append("and no tilt")
}
}
if configuration.showPoles {
axValue.append("with the poles indicated, ")
}
for item in satelliteConfiguration.map({ $0.name }) {
axValue.append("a \(item) orbits close to the earth, ")
}
if moonConfiguration != nil {
axValue.append("the moon orbits at some distance from the earth.")
}
axComponent.value = LocalizedStringResource(stringLiteral: axValue)
// Add custom accessibility actions, if applicable.
if !configuration.axActions.isEmpty {
axComponent.customActions.append(contentsOf: configuration.axActions)
}
return axComponent
}
/// Calculates the orientation of the Earth's tilt on a specified date.
///
/// This method assumes the sun appears at some distance from the Earth
/// along the negative x-axis.
///
/// - Parameter date: The date that the Earth's tilt represents.
///
/// - Returns: A representation of tilt that you apply to an Earth model.
private func tilt(date: Date?) -> simd_quatf {
// Assume a constant magnitude for the Earth's tilt angle.
let tiltAngle: Angle = .degrees(date == nil ? 0 : 23.5)
// Find the day in the year corresponding to the date.
let calendar = Calendar.autoupdatingCurrent
let day = calendar.ordinality(of: .day, in: .year, for: date ?? Date()) ?? 1
// Get an axis angle corresponding to the day of the year, assuming
// the sun appears in the negative x direction.
let axisAngle: Float = (Float(day) / 365.0) * 2.0 * .pi
// Create an axis that points the northern hemisphere toward the
// sun along the positive x-axis when axisAngle is zero.
let tiltAxis: SIMD3<Float> = [
sin(axisAngle),
0,
-cos(axisAngle)
]
// Create and return a tilt orientation from the angle and axis.
return simd_quatf(angle: Float(tiltAngle.radians), axis: tiltAxis)
}
}