1. לפני שמתחילים
ARCore היא פלטפורמה לפיתוח אפליקציות של מציאות רבודה (AR) למכשירים ניידים. באמצעות ממשקי API שונים, ARCore מאפשר למכשיר של המשתמש לקבל מידע על הסביבה שלו ולקבל מידע על הסביבה, ולקיים אינטראקציה עם המידע הזה.
ב-Codelab הזה, עוברים תהליך היצירה של אפליקציה פשוטה שתומכת ב-AR ומשתמשת ב-ARCore Depth API.
דרישות מוקדמות
ה-Codelab הזה נכתב עבור מפתחים בעלי ידע בסיסי במושגים של AR.
מה תפַתחו
פיתוח אפליקציה שמשתמשת בתמונת העומק של כל פריים כדי להמחיש את הגיאומטריה של הסצנה ולבצע הסתרה בנכסים וירטואליים של מיקומים. אתם תבצעו את השלבים הספציפיים של:
- מתבצעת בדיקה אם יש תמיכה ב-Depth API בטלפון
- אחזור תמונת העומק של כל פריים
- המחשת נתוני עומק במספר דרכים (ראו את האנימציה שלמעלה)
- שימוש בעומק כדי להגביר את הריאליזם של אפליקציות עם הסתרה
- הדרכה על טיפול יעיל בטלפונים שלא תומכים ב-Depth API
מה צריך להכין
דרישות החומרה
- מכשיר ARCore נתמך שמחובר באמצעות כבל USB למכונת הפיתוח שלכם. המכשיר הזה צריך גם לתמוך ב-Depth API. כאן מפורטת רשימת המכשירים הנתמכים. Depth API זמין רק ב-Android.
- הפעלה של ניפוי באגים ב-USB במכשיר הזה.
דרישות התוכנה
- ARCore SDK 1.31.0 ואילך.
- מחשב פיתוח עם Android Studio (גרסה 3.0 ואילך).
2. ARCore ו-Depth API
ממשק ה-Depth API משתמש במצלמת RGB של מכשיר נתמך כדי ליצור מפות עומק (שנקראות גם תמונות עומק). אתם יכולים להשתמש במידע שמסופק על ידי מפת עומק כדי להציג אובייקטים וירטואליים בצורה מדויקת, לפני או מאחורי אובייקטים בעולם האמיתי, וכך לספק חוויות משתמש סוחפות ומציאותיות.
ה-ARCore Depth API מספק גישה לתמונות עומק שתואמות לכל פריים שסופק על ידי הסשן של ARCore. כל פיקסל מספק מדידת מרחק מהמצלמה לסביבה, וכך מספק ריאליזם משופר לאפליקציית ה-AR.
אחת היכולות העיקריות של Depth API היא חסימה: היכולת של אובייקטים דיגיטליים להופיע באופן מדויק ביחס לאובייקטים בעולם האמיתי. כך אובייקטים מרגישים כאילו הם נמצאים ממש בסביבה יחד עם המשתמש.
ה-Codelab הזה ידריך אתכם בתהליך הפיתוח של אפליקציה פשוטה שתומכת ב-AR, שמשתמשת בתמונות עומק כדי לבצע הסתרה של אובייקטים וירטואליים מאחורי משטחים בעולם האמיתי, ולראות את הגיאומטריה שזוהתה במרחב.
3. להגדרה
הגדרת מכונת הפיתוח
- מחברים את מכשיר ARCore למחשב באמצעות כבל USB. מוודאים שהמכשיר מאפשר ניפוי באגים ב-USB.
- פותחים טרמינל ומריצים את
adb devices
, כפי שמוצג בהמשך:
adb devices List of devices attached <DEVICE_SERIAL_NUMBER> device
הערך <DEVICE_SERIAL_NUMBER>
יהיה מחרוזת ייחודית למכשיר שלך. לפני שממשיכים, חשוב לוודא שמוצג מכשיר אחד בדיוק.
להוריד ולהתקין את ה-Code
- אפשר לשכפל את המאגר:
git clone https://github.com/googlecodelabs/arcore-depth
אפשר גם להוריד קובץ ZIP ולחלץ אותו:
- מפעילים את Android Studio ולוחצים על פתיחת פרויקט קיים של Android Studio.
- מאתרים את הספרייה שאליה חילצתם את קובץ ה-ZIP שהורדתם ופותחים את הספרייה
depth_codelab_io2020
.
זהו פרויקט Gradle אחד עם כמה מודולים. אם החלונית 'פרויקט' בפינה הימנית העליונה של Android Studio עדיין לא מוצגת בחלונית הפרויקט, לוחצים על פרויקטים בתפריט הנפתח.
התוצאה אמורה להיראות כך:
הפרויקט מכיל את המודולים הבאים:
|
העבודה תתבצע במודול part0_work
. יש גם פתרונות מלאים לכל חלק ב-Codelab. כל מודול הוא אפליקציה שאפשר ליצור.
4. הפעלת אפליקציית Starter
- לוחצים על הפעלה > הפעלה... > 'part0_work' בתיבת הדו-שיח בחירת יעד פריסה שמוצגת, המכשיר אמור להופיע בקטע מכשירים מחוברים.
- בוחרים את המכשיר הרצוי ולוחצים על אישור. מערכת Android Studio תיצור את האפליקציה הראשונית ותפעיל אותה במכשיר.
- האפליקציה תבקש הרשאות גישה למצלמה. מקישים על אישור כדי להמשיך.
איך להשתמש באפליקציה
|
נכון לעכשיו, האפליקציה פשוטה מאוד ואין לה הרבה מודעות לגבי הגיאומטריה של הסצנה בעולם האמיתי.
אם מניחים דמות Android מאחורי כיסא, לדוגמה, הרינדור ייראה כאילו הוא מרחף לפנים, כי האפליקציה לא יודעת שהכיסא נמצא שם וצריך להסתיר את מכשיר ה-Android.
כדי לפתור את הבעיה, נשתמש ב-Depth API כדי לשפר את הניראות והריאליזם באפליקציה הזו.
5. בדיקה אם Depth API נתמך (חלק 1)
ה-ARCore Depth API פועל רק בקבוצת משנה של מכשירים נתמכים. לפני שילוב פונקציונליות באפליקציה באמצעות תמונות העומק האלה, צריך קודם לוודא שהאפליקציה פועלת במכשיר נתמך.
מוסיפים חבר פרטי חדש לקבוצה DepthCodelabActivity
, שישמש כדגל שמציין אם המכשיר הנוכחי תומך בעומק:
private boolean isDepthSupported;
אנחנו יכולים לאכלס את הדגל הזה מתוך הפונקציה onResume()
, שבה נוצר סשן חדש.
מוצאים את הקוד הקיים:
// Creates the ARCore session.
session = new Session(/* context= */ this);
מעדכנים את הקוד כך:
// Creates the ARCore session.
session = new Session(/* context= */ this);
Config config = session.getConfig();
isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC);
if (isDepthSupported) {
config.setDepthMode(Config.DepthMode.AUTOMATIC);
} else {
config.setDepthMode(Config.DepthMode.DISABLED);
}
session.configure(config);
עכשיו סשן ה-AR מוגדר כראוי, והאפליקציה שלך יודעת אם היא יכולה להשתמש בתכונות שמבוססות על עומק.
כמו כן, צריך ליידע את המשתמש אם בסשן הזה נעשה שימוש בעומק או לא.
הוספת הודעה לסרגל הכלים. ההודעה תופיע בתחתית המסך:
// Add this line at the top of the file, with the other messages.
private static final String DEPTH_NOT_AVAILABLE_MESSAGE = "[Depth not supported on this device]";
בתוך onDrawFrame()
, אפשר להציג את ההודעה הזו לפי הצורך:
// Add this if-statement above messageSnackbarHelper.showMessage(this, messageToShow).
if (!isDepthSupported) {
messageToShow += "\n" + DEPTH_NOT_AVAILABLE_MESSAGE;
}
אם האפליקציה פועלת במכשיר שלא תומך בעומק, ההודעה שהוספתם עכשיו תופיע בחלק התחתון:
בשלב הבא מעדכנים את האפליקציה כך שתפעיל את ה-API של העומק ותאחזר תמונות עומק לכל פריים.
6. אחזור תמונות העומק (חלק 2)
Depth API מתעד תצפיות תלת-ממדיות על סביבת המכשיר, ומחזיר לאפליקציה תמונת עומק עם הנתונים האלה. כל פיקסל בתמונת העומק מייצג מדידת מרחק בין מצלמת המכשיר לסביבה בעולם האמיתי.
עכשיו תוכלו להשתמש בתמונות העומק האלה כדי לשפר את הרינדור וההצגה החזותית באפליקציה. השלב הראשון הוא לאחזר את תמונת העומק לכל פריים ולחבר את המרקם הזה לשימוש של ה-GPU.
קודם צריך להוסיף מחלקה חדשה לפרויקט.DepthTextureHandler
אחראית לאחזור תמונת העומק של מסגרת ARCore נתונה.
הוסף את הקובץ הזה:
src/main/java/com/google/ar/core/codelab/depth/DepthTextureHandler.java
package com.google.ar.core.codelab.depth;
import static android.opengl.GLES20.GL_CLAMP_TO_EDGE;
import static android.opengl.GLES20.GL_TEXTURE_2D;
import static android.opengl.GLES20.GL_TEXTURE_MAG_FILTER;
import static android.opengl.GLES20.GL_TEXTURE_MIN_FILTER;
import static android.opengl.GLES20.GL_TEXTURE_WRAP_S;
import static android.opengl.GLES20.GL_TEXTURE_WRAP_T;
import static android.opengl.GLES20.GL_UNSIGNED_BYTE;
import static android.opengl.GLES20.glBindTexture;
import static android.opengl.GLES20.glGenTextures;
import static android.opengl.GLES20.glTexImage2D;
import static android.opengl.GLES20.glTexParameteri;
import static android.opengl.GLES30.GL_LINEAR;
import static android.opengl.GLES30.GL_RG;
import static android.opengl.GLES30.GL_RG8;
import android.media.Image;
import com.google.ar.core.Frame;
import com.google.ar.core.exceptions.NotYetAvailableException;
/** Handle RG8 GPU texture containing a DEPTH16 depth image. */
public final class DepthTextureHandler {
private int depthTextureId = -1;
private int depthTextureWidth = -1;
private int depthTextureHeight = -1;
/**
* Creates and initializes the depth texture. This method needs to be called on a
* thread with a EGL context attached.
*/
public void createOnGlThread() {
int[] textureId = new int[1];
glGenTextures(1, textureId, 0);
depthTextureId = textureId[0];
glBindTexture(GL_TEXTURE_2D, depthTextureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}
/**
* Updates the depth texture with the content from acquireDepthImage16Bits().
* This method needs to be called on a thread with an EGL context attached.
*/
public void update(final Frame frame) {
try {
Image depthImage = frame.acquireDepthImage16Bits();
depthTextureWidth = depthImage.getWidth();
depthTextureHeight = depthImage.getHeight();
glBindTexture(GL_TEXTURE_2D, depthTextureId);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RG8,
depthTextureWidth,
depthTextureHeight,
0,
GL_RG,
GL_UNSIGNED_BYTE,
depthImage.getPlanes()[0].getBuffer());
depthImage.close();
} catch (NotYetAvailableException e) {
// This normally means that depth data is not available yet.
}
}
public int getDepthTexture() {
return depthTextureId;
}
public int getDepthWidth() {
return depthTextureWidth;
}
public int getDepthHeight() {
return depthTextureHeight;
}
}
עכשיו מוסיפים מופע של הכיתה הזו ל-DepthCodelabActivity
, כדי שיהיה עותק נגיש של תמונת העומק בכל פריים.
בפונקציה DepthCodelabActivity.java
, מוסיפים מופע של המחלקה החדשה שלנו כמשתנה 'חבר פרטי':
private final DepthTextureHandler depthTexture = new DepthTextureHandler();
בשלב הבא, מעדכנים את השיטה onSurfaceCreated()
כדי לאתחל את הטקסטורה הזו, כך שיהיה ניתן להשתמש בה על ידי תוכנות הצללה (shader) של GPU:
// Put this at the top of the "try" block in onSurfaceCreated().
depthTexture.createOnGlThread();
לסיום, ברצונך לאכלס את הטקסטורה הזו בכל מסגרת עם תמונת העומק העדכנית ביותר. כדי לעשות זאת, צריך להפעיל את השיטה update()
שיצרת למעלה בפריים האחרון שאוחזר מ-session
.
// Add this just after "frame" is created inside onDrawFrame().
if (isDepthSupported) {
depthTexture.update(frame);
}
עכשיו יש לכם תמונת עומק שמתעדכנת בכל פריים. הוא מוכן לשימוש על ידי תוכנות ההצללה.
עם זאת, שום דבר עדיין לא השתנה בהתנהגות של האפליקציה. עכשיו ניתן להשתמש בתמונת העומק כדי לשפר את האפליקציה.
7. עיבוד תמונת העומק (חלק 3)
עכשיו יש לך תמונת עומק להתנסות, ולכן חשוב לדעת איך היא נראית. בקטע הזה אפשר להוסיף לאפליקציה לחצן כדי להציג את העומק של כל פריים.
הוספת תוכנות הצללה חדשות
יש הרבה דרכים להציג תמונת עומק. כלי ההצללה הבאים מספקים תצוגה חזותית פשוטה של מיפוי צבעים.
הוספת תוכנת הצללה (shader) חדשה מסוג .vertב-Android Studio:
|
לקובץ החדש, מוסיפים את הקוד הבא:
src/main/assets/shaders/background_show_depth_map.vert
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {
v_TexCoord = a_TexCoord;
gl_Position = a_Position;
}
חוזרים על השלבים שלמעלה כדי ליצור את הכלי להצללה של המקטעים באותה ספרייה, ונותנים לו את השם background_show_depth_map.frag
.
מוסיפים את הקוד הבא לקובץ החדש:
src/main/assets/shaders/background_show_depth_map.frag
precision mediump float;
uniform sampler2D u_Depth;
varying vec2 v_TexCoord;
const highp float kMaxDepth = 20000.0; // In millimeters.
float GetDepthMillimeters(vec4 depth_pixel_value) {
return 255.0 * (depth_pixel_value.r + depth_pixel_value.g * 256.0);
}
// Returns an interpolated color in a 6 degree polynomial interpolation.
vec3 GetPolynomialColor(in float x,
in vec4 kRedVec4, in vec4 kGreenVec4, in vec4 kBlueVec4,
in vec2 kRedVec2, in vec2 kGreenVec2, in vec2 kBlueVec2) {
// Moves the color space a little bit to avoid pure red.
// Removes this line for more contrast.
x = clamp(x * 0.9 + 0.03, 0.0, 1.0);
vec4 v4 = vec4(1.0, x, x * x, x * x * x);
vec2 v2 = v4.zw * v4.z;
return vec3(
dot(v4, kRedVec4) + dot(v2, kRedVec2),
dot(v4, kGreenVec4) + dot(v2, kGreenVec2),
dot(v4, kBlueVec4) + dot(v2, kBlueVec2)
);
}
// Returns a smooth Percept colormap based upon the Turbo colormap.
vec3 PerceptColormap(in float x) {
const vec4 kRedVec4 = vec4(0.55305649, 3.00913185, -5.46192616, -11.11819092);
const vec4 kGreenVec4 = vec4(0.16207513, 0.17712472, 15.24091500, -36.50657960);
const vec4 kBlueVec4 = vec4(-0.05195877, 5.18000081, -30.94853351, 81.96403246);
const vec2 kRedVec2 = vec2(27.81927491, -14.87899417);
const vec2 kGreenVec2 = vec2(25.95549545, -5.02738237);
const vec2 kBlueVec2 = vec2(-86.53476570, 30.23299484);
const float kInvalidDepthThreshold = 0.01;
return step(kInvalidDepthThreshold, x) *
GetPolynomialColor(x, kRedVec4, kGreenVec4, kBlueVec4,
kRedVec2, kGreenVec2, kBlueVec2);
}
void main() {
vec4 packed_depth = texture2D(u_Depth, v_TexCoord.xy);
highp float depth_mm = GetDepthMillimeters(packed_depth);
highp float normalized_depth = depth_mm / kMaxDepth;
vec4 depth_color = vec4(PerceptColormap(normalized_depth), 1.0);
gl_FragColor = depth_color;
}
בשלב הבא, צריך לעדכן את המחלקה BackgroundRenderer
כדי להשתמש בכלי ההצללה החדשים האלה, שנמצאים ב-src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java
.
מוסיפים את נתיבי הקבצים לתוכנות ההצללה בחלק העליון של המחלקה:
// Add these under the other shader names at the top of the class.
private static final String DEPTH_VERTEX_SHADER_NAME = "shaders/background_show_depth_map.vert";
private static final String DEPTH_FRAGMENT_SHADER_NAME = "shaders/background_show_depth_map.frag";
מוסיפים עוד משתני חבר למחלקה BackgroundRenderer
, כי יפעלו בה שני תוכנות הצללה:
// Add to the top of file with the rest of the member variables.
private int depthProgram;
private int depthTextureParam;
private int depthTextureId = -1;
private int depthQuadPositionParam;
private int depthQuadTexCoordParam;
צריך להוסיף שיטה חדשה לאכלוס השדות האלה:
// Add this method below createOnGlThread().
public void createDepthShaders(Context context, int depthTextureId) throws IOException {
int vertexShader =
ShaderUtil.loadGLShader(
TAG, context, GLES20.GL_VERTEX_SHADER, DEPTH_VERTEX_SHADER_NAME);
int fragmentShader =
ShaderUtil.loadGLShader(
TAG, context, GLES20.GL_FRAGMENT_SHADER, DEPTH_FRAGMENT_SHADER_NAME);
depthProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(depthProgram, vertexShader);
GLES20.glAttachShader(depthProgram, fragmentShader);
GLES20.glLinkProgram(depthProgram);
GLES20.glUseProgram(depthProgram);
ShaderUtil.checkGLError(TAG, "Program creation");
depthTextureParam = GLES20.glGetUniformLocation(depthProgram, "u_Depth");
ShaderUtil.checkGLError(TAG, "Program parameters");
depthQuadPositionParam = GLES20.glGetAttribLocation(depthProgram, "a_Position");
depthQuadTexCoordParam = GLES20.glGetAttribLocation(depthProgram, "a_TexCoord");
this.depthTextureId = depthTextureId;
}
מוסיפים את השיטה הבאה, שמשמשת לשרטוט באמצעות תוכנות ההצללה הבאות בכל מסגרת:
// Put this at the bottom of the file.
public void drawDepth(@NonNull Frame frame) {
if (frame.hasDisplayGeometryChanged()) {
frame.transformCoordinates2d(
Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
quadCoords,
Coordinates2d.TEXTURE_NORMALIZED,
quadTexCoords);
}
if (frame.getTimestamp() == 0 || depthTextureId == -1) {
return;
}
// Ensure position is rewound before use.
quadTexCoords.position(0);
// No need to test or write depth, the screen quad has arbitrary depth, and is expected
// to be drawn first.
GLES20.glDisable(GLES20.GL_DEPTH_TEST);
GLES20.glDepthMask(false);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId);
GLES20.glUseProgram(depthProgram);
GLES20.glUniform1i(depthTextureParam, 0);
// Set the vertex positions and texture coordinates.
GLES20.glVertexAttribPointer(
depthQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadCoords);
GLES20.glVertexAttribPointer(
depthQuadTexCoordParam, TEXCOORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadTexCoords);
// Draws the quad.
GLES20.glEnableVertexAttribArray(depthQuadPositionParam);
GLES20.glEnableVertexAttribArray(depthQuadTexCoordParam);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(depthQuadPositionParam);
GLES20.glDisableVertexAttribArray(depthQuadTexCoordParam);
// Restore the depth state for further drawing.
GLES20.glDepthMask(true);
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
ShaderUtil.checkGLError(TAG, "BackgroundRendererDraw");
}
הוספת מתג
עכשיו, שיש לכם את היכולת לעבד את מפת העומק, כדאי להשתמש בה! אפשר להוסיף לחצן להפעלה ולהשבתה של הרינדור.
בחלק העליון של הקובץ DepthCodelabActivity
, מוסיפים ייבוא כדי להשתמש בלחצן:
import android.widget.Button;
מעדכנים את הכיתה כדי להוסיף חבר בוליאני שמציין אם הופעל רינדור עומק: (התכונה מושבתת כברירת מחדל):
private boolean showDepthMap = false;
לאחר מכן, מוסיפים את הלחצן ששולט בערך הבוליאני showDepthMap
בסוף ה-method onCreate()
:
final Button toggleDepthButton = (Button) findViewById(R.id.toggle_depth_button);
toggleDepthButton.setOnClickListener(
view -> {
if (isDepthSupported) {
showDepthMap = !showDepthMap;
toggleDepthButton.setText(showDepthMap ? R.string.hide_depth : R.string.show_depth);
} else {
showDepthMap = false;
toggleDepthButton.setText(R.string.depth_not_available);
}
});
מוסיפים את המחרוזות האלה אל res/values/strings.xml
:
<string translatable="false" name="show_depth">Show Depth</string>
<string translatable="false" name="hide_depth">Hide Depth</string>
<string translatable="false" name="depth_not_available">Depth Not Available</string>
צריך להוסיף את הלחצן הזה לתחתית של פריסת האפליקציה ב-res/layout/activity_main.xml
:
<Button
android:id="@+id/toggle_depth_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:gravity="center"
android:text="@string/show_depth"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"/>
הלחצן שולט עכשיו בערך של showDepthMap
הבוליאני. אפשר להשתמש בדגל הזה כדי לקבוע אם מפת העומק תעובד.
חוזרים ל-method onDrawFrame()
ב-DepthCodelabActivity
, מוסיפים:
// Add this snippet just under backgroundRenderer.draw(frame);
if (showDepthMap) {
backgroundRenderer.drawDepth(frame);
}
מעבירים את טקסט העומק אל backgroundRenderer
על ידי הוספת השורה הבאה ב-onSurfaceCreated()
:
// Add to onSurfaceCreated() after backgroundRenderer.createonGlThread(/*context=*/ this);
backgroundRenderer.createDepthShaders(/*context=*/ this, depthTexture.getDepthTexture());
עכשיו אפשר לראות את תמונת העומק של כל פריים באמצעות לחיצה על הלחצן שבפינה הימנית העליונה של המסך.
הפעולה פועלת ללא תמיכה ב-Depth API | פעילות עם תמיכה ב-Depth API |
[אופציונלי] אנימציה של עומק מפואר
האפליקציה מציגה כרגע את מפת העומק באופן ישיר. פיקסלים אדומים מייצגים אזורים קרובים. פיקסלים כחולים מייצגים אזורים רחוקים.
יש הרבה דרכים להציג מידע על עומק. בקטע הזה, תשנה את טווח ההצללה לעובי הדופק מדי פעם, על ידי שינוי של ההצללה כך שיציג רק את העומק בתוך התדרים שמתרחקים שוב ושוב מהמצלמה.
כדי להתחיל, מוסיפים את המשתנים הבאים בחלק העליון של background_show_depth_map.frag
:
uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;
- לאחר מכן, משתמשים בערכים הבאים כדי לסנן אילו פיקסלים ייכללו בערכי העומק בפונקציה
main()
של תוכנת ההצללה:
// Add this line at the end of main().
gl_FragColor.a = clamp(1.0 - abs((depth_mm - u_DepthRangeToRenderMm) / kDepthWidthToRenderMm), 0.0, 1.0);
בשלב הבא, מעדכנים את BackgroundRenderer.java
כדי לשמור את הפרמטרים האלה של תוכנת ההצללה. מוסיפים את השדות הבאים לראש הכיתה:
private static final float MAX_DEPTH_RANGE_TO_RENDER_MM = 20000.0f;
private float depthRangeToRenderMm = 0.0f;
private int depthRangeToRenderMmParam;
בתוך ה-method createDepthShaders()
, מוסיפים את הערכים הבאים כדי להתאים את הפרמטרים האלה עם תוכנת ההצללה:
depthRangeToRenderMmParam = GLES20.glGetUniformLocation(depthProgram, "u_DepthRangeToRenderMm");
- לבסוף, אפשר לשלוט בטווח הזה לאורך זמן באמצעות ה-method
drawDepth()
. מוסיפים את הקוד הבא, שמגדיל את הטווח בכל פעם שמשרטטת מסגרת:
// Enables alpha blending.
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
// Updates range each time draw() is called.
depthRangeToRenderMm += 50.0f;
if (depthRangeToRenderMm > MAX_DEPTH_RANGE_TO_RENDER_MM) {
depthRangeToRenderMm = 0.0f;
}
// Passes latest value to the shader.
GLES20.glUniform1f(depthRangeToRenderMmParam, depthRangeToRenderMm);
עכשיו העומק מוצג באופן חזותי כדופק מונפש שזורם בסצנה.
ניתן לשנות את הערכים המוצגים כאן כדי להאט את הדופק, מהר יותר, רחב יותר, צר יותר וכו'. אפשר גם לנסות דרכים חדשות לגמרי לשנות את תוכנת ההצללה כדי להציג מידע מעמיק.
8. שימוש ב-Depth API להסתרה (חלק 4)
עכשיו מטפלים בחסימת אובייקטים באפליקציה.
'חסימה' מתייחסת למה שקורה כשלא ניתן לעבד את האובייקט הווירטואלי במלואו, כי יש אובייקטים אמיתיים בין האובייקט הווירטואלי לבין המצלמה. כדי שחוויות AR יהיו סוחפות, חשוב לנהל את ההסתרה.
עיבוד נכון של אובייקטים וירטואליים בזמן אמת משפר את המציאותיות והאמינות של הסצנה המשופרת. דוגמאות נוספות זמינות בסרטון שלנו בנושא שילוב מציאות עם Depth API.
בקטע הזה, תעדכנו את האפליקציה כך שתכלול אובייקטים וירטואליים רק אם קיים עומק זמין.
הוספת תוכנות הצללה חדשות לאובייקטים
כמו בקטעים הקודמים, תוכלו להוסיף תוכנות הצללה חדשות כדי לתמוך במידע על העומק. הפעם תוכלו להעתיק את תוכנות ההצללה הקיימות של האובייקטים ולהוסיף פונקציונליות של הסתרה.
חשוב להשאיר את שתי הגרסאות של תוכנות ההצללה לאובייקטים, כדי שהאפליקציה תוכל להחליט בזמן הריצה אם לתמוך בעומק.
יוצרים עותקים של קובצי ההצללה (shader) object.vert
ו-object.frag
בספרייה src/main/assets/shaders
.
|
בתוך occlusion_object.vert
, מוסיפים את המשתנה הבא מעל main()
:
varying vec3 v_ScreenSpacePosition;
כדי להגדיר את המשתנה הזה בתחתית של main()
:
v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;
מעדכנים את occlusion_object.frag
על ידי הוספת המשתנים הבאים מעל main()
בחלק העליון של הקובץ:
varying vec3 v_ScreenSpacePosition;
uniform sampler2D u_Depth;
uniform mat3 u_UvTransform;
uniform float u_DepthTolerancePerMm;
uniform float u_OcclusionAlpha;
uniform float u_DepthAspectRatio;
- כדי שיהיה קל יותר לטפל בנתוני העומק, אפשר להוסיף את פונקציות העזרה הבאות מעל
main()
בחלונית ההצללה:
float GetDepthMillimeters(in vec2 depth_uv) {
// Depth is packed into the red and green components of its texture.
// The texture is a normalized format, storing millimeters.
vec3 packedDepthAndVisibility = texture2D(u_Depth, depth_uv).xyz;
return dot(packedDepthAndVisibility.xy, vec2(255.0, 256.0 * 255.0));
}
// Returns linear interpolation position of value between min and max bounds.
// E.g., InverseLerp(1100, 1000, 2000) returns 0.1.
float InverseLerp(in float value, in float min_bound, in float max_bound) {
return clamp((value - min_bound) / (max_bound - min_bound), 0.0, 1.0);
}
// Returns a value between 0.0 (not visible) and 1.0 (completely visible)
// Which represents how visible or occluded is the pixel in relation to the
// depth map.
float GetVisibility(in vec2 depth_uv, in float asset_depth_mm) {
float depth_mm = GetDepthMillimeters(depth_uv);
// Instead of a hard z-buffer test, allow the asset to fade into the
// background along a 2 * u_DepthTolerancePerMm * asset_depth_mm
// range centered on the background depth.
float visibility_occlusion = clamp(0.5 * (depth_mm - asset_depth_mm) /
(u_DepthTolerancePerMm * asset_depth_mm) + 0.5, 0.0, 1.0);
// Depth close to zero is most likely invalid, do not use it for occlusions.
float visibility_depth_near = 1.0 - InverseLerp(
depth_mm, /*min_depth_mm=*/150.0, /*max_depth_mm=*/200.0);
// Same for very high depth values.
float visibility_depth_far = InverseLerp(
depth_mm, /*min_depth_mm=*/17500.0, /*max_depth_mm=*/20000.0);
float visibility =
max(max(visibility_occlusion, u_OcclusionAlpha),
max(visibility_depth_near, visibility_depth_far));
return visibility;
}
עכשיו, צריך לעדכן את main()
ב-occlusion_object.frag
כדי להיות מודעים לעומק ולהחיל חסימה. מוסיפים את השורות הבאות בתחתית הקובץ:
const float kMToMm = 1000.0;
float asset_depth_mm = v_ViewPosition.z * kMToMm * -1.;
vec2 depth_uvs = (u_UvTransform * vec3(v_ScreenSpacePosition.xy, 1)).xy;
gl_FragColor.a *= GetVisibility(depth_uvs, asset_depth_mm);
עכשיו, כשיש לכם גרסה חדשה של תוכנות הצללה לאובייקטים, אתם יכולים לשנות את הקוד של כלי הרינדור.
הסתרה של אובייקט בעיבוד
צריך ליצור עותק של הכיתה ObjectRenderer
, שנמצאה בsrc/main/java/com/google/ar/core/codelab/common/rendering/ObjectRenderer.java
.
- בחירת הכיתה
ObjectRenderer
- לחיצה ימנית > העתקה
- בוחרים את תיקיית הרינדור.
- לחיצה ימנית > הדבקה
- לשנות את שם הכיתה ל-
OcclusionObjectRenderer
.
המחלקה החדשה עם השם החדש אמורה להופיע עכשיו באותה תיקייה:
פותחים את הקובץ OcclusionObjectRenderer.java
החדש שנוצר ומשנים את הנתיבים של ההצללה בחלק העליון של הקובץ:
private static final String VERTEX_SHADER_NAME = "shaders/occlusion_object.vert";
private static final String FRAGMENT_SHADER_NAME = "shaders/occlusion_object.frag";
- מוסיפים את משתני החברים בעלי הקשר ביניהם עם האחרים בחלק העליון של הכיתה. המשתנים יתאימו את החדות של גבול ההסתרה.
// Shader location: depth texture
private int depthTextureUniform;
// Shader location: transform to depth uvs
private int depthUvTransformUniform;
// Shader location: depth tolerance property
private int depthToleranceUniform;
// Shader location: maximum transparency for the occluded part.
private int occlusionAlphaUniform;
private int depthAspectRatioUniform;
private float[] uvTransform = null;
private int depthTextureId;
יוצרים את משתני החברים הבאים עם ערכי ברירת המחדל בחלק העליון של המחלקה:
// These values will be changed each frame based on the distance to the object.
private float depthAspectRatio = 0.0f;
private final float depthTolerancePerMm = 0.015f;
private final float occlusionsAlpha = 0.0f;
מאתחלים את הפרמטרים האחידים של תוכנת ההצללה בשיטה createOnGlThread()
:
// Occlusions Uniforms. Add these lines before the first call to ShaderUtil.checkGLError
// inside the createOnGlThread() method.
depthTextureUniform = GLES20.glGetUniformLocation(program, "u_Depth");
depthUvTransformUniform = GLES20.glGetUniformLocation(program, "u_UvTransform");
depthToleranceUniform = GLES20.glGetUniformLocation(program, "u_DepthTolerancePerMm");
occlusionAlphaUniform = GLES20.glGetUniformLocation(program, "u_OcclusionAlpha");
depthAspectRatioUniform = GLES20.glGetUniformLocation(program, "u_DepthAspectRatio");
- חשוב לוודא שהערכים האלה מתעדכנים בכל פעם שהם מוצגים על ידי עדכון ה-method
draw()
:
// Add after other GLES20.glUniform calls inside draw().
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId);
GLES20.glUniform1i(depthTextureUniform, 1);
GLES20.glUniformMatrix3fv(depthUvTransformUniform, 1, false, uvTransform, 0);
GLES20.glUniform1f(depthToleranceUniform, depthTolerancePerMm);
GLES20.glUniform1f(occlusionAlphaUniform, occlusionsAlpha);
GLES20.glUniform1f(depthAspectRatioUniform, depthAspectRatio);
מוסיפים את השורות הבאות בתוך draw()
כדי להפעיל מצב מיזוג בעיבוד, כך שאפשר יהיה להחיל שקיפות על אובייקטים וירטואליים כשהם חסומים:
// Add these lines just below the code-block labeled "Enable vertex arrays"
GLES20.glEnable(GLES20.GL_BLEND);
GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
// Add these lines just above the code-block labeled "Disable vertex arrays"
GLES20.glDisable(GLES20.GL_BLEND);
GLES20.glDepthMask(true);
- צריך להוסיף את השיטות הבאות כדי שמתקשרים של
OcclusionObjectRenderer
יוכלו לספק את נתוני העומק:
// Add these methods at the bottom of the OcclusionObjectRenderer class.
public void setUvTransformMatrix(float[] transform) {
uvTransform = transform;
}
public void setDepthTexture(int textureId, int width, int height) {
depthTextureId = textureId;
depthAspectRatio = (float) width / (float) height;
}
שליטה בהסתרה של אובייקטים
עכשיו, אחרי שיש לך OcclusionObjectRenderer
חדש, אפשר להוסיף אותו ל-DepthCodelabActivity
ולבחור מתי ואיך להשתמש ברינדור של הסתרה.
כדי להפעיל את הלוגיקה הזו, מוסיפים לפעילות מופע של OcclusionObjectRenderer
, כך שגם ObjectRenderer
וגם OcclusionObjectRenderer
יהיו חברים בקבוצה DepthCodelabActivity
:
// Add this include at the top of the file.
import com.google.ar.core.codelab.common.rendering.OcclusionObjectRenderer;
// Add this member just below the existing "virtualObject", so both are present.
private final OcclusionObjectRenderer occludedVirtualObject = new OcclusionObjectRenderer();
- יש לך אפשרות לקבוע מתי ייעשה שימוש ב
occludedVirtualObject
הזה, על סמך התמיכה של המכשיר הנוכחי ב-Depth API. מוסיפים את השורות הבאות בתוך ה-methodonSurfaceCreated
, מתחת למיקום שבו מוגדרvirtualObject
:
if (isDepthSupported) {
occludedVirtualObject.createOnGlThread(/*context=*/ this, "models/andy.obj", "models/andy.png");
occludedVirtualObject.setDepthTexture(
depthTexture.getDepthTexture(),
depthTexture.getDepthWidth(),
depthTexture.getDepthHeight());
occludedVirtualObject.setMaterialProperties(0.0f, 2.0f, 0.5f, 6.0f);
}
במכשירים שבהם אין תמיכה בעומק, המכונה occludedVirtualObject
נוצרת אבל לא נעשה בה שימוש. בטלפונים עם עומק, שתי הגרסאות מופעלות והחלטה בזמן הריצה באיזו גרסה של כלי הרינדור להשתמש במהלך השרטוט.
בתוך ה-method onDrawFrame()
, מאתרים את הקוד הקיים:
virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
virtualObject.draw(viewmtx, projmtx, colorCorrectionRgba, OBJECT_COLOR);
מחליפים את הקוד הזה בקוד הבא:
if (isDepthSupported) {
occludedVirtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
occludedVirtualObject.draw(viewmtx, projmtx, colorCorrectionRgba, OBJECT_COLOR);
} else {
virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
virtualObject.draw(viewmtx, projmtx, colorCorrectionRgba, OBJECT_COLOR);
}
לבסוף, יש לוודא שתמונת העומק ממופה כראוי לעיבוד הפלט. מכיוון שתמונת העומק היא ברזולוציה שונה, ועשויה להיות גם יחס גובה-רוחב שונה מזה של המסך, יכול להיות שהקואורדינטות של המרקם יהיו שונות בינן לבין תמונת המצלמה.
- מוסיפים את שיטת העזרה
getTextureTransformMatrix()
בתחתית הקובץ. השיטה הזו מחזירה מטריצת טרנספורמציה שכשמפעילים אותה, קרינת UV של שטח המסך מתאימה בצורה נכונה לקואורדינטות של טקסטורת הריבועים שמשמשת לעיבוד פיד המצלמה. היא לוקחת בחשבון גם את כיוון התצוגה של המכשיר.
private static float[] getTextureTransformMatrix(Frame frame) {
float[] frameTransform = new float[6];
float[] uvTransform = new float[9];
// XY pairs of coordinates in NDC space that constitute the origin and points along the two
// principal axes.
float[] ndcBasis = {0, 0, 1, 0, 0, 1};
// Temporarily store the transformed points into outputTransform.
frame.transformCoordinates2d(
Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
ndcBasis,
Coordinates2d.TEXTURE_NORMALIZED,
frameTransform);
// Convert the transformed points into an affine transform and transpose it.
float ndcOriginX = frameTransform[0];
float ndcOriginY = frameTransform[1];
uvTransform[0] = frameTransform[2] - ndcOriginX;
uvTransform[1] = frameTransform[3] - ndcOriginY;
uvTransform[2] = 0;
uvTransform[3] = frameTransform[4] - ndcOriginX;
uvTransform[4] = frameTransform[5] - ndcOriginY;
uvTransform[5] = 0;
uvTransform[6] = ndcOriginX;
uvTransform[7] = ndcOriginY;
uvTransform[8] = 1;
return uvTransform;
}
getTextureTransformMatrix()
מחייב את הייבוא הבא בחלק העליון של הקובץ:
import com.google.ar.core.Coordinates2d;
כדאי לחשב את הטרנספורמציה בין הקואורדינטות האלה של הטקסטורה בכל פעם שמרקם המסך משתנה (למשל, אם המסך מסתובב). הפונקציונליות הזו מוגבלת.
מוסיפים את הדגל הבא בחלק העליון של הקובץ:
// Add this member at the top of the file.
private boolean calculateUVTransform = true;
- בתוך
onDrawFrame()
, בודקים אם צריך לחשב מחדש את הטרנספורמציה שנשמרה אחרי יצירת הפריים והמצלמה:
// Add these lines inside onDrawFrame() after frame.getCamera().
if (frame.hasDisplayGeometryChanged() || calculateUVTransform) {
calculateUVTransform = false;
float[] transform = getTextureTransformMatrix(frame);
occludedVirtualObject.setUvTransformMatrix(transform);
}
לאחר ביצוע השינויים האלה, אפשר להריץ את האפליקציה עם חסימה של אובייקט וירטואלי!
עכשיו האפליקציה אמורה לפעול בצורה חלקה בכל הטלפונים, ולהשתמש באופן אוטומטי בעומק להסתרה כאשר היא נתמכת.
אפליקציה פועלת עם תמיכה ב-Depth API | אפליקציה פועלת ללא תמיכה ב-Depth API |
9. [אופציונלי] שיפור איכות ההסתרה
השיטה להסתרה מבוססת-עומק, שיושמה למעלה, מספקת חסימה עם גבולות חדים. ככל שהמצלמה מתרחקת מהעצם, מדידות העומק עשויות להיות פחות מדויקות ולכן עשויים להיות פגמים חזותיים.
אנחנו יכולים לצמצם את הבעיה הזו על ידי הוספת טשטוש נוסף לבדיקת ההסתרה, כך שהקצה יהיה חלק יותר לאובייקטים וירטואליים מוסתרים.
occlusion_object.frag
מוסיפים את המשתנה האחיד הבא בחלק העליון של occlusion_object.frag
:
uniform float u_OcclusionBlurAmount;
מוסיפים את פונקציית העזרה הזו מעל main()
בכלי ההצללה, שמחילה טשטוש ליבה (kernel) על דגימת ההסתרה:
float GetBlurredVisibilityAroundUV(in vec2 uv, in float asset_depth_mm) {
// Kernel used:
// 0 4 7 4 0
// 4 16 26 16 4
// 7 26 41 26 7
// 4 16 26 16 4
// 0 4 7 4 0
const float kKernelTotalWeights = 269.0;
float sum = 0.0;
vec2 blurriness = vec2(u_OcclusionBlurAmount,
u_OcclusionBlurAmount * u_DepthAspectRatio);
float current = 0.0;
current += GetVisibility(uv + vec2(-1.0, -2.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(+1.0, -2.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(-1.0, +2.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(+1.0, +2.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(-2.0, +1.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(+2.0, +1.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(-2.0, -1.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(+2.0, -1.0) * blurriness, asset_depth_mm);
sum += current * 4.0;
current = 0.0;
current += GetVisibility(uv + vec2(-2.0, -0.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(+2.0, +0.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(+0.0, +2.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(-0.0, -2.0) * blurriness, asset_depth_mm);
sum += current * 7.0;
current = 0.0;
current += GetVisibility(uv + vec2(-1.0, -1.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(+1.0, -1.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(-1.0, +1.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(+1.0, +1.0) * blurriness, asset_depth_mm);
sum += current * 16.0;
current = 0.0;
current += GetVisibility(uv + vec2(+0.0, +1.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(-0.0, -1.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(-1.0, -0.0) * blurriness, asset_depth_mm);
current += GetVisibility(uv + vec2(+1.0, +0.0) * blurriness, asset_depth_mm);
sum += current * 26.0;
sum += GetVisibility(uv , asset_depth_mm) * 41.0;
return sum / kKernelTotalWeights;
}
החלפת השורה הקיימת הזו ב-main()
:
gl_FragColor.a *= GetVisibility(depth_uvs, asset_depth_mm);
בשורה הזו:
gl_FragColor.a *= GetBlurredVisibilityAroundUV(depth_uvs, asset_depth_mm);
כדאי לעדכן את כלי הרינדור כדי לנצל את הפונקציונליות החדשה של תוכנת ההצללה.
OcclusionObjectRenderer.java
מוסיפים את משתני החברים הבאים בחלק העליון של הכיתה:
private int occlusionBlurUniform;
private final float occlusionsBlur = 0.01f;
מוסיפים את קטעי הקוד הבאים בתוך ה-method createOnGlThread
:
// Add alongside the other calls to GLES20.glGetUniformLocation.
occlusionBlurUniform = GLES20.glGetUniformLocation(program, "u_OcclusionBlurAmount");
מוסיפים את קטעי הקוד הבאים בתוך ה-method draw
:
// Add alongside the other calls to GLES20.glUniform1f.
GLES20.glUniform1f(occlusionBlurUniform, occlusionsBlur);
השוואה חזותיתעכשיו גבול החסימה אמור להיות חלק יותר עם השינויים האלה. |
10. Build-Run-Test
יצירה והפעלה של אפליקציה
- מחברים מכשיר Android באמצעות USB.
- בוחרים באפשרות File > (קובץ >) יצירה והפעלה.
- שמירה בשם: ARCodeLab.apk
- ממתינים להשלמת תהליך היצירה של האפליקציה ופריסת הפריסה במכשיר.
בפעם הראשונה שמנסים לפרוס את האפליקציה במכשיר:
- צריך לאפשר ניפוי באגים ב-USB במכשיר. כדי להמשיך, צריך לבחור באפשרות 'אישור'.
- תוצג לך שאלה אם לאפליקציה יש הרשאה להשתמש במצלמת המכשיר. כדי להמשיך להשתמש בפונקציונליות של AR, צריך לאפשר גישה.
בדיקת האפליקציה
בזמן הפעלת האפליקציה, אפשר לבדוק את ההתנהגות הבסיסית שלה על ידי החזקת המכשיר, לנוע על פני השטח ולסרוק אזור לאט. כדאי לנסות לאסוף נתונים במשך 10 שניות לפחות ולסרוק את האזור מכמה כיוונים לפני שעוברים לשלב הבא.
פתרון בעיות
הגדרת מכשיר Android לפיתוח
- מחברים את המכשיר למכונת הפיתוח באמצעות כבל USB. אם אתם מפתחים באמצעות Windows, ייתכן שתצטרכו להתקין את מנהל התקן ה-USB המתאים למכשיר שלכם.
- כדי להפעיל ניפוי באגים ב-USB בחלון אפשרויות למפתחים, פועלים לפי השלבים הבאים:
- פותחים את אפליקציית ההגדרות.
- אם במכשיר מותקנת מערכת Android מגרסה 8.0 ואילך, בוחרים באפשרות מערכת. אם לא, תוכלו לדלג לשלב הבא.
- גוללים לחלק התחתון ובוחרים באפשרות מידע על הטלפון.
- גוללים לחלק התחתון ומקישים על מספר Build 7 פעמים.
- חוזרים למסך הקודם, גוללים לחלק התחתון ומקישים על אפשרויות למפתחים.
- בחלון אפשרויות למפתחים, גוללים למטה כדי למצוא את האפשרות ניפוי באגים ב-USB ולהפעיל אותה.
מידע מפורט יותר על התהליך הזה זמין באתר למפתחי Android של Google.
כשלים ב-build שקשורים לרישיונות
אם יש כשל ב-build שקשור לרישיונות (ההתקנה של חבילות ה-Android SDK הבאות נכשלה כי חלק מהרישיונות לא אושרו), אפשר להשתמש בפקודות הבאות כדי לבדוק ולאשר את הרישיונות האלה:
cd
<path to Android SDK>
tools/bin/sdkmanager --licenses
11. מזל טוב
כל הכבוד! יצרת בהצלחה את אפליקציית המציאות הרבודה (Augmented Reality) הראשונה שלך, שמבוססת על עומק, באמצעות ARCore Depth API של Google.