1. לפני שמתחילים
ARCore היא פלטפורמה ליצירת אפליקציות של מציאות רבודה (AR) במכשירים ניידים. באמצעות ממשקי API שונים, ARCore מאפשר למכשיר של המשתמש לצפות בסביבה שלו ולקבל מידע עליה, ולקיים אינטראקציה עם המידע הזה.
ב-Codelab הזה תעברו את התהליך של בניית אפליקציה פשוטה עם תכונות AR שמשתמשת ב-ARCore Depth API.
דרישות מוקדמות
ה-Codelab הזה מיועד למפתחים עם ידע במושגי יסוד של AR.
מה תפַתחו

תבנו אפליקציה שמשתמשת בתמונת העומק של כל פריים כדי להציג את הגיאומטריה של הסצנה ולבצע הסתרה של נכסים וירטואליים שהוצבו. תקבלו הסבר על הפעולות הבאות:
- בדיקה אם יש תמיכה ב-Depth API בטלפון
- שליפת תמונת העומק של כל פריים
- הדמיה של מידע על עומק בכמה דרכים (ראו את האנימציה שלמעלה)
- שימוש בעומק כדי להגביר את הריאליזם של אפליקציות עם הסתרה
- איך לטפל בצורה נכונה בטלפונים שלא תומכים ב-Depth API
מה תצטרכו
דרישות חומרה
- מכשיר שתומך ב-ARCore, שמחובר למחשב הפיתוח באמצעות כבל USB. המכשיר צריך לתמוך גם ב-Depth API. רשימת המכשירים הנתמכים ה-API של עומק זמין רק ב-Android.
- מפעילים ניפוי באגים ב-USB במכשיר.
דרישות תוכנה
- ARCore SDK 1.31.0 ואילך.
- מחשב פיתוח עם Android Studio (גרסה 3.0 ואילך).
2. ARCore ו-Depth API
Depth API משתמש במצלמת RGB של מכשיר נתמך כדי ליצור מפות עומק (נקראות גם תמונות עומק). אתם יכולים להשתמש במידע שמופיע במפת העומק כדי לגרום לאובייקטים וירטואליים להיראות בצורה מדויקת, לפני או מאחורי אובייקטים בעולם האמיתי, וכך ליצור חוויות משתמש סוחפות ומציאותיות.
ARCore Depth API מספק גישה לתמונות עומק שתואמות לכל פריים שמסופק על ידי ARCore Session. כל פיקסל מספק מדידת מרחק מהמצלמה לסביבה, וכך משפר את הריאליזם של אפליקציית ה-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 ולוחצים על Open an existing Android Studio project (פתיחת פרויקט קיים של Android Studio).
- מוצאים את הספרייה שבה חילצתם את קובץ ה-ZIP שהורדתם למעלה, ופותחים את הספרייה
depth_codelab_io2020.
זהו פרויקט Gradle יחיד עם כמה מודולים. אם החלונית Project בפינה הימנית העליונה של Android Studio לא מוצגת כבר בחלונית Project, לוחצים על Projects בתפריט הנפתח.
התוצאה אמורה להיראות כך:
| הפרויקט הזה מכיל את המודולים הבאים:
|
תעבדו במודול part0_work. יש גם פתרונות מלאים לכל חלק ב-Codelab. כל מודול הוא אפליקציה שאפשר לבנות.
4. הפעלת האפליקציה למתחילים
- לוחצים על הפעלה > הפעלה... > 'part0_work'. בתיבת הדו-שיח Select Deployment Target שמופיעה, המכשיר שלכם אמור להופיע בקטע Connected Devices.
- בוחרים את המכשיר ולוחצים על אישור. Android Studio ייצור את האפליקציה הראשונית ויפעיל אותה במכשיר.
- האפליקציה תבקש הרשאות גישה למצלמה. מקישים על אישור כדי להמשיך.

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

כדי לפתור את הבעיה, נשתמש ב-Depth API כדי לשפר את חוויית הצפייה המציאותית באפליקציה הזו.
5. בדיקה אם יש תמיכה ב-Depth API (חלק 1)
ממשק ה-API של ARCore Depth פועל רק בקבוצת משנה של מכשירים נתמכים. לפני שמשלבים פונקציונליות באפליקציה באמצעות תמונות העומק האלה, צריך לוודא שהאפליקציה פועלת במכשיר נתמך.
מוסיפים חבר פרטי חדש ל-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;
}
אם האפליקציה מופעלת במכשיר שלא תומך בעומק, ההודעה שזה עתה הוספתם מופיעה בתחתית:

לאחר מכן, תעדכנו את האפליקציה כדי לקרוא ל-Depth API ולאחזר תמונות עומק לכל פריים.
6. שליפת תמונות העומק (חלק 2)
ממשק ה-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() כדי לאתחל את הטקסטורה הזו, כך שניתן יהיה להשתמש בה ב-shaders של ה-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)
עכשיו יש לכם תמונה עם עומק שאפשר לשחק איתה, ורצוי לראות איך היא נראית. בקטע הזה תוסיפו לאפליקציה לחצן להצגת העומק של כל פריים.
הוספת הצללות חדשות
יש הרבה דרכים לצפות בתמונת עומק. ה-shaders הבאים מספקים ויזואליזציה פשוטה של מיפוי צבעים.
| הוספת קובץ 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;
}
חוזרים על השלבים שלמעלה כדי ליצור את shader השברים באותה ספרייה, ונותנים לו את השם 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 כדי להשתמש ב-shaders החדשים האלה, שנמצאים ב-src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java.
מוסיפים את נתיבי הקבצים של ה-shaders בחלק העליון של המחלקה:
// 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, כי היא תפעיל שני shaders:
// 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 לסוף השיטה 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. משתמשים בדגל הזה כדי לקבוע אם מפת העומק תוצג.
חוזרים לשיטה 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 | ||
[Optional] אנימציית עומק מתקדמת
האפליקציה מציגה כרגע את מפת העומק ישירות. פיקסלים אדומים מייצגים אזורים קרובים. פיקסלים כחולים מייצגים אזורים מרוחקים.
|
|
יש הרבה דרכים להעביר מידע על עומק. בקטע הזה, תשנו את ה-shader כך שעומק הדופק ישתנה מעת לעת. לשם כך, תשנו את ה-shader כך שיוצג רק עומק בתוך פסים שמתרחקים מהמצלמה שוב ושוב.
מתחילים בהוספת המשתנים האלה לחלק העליון של background_show_depth_map.frag:
uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;
- לאחר מכן, משתמשים בערכים האלה כדי לסנן את הפיקסלים שרוצים לכסות בערכי עומק בפונקציה
main()של ה-shader:
// 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;
בתוך הפונקציה createDepthShaders(), מוסיפים את הקוד הבא כדי להתאים את הפרמטרים האלה לתוכנית של Shader:
depthRangeToRenderMmParam = GLES20.glGetUniformLocation(depthProgram, "u_DepthRangeToRenderMm");
- לבסוף, אפשר לשלוט בטווח הזה לאורך זמן באמצעות השיטה
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);
עכשיו העומק מוצג כדופק מונפש שזורם בסצנה.

אתם יכולים לשנות את הערכים שמופיעים כאן כדי להאט או להאיץ את הפעימה, להרחיב או לצמצם אותה וכו'. אתם יכולים גם לנסות דרכים חדשות לגמרי לשנות את ה-Shader כדי להציג את פרטי העומק.
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()ב-Shader:
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. - לוחצים לחיצה ימנית > העתקה
- בוחרים את התיקייה rendering
- לחיצה ימנית > הדבקה

- שינוי שם הכיתה ל
OcclusionObjectRenderer

הכיתה החדשה ששמה שונה אמורה להופיע עכשיו באותה תיקייה:

פותחים את הקובץ OcclusionObjectRenderer.java שנוצר ומשנים את נתיבי ה-Shader בחלק העליון של הקובץ:
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;
מאתחלים את הפרמטרים האחידים של תוכנת הצללה (shader) בשיטה 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");
- כדי לוודא שהערכים האלה מתעדכנים בכל פעם שהם נמשכים, צריך לעדכן את השיטה
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. מוסיפים את השורות האלה בתוך השיטהonSurfaceCreated, מתחת למיקום שבו מוגדר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 אבל הוא לא בשימוש. בטלפונים עם עומק, שתי הגרסאות מאותחלות, ובזמן הריצה מתקבלת החלטה איזו גרסה של הרכיב לעיבוד תלת-ממד תשמש לציור.
בתוך השיטה 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() ב-shader, שמחיל טשטוש ליבה על דגימת ההסתרה:
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);
כדי לנצל את הפונקציונליות החדשה של תוכנת ההצללה (shader), צריך לעדכן את מנוע הרינדור.
OcclusionObjectRenderer.java
מוסיפים את משתני החברים הבאים בראש המחלקה:
private int occlusionBlurUniform;
private final float occlusionsBlur = 0.01f;
מוסיפים את הטקסט הבא בתוך השיטה createOnGlThread:
// Add alongside the other calls to GLES20.glGetUniformLocation.
occlusionBlurUniform = GLES20.glGetUniformLocation(program, "u_OcclusionBlurAmount");
מוסיפים את הטקסט הבא בתוך השיטה draw:
// Add alongside the other calls to GLES20.glUniform1f.
GLES20.glUniform1f(occlusionBlurUniform, occlusionsBlur);
| השוואה ויזואליתאחרי השינויים האלה, הגבול של האזור המוסתר אמור להיות חלק יותר. |
10. Build-Run-Test
איך יוצרים ומריצים את האפליקציה
- מחברים מכשיר Android באמצעות USB.
- בוחרים באפשרות קובץ > בנייה והפעלה.
- שמירה בשם: ARCodeLab.apk.
- ממתינים עד שהאפליקציה תיבנה ותיפרס במכשיר.
בפעם הראשונה שמנסים לפרוס את האפליקציה במכשיר:
- תצטרכו לאשר ניפוי באגים ב-USB במכשיר. בוחרים באפשרות 'אישור' כדי להמשיך.
- תופיע בקשה לאשר לאפליקציה להשתמש במצלמה של המכשיר. צריך לתת גישה כדי להמשיך להשתמש בתכונות ה-AR.
בדיקת האפליקציה
כשמריצים את האפליקציה, אפשר לבדוק את ההתנהגות הבסיסית שלה על ידי החזקת המכשיר, תנועה במרחב וסריקה איטית של אזור מסוים. כדאי לנסות לאסוף נתונים למשך 10 שניות לפחות ולסרוק את האזור מכמה כיוונים לפני שממשיכים לשלב הבא.
פתרון בעיות
הגדרת מכשיר Android לפיתוח
- מחברים את המכשיר למחשב הפיתוח באמצעות כבל USB. אם אתם מפתחים באמצעות Windows, יכול להיות שתצטרכו להתקין את דרייבר של התקן USB המתאים למכשיר שלכם.
- כדי להפעיל את ניפוי באגים ב-USB בחלון אפשרויות למפתחים:
- פותחים את אפליקציית ההגדרות.
- אם במכשיר שלכם פועלת גרסה Android v8.0 ואילך, בוחרים באפשרות מערכת. אם לא, עוברים לשלב הבא.
- גוללים לחלק התחתון של המסך ובוחרים באפשרות מידע על הטלפון.
- גוללים למטה ומקישים 7 פעמים על מספר Build.
- חוזרים למסך הקודם, גוללים לתחתית ולוחצים על אפשרויות למפתחים.
- בחלון אפשרויות למפתחים, גוללים למטה כדי למצוא את האפשרות ניפוי באגים ב-USB ומפעילים אותה.
מידע מפורט יותר על התהליך הזה זמין באתר המפתחים של Android.
כשלים בבנייה שקשורים לרישיונות

אם נתקלתם בבעיה בבנייה שקשורה לרישיונות (Failed to install the following Android SDK packages as some licences have not been accepted), אתם יכולים להשתמש בפקודות הבאות כדי לבדוק את הרישיונות האלה ולאשר אותם:
cd <path to Android SDK>
tools/bin/sdkmanager --licenses
11. מזל טוב
מזל טוב! הצלחת ליצור ולהפעיל את האפליקציה הראשונה שלך למציאות רבודה שמבוססת על עומק באמצעות Google ARCore Depth API.












