הצגת מקומות בסביבה ב-AR ב-Android (Kotlin)

1. לפני שמתחילים

תקציר

בשיעור הזה תלמדו איך להשתמש בנתונים מהפלטפורמה של מפות Google כדי להציג מקומות בסביבה במציאות רבודה (AR) ב-Android.

2344909dd9a52c60.png

דרישות מוקדמות

  • הבנה בסיסית של פיתוח ל-Android באמצעות Android Studio
  • היכרות עם Kotlin

מה תלמדו

  • בקשת הרשאה מהמשתמש לגשת למצלמה ולמיקום של המכשיר.
  • אפשר לשלב עם Places API כדי לאחזר מקומות בקרבת המיקום של המכשיר.
  • שילוב עם ARCore כדי למצוא משטחים במישור האופקי, כך שאפשר לעגן ולהציב אובייקטים וירטואליים במרחב תלת-ממדי באמצעות Sceneform.
  • איסוף מידע על המיקום של המכשיר במרחב באמצעות SensorManager ושימוש ב-Maps SDK for Android Utility Library כדי למקם אובייקטים וירטואליים בכיוון הנכון.

מה צריך להכין

2. להגדרה

Android Studio

ב-codelab הזה נעשה שימוש ב-Android 10.0 (רמת API‏ 29), ונדרשת התקנה של Google Play Services ב-Android Studio. כדי להתקין את שני יחסי התלות האלה, מבצעים את השלבים הבאים:

  1. עוברים אל SDK Manager (כלי לניהול SDK). כדי לגשת אליו, לוחצים על Tools (כלים) > SDK Manager (כלי לניהול SDK).

6c44a9cb9cf6c236.png

  1. בודקים אם גרסה Android 10.0 מותקנת. אם לא, מסמנים את התיבה לצד Android 10.0 (Q)‎, לוחצים על אישור ואז שוב על אישור בתיבת הדו-שיח שמופיעה.

368f17a974c75c73.png

  1. לבסוף, כדי להתקין את Google Play Services, עוברים לכרטיסייה SDK Tools, מסמנים את תיבת הסימון לצד Google Play Services, לוחצים על OK ואז שוב על OK בתיבת הדו-שיח שמופיעה**.**

497a954b82242f4b.png

ממשקי API נדרשים

בשלב 3 בקטע הבא, מפעילים את Maps SDK ל-Android ואת Places API בשביל ה-codelab הזה.

תחילת העבודה עם הפלטפורמה של מפות Google

אם לא השתמשתם בפלטפורמה של מפות Google בעבר, תוכלו לעיין במדריך לתחילת העבודה עם הפלטפורמה של מפות Google או לצפות בפלייליסט לתחילת העבודה עם הפלטפורמה של מפות Google כדי לבצע את השלבים הבאים:

  1. יוצרים חשבון לחיוב.
  2. יוצרים פרויקט.
  3. מפעילים את ממשקי ה-API וערכות ה-SDK של הפלטפורמה של מפות Google (שמופיעים בקטע הקודם).
  4. יוצרים מפתח API.

אופציונלי: אמולטור Android

אם אין לכם מכשיר שתומך ב-ARCore, אתם יכולים להשתמש במקום זאת באמולטור של Android כדי לדמות סצנת AR וגם לזייף את המיקום של המכשיר. בנוסף, מכיוון שתשתמשו גם ב-Sceneform בתרגיל הזה, תצטרכו לוודא שפעלתם לפי השלבים שבקטע 'הגדרת האמולטור לתמיכה ב-Sceneform'.

3. התחלה מהירה

כדי שתוכלו להתחיל כמה שיותר מהר, הנה קוד התחלתי שיעזור לכם לעקוב אחרי ההוראות ב-codelab הזה. אתם יכולים לדלג לפתרון, אבל אם אתם רוצים לראות את כל השלבים, המשיכו לקרוא.

אפשר לשכפל את המאגר אם git מותקן.

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

אפשר גם ללחוץ על הכפתור שלמטה כדי להוריד את קוד המקור.

אחרי שמקבלים את הקוד, פותחים את הפרויקט שנמצא בספרייה starter.

4. סקירה כללית של הפרויקט

בודקים את הקוד שהורדתם בשלב הקודם. במאגר הזה אמור להיות מודול יחיד בשם app, שמכיל את החבילה com.google.codelabs.findnearbyplacesar.

AndroidManifest.xml

המאפיינים הבאים מוצהרים בקובץ AndroidManifest.xml כדי לאפשר לכם להשתמש בתכונות שנדרשות ב-codelab הזה:

<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, שבו מצוין אילו תכונות חומרה נדרשות לאפליקציה הזו, מוצהר על הדברים הבאים:

  • נדרשת גרסה 3.0 של OpenGL ES.
  • נדרש מכשיר עם תמיכה ב-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 כדי שהאפליקציה הזו תפעל, והערך השני מציין איך מספקים את מפתח ה-API של הפלטפורמה של מפות Google ל-Maps SDK ל-Android.

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"
}

הנה תיאור קצר של כל תלות:

  • הספריות עם מזהה הקבוצה com.google.android.gms, כלומר play-services-location ו-play-services-maps, משמשות לגישה למידע על המיקום של המכשיר ולגישה לפונקציונליות שקשורה למפות Google.
  • com.google.maps.android:maps-utils-ktx היא ספריית ההרחבות של Kotlin‏ (KTX) לספריית כלי העזר של Maps SDK ל-Android. הפונקציונליות הזו תשמש בספרייה הזו כדי למקם אובייקטים וירטואליים במרחב אמיתי בשלב מאוחר יותר.
  • com.google.ar.sceneform.ux:sceneform-ux היא ספריית Sceneform, שתאפשר לכם לעבד סצנות תלת-ממדיות ריאליסטיות בלי שתצטרכו ללמוד OpenGL.
  • יחסי התלות במזהה הקבוצה com.squareup.retrofit2 הם יחסי התלות של Retrofit, שמאפשרים לכתוב במהירות לקוח HTTP כדי ליצור אינטראקציה עם Places API.

מבנה הפרויקט

כאן מופיעים הקבצים והחבילות הבאים:

  • ‫**api –**החבילה הזו מכילה מחלקות שמשמשות לאינטראקציה עם Places API באמצעות Retrofit.
  • ‫**ar—**החבילה הזו מכילה את כל הקבצים שקשורים ל-ARCore.
  • ‫**model –**החבילה הזו מכילה מחלקה אחת של נתונים Place, שמשמשת להצפנה של מקום יחיד שמוחזר על ידי Places API.
  • MainActivity.kt – זהו ה-Activity היחיד שנכלל באפליקציה, שיוצגו בו מפה ותצוגת מצלמה.

5. הגדרת הסצנה

כדאי להתחיל עם רכיבי המציאות הרבודה של האפליקציה.

MainActivity מכיל את SupportMapFragment, שאחראי להצגת אובייקט המפה, ומחלקת משנה של ArFragmentPlacesArFragment – שאחראית להצגת סצנת המציאות הרבודה.

הגדרת מציאות רבודה

בנוסף להצגת סצנת המציאות הרבודה, PlacesArFragment גם יטפל בבקשת הרשאת גישה למצלמה מהמשתמש אם הוא עדיין לא העניק אותה. אפשר גם לבקש הרשאות נוספות על ידי ביטול השיטה getAdditionalPermissions. בהנחה שאתם צריכים גם הרשאת מיקום, מציינים זאת ומבטלים את השיטה getAdditionalPermissions:

class PlacesArFragment : ArFragment() {

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

הפעלת התוסף

פותחים את קוד השלד בספרייה starter ב-Android Studio. אם לוחצים על Run (הפעלה) > Run ‘app' (הפעלת האפליקציה) בסרגל הכלים ומפעילים את האפליקציה במכשיר או באמולטור, קודם תתבקשו להפעיל את הרשאת המיקום והמצלמה. אפשר ללחוץ על אישור. אחרי שתעשו את זה, אמורות להופיע תצוגת מצלמה ותצוגת מפה זו לצד זו, כמו בתמונה הבאה:

e3e3073d5c86f427.png

זיהוי מטוסים

כשמסתכלים עם המצלמה על הסביבה, יכול להיות שתראו כמה נקודות לבנות שמופיעות על משטחים אופקיים, כמו הנקודות הלבנות על השטיח בתמונה הזו.

2a9b6ea7dcb2e249.png

הנקודות הלבנות האלה הן הנחיות ש-ARCore מספק כדי לציין שזוהה מישור אופקי. המישורים שזוהו מאפשרים ליצור מה שנקרא 'עוגן', כדי שתוכלו למקם אובייקטים וירטואליים במרחב אמיתי.

למידע נוסף על ARCore ועל האופן שבו הוא מבין את הסביבה שלכם, אפשר לקרוא על המושגים הבסיסיים שלו.

6. חיפוש מקומות בסביבה

לאחר מכן, תצטרכו לגשת למיקום הנוכחי של המכשיר ולהציג אותו, ואז לאחזר מקומות בקרבת מקום באמצעות Places API.

הגדרת מפות

מפתח Google Maps Platform API

קודם לכן יצרתם מפתח Google Maps Platform API כדי להפעיל שאילתות ב-Places API וכדי שתוכלו להשתמש ב-Maps SDK ל-Android. פותחים את הקובץ gradle.properties ומחליפים את המחרוזת "YOUR API KEY HERE" במפתח ה-API שיצרתם.

הצגת מיקום המכשיר במפה

אחרי שמוסיפים את מפתח ה-API, מוסיפים למפה רכיב עזר שיעזור למשתמשים להבין איפה הם נמצאים ביחס למפה. כדי לעשות זאת, עוברים לשיטה setUpMaps ובתוך הקריאה mapFragment.getMapAsync מגדירים את googleMap.isMyLocationEnabled ל-true.. כך הנקודה הכחולה תוצג במפה.

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

קבלת המיקום הנוכחי

כדי לקבל את מיקום המכשיר, צריך להשתמש במחלקה FusedLocationProviderClient. השגת מופע של זה כבר בוצעה בשיטה onCreate של MainActivity. כדי להשתמש באובייקט הזה, צריך למלא את השיטה getCurrentLocation, שמקבלת ארגומנט lambda כדי שיהיה אפשר להעביר מיקום למבצע הקריאה של השיטה הזו.

כדי להשלים את השיטה הזו, אפשר לגשת למאפיין lastLocation של האובייקט FusedLocationProviderClient ואז להוסיף addOnSuccessListener באופן הבא:

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

השיטה getCurrentLocation מופעלת מתוך ה-lambda שמוגדר ב-getMapAsync בשיטה setUpMaps שממנה נשלפים המקומות הקרובים.

התחלת שיחה ברשת של מקומות

בשיחת השיטה getNearbyPlaces, שימו לב שהפרמטרים הבאים מועברים לשיטה placesServices.nearbyPlaces – מפתח API, מיקום המכשיר, רדיוס במטרים (שמוגדר ל-2 ק"מ) וסוג מקום (שמוגדר כרגע ל-park).

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

כדי להשלים את הקריאה לרשת, מעבירים את מפתח ה-API שהגדרתם בקובץ gradle.properties. קטע הקוד הבא מוגדר בקובץ build.gradle בקטע android > defaultConfig:

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

ערך משאב המחרוזת google_maps_key יהיה זמין בזמן הבנייה.

כדי להשלים את הקריאה לרשת, אפשר פשוט לקרוא את משאב המחרוזת הזה באמצעות getString באובייקט Context.

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

7. מקומות ב-AR

עד עכשיו ביצעת את הפעולות הבאות:

  1. בקשת הרשאות גישה למצלמה ולמיקום מהמשתמש כשהאפליקציה מופעלת בפעם הראשונה
  2. הגדרה של ARCore כדי להתחיל לעקוב אחרי מישורים אופקיים
  3. הגדרת Maps SDK באמצעות מפתח ה-API
  4. קבלת המיקום הנוכחי של המכשיר
  5. אחזור מקומות בסביבה (במיוחד פארקים) באמצעות Places API

השלב האחרון בתרגיל הזה הוא למקם את המקומות שאתם מאחזרים במציאות רבודה.

הבנת סצנות

‫ARCore יכול להבין את הסצנה בעולם האמיתי באמצעות המצלמה של המכשיר, על ידי זיהוי נקודות מעניינות וייחודיות שנקראות נקודות מאפיין בכל פריים של התמונה. כשנקודות התכונה האלה מקובצות ונראות כאילו הן נמצאות במישור אופקי משותף, כמו שולחנות ורצפות, ARCore יכול להפוך את התכונה הזו לזמינה לאפליקציה כמישור אופקי.

כפי שראיתם קודם, ARCore עוזר למשתמשים כשהמערכת מזהה משטח על ידי הצגת נקודות לבנות.

2a9b6ea7dcb2e249.png

הוספת מודעות עוגן

אחרי שמזהים מישור, אפשר לצרף אליו אובייקט שנקרא עוגן. באמצעות עוגן, אפשר למקם אובייקטים וירטואליים ולהבטיח שהאובייקטים האלה יישארו באותו מיקום במרחב. אפשר לשנות את הקוד כדי לצרף מטוס ברגע שהוא מזוהה.

ב-setUpAr, קובץ OnTapArPlaneListener מצורף אל PlacesArFragment. המאזין הזה מופעל בכל פעם שמקישים על מישור בסצנת ה-AR. בתוך הקריאה הזו, אפשר ליצור Anchor ו-AnchorNode מה-HitResult שסופק במאזין באופן הבא:

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

ב-AnchorNode מצורפים אובייקטים של צומת צאצא – מופעים של PlaceNode – בסצנה שמטופלת בקריאה לשיטה addPlaces.

הפעלה

אם מריצים את האפליקציה עם השינויים שלמעלה, מסתכלים סביב עד שמזוהה מישור. מקישים על הנקודות הלבנות שמציינות מטוס. אחרי שתעשו את זה, תוכלו לראות סמנים במפה של כל הפארקים הקרובים אליכם. אבל אם תשימו לב, האובייקטים הווירטואליים תקועים בנקודת העוגן שנוצרה ולא ממוקמים ביחס למיקום של הפארקים האלה במרחב.

f93eb87c98a0098d.png

בשלב האחרון, תשתמשו ב-Maps SDK for Android Utility Library וב-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]).

כותרת פוטוספרית

אחרי שקובעים את הצפון, השלב הבא הוא לקבוע את הזווית בין הצפון לבין כל מקום, ואז להשתמש במידע הזה כדי למקם את המקומות בכיוון הנכון במציאות רבודה.

כדי לחשב את הכיוון, תשתמשו בספריית כלי העזר של Maps SDK for Android, שמכילה כמה פונקציות עזר לחישוב מרחקים וכיוונים באמצעות גיאומטריה כדורית. מידע נוסף זמין בסקירה הכללית הזו של הספרייה.

בשלב הבא, תשתמשו בשיטה sphericalHeading בספריית כלי העזר, שמחשבת את הכיוון/המסלול בין שני אובייקטים מסוג LatLng. המידע הזה נדרש בתוך השיטה getPositionVector שמוגדרת ב-Place.kt. בסופו של דבר, השיטה הזו תחזיר אובייקט 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 הוא להשתמש בתוצאה של getPositionVector כשמוסיפים אובייקטים של PlaceNode לסצנה. עוברים אל addPlaces ב-MainActivity, ממש מתחת לשורה שבה מוגדר ההורה בכל placeNode (ממש מתחת ל-placeNode.setParent(anchorNode)). מגדירים את localPosition של placeNode לתוצאה של קריאה ל-getPositionVector באופן הבא:

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

כברירת מחדל, השיטה getPositionVector מגדירה את המרחק האנכי של הצומת למטר אחד, כפי שצוין בערך y בשיטה getPositionVector. אם רוצים לשנות את המרחק הזה, למשל ל-2 מטרים, אפשר לשנות את הערך לפי הצורך.

בעקבות השינוי, אובייקטים של PlaceNode שנוספו אמורים להיות מכוונים לכותרת הנכונה. עכשיו אפשר להריץ את האפליקציה ולראות את התוצאה.

9. מזל טוב

כל הכבוד שהגעת עד לכאן!

מידע נוסף