1. Zanim zaczniesz
ARCore to platforma do tworzenia aplikacji rzeczywistości rozszerzonej (AR) na urządzenia mobilne. Dzięki różnym interfejsom API ARCore umożliwia urządzeniu użytkownika obserwowanie otoczenia i otrzymywanie informacji o nim oraz interakcję z tymi informacjami.
W tym ćwiczeniu z programowania dowiesz się, jak utworzyć prostą aplikację z rozszerzoną rzeczywistością, która korzysta z interfejsu ARCore Depth API.
Wymagania wstępne
Te ćwiczenia z programowania są przeznaczone dla deweloperów, którzy znają podstawowe pojęcia związane z AR.
Co utworzysz

Utworzysz aplikację, która będzie używać obrazu głębi dla każdej klatki, aby wizualizować geometrię sceny i wykonywać okluzję umieszczonych wirtualnych komponentów. Wykonasz te czynności:
- Sprawdzanie, czy telefon obsługuje Depth API
- Pobieranie obrazu głębi dla każdej klatki
- Wizualizacja informacji o głębi na różne sposoby (patrz animacja powyżej)
- Wykorzystanie głębi do zwiększenia realizmu aplikacji z okluzją
- Dowiedz się, jak prawidłowo obsługiwać telefony, które nie obsługują interfejsu Depth API
Czego potrzebujesz
Wymagania sprzętowe
- Obsługiwane urządzenie ARCore połączone z komputerem używanym do programowania kablem USB. Urządzenie musi też obsługiwać interfejs Depth API. Zapoznaj się z tą listą obsługiwanych urządzeń. Interfejs Depth API jest dostępny tylko na Androidzie.
- Włącz debugowanie USB na tym urządzeniu.
Wymagania dotyczące oprogramowania
- pakiet SDK ARCore w wersji 1.31.0 lub nowszej.
- komputer używany do programowania z Androidem Studio (wersja 3.0 lub nowsza);
2. ARCore i Depth API
Interfejs Depth API używa kamery RGB obsługiwanego urządzenia do tworzenia map głębi (zwanych też obrazami głębi). Informacje z mapy głębi możesz wykorzystać do dokładnego wyświetlania obiektów wirtualnych przed obiektami ze świata rzeczywistego lub za nimi, co pozwala tworzyć realistyczne i wciągające doświadczenia użytkownika.
Interfejs ARCore Depth API zapewnia dostęp do obrazów głębi pasujących do każdej klatki dostarczanej przez sesję ARCore. Każdy piksel zapewnia pomiar odległości od aparatu do otoczenia, co zwiększa realizm aplikacji AR.
Kluczową funkcją interfejsu Depth API jest okluzja, czyli możliwość dokładnego wyświetlania obiektów cyfrowych w stosunku do obiektów ze świata rzeczywistego. Dzięki temu obiekty wyglądają tak, jakby znajdowały się w środowisku użytkownika.
Ten przewodnik przeprowadzi Cię przez proces tworzenia prostej aplikacji obsługującej AR, która wykorzystuje obrazy głębi do zasłaniania wirtualnych obiektów za powierzchniami w rzeczywistym świecie i wizualizacji wykrytej geometrii przestrzeni.
3. Konfiguracja
Konfigurowanie komputera używanego do programowania
- Podłącz urządzenie ARCore do komputera za pomocą kabla USB. Upewnij się, że urządzenie umożliwia debugowanie USB.
- Otwórz terminal i uruchom polecenie
adb devices, jak pokazano poniżej:
adb devices List of devices attached <DEVICE_SERIAL_NUMBER> device
<DEVICE_SERIAL_NUMBER> to ciąg znaków unikalny dla Twojego urządzenia. Zanim przejdziesz dalej, upewnij się, że widzisz dokładnie 1 urządzenie.
Pobierz i zainstaluj Code.
- Możesz sklonować repozytorium:
git clone https://github.com/googlecodelabs/arcore-depth
Możesz też pobrać plik ZIP i go rozpakować:
- Uruchom Android Studio i kliknij Open an existing Android Studio project (Otwórz istniejący projekt Android Studio).
- Znajdź katalog, do którego wyodrębniono pobrany powyżej plik ZIP, i otwórz katalog
depth_codelab_io2020.
Jest to pojedynczy projekt Gradle z wieloma modułami. Jeśli w lewym górnym rogu Androida Studio nie widzisz jeszcze okienka Projekt, w menu kliknij Projekty.
Wynik powinien wyglądać tak:
| Ten projekt zawiera te moduły:
|
Będziesz pracować w part0_work. Dostępne są też kompletne rozwiązania dla każdej części ćwiczenia (w Codelabs). Każdy moduł to aplikacja, którą można skompilować.
4. Uruchamianie aplikacji wyjściowej
- Kliknij Uruchom > Uruchom... > ‘part0_work'. W wyświetlonym oknie Select Deployment Target (Wybierz urządzenie docelowe) urządzenie powinno być widoczne w sekcji Connected Devices (Połączone urządzenia).
- Wybierz urządzenie i kliknij OK. Android Studio skompiluje początkową aplikację i uruchomi ją na urządzeniu.
- Aplikacja poprosi o uprawnienia dostępu do aparatu. Aby kontynuować, kliknij Zezwól.

| Jak korzystać z aplikacji
|
Obecnie aplikacja jest bardzo prosta i nie ma zbyt wielu informacji o geometrii sceny w rzeczywistym świecie.
Jeśli na przykład umieścisz figurkę Androida za krzesłem, renderowanie będzie wyglądać tak, jakby unosiła się przed nim, ponieważ aplikacja nie wie, że krzesło tam jest i powinno zasłaniać Androida.

Aby rozwiązać ten problem, użyjemy interfejsu Depth API, aby zwiększyć immersję i realizm w tej aplikacji.
5. Sprawdzanie, czy interfejs Depth API jest obsługiwany (część 1)
Interfejs ARCore Depth API działa tylko na niektórych obsługiwanych urządzeniach. Zanim zintegrujesz funkcję z aplikacją za pomocą tych obrazów głębi, musisz najpierw sprawdzić, czy aplikacja działa na obsługiwanym urządzeniu.
Dodaj nowego prywatnego elementu DepthCodelabActivity, który będzie służyć jako flaga przechowująca informację o tym, czy bieżące urządzenie obsługuje głębię:
private boolean isDepthSupported;
Możemy wypełnić ten atrybut w funkcji onResume(), w której tworzona jest nowa sesja.
Znajdź istniejący kod:
// Creates the ARCore session.
session = new Session(/* context= */ this);
Zaktualizuj kod do:
// 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);
Sesja AR jest teraz odpowiednio skonfigurowana, a aplikacja wie, czy może korzystać z funkcji opartych na głębi.
Poinformuj też użytkownika, czy w tej sesji jest używana głębia.
Dodaj kolejną wiadomość do paska powiadomień. Pojawi się on u dołu ekranu:
// 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]";
W onDrawFrame() możesz w razie potrzeby wyświetlić ten komunikat:
// Add this if-statement above messageSnackbarHelper.showMessage(this, messageToShow).
if (!isDepthSupported) {
messageToShow += "\n" + DEPTH_NOT_AVAILABLE_MESSAGE;
}
Jeśli aplikacja jest uruchamiana na urządzeniu, które nie obsługuje głębi, u dołu pojawi się dodany przez Ciebie komunikat:

Następnie zaktualizujesz aplikację, aby wywoływała interfejs Depth API i pobierała obrazy głębi dla każdej klatki.
6. Pobieranie obrazów głębi (część 2)
Interfejs Depth API rejestruje trójwymiarowe obserwacje otoczenia urządzenia i zwraca do aplikacji obraz głębi z tymi danymi. Każdy piksel na obrazie głębi reprezentuje pomiar odległości od aparatu urządzenia do otoczenia w rzeczywistym świecie.
Teraz użyjesz tych obrazów głębi, aby ulepszyć renderowanie i wizualizację w aplikacji. Pierwszym krokiem jest pobranie obrazu głębi dla każdej klatki i powiązanie tej tekstury z GPU.
Najpierw dodaj do projektu nową klasę.DepthTextureHandler odpowiada za pobieranie obrazu głębi dla danej klatki ARCore.
Dodaj ten plik:

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;
}
}
Teraz dodasz instancję tej klasy do DepthCodelabActivity, dzięki czemu będziesz mieć łatwo dostępną kopię obrazu głębi dla każdej klatki.
W DepthCodelabActivity.java dodaj instancję nowej klasy jako prywatną zmienną członkowską:
private final DepthTextureHandler depthTexture = new DepthTextureHandler();
Następnie zaktualizuj metodę onSurfaceCreated(), aby zainicjować tę teksturę, tak aby można było jej używać w shaderach GPU:
// Put this at the top of the "try" block in onSurfaceCreated().
depthTexture.createOnGlThread();
Na koniec musisz wypełnić tę teksturę w każdej klatce najnowszym obrazem głębi. Możesz to zrobić, wywołując metodę update() utworzoną powyżej na najnowszej klatce pobranej z session.
Obsługa głębi jest w tej aplikacji opcjonalna, więc używaj tego wywołania tylko wtedy, gdy korzystasz z głębi.
// Add this just after "frame" is created inside onDrawFrame().
if (isDepthSupported) {
depthTexture.update(frame);
}
Teraz masz obraz głębi, który jest aktualizowany z każdą klatką. Jest gotowa do użycia przez shadery.
Jednak działanie aplikacji nie uległo jeszcze zmianie. Teraz użyjesz obrazu głębi, aby ulepszyć aplikację.
7. Renderowanie obrazu głębi (część 3)
Teraz, gdy masz już obraz głębi, możesz sprawdzić, jak wygląda. W tej sekcji dodasz do aplikacji przycisk, który będzie renderować głębię każdej klatki.
Dodawanie nowych shaderów
Obraz głębi można wyświetlać na wiele sposobów. Te shadery zapewniają prostą wizualizację mapowania kolorów.
| Dodawanie nowego shadera .vertW Android Studio:
|
W nowym pliku dodaj ten kod:
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;
}
Powtórz powyższe kroki, aby utworzyć w tym samym katalogu program cieniujący fragmentów i nazwać go background_show_depth_map.frag.
Dodaj do tego nowego pliku ten kod:
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;
}
Następnie zaktualizuj klasę BackgroundRenderer, aby używać tych nowych shaderów, które znajdują się w src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java.
U góry klasy dodaj ścieżki do plików z shaderami:
// 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";
Dodaj więcej zmiennych składowych do klasy BackgroundRenderer, ponieważ będzie ona uruchamiać 2 shadery:
// 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;
Dodaj nową metodę wypełniania tych pól:
// 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;
}
Dodaj tę metodę, która służy do rysowania za pomocą tych shaderów w każdej klatce:
// 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");
}
Dodawanie przycisku przełączania
Teraz, gdy masz już możliwość renderowania mapy głębi, wykorzystaj ją. Dodaj przycisk, który włącza i wyłącza to renderowanie.
U góry pliku DepthCodelabActivity dodaj import, aby używać przycisku:
import android.widget.Button;
Zaktualizuj klasę, aby dodać element logiczny wskazujący, czy renderowanie głębi jest włączone (domyślnie jest wyłączone):
private boolean showDepthMap = false;
Następnie dodaj przycisk, który steruje wartością logiczną showDepthMap, na końcu metody 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);
}
});
Dodaj te ciągi znaków do pliku 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>
Dodaj ten przycisk na dole układu aplikacji w 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"/>
Przycisk kontroluje teraz wartość logiczną showDepthMap. Użyj tego parametru, aby określić, czy mapa głębi ma być renderowana.
Wróć do metody onDrawFrame() w DepthCodelabActivity i dodaj:
// Add this snippet just under backgroundRenderer.draw(frame);
if (showDepthMap) {
backgroundRenderer.drawDepth(frame);
}
Przekaż teksturę głębi do funkcji backgroundRenderer, dodając ten wiersz w pliku onSurfaceCreated():
// Add to onSurfaceCreated() after backgroundRenderer.createonGlThread(/*context=*/ this);
backgroundRenderer.createDepthShaders(/*context=*/ this, depthTexture.getDepthTexture());
Obraz głębi każdej klatki możesz teraz wyświetlić, naciskając przycisk w prawym górnym rogu ekranu.
|
| ||
Działanie bez obsługi Depth API | Uruchamianie z obsługą interfejsu Depth API | ||
[Opcjonalnie] Animacja głębi
Aplikacja wyświetla obecnie mapę głębi bezpośrednio. Czerwone piksele oznaczają obszary, które są blisko. Niebieskie piksele oznaczają obszary, które są daleko.
|
|
Informacje o głębi można przekazywać na wiele sposobów. W tej sekcji zmodyfikujesz shader, aby okresowo pulsował głębią. Zrobisz to, zmieniając shader tak, aby wyświetlał głębię tylko w pasach, które wielokrotnie oddalają się od kamery.
Zacznij od dodania tych zmiennych na początku pliku background_show_depth_map.frag:
uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;
- Następnie użyj tych wartości, aby odfiltrować piksele, które mają być pokryte wartościami głębi w funkcji
main()shadera:
// Add this line at the end of main().
gl_FragColor.a = clamp(1.0 - abs((depth_mm - u_DepthRangeToRenderMm) / kDepthWidthToRenderMm), 0.0, 1.0);
Następnie zaktualizuj BackgroundRenderer.java, aby zachować te parametry shadera. Dodaj te pola na początku klasy:
private static final float MAX_DEPTH_RANGE_TO_RENDER_MM = 20000.0f;
private float depthRangeToRenderMm = 0.0f;
private int depthRangeToRenderMmParam;
W metodzie createDepthShaders() dodaj te elementy, aby dopasować parametry do programu cieniującego:
depthRangeToRenderMmParam = GLES20.glGetUniformLocation(depthProgram, "u_DepthRangeToRenderMm");
- Możesz też kontrolować ten zakres w czasie za pomocą metody
drawDepth(). Dodaj ten kod, który zwiększa ten zakres za każdym razem, gdy rysowana jest klatka:
// 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);
Głębia jest teraz wizualizowana jako animowany puls przepływający przez scenę.

Możesz zmienić podane tu wartości, aby puls był wolniejszy, szybszy, szerszy, węższy itp. Możesz też wypróbować zupełnie nowe sposoby zmiany shadera, aby wyświetlać informacje o głębi.
8. Używanie interfejsu Depth API do okluzji (część 4)
Teraz zajmiesz się zasłanianiem obiektów w aplikacji.
Określenie „okluzja” odnosi się do sytuacji, w której obiekt wirtualny nie może zostać w pełni wyrenderowany, ponieważ między nim a kamerą znajdują się obiekty rzeczywiste. Zarządzanie okluzją jest niezbędne, aby doświadczenia AR były wciągające.
Prawidłowe renderowanie obiektów wirtualnych w czasie rzeczywistym zwiększa realizm i wiarygodność sceny rozszerzonej. Więcej przykładów znajdziesz w naszym filmie o mieszaniu rzeczywistości za pomocą interfejsu Depth API.
W tej sekcji zaktualizujesz aplikację, aby zawierała obiekty wirtualne tylko wtedy, gdy dostępna jest głębia.
Dodawanie nowych shaderów obiektów
Podobnie jak w poprzednich sekcjach dodasz nowe shadery, aby obsługiwać informacje o głębi. Tym razem możesz skopiować istniejące shadery obiektów i dodać funkcję okluzji.
Ważne jest, aby zachować obie wersje shaderów obiektów, aby aplikacja mogła w czasie działania podjąć decyzję, czy obsługiwać głębię.
Utwórz kopie plików shadera object.vert i object.frag w katalogu src/main/assets/shaders.
|
|
W sekcji occlusion_object.vert dodaj tę zmienną nad main():
varying vec3 v_ScreenSpacePosition;
Ustaw tę zmienną u dołu sekcji main():
v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;
Zaktualizuj plik occlusion_object.frag, dodając te zmienne powyżej main() u góry pliku:
varying vec3 v_ScreenSpacePosition;
uniform sampler2D u_Depth;
uniform mat3 u_UvTransform;
uniform float u_DepthTolerancePerMm;
uniform float u_OcclusionAlpha;
uniform float u_DepthAspectRatio;
- Dodaj te funkcje pomocnicze powyżej
main()w cieniowaniu, aby ułatwić sobie pracę z informacjami o głębi:
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;
}
Teraz zaktualizuj main() w occlusion_object.frag, aby uwzględniać głębię i stosować okluzję. Na końcu pliku dodaj te wiersze:
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);
Teraz, gdy masz nową wersję shaderów obiektów, możesz zmodyfikować kod renderera.
Renderowanie okluzji obiektu
Następnie utwórz kopię klasy ObjectRenderer, która znajduje się w src/main/java/com/google/ar/core/codelab/common/rendering/ObjectRenderer.java.
- Wybierz zajęcia
ObjectRenderer. - Kliknij prawym przyciskiem myszy > Kopiuj.
- Wybierz folder rendering.
- Kliknij prawym przyciskiem myszy > Wklej.

- Zmień nazwę zajęć na
OcclusionObjectRenderer

Nowa, zmieniona klasa powinna teraz pojawić się w tym samym folderze:

Otwórz nowo utworzony plik OcclusionObjectRenderer.java i zmień ścieżki shadera u góry pliku:
private static final String VERTEX_SHADER_NAME = "shaders/occlusion_object.vert";
private static final String FRAGMENT_SHADER_NAME = "shaders/occlusion_object.frag";
- Dodaj te zmienne składowe związane z głębokością wraz z innymi zmiennymi na początku klasy. Zmienne dostosują ostrość obramowania okluzji.
// 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;
Utwórz te zmienne składowe z wartościami domyślnymi u góry klasy:
// 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;
Zainicjuj jednolite parametry shadera w metodzie 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");
- Zadbaj o to, aby te wartości były aktualizowane za każdym razem, gdy są rysowane, poprzez zaktualizowanie metody
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);
Dodaj te wiersze w draw(), aby włączyć tryb mieszania podczas renderowania, dzięki czemu do wirtualnych obiektów można stosować przezroczystość, gdy są zasłonięte:
// 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);
- Dodaj te metody, aby osoby dzwoniące do
OcclusionObjectRenderermogły podać informacje o głębokości:
// 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;
}
Kontrolowanie okluzji obiektów
Teraz, gdy masz już nowy OcclusionObjectRenderer, możesz dodać go do DepthCodelabActivity i wybrać, kiedy i jak chcesz używać renderowania okluzji.
Aby włączyć tę logikę, dodaj do aktywności instancję OcclusionObjectRenderer, tak aby zarówno ObjectRenderer, jak i OcclusionObjectRenderer należały do 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();
- Następnie możesz określić, kiedy to
occludedVirtualObjectma być używane, na podstawie tego, czy bieżące urządzenie obsługuje interfejs Depth API. Dodaj te wiersze w metodzieonSurfaceCreated, poniżej miejsca, w którym skonfigurowanovirtualObject:
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);
}
Na urządzeniach, które nie obsługują głębi, tworzona jest instancja occludedVirtualObject, ale nie jest używana. Na telefonach z informacjami o głębi inicjowane są obie wersje, a w czasie działania podejmowana jest decyzja, której wersji renderera użyć do rysowania.
W metodzie onDrawFrame() znajdź istniejący kod:
virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
virtualObject.draw(viewmtx, projmtx, colorCorrectionRgba, OBJECT_COLOR);
Zastąp ten kod tym:
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);
}
Na koniec upewnij się, że obraz głębi jest prawidłowo odwzorowany na renderowanym obrazie wyjściowym. Obraz głębi ma inną rozdzielczość i potencjalnie inny współczynnik proporcji niż ekran, więc współrzędne tekstury mogą się różnić od obrazu z kamery.
- Dodaj metodę pomocniczą
getTextureTransformMatrix()na końcu pliku. Ta metoda zwraca macierz przekształcenia, która po zastosowaniu sprawia, że współrzędne UV przestrzeni ekranu są prawidłowo dopasowane do współrzędnych tekstury czworokąta używanych do renderowania przekazu z kamery. Uwzględnia też orientację urządzenia.
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() wymaga tego importu u góry pliku:
import com.google.ar.core.Coordinates2d;
Chcesz obliczać przekształcenie między tymi współrzędnymi tekstury za każdym razem, gdy zmienia się tekstura ekranu (np. gdy ekran się obraca). Ta funkcja jest ograniczona.
Dodaj na początku pliku to oznaczenie:
// Add this member at the top of the file.
private boolean calculateUVTransform = true;
- W
onDrawFrame()sprawdź, czy po utworzeniu ramki i kamery należy ponownie obliczyć zapisaną transformację:
// Add these lines inside onDrawFrame() after frame.getCamera().
if (frame.hasDisplayGeometryChanged() || calculateUVTransform) {
calculateUVTransform = false;
float[] transform = getTextureTransformMatrix(frame);
occludedVirtualObject.setUvTransformMatrix(transform);
}
Po wprowadzeniu tych zmian możesz uruchomić aplikację z zasłanianiem wirtualnych obiektów.
Aplikacja powinna teraz działać prawidłowo na wszystkich telefonach i automatycznie korzystać z funkcji głębi do okluzji, gdy jest ona obsługiwana.
|
| ||
Uruchamianie aplikacji z obsługą interfejsu Depth API | Uruchamianie aplikacji bez obsługi interfejsu Depth API | ||
9. [Opcjonalnie] Popraw jakość okluzji
Zaimplementowana powyżej metoda okluzji na podstawie głębi zapewnia okluzję z wyraźnymi granicami. Gdy aparat oddala się od obiektu, pomiary głębi mogą być mniej dokładne, co może powodować artefakty wizualne.
Możemy rozwiązać ten problem, dodając do testu okluzji dodatkowe rozmycie, które wygładzi krawędzie ukrytych obiektów wirtualnych.
occlusion_object.frag
U góry pliku occlusion_object.frag dodaj tę zmienną jednolitą:
uniform float u_OcclusionBlurAmount;
Dodaj tę funkcję pomocniczą tuż nad main() w cieniowaniu, która stosuje rozmycie jądra do próbkowania okluzji:
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;
}
Zastąp ten wiersz w pliku main():
gl_FragColor.a *= GetVisibility(depth_uvs, asset_depth_mm);
z tym wierszem:
gl_FragColor.a *= GetBlurredVisibilityAroundUV(depth_uvs, asset_depth_mm);
Zaktualizuj moduł renderujący, aby korzystać z tej nowej funkcji shadera.
OcclusionObjectRenderer.java
U góry klasy dodaj te zmienne składowe:
private int occlusionBlurUniform;
private final float occlusionsBlur = 0.01f;
W metodzie createOnGlThread dodaj ten kod:
// Add alongside the other calls to GLES20.glGetUniformLocation.
occlusionBlurUniform = GLES20.glGetUniformLocation(program, "u_OcclusionBlurAmount");
W metodzie draw dodaj ten kod:
// Add alongside the other calls to GLES20.glUniform1f.
GLES20.glUniform1f(occlusionBlurUniform, occlusionsBlur);
| Porównanie wizualnePo wprowadzeniu tych zmian granica okluzji powinna być bardziej płynna. |
10. Kompilacja, uruchamianie i testowanie
Tworzenie i uruchamianie aplikacji
- Podłącz urządzenie z Androidem przez USB.
- Wybierz Plik > Kompilacja i uruchomienie.
- Zapisz jako: ARCodeLab.apk.
- Poczekaj, aż aplikacja zostanie skompilowana i wdrożona na urządzeniu.
Gdy po raz pierwszy spróbujesz wdrożyć aplikację na urządzeniu:
- Musisz zezwolić na debugowanie USB na urządzeniu. Aby kontynuować, wybierz OK.
- Pojawi się pytanie, czy aplikacja ma uprawnienia do korzystania z aparatu urządzenia. Zezwól na dostęp, aby dalej korzystać z funkcji AR.
Testowanie aplikacji
Po uruchomieniu aplikacji możesz przetestować jej podstawowe działanie, trzymając urządzenie, poruszając się po pomieszczeniu i powoli skanując obszar. Przed przejściem do następnego kroku spróbuj zebrać dane przez co najmniej 10 sekund i zeskanować obszar z kilku kierunków.
Rozwiązywanie problemów
Konfigurowanie urządzenia z Androidem na potrzeby programowania
- Podłącz urządzenie do komputera używanego do programowania za pomocą kabla USB. Jeśli tworzysz aplikacje w systemie Windows, może być konieczne zainstalowanie odpowiedniego sterownika do USB dla urządzenia.
- Aby włączyć debugowanie USB w oknie Opcje programisty, wykonaj te czynności:
- Otwórz aplikację Ustawienia.
- Jeśli masz urządzenie z Androidem w wersji 8.0 lub nowszej, wybierz System. W przeciwnym razie przejdź do następnego kroku.
- Przewiń w dół i kliknij Informacje o telefonie.
- Przewiń na dół i 7 razy kliknij Numer kompilacji.
- Wróć do poprzedniego ekranu, przewiń w dół i kliknij Opcje programisty.
- W oknie Opcje programisty przewiń w dół, aby znaleźć i włączyć Debugowanie USB.
Więcej szczegółowych informacji o tym procesie znajdziesz na stronie Google dla deweloperów aplikacji na Androida.
Błędy kompilacji związane z licencjami

Jeśli napotkasz błąd kompilacji związany z licencjami (Failed to install the following Android SDK packages as some licences have not been accepted), możesz użyć tych poleceń, aby przejrzeć i zaakceptować te licencje:
cd <path to Android SDK>
tools/bin/sdkmanager --licenses
11. Gratulacje
Gratulacje! Udało Ci się utworzyć i uruchomić pierwszą aplikację rzeczywistości rozszerzonej opartą na głębi przy użyciu interfejsu ARCore Depth API od Google.












