|
| 1 | +// Copyright 2026 Esri |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +import ArcGIS |
| 16 | +import ArcGISToolkit |
| 17 | +import ARCore |
| 18 | +import ARCoreGeospatial |
| 19 | +import ARKit |
| 20 | +import RealityKit |
| 21 | + |
| 22 | +/// A world-tracking provider backed by the Google ARCore SDK. |
| 23 | +final class CustomWorldTracking { |
| 24 | + @MainActor let arSessionProvider = ARSessionProvider<ARView>() |
| 25 | + /// The ARCore session that produces geospatial and streetscape updates. |
| 26 | + let garSession: GARSession |
| 27 | + /// The root world origin anchor for all generated streetscape content. |
| 28 | + @MainActor let worldOrigin = AnchorEntity(world: matrix_identity_float4x4) |
| 29 | + /// A cache of generated streetscape models keyed by ARCore geometry |
| 30 | + /// identifier. |
| 31 | + var streetscapeGeometryModels: [UUID: ModelEntity] = [:] |
| 32 | + |
| 33 | + /// Creates an instance with the given API key and bundle identifier. |
| 34 | + /// - Parameters: |
| 35 | + /// - apiKey: An API key for Google Cloud Services. |
| 36 | + /// - bundleIdentifier: The bundle identifier associated to the API key. |
| 37 | + /// If `nil`, defaults to `Bundle.main.bundleIdentifier`. |
| 38 | + @MainActor |
| 39 | + init(apiKey: String, bundleIdentifier: String? = nil) throws { |
| 40 | + garSession = try GARSession(apiKey: apiKey, bundleIdentifier: bundleIdentifier) |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +extension CustomWorldTracking: WorldTrackingProvider { |
| 45 | + typealias CameraFeedView = CustomWorldTrackingCameraFeedView |
| 46 | + |
| 47 | + var arConfiguration: ARConfiguration { |
| 48 | + let configuration = ARWorldTrackingConfiguration() |
| 49 | + configuration.worldAlignment = .gravity |
| 50 | + configuration.planeDetection = [.horizontal, .vertical] |
| 51 | + return configuration |
| 52 | + } |
| 53 | + |
| 54 | + func start() async { |
| 55 | + if TransformationCatalog.projectionEngineDirectoryURL == nil { |
| 56 | + // Set projection data for ellipsoidal projection. |
| 57 | + setProjectionEngineDirectoryURL() |
| 58 | + } |
| 59 | + |
| 60 | + do { |
| 61 | + let session = CLServiceSession(authorization: .whenInUse) |
| 62 | + for try await diagnostic in session.diagnostics { |
| 63 | + if !diagnostic.authorizationRequestInProgress { |
| 64 | + // A denial occurred. |
| 65 | + break |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + runARSession() |
| 70 | + arSessionProvider.scene.addAnchor(worldOrigin) |
| 71 | + setupGARSession() |
| 72 | + } catch { |
| 73 | + // Do nothing. |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + func stop() { |
| 78 | + pauseARSession() |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +extension CustomWorldTracking { |
| 83 | + /// Configures the ARCore session. |
| 84 | + private func setupGARSession() { |
| 85 | + guard garSession.isGeospatialModeSupported(.enabled) else { return } |
| 86 | + |
| 87 | + let configuration = GARSessionConfiguration() |
| 88 | + configuration.geospatialMode = .enabled |
| 89 | + configuration.streetscapeGeometryMode = .enabled |
| 90 | + var error: NSError? |
| 91 | + garSession.setConfiguration(configuration, error: &error) |
| 92 | + } |
| 93 | +} |
| 94 | + |
| 95 | +private extension CustomWorldTracking { |
| 96 | + /// Sets the ArcGIS projection engine directory from bundled projection data |
| 97 | + /// when available. |
| 98 | + func setProjectionEngineDirectoryURL() { |
| 99 | + if let pedataURL = Bundle.main.url(forResource: "pedata", withExtension: nil) { |
| 100 | + do { |
| 101 | + print("Setting Projection Engine Directory: \(pedataURL)") |
| 102 | + try TransformationCatalog.setProjectionEngineDirectoryURL(pedataURL) |
| 103 | + } catch { |
| 104 | + print("Error setting Projection Engine Directory: \(error)") |
| 105 | + } |
| 106 | + } else if let egm96URL = Bundle.main.url(forResource: "egm96", withExtension: "grd") { |
| 107 | + let pedataURL = egm96URL.deletingLastPathComponent() |
| 108 | + do { |
| 109 | + try TransformationCatalog.setProjectionEngineDirectoryURL(pedataURL) |
| 110 | + } catch { |
| 111 | + print("Error setting Projection Engine Directory: \(error)") |
| 112 | + } |
| 113 | + } else { |
| 114 | + print("Note: PE data not found - using built-in transformations") |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | + |
| 119 | +extension CustomWorldTracking { |
| 120 | + /// Builds a `ModelEntity` representation of ARCore streetscape geometry. |
| 121 | + /// - Parameter geometry: The ARCore streetscape geometry mesh and type |
| 122 | + /// data. |
| 123 | + /// - Returns: A model with filled and wireframe rendering, or `nil` if mesh |
| 124 | + /// creation fails. |
| 125 | + @MainActor |
| 126 | + static func makeStreetscapeGeometryModel(geometry: GARStreetscapeGeometry) -> ModelEntity? { |
| 127 | + var descriptor = MeshDescriptor() |
| 128 | + |
| 129 | + let garMesh = geometry.mesh |
| 130 | + let vertices = UnsafeBufferPointer(start: garMesh.vertices, count: Int(garMesh.vertexCount)) |
| 131 | + .map { vertex in |
| 132 | + simd_float3(x: vertex.x, y: vertex.y, z: vertex.z) |
| 133 | + } |
| 134 | + descriptor.positions = MeshBuffers.Positions(vertices) |
| 135 | + |
| 136 | + let triangleIndices = UnsafeBufferPointer(start: garMesh.triangles, count: Int(garMesh.triangleCount)) |
| 137 | + .flatMap { triangle in |
| 138 | + [triangle.indices.0, triangle.indices.1, triangle.indices.2] |
| 139 | + } |
| 140 | + descriptor.primitives = .triangles(triangleIndices) |
| 141 | + |
| 142 | + guard let mesh = try? MeshResource.generate(from: [descriptor]) else { return nil } |
| 143 | + let material = if geometry.type == .terrain { |
| 144 | + terrainMaterial() |
| 145 | + } else { |
| 146 | + randomBuildingMaterial() |
| 147 | + } |
| 148 | + let model = ModelEntity(mesh: mesh, materials: [material]) |
| 149 | + |
| 150 | + var linesMaterial = UnlitMaterial(color: .black) |
| 151 | + linesMaterial.triangleFillMode = .lines |
| 152 | + model.addChild(ModelEntity(mesh: mesh, materials: [linesMaterial])) |
| 153 | + |
| 154 | + return model |
| 155 | + } |
| 156 | + |
| 157 | + /// Creates a semi-transparent material for non-terrain streetscape |
| 158 | + /// geometry. |
| 159 | + /// - Returns: A randomly selected unlit building material. |
| 160 | + static func randomBuildingMaterial() -> UnlitMaterial { |
| 161 | + let colors = [ |
| 162 | + UIColor(red: 0.7, green: 0, blue: 0.7, alpha: 0.8), |
| 163 | + UIColor(red: 0.7, green: 0.7, blue: 0, alpha: 0.8), |
| 164 | + UIColor(red: 0, green: 0.7, blue: 0.7, alpha: 0.8) |
| 165 | + ] |
| 166 | + var material = UnlitMaterial(color: colors.randomElement()!) |
| 167 | + material.blending = .transparent(opacity: 0.8) |
| 168 | + return material |
| 169 | + } |
| 170 | + |
| 171 | + /// Creates a semi-transparent material for terrain geometry. |
| 172 | + /// - Returns: An unlit green terrain material. |
| 173 | + static func terrainMaterial() -> UnlitMaterial { |
| 174 | + var material = UnlitMaterial(color: UIColor(red: 0, green: 0.5, blue: 0, alpha: 0.7)) |
| 175 | + material.blending = .transparent(opacity: 0.9) |
| 176 | + return material |
| 177 | + } |
| 178 | + |
| 179 | + /// Removes all currently rendered streetscape models from the scene and |
| 180 | + /// cache. |
| 181 | + @MainActor |
| 182 | + func removeStreetscapeGeometry() { |
| 183 | + for model in streetscapeGeometryModels.values { |
| 184 | + model.removeFromParent() |
| 185 | + } |
| 186 | + streetscapeGeometryModels.removeAll() |
| 187 | + } |
| 188 | +} |
0 commit comments