Android에서 AR 모드로 주변 장소 표시(Kotlin)

1. 시작하기 전에

개요

이 Codelab에서는 Google Maps Platform의 데이터를 사용하여 Android에서 증강 현실(AR)로 주변 장소를 표시하는 방법을 배웁니다.

2344909dd9a52c60.png

기본 요건

  • Android 스튜디오를 사용한 Android 개발 관련 기본 지식
  • Kotlin 관련 전문 지식

과정 내용

  • 사용자에게 기기의 카메라와 위치에 액세스할 수 있는 권한을 요청합니다.
  • Places API와 통합하여 기기 위치 주변 장소를 가져옵니다.
  • ARCore와 통합하여 수평면을 찾아, Sceneform을 사용하여 가상 개체를 3D 공간에 고정하고 배치합니다.
  • SensorManager를 사용하여 공간 내 기기 위치 정보를 수집하고, Android용 Maps SDK 유틸리티 라이브러리를 사용하여 가상 개체를 올바른 방향으로 배치합니다.

필요한 사항

2. 설정

Android 스튜디오

이 Codelab을 진행하려면 Android 10.0(API 레벨 29)을 사용하며, Android 스튜디오에 Google Play 서비스가 설치되어 있어야 합니다. 두 종속 항목을 모두 설치하려면 다음 단계를 완료하세요.

  1. SDK Manager로 이동합니다. 도구 > SDK 관리자를 클릭해야 합니다.

6c44a9cb9cf6c236.png

  1. Android 10.0이 설치되어 있는지 확인합니다. 설치되어 있지 않다면 Android 10.0 (Q) 옆에 있는 체크박스를 선택한 다음 확인을 클릭하고, 표시되는 대화상자에서 확인을 다시 클릭합니다.

368f17a974c75c73.png

  1. 마지막으로 SDK 도구 탭으로 이동하여 Google Play 서비스 옆에 있는 체크박스를 선택한 다음 확인을 클릭하고, 표시되는 대화상자에서 확인을 다시 선택합니다**.**

497a954b82242f4b.png

필수 API

다음 섹션의 3단계에서 이 Codelab에 Android용 Maps SDKPlaces API를 사용 설정합니다.

Google Maps Platform 시작

Google Maps Platform을 처음 사용한다면 Google Maps Platform 시작하기 가이드를 따르거나 Google Maps Platform 시작하기 재생목록을 시청하여 다음 단계를 완료하세요.

  1. 결제 계정을 만듭니다.
  2. 프로젝트를 만듭니다.
  3. 이전 섹션에 표시된 Google Maps Platform API와 SDK를 사용 설정합니다.
  4. API 키를 생성합니다.

선택사항: Android Emulator

ARCore 지원 기기가 없다면 Android Emulator를 사용하여 AR 장면을 시뮬레이션하고 기기의 위치를 가장할 수 있습니다. 이 실습에서는 Sceneform도 사용하므로, 'Sceneform을 지원하도록 에뮬레이터 구성'에 나오는 단계도 따라야 합니다.

3. 빠른 시작

빠르게 시작할 수 있도록 이 Codelab을 따라 하는 데 도움이 되는 시작 코드가 있습니다. 해법으로 바로 넘어갈 수 있지만, 모든 단계를 확인하고 싶다면 계속 읽으시기 바랍니다.

git를 설치했다면 저장소를 복제할 수 있습니다.

git clone https://github.com/googlecodelabs/display-nearby-places-ar-android.git

또는 아래 버튼을 클릭하여 소스 코드를 다운로드할 수도 있습니다.

코드를 받으면 starter 디렉터리 내에 있는 프로젝트를 엽니다.

4. 프로젝트 개요

이전 단계에서 다운로드한 코드를 살펴봅니다. 이 저장소에는 com.google.codelabs.findnearbyplacesar이라는 패키지가 포함된 app이라는 단일 모듈이 있습니다.

AndroidManifest.xml

이 Codelab에서 필요한 기능을 사용할 수 있도록 AndroidManifest.xml 파일에 다음 속성이 선언되어 있습니다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Sceneform requires OpenGL ES 3.0 or later. -->
<uses-feature
   android:glEsVersion="0x00030000"
   android:required="true" />

<!-- Indicates that app requires ARCore ("AR Required"). Ensures the app is visible only in the Google Play Store on devices that support ARCore. For "AR Optional" apps remove this line. -->
<uses-feature android:name="android.hardware.camera.ar" />

이러한 기능을 사용하기 전에 사용자에게 부여해야 하는 권한을 지정하는 uses-permission에는 다음이 선언됩니다.

  • android.permission.INTERNET - 앱에서 네트워크 작업을 처리하고 인터넷을 통해 데이터를 가져오는(예: Places API를 통한 장소 정보 수집) 작업을 할 수 있게 합니다.
  • android.permission.CAMERA: 기기의 카메라를 사용하여 객체를 증강 현실에서 표현하기 위해 카메라에 액세스해야 합니다.
  • android.permission.ACCESS_FINE_LOCATION: 기기 위치를 기준으로 주변 장소를 가져오려면 위치 액세스 권한이 필요합니다.

이 앱에 필요한 하드웨어 기능을 지정하는 uses-feature에는 다음이 선언됩니다.

  • OpenGL ES 버전 3.0이 필요합니다.
  • ARCore 지원 기기가 필요합니다.

또한 다음 메타데이터 태그가 애플리케이션 객체 아래에 추가됩니다.

<application
  android:allowBackup="true"
  android:icon="@mipmap/ic_launcher"
  android:label="@string/app_name"
  android:roundIcon="@mipmap/ic_launcher_round"
  android:supportsRtl="true"
  android:theme="@style/AppTheme">

  <!--
     Indicates that this app requires Google Play Services for AR ("AR Required") and causes
     the Google Play Store to download and install Google Play Services for AR along with
     the app. For an "AR Optional" app, specify "optional" instead of "required".
  -->

  <meta-data
     android:name="com.google.ar.core"
     android:value="required" />

  <meta-data
     android:name="com.google.android.geo.API_KEY"
     android:value="@string/google_maps_key" />

  <!-- Additional elements here -->

</application>

첫 번째 메타데이터 항목은 이 앱을 실행하기 위해 ARCore가 필요함을 나타내며, 두 번째 항목은 Google Maps Platform API 키를 Android용 Maps SDK에 제공하는 방법입니다.

build.gradle

build.gradle에는 다음과 같은 추가 종속 항목이 지정됩니다.

dependencies {
    // Maps & Location
    implementation 'com.google.android.gms:play-services-location:17.0.0'
    implementation 'com.google.android.gms:play-services-maps:17.0.0'
    implementation 'com.google.maps.android:maps-utils-ktx:1.7.0'

    // ARCore
    implementation "com.google.ar.sceneform.ux:sceneform-ux:1.15.0"

    // Retrofit
    implementation "com.squareup.retrofit2:retrofit:2.7.1"
    implementation "com.squareup.retrofit2:converter-gson:2.7.1"
}

다음은 각 종속성에 대한 간단한 설명입니다.

  • 그룹 ID가 com.google.android.gms인 라이브러리, 즉 play-services-locationplay-services-maps는 기기의 위치 정보에 액세스하고 Google 지도와 관련된 기능에 액세스하는 데 사용합니다.
  • com.google.maps.android:maps-utils-ktx는 Android용 Maps SDK 유틸리티 라이브러리의 Kotlin 확장 프로그램(KTX) 라이브러리입니다. 나중에 이 라이브러리에서 기능을 사용하여 가상 객체를 실제 공간에 배치합니다.
  • com.google.ar.sceneform.ux:sceneform-ux는 OpenGL을 학습하지 않고도 현실적인 3D 장면을 렌더링할 수 있는 Sceneform 라이브러리입니다.
  • 그룹 ID com.squareup.retrofit2에 있는 종속성은 Retrofit 종속성으로, Places API와 상호작용하기 위해 HTTP 클라이언트를 빠르게 작성할 수 있습니다.

프로젝트 구조

여기서는 다음과 같은 패키지와 파일을 찾을 수 있습니다.

  • **api—**이 패키지에는 Retrofit를 사용하여 Places API와 상호작용하는 데 사용하는 클래스가 포함되어 있습니다.
  • **ar—**이 패키지에는 ARCore와 관련된 모든 파일이 포함됩니다.
  • **model—**이 패키지에는 단일 데이터 클래스인 Place가 포함되어 있으며, 이는 Places API에서 반환한 단일 장소를 캡슐화하는 데 사용됩니다.
  • MainActivity.kt - 앱에 포함된 단일 Activity로, 지도 및 카메라 보기를 표시합니다.

5. 장면 설정

지금부터는 앱의 핵심 구성요소를 살펴보겠습니다. 첫 번째 구성요소는 증강 현실입니다.

MainActivity에는 지도 객체 표시를 처리하는 SupportMapFragment와 증강 현실 장면 표시를 처리하는 ArFragment(PlacesArFragment)의 서브클래스가 포함되어 있습니다.

증강 현실 설정

증강 현실 장면 표시에 더해, PlacesArFragment는 아직 카메라 액세스 권한을 부여하지 않은 사용자에 대한 카메라 액세스 권한 요청도 처리합니다. getAdditionalPermissions 메서드를 재정의하여 추가 권한을 요청할 수도 있습니다. 위치 권한도 부여해야 한다면 이 메서드를 지정하고 getAdditionalPermissions 메서드를 재정의해야 합니다.

class PlacesArFragment : ArFragment() {

   override fun getAdditionalPermissions(): Array<String> =
       listOf(Manifest.permission.ACCESS_FINE_LOCATION)
           .toTypedArray()
}

실행

Android 스튜디오로 이동하여 starter 디렉터리에서 스켈레톤 코드를 엽니다. 툴바에서 실행 > '앱' 실행을 클릭하고 앱을 기기나 에뮬레이터에 배포했다면, 먼저 위치 및 카메라 액세스 권한을 사용 설정하라는 메시지가 표시됩니다. 허용을 클릭합니다. 이렇게 하면 다음처럼 카메라 보기와 지도 보기가 나란히 표시됩니다.

e3e3073d5c86f427.png

평면 감지

카메라를 이용해 현재 환경을 둘러보면, 이 이미지의 카펫에 있는 흰색 점처럼 수평면에 흰색 점 몇 개가 표시될 수 있습니다.

2a9b6ea7dcb2e249.png

이 흰색 점은 ARCore에서 제공하여 수평면이 감지되었음을 나타내기 위해 ARCore에서 제공하는 가이드라인입니다. 이러한 감지된 평면을 사용하면 '앵커'라는 객체를 만들어 실제 공간에 가상 객체를 배치할 수 있습니다.

ARCore와 ARCore가 사용자 주변 환경을 이해하는 방법에 대한 자세한 내용은 ARCore 기본 개념을 참고하세요.

6. 주변 장소 가져오기

그런 다음에는 기기의 현재 위치에 액세스하고 현재 위치를 표시한 다음 Places API를 사용하여 주변 장소를 가져와야 합니다.

지도 설정

Google Maps Platform API 키

앞에서 Places API를 쿼리하고 Android용 Maps SDK를 사용할 수 있도록 Google Maps Platform API 키를 만들었습니다. gradle.properties 파일을 열고 "YOUR API KEY HERE" 문자열을 이전에 만든 API 키로 교체합니다.

지도에 기기 위치 표시

API 키를 추가한 후, 사용자와 관련된 방향을 지도에 표시할 수 있도록 지도에 도우미를 추가합니다. 이렇게 하려면 setUpMaps 메서드로 이동하고 mapFragment.getMapAsync 호출에서 googleMap.isMyLocationEnabledtrue.로 설정해야 합니다. 그러면 지도에 파란색 점이 표시됩니다.

private fun setUpMaps() {
   mapFragment.getMapAsync { googleMap ->
       googleMap.isMyLocationEnabled = true
       // ...
   }
}

현재 위치 가져오기

기기의 위치를 가져오려면 FusedLocationProviderClient 클래스를 사용해야 합니다. 이 클래스의 인스턴스를 가져오는 작업은 MainActivityonCreate 메서드에서 이미 수행하였습니다. 이 객체를 사용하려면 위치를 이 메서드의 호출자에게 전달할 수 있도록 람다 인수를 허용하는 getCurrentLocation 메서드를 작성해야 합니다.

FusedLocationProviderClient 객체의 lastLocation 속성에 액세스하고 다음과 같이 addOnSuccessListener에 추가하면 이 메서드를 완료할 수 있습니다.

fusedLocationClient.lastLocation.addOnSuccessListener { location ->
    currentLocation = location
    onSuccess(location)
}.addOnFailureListener {
    Log.e(TAG, "Could not get location")
}

getCurrentLocation 메서드는 주변 장소를 가져오는 setUpMaps 메서드의 getMapAsync에 제공된 람다에서 호출됩니다.

장소 네트워크 호출 시작

getNearbyPlaces 메서드 호출에서 API 키, 기기 위치, 미터 단위의 반경(2km로 설정됨), 장소 유형(현재 park로 설정됨)이라는 매개변수가 placesServices.nearbyPlaces 메서드에 전달됩니다.

val apiKey = "YOUR API KEY"
placesService.nearbyPlaces(
   apiKey = apiKey,
   location = "${location.latitude},${location.longitude}",
   radiusInMeters = 2000,
   placeType = "park"
)

네트워크 호출을 완료하려면 gradle.properties 파일에 정의한 API 키를 전달하세요. 다음 코드 스니펫은 android > defaultConfig 구성에 있는 build.gradle 파일에 정의됩니다.

android {
   defaultConfig {
       resValue "string", "google_maps_key", (project.findProperty("GOOGLE_MAPS_API_KEY") ?: "")
   }
}

이렇게 하면 빌드 시간에 문자열 리소스 값 google_maps_key를 사용할 수 있게 됩니다.

네트워크 호출을 완료하려면 Context 객체에서 getString을 통해 이 문자열 리소스를 읽으면 됩니다.

val apiKey = this.getString(R.string.google_maps_key)

7. AR 모드에서의 장소

지금까지 다음 작업을 완료했습니다.

  1. 앱을 처음 실행할 때 사용자에게 카메라 및 위치 정보 액세스 권한을 요청함
  2. 수평면 추적을 시작하도록 ARCore 설정
  3. API 키를 사용하여 Maps SDK 설정
  4. 기기의 현재 위치 얻기
  5. Places API를 사용하여 주변 장소(특히 공원)를 가져옴

이 실습을 완료하기 위해 해야 하는 남은 단계는 가져오는 장소를 증강 현실에 배치하는 것입니다.

장면 이해

ARCore는 각 이미지 프레임에서 특징점이라고 하는 흥미롭고 눈에 띄는 지점을 감지하여 기기의 카메라를 통해 실제 풍경을 이해할 수 있습니다. 이러한 특징점이 군집을 이루고 테이블이나 바닥 같은 공통 수평면에 표시되면, ARCore는 이 지점을 수평면으로 앱에 제공할 수 있습니다.

앞에서 살펴보았듯이 ARCore는 흰색 점을 표시하여 사용자에게 평면이 감지되었음을 알립니다.

2a9b6ea7dcb2e249.png

앵커 추가

평면이 감지되면 앵커라는 객체를 연결할 수 있습니다. 앵커를 이용하면 가상 객체를 배치하고, 이러한 객체가 공간 내 동일한 위치에 머무르도록 보장할 수 있습니다. 평면이 감지되면 앵커를 추가하도록 코드를 수정하세요.

setUpAr에서 OnTapArPlaneListenerPlacesArFragment에 연결됩니다. 이 리스너는 AR 장면에서 평면을 탭할 때마다 호출됩니다. 이 호출에서는 다음과 같이 리스너에 제공된 HitResult에서 AnchorAnchorNode를 만들 수 있습니다.

arFragment.setOnTapArPlaneListener { hitResult, _, _ ->
   val anchor = hitResult.createAnchor()
   anchorNode = AnchorNode(anchor)
   anchorNode?.setParent(arFragment.arSceneView.scene)
   addPlaces(anchorNode!!)
}

AnchorNodeaddPlaces 메서드 호출에서 처리되는 하위 노드 객체인 PlaceNode 인스턴스를 장면에서 연결하는 위치합니다.

실행

위의 수정사항을 적용하여 앱을 실행했다면, 평면이 감지될 때까지 주변을 둘러보세요. 평면을 나타내는 흰색 점을 탭하세요. 이렇게 하면 가까이에 있는 공원을 모두 표시하는 마커가 지도에 표시됩니다. 하지만 보시다시피, 가상 객체는 생성된 앵커에 막혀 있고 공원의 공간 내 위치와 관련하여 배치되지 않습니다.

f93eb87c98a0098d.png

마지막 단계에서는 기기에서 Android용 Maps SDK 유틸리티 라이브러리SensorManager를 사용하여 이 부분을 수정합니다.

8. 장소 위치 지정

증강 현실에서 가상 장소 아이콘을 정확한 위치에 배치하려면 다음 두 가지 정보가 필요합니다.

  • 진북 위치
  • 북쪽과 각 장소 간의 각도

북쪽 결정

북쪽은 기기에서 사용할 수 있는 위치 센서(지자기 및 가속도계)를 이용해 확인할 수 있습니다. 이 두 센서를 사용하면 공간 내 기기 위치에 관한 실시간 정보를 수집할 수 있습니다. 위치 센서에 관한 자세한 내용은 기기 방향 계산을 참고하세요.

두 센서에 액세스하려면 SensorManager를 얻은 후 센서에 SensorEventListener를 등록해야 합니다. 다음 단계는 MainActivity의 수명 주기 메서드에서 이미 완료되었습니다.

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   // ...
   sensorManager = getSystemService()!!
   // ...
}

override fun onResume() {
   super.onResume()
   sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
   sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also {
       sensorManager.registerListener(
           this,
           it,
           SensorManager.SENSOR_DELAY_NORMAL
       )
   }
}

override fun onPause() {
   super.onPause()
   sensorManager.unregisterListener(this)
}

onSensorChanged 메서드에서 SensorEvent 객체를 제공합니다. 이 객체에는 시간에 따라 변하는 센서의 데이터 관련 세부정보가 포함되어 있습니다. 이 메서드에 다음 코드를 추가하세요.

override fun onSensorChanged(event: SensorEvent?) {
   if (event == null) {
       return
   }
   if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
       System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)
   } else if (event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD) {
       System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)
   }

   // Update rotation matrix, which is needed to update orientation angles.
   SensorManager.getRotationMatrix(
       rotationMatrix,
       null,
       accelerometerReading,
       magnetometerReading
   )
   SensorManager.getOrientation(rotationMatrix, orientationAngles)
}

위의 코드는 센서 유형을 확인하고, 유형에 맞게 적절한 센서 판독값(가속도계 또는 자기계 판독값)을 업데이트합니다. 이제 이러한 센서 판독값을 사용하여 기기를 기준으로 북쪽이 몇 도 위치에 있는지를(즉 orientationAngles[0] 값을) 결정할 수 있습니다.

구형 방향

이제 북쪽이 결정되었으므로, 다음 단계는 북쪽과 각 장소 사이의 각도를 결정하고 이 정보를 사용하여 증강 현실에서 올바른 방향으로 장소를 배치하는 것입니다.

방향 계산에는 구형을 통해 거리와 방향을 계산하는 유용한 도우미 함수가 포함된 Android용 Maps SDK 유틸리티 라이브러리를 사용합니다. 자세한 내용은 이 라이브러리 개요를 참고하세요.

그런 다음 두 LatLng 객체 간의 방향/방위를 계산하는, 유틸리티 라이브러리의 sphericalHeading 메서드를 사용합니다. 이 정보는 Place.kt에 정의된 getPositionVector 메서드에서 필요합니다. 이 메서드는 최종적으로 Vector3 객체를 반환하며, 이 객체는 각 PlaceNode에서 AR 공간 내 지역 위치로 사용합니다.

이 메서드의 방향 정의를 다음으로 교체합니다.

val heading = latLng.sphericalHeading(placeLatLng)

그러면 다음과 같은 메서드 정의가 생성됩니다.

fun Place.getPositionVector(azimuth: Float, latLng: LatLng): Vector3 {
   val placeLatLng = this.geometry.location.latLng
   val heading = latLng.sphericalHeading(placeLatLng)
   val r = -2f
   val x = r * sin(azimuth + heading).toFloat()
   val y = 1f
   val z = r * cos(azimuth + heading).toFloat()
   return Vector3(x, y, z)
}

지역 위치

AR에서 장소의 방향을 올바르게 지정하는 마지막 단계는 PlaceNode 객체가 장면에 추가될 때 getPositionVector의 결과를 사용하는 것입니다. MainActivityaddPlaces로 이동합니다. 선 아래에는 상위 요소가 (placeNode.setParent(anchorNode) 바로 아래에 있는) 각 placeNode에 설정되어 있습니다. placeNodelocalPosition을 다음과 같이 getPositionVector 호출의 결과로 설정합니다.

val placeNode = PlaceNode(this, place)
placeNode.setParent(anchorNode)
placeNode.localPosition = place.getPositionVector(orientationAngles[0], currentLocation.latLng)

기본적으로 getPositionVector 메서드는 getPositionVector 메서드의 y 값에 지정된 것처럼 노드의 y 거리를 1미터로 설정합니다. 이 거리를 예를 들어 2미터로 조정하고 싶다면 필요에 따라 값을 수정하세요.

이 거리를 변경하면 추가된 PlaceNode 객체가 이제 올바른 방향을 향할 것입니다. 이제 앱을 실행하여 결과를 확인해 보세요.

9. 축하합니다

여기까지 오신 여러분을 축하합니다.

자세히 알아보기