Machineboy空

[AR Navigation 개발일지#1] ARView의 물체의 위치를 실제 위도, 경도 좌표로 변환해 표시해보자! 본문

언어/swift

[AR Navigation 개발일지#1] ARView의 물체의 위치를 실제 위도, 경도 좌표로 변환해 표시해보자!

안녕도라 2024. 10. 9. 21:46

구현 내용1: ARView를 생성하고 AR World 좌표상 원점이 정해지는 순간, 현 위치의 위도, 경도를 구해 표시해보자!

* 하고 싶은 것

ARView의 makeUI 함수, 즉 ARView가 생성되는 순간 가장 먼저 한 번 호출되는 함수에 현 위치의 값을 넣어 표시하고 싶다.

그렇게 원점에 현 위치를 맵핑해 두고, ARWorld의 다른 좌표들도 현실의 위도 경도와 짝을 지어주고 싶다.


* 당면한 문제 1 : location값이 nil

LocationManager.location.coordinate에 접근해 띄우고 싶은데,

LocationManager의 coordinate는 double타입으로 미세한데다, 위치가 변할 때마다 update하고 있기 때문인지,

무언가 변하는 상태를 내가 ARView에서 감지를 잘해주지 못하도록 타입을 선언한 것인지,

LocationManger와 AR의 실행 순서가 꼬인 건지,

LocationManager.location.coordinate값에 계속 nil이 뜬다.

 

* 해결 1: P.info 위치 권한 설정 .notDetermined

권한에 따른 로그를 보니, .notDetermined 상태였다..

p.infolist에서 위치 권한 설정을 추가해주지 않아서 location을 받아올 수 없는 것이었음. 정말..

  • Privacy - Location When In Use Usage Description : When in Use / Always 권한 모두 필요
  • Privacy - Location Always and When In Use Usage Description : Always일 때 필요
  • Privacy - Location Usage Description : macOS 앱일 경우 필요

* 당면한 문제 2 : ObservableObject를 상속하는 Class의 값을 변경

@StateObject, @ObservedObject, @ObservableObject 관련 내용은 매번 공부해도 또 잊는다.

여튼 class에 직접 접근해서 값을 변경해 줘야 하는데, 또 생성을 해서 변경하고 있었으므로 원본의 해당값에 접근해도 nil이 떴던 것.

 

* 해결 2: @ObservedObject

// 수정 전
@StateObject var arCoordinatorConverter = ARCoordinateConverter()

// 수정 후
@ObservedObject var arCoordinatorConverter: ARCoordinateConverter

 


* 당면한 문제 3 : 로컬 폴더 원격 저장소로 올리기!

 

시행착오를 최대한 줄이는 방법은 git에 부지런히 커밋을 하는 것이란 생각에..

올리려니 이미 프로젝트를 만들어 버린 로컬 폴더를 원격 repository에 제대로 올리는 방법을 다시 공부

 

* 해결 3: CLI 활용

https://velog.io/@wnsdud0721/%EC%B2%98%EC%9D%8C-%EB%A1%9C%EC%BB%AC%ED%8C%8C%EC%9D%BC%EA%B3%BC-Git-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0

 

[GitHub] 로컬 폴더와 github 연결하기

처음 로컬 폴더와 github를 연결시키는 분들을 위한 설명입니다.

velog.io

 

성공

 


구현 내용2:  AR 공간에 존재하는 큐브의 위도 경도를 표시해보자.

*당면한 문제 1 : 관련 개념이 어렵다...

 

개념 1) 진북(true North), 자북(magnetic North)

  • 진북(True North)
    • 언제나 변하지 않는 북쪽
    • 북극성의 방향
  • 자북(Magnetic North)
    • 나침반 N극이 가리키는 북쪽
    • 캐나다 북쪽 허드스만 부근에 위치한 천연자력 지대 방향으로 자력은 해마다 조금씩 이동

진북과 자북 사이의 편차가 생긴다.

 

https://terms.naver.com/entry.naver?docId=1825847&cid=42897&categoryId=42897

 

진북, 자북, 도북의 의미는?

Q. 지도를 보면 북쪽이 세 곳이나 됩니다. 진북, 자북, 도북이 있어 어떤 것을 기준으로 북쪽을 정하는 것인지 매우 혼란스럽습니다. 진북, 자북, 도북은 어떻게 다른지 알고 싶어요. A. 지도 아랫

terms.naver.com

 

 

개념 2) 방위와 방위각이 뭐지?

  • 방위(bearing) 
    • 동서남북의 방향
    • 어떤 측선이 기준선(NS선)과 이루는 90°보다 작은 수평각
    • 방위각과 같은 의미
  • 방위각(azimuth angle)
    • 진북(True North)를 기준으로하여 우회로 측정한 각을 그 직선의 방위각이라 한다.
    • 북극성(12시 방향)을 기준으로 오른쪽(시계 방향)으로 잰 각도
     

https://terms.naver.com/entry.naver?docId=1575101&cid=60249&categoryId=60249

 

방위각

방위각은 천문학ㆍ측량학ㆍ포술ㆍ항해술 및 기타 분야에서 지표 위에 있는 물체의 위치를 표시하는 지평 좌표(地坪座標)의 하나이다. 천문학에서는 대개 정남에서 시계 방향으로 천체의 수직

terms.naver.com

 

https://terms.naver.com/entry.naver?docId=613399&cid=50316&categoryId=50316

 

방위

① 동서 남북의 방향. ② 어떤 측선이 기준선(NS 선)과 이루는 90°보다 작은 수평각. 이 경우 상한(象限)을 표시하기 위하여 NE, NW, SE, SW의 문자를 넣는다. ③ 방위각과 동일한 의미로 쓰인다.

terms.naver.com

 

개념 3) 구면삼각법?

 

뭔진 잘 모르겠지만 지구는 구니까 점과 점을 잇는 측선은 곡선일 것!

이를 계산하기 위해서 cos, sin, tan 등의 삼각함수를 사용하는 것이 구면삼각법인듯 하다.

 

https://terms.naver.com/entry.naver?docId=3404976&ref=y&cid=47324&categoryId=47324

 

구면삼각형

구면에서 같은 대원 위에 있지 않은 세 점 \text{A}, \text{B}, \text{C}에대하여 세 선분 \text{AB}, \text{BC}, \text{CA}의 합집합을 구면삼각형 \text{ABC}라고 한다. [목차] 1.구면삼각형 2.구면삼각형의 내부 3.

terms.naver.com

 

https://terms.naver.com/entry.naver?docId=3404975&cid=47324&categoryId=47324

 

구면삼각법

구면삼각형의 각의 크기와 변의 길이의 관계를 삼각함수를 이용하여 나타내는 방법을 구면삼각법이라고 한다. 천문학이나 측지학, 항해 등에서 거리를 계산할 때 많이 이용된다. [목차] 1.평면

terms.naver.com

 

 

* 해결 1: 원점과 물체를 잇는 측선을 그은 다음, 방위각을 구하고 이를 바탕으로 위도, 경도를 계산한다는 원리를 가지고 GPT에게 수식을 부탁했다.

import Foundation
import CoreLocation


class ARCoordinateConverter : NSObject, ObservableObject, CLLocationManagerDelegate {
    
    /// 원점의 경도 위도 저장할 변수
    @Published var worldOriginCLLocation: CLLocationCoordinate2D?
    
    /// CLLocation >>> AR 좌표로 변환
    func ConvertCLLocationtoAR(CLLocation: CLLocationCoordinate2D) -> SIMD3<Float> {
        guard let origin = worldOriginCLLocation else { return SIMD3<Float>(0, 0, 0) }
        
        let distance = origin.distance(from: CLLocation)
        let bearing = calculateBearing(from: origin, to: CLLocation)
        
        // 거리에 따라 AR 좌표 변환 (단순화)
        let x = Float(distance * cos(bearing * .pi / 180))
        let z = Float(distance * sin(bearing * .pi / 180))
        
        return SIMD3<Float>(x, 0, -z)  // Y축은 0으로 설정
    }
    
    /// AR 좌표 >>> CLLocation 좌표로 변환
    func ConvertARtoCLLocation(ARWorldCoordinate: SIMD3<Float>) -> CLLocationCoordinate2D {
        guard let origin = worldOriginCLLocation else { return CLLocationCoordinate2D() }
        
        let distance = sqrt(pow(Double(ARWorldCoordinate.x), 2) + pow(Double(ARWorldCoordinate.z), 2))
        let bearing = atan2(-Double(ARWorldCoordinate.z), Double(ARWorldCoordinate.x)) * 180 / .pi
        
        return calculateCoordinate(from: origin, distance: distance, bearing: bearing)
    }
    
    /// 방위각 계산 (시작점, 끝점을 이은 측선의 방위각)
    private func calculateBearing(from origin: CLLocationCoordinate2D, to destination: CLLocationCoordinate2D) -> Double {
        
        /// 방위각 공식:  Θ = tan-1(△Y/△X)
        let deltaLongitude = destination.longitude - origin.longitude
        
        let y = sin(deltaLongitude) * cos(destination.latitude)
        let x = cos(origin.latitude) * sin(destination.latitude) - sin(origin.latitude) * cos(destination.latitude) * cos(deltaLongitude)
        let bearing = atan2(y, x) * 180 / .pi
        
        return fmod((bearing + 360), 360)   /// fmod() : 부동 소수점 나머지 연산
    }
    
    /// 원점에서 주어진 거리와 방위각을 기준으로 새로운 좌표를 계산
    private func calculateCoordinate(from origin: CLLocationCoordinate2D, distance: Double, bearing: Double) -> CLLocationCoordinate2D {
        let earthRadius = 6371000.0 // 미터 단위
        
        let bearingRad = bearing * .pi / 180
        let originLatRad = origin.latitude * .pi / 180
        let originLonRad = origin.longitude * .pi / 180
        
        let destinationLatRad = asin(sin(originLatRad) * cos(distance / earthRadius) +
                                     cos(originLatRad) * sin(distance / earthRadius) * cos(bearingRad))
        let destinationLonRad = originLonRad + atan2(sin(bearingRad) * sin(distance / earthRadius) * cos(originLatRad),
                                                     cos(distance / earthRadius) - sin(originLatRad) * sin(destinationLatRad))
        
        return CLLocationCoordinate2D(latitude: destinationLatRad * 180 / .pi, longitude: destinationLonRad * 180 / .pi)
    }
    
    
}

경도 위도가 다른 값이 뜬다! 정확도는 얼마나되는지 모르겠지만!

 

struct ARViewContainer: UIViewRepresentable {
    
    @StateObject var locationManager = LocationManager()
    @ObservedObject var arCoordinatorConverter: ARCoordinateConverter
    
    func makeUIView(context: Context) -> ARView {
        // 원점 표시
        let arView = ARView(frame: .zero)
        arView.debugOptions = [.showWorldOrigin]
        
        // AR뷰 생성시, World Origin의 위치 경도 저장
        arCoordinatorConverter.worldOriginCLLocation = locationManager.locationManager.location?.coordinate
        
        // 1m 단위 큐브
        let mesh = MeshResource.generateBox(size: 1, cornerRadius: 0.005)
        let material = SimpleMaterial(color: .gray, roughness: 0.15, isMetallic: true)
        let model = ModelEntity(mesh: mesh, materials: [material])
        let model2 = ModelEntity(mesh: mesh, materials: [material])
        
        // 큐브를 Z축 +5 방향으로 5m 떨어진 곳에 배치
        let anchor = AnchorEntity(world: SIMD3<Float>(-5, 0, -5))
        let anchor2 = AnchorEntity(world: SIMD3<Float>(5, 0, -5))
        
        anchor.children.append(model)
        anchor2.children.append(model2)
        
        // anchor1
        let latitude = String(arCoordinatorConverter.ConvertARtoCLLocation(ARWorldCoordinate: SIMD3<Float>(-5,0,-5)).latitude)
        let longitude = String(arCoordinatorConverter.ConvertARtoCLLocation(ARWorldCoordinate: SIMD3<Float>(-5,0,-5)).longitude)
        let text1 = "Anchor1의\n위도는 \(latitude) \n경도는 \(longitude) \n월드좌표는 \(SIMD3<Float>(-5,0,-5))"
        let textMesh = MeshResource.generateText(text1, extrusionDepth: 0.1, font: .systemFont(ofSize: 0.5), containerFrame: .zero, alignment: .center)
        
        // anchor2
        let latitude2 = String(arCoordinatorConverter.ConvertARtoCLLocation(ARWorldCoordinate: SIMD3<Float>(5,0,-5)).latitude)
        let longitude2 = String(arCoordinatorConverter.ConvertARtoCLLocation(ARWorldCoordinate: SIMD3<Float>(5,0,-5)).longitude)
        let text2 = "Anchor2의\n위도는 \(latitude2) \n경도는 \(longitude2) \n월드좌표는 \(SIMD3<Float>(5,0,-5))"
        let textMesh2 = MeshResource.generateText(text2, extrusionDepth: 0.1, font: .systemFont(ofSize: 0.5), containerFrame: .zero, alignment: .center)
        
        let coordinateModel = ModelEntity(mesh: textMesh, materials: [material])
        let coordinateModel2 = ModelEntity(mesh: textMesh2, materials: [material])
        
        coordinateModel.position = model.position + SIMD3<Float>(-model.scale.x * 2,1,0)
        coordinateModel2.position = model.position + SIMD3<Float>(-model.scale.x * 2,1,0)
        
        anchor.children.append(coordinateModel)
        anchor2.children.append(coordinateModel2)
        
        arView.scene.anchors.append(anchor)
        arView.scene.anchors.append(anchor2)
        
        return arView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {
        
        
    }
    
    
    
}

 

해결해야 할 문제..

AR의 원점이 reset되는 경우를 고려해봐야겠다..