v3: Globes and Vision Pro !39
23 files changed, +266 −10
Layout: unified · split
App/Globes/callisto.jpg added
Binary file not shown.
App/Globes/earth.jpg added
Binary file not shown.
App/Globes/enceladus.jpg added
Binary file not shown.
App/Globes/europa.jpg added
Binary file not shown.
App/Globes/ganymede.jpg added
Binary file not shown.
App/Globes/io.jpg added
Binary file not shown.
App/Globes/jupiter.jpg added
Binary file not shown.
App/Globes/mars.jpg added
Binary file not shown.
App/Globes/mercury.jpg added
Binary file not shown.
App/Globes/moon.jpg added
Binary file not shown.
App/Globes/pluto.jpg added
Binary file not shown.
App/Globes/titan.jpg added
Binary file not shown.
App/Globes/venus.jpg added
Binary file not shown.
App/Sources/ColorHex.swift added +8
| @@ -0,0 +1,8 @@ | |||
| 1 | import SwiftUI | ||
| 2 | |||
| 3 | extension Color { | ||
| 4 | init?(hex: String?) { | ||
| 5 | guard let hex, hex.hasPrefix("#"), hex.count == 7, let v = UInt32(hex.dropFirst(), radix: 16) else { return nil } | ||
| 6 | self.init(red: Double(v >> 16 & 0xFF) / 255, green: Double(v >> 8 & 0xFF) / 255, blue: Double(v & 0xFF) / 255) | ||
| 7 | } | ||
| 8 | } | ||
App/Sources/GlobeMaps.swift added +19
| @@ -0,0 +1,19 @@ | |||
| 1 | /// Credits for the global maps in Globes/, by Library page id. All are public domain (NASA, USGS); | ||
| 2 | /// sources and dates are in THIRD_PARTY.org. | ||
| 3 | enum GlobeMaps { | ||
| 4 | static let credit: [String: String] = [ | ||
| 5 | "mercury": "NASA/JHU APL/Carnegie Institution of Washington (MESSENGER, PIA15063)", | ||
| 6 | "venus": "NASA/JPL/USGS (Magellan radar topography, colourised)", | ||
| 7 | "earth": "NASA Earth Observatory (Blue Marble Next Generation, December)", | ||
| 8 | "moon": "NASA's Scientific Visualization Studio (LRO LROC and LOLA)", | ||
| 9 | "mars": "NASA/JPL/USGS (Viking MDIM 2.1 colour mosaic)", | ||
| 10 | "jupiter": "NASA/JPL/Space Science Institute (Cassini, PIA07782)", | ||
| 11 | "pluto": "NASA/JHU APL/SwRI; USGS Astrogeology (New Horizons)", | ||
| 12 | "io": "NASA/JPL/USGS (Galileo SSI colour merge)", | ||
| 13 | "europa": "NASA/JPL/USGS (Voyager and Galileo SSI)", | ||
| 14 | "ganymede": "NASA/JPL/USGS (Voyager and Galileo SSI)", | ||
| 15 | "callisto": "NASA/JPL/USGS (Voyager and Galileo SSI)", | ||
| 16 | "titan": "NASA/JPL-Caltech/University of Arizona (Cassini VIMS, near-infrared, PIA22770)", | ||
| 17 | "enceladus": "NASA/JPL-Caltech/SSI; mosaic by P. Schenk and W. McKinnon, LPI/USRA", | ||
| 18 | ] | ||
| 19 | } | ||
App/Sources/GlobeView.swift added +89
| @@ -0,0 +1,89 @@ | |||
| 1 | import Library | ||
| 2 | import RealityKit | ||
| 3 | import SwiftUI | ||
| 4 | |||
| 5 | /// A Library body as a sphere you can turn with a drag: a global map from the app bundle | ||
| 6 | /// (Globes/<page id>.jpg) when there is one, otherwise the page's colour. Tilted by its axial tilt. | ||
| 7 | struct GlobeView: View { | ||
| 8 | var page: LibraryPage | ||
| 9 | var credit: String? | ||
| 10 | @State private var yaw: Float = 0 | ||
| 11 | @State private var pitch: Float = 0 | ||
| 12 | @State private var dragStart: (Float, Float)? | ||
| 13 | |||
| 14 | /// Pitch from the drag, then the axial tilt, then the spin. | ||
| 15 | private var orientation: simd_quatf { | ||
| 16 | let degrees: Double = page.drawing?.axialTiltDeg ?? 0 | ||
| 17 | let tilt = simd_quatf(angle: Float(degrees * .pi / 180), axis: SIMD3<Float>(0, 0, 1)) | ||
| 18 | let spin = simd_quatf(angle: yaw, axis: SIMD3<Float>(0, 1, 0)) | ||
| 19 | return simd_quatf(angle: pitch, axis: SIMD3<Float>(1, 0, 0)) * tilt * spin | ||
| 20 | } | ||
| 21 | |||
| 22 | static func mapURL(_ id: String) -> URL? { Bundle.main.url(forResource: id, withExtension: "jpg", subdirectory: "Globes") } | ||
| 23 | |||
| 24 | var body: some View { | ||
| 25 | VStack(spacing: 12) { | ||
| 26 | RealityView { content in | ||
| 27 | let globe = await Self.makeGlobe(page) | ||
| 28 | #if os(visionOS) | ||
| 29 | // A 30 cm globe in the window; the room's lighting shades it. | ||
| 30 | globe.scale = .init(repeating: 0.3) | ||
| 31 | content.add(globe) | ||
| 32 | #else | ||
| 33 | content.camera = .virtual | ||
| 34 | content.add(globe) | ||
| 35 | |||
| 36 | let camera = PerspectiveCamera() | ||
| 37 | camera.position = [0, 0, 1.9] | ||
| 38 | content.add(camera) | ||
| 39 | |||
| 40 | let sun = DirectionalLight() | ||
| 41 | sun.light.intensity = 4000 | ||
| 42 | sun.look(at: .zero, from: [-1.5, 0.3, 0.5], relativeTo: nil) | ||
| 43 | content.add(sun) | ||
| 44 | #endif | ||
| 45 | } update: { content in | ||
| 46 | guard let globe = content.entities.first(where: { $0.name == "globe" }) else { return } | ||
| 47 | globe.orientation = orientation | ||
| 48 | } | ||
| 49 | .gesture(DragGesture() | ||
| 50 | .onChanged { g in | ||
| 51 | if dragStart == nil { dragStart = (yaw, pitch) } | ||
| 52 | yaw = dragStart!.0 + Float(g.translation.width) * 0.01 | ||
| 53 | pitch = min(max(dragStart!.1 + Float(g.translation.height) * 0.01, -1.2), 1.2) | ||
| 54 | } | ||
| 55 | .onEnded { _ in dragStart = nil }) | ||
| 56 | .accessibilityLabel("\(page.title) as a globe. Drag to turn it.") | ||
| 57 | Text(Self.mapURL(page.id) == nil | ||
| 58 | ? "No global map yet; shown in its average colour. Drag to turn." | ||
| 59 | : "Drag to turn. Map: \(credit ?? "NASA").") | ||
| 60 | .font(.caption2).foregroundStyle(.secondary) | ||
| 61 | .padding(.horizontal) | ||
| 62 | } | ||
| 63 | .background(Color.black.ignoresSafeArea()) | ||
| 64 | .navigationTitle(page.title) | ||
| 65 | .navigationBarTitleDisplayMode(.inline) | ||
| 66 | } | ||
| 67 | |||
| 68 | @MainActor | ||
| 69 | static func makeGlobe(_ page: LibraryPage) async -> Entity { | ||
| 70 | var material = PhysicallyBasedMaterial() | ||
| 71 | material.roughness = .init(floatLiteral: 1) | ||
| 72 | material.metallic = .init(floatLiteral: 0) | ||
| 73 | if let url = mapURL(page.id), let texture = try? await TextureResource(contentsOf: url) { | ||
| 74 | material.baseColor = .init(texture: .init(texture)) | ||
| 75 | } else { | ||
| 76 | material.baseColor = .init(tint: UIColor(Color(hex: page.drawing?.color ?? "") ?? .gray)) | ||
| 77 | } | ||
| 78 | if page.id == "sun" { | ||
| 79 | var glow = UnlitMaterial(color: UIColor(Color(hex: page.drawing?.color ?? "") ?? .yellow)) | ||
| 80 | if let url = mapURL(page.id), let texture = try? await TextureResource(contentsOf: url) { glow.color = .init(texture: .init(texture)) } | ||
| 81 | let sun = ModelEntity(mesh: .generateSphere(radius: 0.5), materials: [glow]) | ||
| 82 | sun.name = "globe" | ||
| 83 | return sun | ||
| 84 | } | ||
| 85 | let globe = ModelEntity(mesh: .generateSphere(radius: 0.5), materials: [material]) | ||
| 86 | globe.name = "globe" | ||
| 87 | return globe | ||
| 88 | } | ||
| 89 | } | ||
App/Sources/LibraryScreen.swift +6
| @@ -104,6 +104,12 @@ struct LibraryPageView: View { | |||
| 104 | } | 104 | } |
| 105 | .padding(.top, 10) | 105 | .padding(.top, 10) |
| 106 | } | 106 | } |
| 107 | if page.drawing?.kind == "planet" { | ||
| 108 | NavigationLink { GlobeView(page: page, credit: GlobeMaps.credit[page.id]) } label: { | ||
| 109 | Label("View as a globe", systemImage: "globe").font(.subheadline) | ||
| 110 | } | ||
| 111 | .padding(.top, 10) | ||
| 112 | } | ||
| 107 | 113 | ||
| 108 | facts.padding(.top, 16) | 114 | facts.padding(.top, 16) |
| 109 | 115 | ||
App/Sources/PageDrawing.swift −7
| @@ -228,10 +228,3 @@ struct PageDrawing: View { | |||
| 228 | } | 228 | } |
| 229 | } | 229 | } |
| 230 | } | 230 | } |
| 231 | |||
| 232 | extension Color { | ||
| 233 | init?(hex: String?) { | ||
| 234 | guard let hex, hex.hasPrefix("#"), hex.count == 7, let v = UInt32(hex.dropFirst(), radix: 16) else { return nil } | ||
| 235 | self.init(red: Double(v >> 16 & 0xFF) / 255, green: Double(v >> 8 & 0xFF) / 255, blue: Double(v & 0xFF) / 255) | ||
| 236 | } | ||
| 237 | } | ||
App/Vision/MoonriseVisionApp.swift added +103
| @@ -0,0 +1,103 @@ | |||
| 1 | import Library | ||
| 2 | import RealityKit | ||
| 3 | import SkyEngine | ||
| 4 | import SkyRenderer | ||
| 5 | import SwiftUI | ||
| 6 | |||
| 7 | @main | ||
| 8 | struct MoonriseVisionApp: App { | ||
| 9 | var body: some SwiftUI.Scene { | ||
| 10 | WindowGroup { VisionHome() } | ||
| 11 | |||
| 12 | WindowGroup(id: "orrery") { OrreryVolume() } | ||
| 13 | .windowStyle(.volumetric) | ||
| 14 | .defaultSize(width: 0.8, height: 0.4, depth: 0.8, in: .meters) | ||
| 15 | |||
| 16 | ImmersiveSpace(id: "sky") { ImmersiveSky() } | ||
| 17 | } | ||
| 18 | } | ||
| 19 | |||
| 20 | /// Library globes, plus buttons for the orrery volume and the immersive sky. | ||
| 21 | struct VisionHome: View { | ||
| 22 | @Environment(\.openWindow) private var openWindow | ||
| 23 | @Environment(\.openImmersiveSpace) private var openImmersiveSpace | ||
| 24 | @Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace | ||
| 25 | @State private var immersed = false | ||
| 26 | |||
| 27 | private var bodies: [LibraryPage] { | ||
| 28 | [LibrarySection.sun, .planets, .dwarfPlanets, .moons].flatMap { Library.pages(in: $0) } | ||
| 29 | } | ||
| 30 | |||
| 31 | var body: some View { | ||
| 32 | NavigationSplitView { | ||
| 33 | List(bodies) { page in | ||
| 34 | NavigationLink(page.title) { GlobeView(page: page, credit: GlobeMaps.credit[page.id]) } | ||
| 35 | } | ||
| 36 | .navigationTitle("Moonrise") | ||
| 37 | .toolbar { | ||
| 38 | ToolbarItem(placement: .bottomOrnament) { | ||
| 39 | HStack { | ||
| 40 | Button("Orrery") { openWindow(id: "orrery") } | ||
| 41 | Button(immersed ? "Leave the sky" : "Tonight's sky") { | ||
| 42 | Task { | ||
| 43 | if immersed { await dismissImmersiveSpace() } else { _ = await openImmersiveSpace(id: "sky") } | ||
| 44 | immersed.toggle() | ||
| 45 | } | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | } | ||
| 50 | } detail: { | ||
| 51 | Text("Choose a body to see it as a globe.").foregroundStyle(.secondary) | ||
| 52 | } | ||
| 53 | } | ||
| 54 | } | ||
| 55 | |||
| 56 | /// Outside the view, where `Body` is SkyEngine's rather than the view's body type. | ||
| 57 | private let orreryPlanets = Body.planets | ||
| 58 | |||
| 59 | /// The Sun and planets at today's heliocentric positions. Distances are square-rooted so Neptune fits. | ||
| 60 | struct OrreryVolume: View { | ||
| 61 | var body: some View { | ||
| 62 | RealityView { content in | ||
| 63 | let sun = ModelEntity(mesh: .generateSphere(radius: 0.025), materials: [UnlitMaterial(color: .yellow)]) | ||
| 64 | content.add(sun) | ||
| 65 | for body in orreryPlanets { | ||
| 66 | guard let p = try? SkyEngine.heliocentricEcliptic(body, at: .now) else { continue } | ||
| 67 | let au = (p * p).sum().squareRoot() | ||
| 68 | let scale = 0.065 * au.squareRoot() / au | ||
| 69 | let page = Library.page(body: body.rawValue) | ||
| 70 | let colour = UIColor(Color(hex: page?.drawing?.color) ?? .white) | ||
| 71 | let planet = ModelEntity(mesh: .generateSphere(radius: 0.008), materials: [SimpleMaterial(color: colour, isMetallic: false)]) | ||
| 72 | // Ecliptic x, y in the horizontal plane; ecliptic north up. | ||
| 73 | planet.position = [Float(p.x * scale), Float(p.z * scale), Float(-p.y * scale)] | ||
| 74 | content.add(planet) | ||
| 75 | } | ||
| 76 | } | ||
| 77 | } | ||
| 78 | } | ||
| 79 | |||
| 80 | /// Stars brighter than magnitude 4.5 on a 20 m sphere, placed for now from Chicago, | ||
| 81 | /// with the horizon as a faint disc. | ||
| 82 | struct ImmersiveSky: View { | ||
| 83 | var body: some View { | ||
| 84 | RealityView { content in | ||
| 85 | let observer = Observer(latitude: 41.8781, longitude: -87.6298) | ||
| 86 | let stars = Catalog.stars.filter { $0.magnitude < 4.5 } | ||
| 87 | let positions = SkyEngine.horizontal(j2000: stars.map { $0.position(at: .now) }, at: .now, from: observer) | ||
| 88 | let mesh = MeshResource.generateSphere(radius: 0.04) | ||
| 89 | for (star, h) in zip(stars, positions) where h.altitude > -2 { | ||
| 90 | let alt = Float(h.altitude * .pi / 180), az = Float(h.azimuth * .pi / 180) | ||
| 91 | let r: Float = 20 | ||
| 92 | let s = ModelEntity(mesh: mesh, materials: [UnlitMaterial(color: .white)]) | ||
| 93 | s.scale = .init(repeating: max(0.3, Float(2.2 - 0.4 * star.magnitude))) | ||
| 94 | // Azimuth from north (−z) towards east (+x). | ||
| 95 | s.position = [r * cos(alt) * sin(az), r * sin(alt), -r * cos(alt) * cos(az)] | ||
| 96 | content.add(s) | ||
| 97 | } | ||
| 98 | let ground = ModelEntity(mesh: .generatePlane(width: 40, depth: 40), materials: [UnlitMaterial(color: UIColor(white: 0.05, alpha: 0.9))]) | ||
| 99 | ground.position.y = -0.01 | ||
| 100 | content.add(ground) | ||
| 101 | } | ||
| 102 | } | ||
| 103 | } | ||
App/project.yml +34 −1
| @@ -4,6 +4,7 @@ options: | |||
| 4 | deploymentTarget: | 4 | deploymentTarget: |
| 5 | iOS: "26.0" | 5 | iOS: "26.0" |
| 6 | watchOS: "26.0" | 6 | watchOS: "26.0" |
| 7 | visionOS: "26.0" | ||
| 7 | packages: | 8 | packages: |
| 8 | MoonriseKit: | 9 | MoonriseKit: |
| 9 | path: ../Packages/MoonriseKit | 10 | path: ../Packages/MoonriseKit |
| @@ -13,7 +14,11 @@ targets: | |||
| 13 | platform: iOS | 14 | platform: iOS |
| 14 | scheme: | 15 | scheme: |
| 15 | testTargets: [MoonriseUITests] | 16 | testTargets: [MoonriseUITests] |
| 16 | sources: [Sources, Shared] | 17 | sources: |
| 18 | - Sources | ||
| 19 | - Shared | ||
| 20 | - path: Globes | ||
| 21 | type: folder | ||
| 17 | dependencies: | 22 | dependencies: |
| 18 | - package: MoonriseKit | 23 | - package: MoonriseKit |
| 19 | product: SkyEngine | 24 | product: SkyEngine |
| @@ -141,6 +146,34 @@ targets: | |||
| 141 | SWIFT_VERSION: "6.0" | 146 | SWIFT_VERSION: "6.0" |
| 142 | MARKETING_VERSION: "1.0" | 147 | MARKETING_VERSION: "1.0" |
| 143 | CURRENT_PROJECT_VERSION: "1" | 148 | CURRENT_PROJECT_VERSION: "1" |
| 149 | MoonriseVision: | ||
| 150 | type: application | ||
| 151 | platform: visionOS | ||
| 152 | sources: | ||
| 153 | - Vision | ||
| 154 | - Sources/GlobeView.swift | ||
| 155 | - Sources/GlobeMaps.swift | ||
| 156 | - Sources/ColorHex.swift | ||
| 157 | - path: Globes | ||
| 158 | type: folder | ||
| 159 | dependencies: | ||
| 160 | - package: MoonriseKit | ||
| 161 | product: SkyEngine | ||
| 162 | - package: MoonriseKit | ||
| 163 | product: SkyRenderer | ||
| 164 | - package: MoonriseKit | ||
| 165 | product: Library | ||
| 166 | settings: | ||
| 167 | base: | ||
| 168 | PRODUCT_BUNDLE_IDENTIFIER: sh.krz.moonrise.vision | ||
| 169 | DEVELOPMENT_TEAM: ZCNAX3VL9D | ||
| 170 | CODE_SIGN_STYLE: Automatic | ||
| 171 | SWIFT_VERSION: "6.0" | ||
| 172 | MARKETING_VERSION: "1.0" | ||
| 173 | CURRENT_PROJECT_VERSION: "1" | ||
| 174 | GENERATE_INFOPLIST_FILE: YES | ||
| 175 | INFOPLIST_KEY_CFBundleDisplayName: Moonrise | ||
| 176 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation: YES | ||
| 144 | MoonriseUITests: | 177 | MoonriseUITests: |
| 145 | type: bundle.ui-testing | 178 | type: bundle.ui-testing |
| 146 | platform: iOS | 179 | platform: iOS |
PLAN.org +5 −1
| @@ -413,7 +413,11 @@ Sky, System, Sun, Launches, Library core (above). | |||
| 413 | to Stellarium). | 413 | to Stellarium). |
| 414 | - Historical sky for any date and place, with precession. Done: 3000 BCE | 414 | - Historical sky for any date and place, with precession. Done: 3000 BCE |
| 415 | to 3000 CE, with BSC5 proper motions and the pole star of the year. | 415 | to 3000 CE, with BSC5 proper motions and the pole star of the year. |
| 416 | - 3D planets from USGS maps; immersive sky and orrery on Vision Pro. | 416 | - 3D planets from USGS maps; immersive sky and orrery on Vision Pro. Done: |
| 417 | globes for the Sun, planets, dwarf planets and moons (NASA/USGS maps for 13, | ||
| 418 | colour for the rest); visionOS app with globes, an orrery volume and an | ||
| 419 | immersive star field. visionOS is compiled but not yet run (no simulator | ||
| 420 | runtime installed). | ||
| 417 | - Guided seasonal sky tours and mission stories. Tours done: four, by | 421 | - Guided seasonal sky tours and mission stories. Tours done: four, by |
| 418 | months, 5 to 8 stops each, in Library/Resources/tours. Mission stories | 422 | months, 5 to 8 stops each, in Library/Resources/tours. Mission stories |
| 419 | done for Voyager 1 and 2, Pioneer 10 and 11 and New Horizons, on the orrery. | 423 | done for Voyager 1 and 2, Pioneer 10 and 11 and New Horizons, on the orrery. |
Packages/MoonriseKit/Package.swift +1 −1
| @@ -3,7 +3,7 @@ import PackageDescription | |||
| 3 | 3 | ||
| 4 | let package = Package( | 4 | let package = Package( |
| 5 | name: "MoonriseKit", | 5 | name: "MoonriseKit", |
| 6 | platforms: [.iOS(.v26), .macOS(.v26), .watchOS(.v26)], | 6 | platforms: [.iOS(.v26), .macOS(.v26), .watchOS(.v26), .visionOS(.v26)], |
| 7 | products: [ | 7 | products: [ |
| 8 | .library(name: "SkyEngine", targets: ["SkyEngine"]), | 8 | .library(name: "SkyEngine", targets: ["SkyEngine"]), |
| 9 | .library(name: "SkyRenderer", targets: ["SkyRenderer"]), | 9 | .library(name: "SkyRenderer", targets: ["SkyRenderer"]), |
THIRD_PARTY.org +1
| @@ -7,6 +7,7 @@ | |||
| 7 | | BSC5 proper motions (pmRA, pmDE) | same file, columns 6 and 7 | as above | https://cdsarc.cds.unistra.fr/viz-bin/cat/V/50 | | 7 | | BSC5 proper motions (pmRA, pmDE) | same file, columns 6 and 7 | as above | https://cdsarc.cds.unistra.fr/viz-bin/cat/V/50 | |
| 8 | | Stellarium sky cultures: Chinese, Indian, Mongolian, Japanese moon stations, Hawaiian starlines, Māori, Tongan, Boorong, Sámi, Norse (lines and names, converted to HR numbers by =scripts/make-skycultures.py=) | =Packages/MoonriseKit/Sources/SkyRenderer/Resources/skycultures.json= | CC BY-SA (per culture, stated in each culture's description.md); the converted file is under the same licence | https://github.com/Stellarium/stellarium-skycultures | | 8 | | Stellarium sky cultures: Chinese, Indian, Mongolian, Japanese moon stations, Hawaiian starlines, Māori, Tongan, Boorong, Sámi, Norse (lines and names, converted to HR numbers by =scripts/make-skycultures.py=) | =Packages/MoonriseKit/Sources/SkyRenderer/Resources/skycultures.json= | CC BY-SA (per culture, stated in each culture's description.md); the converted file is under the same licence | https://github.com/Stellarium/stellarium-skycultures | |
| 9 | | Hipparcos main catalogue positions (used only to match HIP to HR when building skycultures.json; not bundled) | — | ESA | https://cdsarc.cds.unistra.fr/viz-bin/cat/I/239 | | 9 | | Hipparcos main catalogue positions (used only to match HIP to HR when building skycultures.json; not bundled) | — | ESA | https://cdsarc.cds.unistra.fr/viz-bin/cat/I/239 | |
| 10 | | Global maps for globes (Mercury PIA15063; Venus Magellan C3-MDIR colourised topography; Earth Blue Marble Next Generation, December 2004; Moon SVS 4720 LROC colour 2K; Mars Viking MDIM 2.1 colour; Jupiter PIA07782; Pluto New Horizons global mosaic; Io, Europa, Ganymede, Callisto USGS Galileo/Voyager mosaics; Titan PIA22770; Enceladus Schenk 2024), downscaled to at most 2048 px | =App/Globes/*.jpg= | Public domain (NASA, USGS); credit per map in =App/Sources/GlobeMaps.swift= | https://science.nasa.gov/photojournal, https://svs.gsfc.nasa.gov/4720/, https://astrogeology.usgs.gov | | ||
| 10 | | ConstellationLines v1.4, Copyright (c) 2005-2026 Marc van der Sluys | =Packages/MoonriseKit/Sources/SkyRenderer/Resources/constellations.json= | CC BY 4.0 | https://github.com/MarcvdSluys/ConstellationLines, doi:10.5281/zenodo.10397192 | | 11 | | ConstellationLines v1.4, Copyright (c) 2005-2026 Marc van der Sluys | =Packages/MoonriseKit/Sources/SkyRenderer/Resources/constellations.json= | CC BY 4.0 | https://github.com/MarcvdSluys/ConstellationLines, doi:10.5281/zenodo.10397192 | |
| 11 | | Natural Earth 1:110m coastline v5.1.2 | =Packages/MoonriseKit/Sources/SkyRenderer/Resources/coastline.json= | Public domain | https://www.naturalearthdata.com | | 12 | | Natural Earth 1:110m coastline v5.1.2 | =Packages/MoonriseKit/Sources/SkyRenderer/Resources/coastline.json= | Public domain | https://www.naturalearthdata.com | |
| 12 | | Launch Library 2 (The Space Devs) | fetched at runtime by =server/= | Free API; credit shown in the app | https://thespacedevs.com/llapi | | 13 | | Launch Library 2 (The Space Devs) | fetched at runtime by =server/= | Free API; credit shown in the app | https://thespacedevs.com/llapi | |