1. Zanim zaczniesz
ARCore to platforma do tworzenia aplikacji rzeczywistości rozszerzonej (AR) na urządzenia mobilne. Korzystając z różnych interfejsów API, ARCore umożliwia urządzeniu użytkownika obserwowanie i otrzymywanie informacji o środowisku, a także interakcję z tymi informacjami.
W ramach tego ćwiczenia w Codelabs dowiesz się, jak utworzyć prostą aplikację obsługującą AR, która korzysta z interfejsu ARCore Depth API.
Wymagania wstępne
To ćwiczenie w Codelabs zostało stworzone z myślą o deweloperach, którzy znają podstawowe pojęcia związane z AR.
Co utworzysz
Tworzysz aplikację, która w każdej klatce wykorzystuje obraz głębi, aby wizualizować geometrię sceny i zasłaniać umieszczone zasoby wirtualne. Od tego momentu:
- Sprawdzam, czy telefon obsługuje Depth API
- Pobieram obraz głębi dla każdej klatki
- Wizualizacja informacji o głębi na wiele sposobów (patrz animacja powyżej)
- Użycie głębi w celu zwiększenia realizmu aplikacji z zasłonięciem
- nauczyć się, jak sprawnie obsługiwać telefony, które nie obsługują Depth API;
Czego potrzebujesz
Wymagania sprzętowe
- Obsługiwane urządzenie ARCore podłączone do maszyny programistycznej kablem USB. To urządzenie musi też obsługiwać interfejs Depth API. Zapoznaj się z tą listą obsługiwanych urządzeń. Depth API jest dostępny tylko na Androidzie.
- Włącz debugowanie USB na tym urządzeniu.
Wymagania dotyczące oprogramowania
- ARCore SDK w wersji 1.31.0 lub nowszej.
- Maszyna deweloperska z Android Studio (w wersji 3.0 lub nowszej).
2. ARCore i Depth API
Interfejs Depth API używa aparatu RGB obsługiwanego urządzenia do tworzenia map głębi (nazywanych też zdjęciami głębi). Na podstawie informacji z mapy głębi możesz precyzyjnie wyświetlać wirtualne obiekty przed nimi lub za nimi, co zapewnia użytkownikom realistyczne wrażenia.
Interfejs ARCore Depth API zapewnia dostęp do obrazów głębi pasujących do każdej klatki z sesji ARCore. Każdy piksel zapewnia pomiar odległości od aparatu do otoczenia, co zwiększa realizm aplikacji AR.
Ważną cechą interfejsu Depth API jest okluzja, czyli zdolność do precyzyjnego wyświetlania obiektów cyfrowych względem rzeczywistych obiektów. Dzięki temu użytkownicy mają wrażenie, że znajdują się w rzeczywistym otoczeniu.
Dzięki nim dowiesz się, jak utworzyć prostą aplikację z obsługą AR, która wykorzystuje obrazy głębi, aby zasłaniać wirtualne obiekty za rzeczywistych powierzchniami i wizualizować wykrytą geometrię.
3. Konfiguracja
Konfigurowanie maszyny do programowania
- Podłącz urządzenie ARCore do komputera kablem USB. Upewnij się, że Twoje 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>
będzie ciągiem tekstowym unikalnym dla Twojego urządzenia. Zanim przejdziesz dalej, upewnij się, że widzisz dokładnie jedno urządzenie.
Pobierz i zainstaluj kode
- Możesz sklonować repozytorium:
git clone https://github.com/googlecodelabs/arcore-depth
Możesz też pobrać plik ZIP i wyodrębnić go:
- Uruchom Android Studio i kliknij Otwórz istniejący projekt Android Studio.
- Znajdź katalog, w którym został wyodrębniony plik ZIP pobrany powyżej, i otwórz katalog
depth_codelab_io2020
.
Jest to pojedynczy projekt Gradle z wieloma modułami. Jeśli panelu Projekt w lewym górnym rogu Android Studio nie jest jeszcze wyświetlany w panelu Projekt, kliknij Projekty w menu.
Wynik powinien wyglądać tak:
Ten projekt zawiera następujące moduły:
|
Będziesz pracować w module part0_work
. Dostępne są też kompletne rozwiązania dotyczące każdej części ćwiczenia. Każdy moduł jest aplikacją do kompilacji.
4. Uruchom aplikację Starter
- Kliknij Uruchom > Uruchom... > „part0_work”. W wyświetlonym oknie Wybierz cel wdrożenia Twoje urządzenie powinno znajdować się na liście Połączone urządzenia.
- Wybierz swoje urządzenie i kliknij OK. Android Studio skompiluje początkową aplikację i uruchomi ją na Twoim urządzeniu.
- Aplikacja poprosi o uprawnienia do korzystania z aparatu. Aby kontynuować, kliknij Zezwól.
Jak używać aplikacji
|
Obecnie aplikacja jest bardzo prosta i nie wie zbyt wiele o geometrii sceny ze świata rzeczywistego.
Jeśli na przykład umieścisz postać Androida za krzesłem, obraz będzie sprawiał wrażenie unoszącego się z przodu, ponieważ aplikacja nie wie, że krzesło tam jest, i powinna ukrywać Androida.
Aby rozwiązać ten problem, użyjemy interfejsu Depth API, aby zwiększyć realizm i realizm tej aplikacji.
5. Sprawdzanie, czy interfejs Depth API jest obsługiwany (część 1)
Interfejs ARCore Depth API działa tylko na podzbiorze obsługiwanych urządzeń. Przed integracją funkcji z użyciem tych szczegółowych obrazów musisz się najpierw upewnić, że aplikacja działa na obsługiwanym urządzeniu.
Dodaj do DepthCodelabActivity
nowego użytkownika prywatnego. Służy on jako flaga zapisująca, czy bieżące urządzenie obsługuje głębokość:
private boolean isDepthSupported;
Tę flagę możemy wypełnić z poziomu funkcji onResume()
, gdzie 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);
Teraz sesja AR jest odpowiednio skonfigurowana, a aplikacja wie, czy może korzystać z funkcji opartych na głębi.
Poinformuj użytkownika też, czy ta sesja będzie wykorzystywana z uwzględnieniem głębokości.
Dodaj kolejną wiadomość do paska powiadomień. Pojawi się ona 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 aplikacji onDrawFrame()
możesz w razie potrzeby wyświetlić tę wiadomość:
// Add this if-statement above messageSnackbarHelper.showMessage(this, messageToShow).
if (!isDepthSupported) {
messageToShow += "\n" + DEPTH_NOT_AVAILABLE_MESSAGE;
}
Jeśli Twoja aplikacja działa na urządzeniu, które nie obsługuje głębi, nowo dodany komunikat wyświetla się u dołu:
Następnie zaktualizujesz aplikację, aby wywoływała interfejs Depth API i pobierała obrazy głębi dla każdej klatki.
6. Pobieranie zdjęć głębi (część 2)
Depth API rejestruje obserwacje 3D środowiska urządzenia i wyświetla Twojej aplikacji głębię obrazu z tymi danymi. Każdy piksel na obrazie głębi odzwierciedla odległość od aparatu urządzenia do rzeczywistego środowiska.
Teraz użyjesz tych obrazów do ulepszenia renderowania i wizualizacji w aplikacji. Pierwszym krokiem jest pobranie obrazu głębi dla każdej klatki i powiązanie tekstury, która ma być używana przez GPU.
Najpierw dodaj do projektu nową klasę.DepthTextureHandler
odpowiada za pobieranie obrazu głębi w przypadku 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ć łatwy dostęp do kopii obrazu głębi dla każdej klatki.
W DepthCodelabActivity.java
dodaj instancję nowej klasy jako zmienną członka prywatnego:
private final DepthTextureHandler depthTexture = new DepthTextureHandler();
Następnie zaktualizuj metodę onSurfaceCreated()
, aby zainicjować tę teksturę, by mogła być używana przez nasze programy do cieniowania GPU:
// Put this at the top of the "try" block in onSurfaceCreated().
depthTexture.createOnGlThread();
Na koniec chcesz wypełnić tę teksturę w każdej klatce najnowszym obrazem głębi. W tym celu wywołaj utworzoną powyżej metodę update()
z najnowszą klatką pobraną z session
.
Obsługa głębokości jest w tej aplikacji opcjonalna, więc korzystaj z tego połączenia tylko wtedy, gdy korzystasz z głębi.
// Add this just after "frame" is created inside onDrawFrame().
if (isDepthSupported) {
depthTexture.update(frame);
}
Masz teraz obraz głębi, który jest aktualizowany z każdą klatką. Jest gotowy do użycia przez cieniowanie.
Jednak działanie aplikacji nie uległo jeszcze zmianie. Teraz użyjesz obrazu głębi, by ulepszyć aplikację.
7. Renderowanie obrazu głębi (część 3)
Teraz mamy już obraz głębi, który chcesz wykorzystać. Teraz musisz zobaczyć, jak on wygląda. W tej sekcji dodasz do aplikacji przycisk renderowania głębi każdej klatki.
Dodawanie nowych mechanizmów cieniowania
Obraz głębi można wyświetlić na wiele sposobów. Opisane poniżej cieniowanie pozwalają w prosty sposób przedstawić mapowanie kolorów.
Dodaj nowy program do cieniowania plików .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 umieścić cieniowanie fragmentów w tym samym katalogu i nadać mu nazwę background_show_depth_map.frag
.
Dodaj do 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ła nowych mechanizmów cieniowania dostępnych w lokalizacji src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java
.
Dodaj ścieżki do plików do cieniowania na początku klasy:
// 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ż uruchomi ona 2 mechanizmy cieniowania:
// 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 metodę, która pozwala rysować przy użyciu tych cieniowania 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");
}
Dodaj przycisk przełączania
Możesz już renderować mapę głębi. Teraz możesz ją wykorzystać. Dodaj przycisk, który włącza i wyłącza tę funkcję.
U góry pliku DepthCodelabActivity
dodaj plik importu dla przycisku:
import android.widget.Button;
Zaktualizuj klasę, dodając wartość logiczną wskazującą, czy renderowanie głębi jest włączone (domyślnie jest wyłączone):
private boolean showDepthMap = false;
Następnie na końcu metody onCreate()
dodaj przycisk sterujący wartością logiczną showDepthMap
:
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 do 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 u dołu 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ść wartości logicznej showDepthMap
. Użyj tej flagi, by określić, czy mapa głębi ma być renderowana.
Wróć do metody onDrawFrame()
w DepthCodelabActivity
, dodaj:
// Add this snippet just under backgroundRenderer.draw(frame);
if (showDepthMap) {
backgroundRenderer.drawDepth(frame);
}
Przekaż teksturę głębi do elementu backgroundRenderer
, dodając w narzędziu onSurfaceCreated()
ten wiersz:
// Add to onSurfaceCreated() after backgroundRenderer.createonGlThread(/*context=*/ this);
backgroundRenderer.createDepthShaders(/*context=*/ this, depthTexture.getDepthTexture());
Teraz możesz wyświetlić obraz głębi każdej klatki, naciskając przycisk w prawym górnym rogu ekranu.
Działa bez obsługi interfejsu Depth API | Działanie z obsługą interfejsu Depth API |
[Opcjonalnie] Animacja z fantazją głębi
Aplikacja wyświetla obecnie bezpośrednio mapę głębi. Czerwone piksele to znajdujące się blisko siebie obszary. Niebieskie piksele oznaczają odległe obszary.
Jest wiele sposobów przekazywania szczegółowych informacji. W tej sekcji będziesz okresowo modyfikować cieniowanie, by zmieniało się na puls głębi, zmieniając ją tak, by pokazywała tylko głębię w pasmach, które wielokrotnie oddalają się od kamery.
Zacznij od dodania tych zmiennych na górze background_show_depth_map.frag
:
uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;
- Następnie na podstawie tych wartości przefiltruj piksele, które mają zostać uwzględnione z wartościami głębi w funkcji
main()
cieniowania:
// 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 cieniowania. Na początku klasy dodaj te pola:
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 parametry, aby dopasować te parametry do programu do cieniowania:
depthRangeToRenderMmParam = GLES20.glGetUniformLocation(depthProgram, "u_DepthRangeToRenderMm");
- Dodatkowo zakres ten możesz kontrolować w czasie w ramach metody
drawDepth()
. Dodaj ten kod, który zwiększa ten zakres przy każdym rysowaniu klatki:
// 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);
Teraz głębia pojawia się w postaci animowanego pulsu przepływającego przez scenę.
Możesz zmienić podane tu wartości, żeby puls było wolniejsze, szybsze, poszerzone, zwężone itd. Możesz też wypróbować zupełnie nowe sposoby na zmianę cieniowania i pokazać szczegółowe informacje.
8. Używanie interfejsu Depth API do przesłonięcia (część 4)
Teraz zajmiemy się przesłaniam obiektów w aplikacji.
Przesłonięcie odnosi się do sytuacji, w której nie można w pełni wyrenderować wirtualnego obiektu, ponieważ między nim a kamerą znajdują się rzeczywiste obiekty. Radzenie sobie z przesłonięciem jest kluczowe, jeśli chcesz, aby funkcje AR były wciągające.
Prawidłowe renderowanie wirtualnych obiektów w czasie rzeczywistym zwiększa realizm i wiarygodność rozszerzonej sceny. Więcej przykładów znajdziesz w naszym filmie o łączeniu rzeczywistości z interfejsem Depth API.
W tej sekcji zaktualizujesz aplikację, tak aby obejmowała obiekty wirtualne tylko wtedy, gdy dostępna jest głębia.
Dodawanie nowych modułów do cieniowania obiektów
Podobnie jak w poprzednich sekcjach, dodasz nowe rodzaje cieniowania, które obsługują informacje o głębi. Teraz możesz skopiować istniejące moduły do cieniowania obiektów i dodać funkcję przesłaniania.
Ważne jest zachowanie obu wersji cieniowania obiektów, ponieważ dzięki temu aplikacja będzie mogła w czasie działania zdecydować, czy obsługiwać głębię.
Utwórz kopie plików cieniowania object.vert
i object.frag
w katalogu src/main/assets/shaders
.
|
W elemencie occlusion_object.vert
dodaj tę zmienną nad elementem main()
:
varying vec3 v_ScreenSpacePosition;
Ustaw tę zmienną na końcu main()
:
v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;
Zaktualizuj plik occlusion_object.frag
, dodając te zmienne powyżej main()
na górze 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()
do cieniowania, aby ułatwić sobie zarządzanie 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 aplikację main()
w aplikacji occlusion_object.frag
, aby otrzymywać informacje o głębi i stosować przesłonięcie. Na dole 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 już nową wersję cieniowania obiektów, możesz zmodyfikować kod mechanizmu renderowania.
Zasłanianie obiektów podczas renderowania
Utwórz kopię następnych zajęć (ObjectRenderer
), które znajdują 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 klasa o zmienionej nazwie powinna pojawić się teraz w tym samym folderze:
Otwórz nowo utworzony plik OcclusionObjectRenderer.java
i zmień ścieżki cieniowania 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 użytkowników związane ze szczegółowymi informacjami o członkostwie wraz z pozostałymi u góry klasy. Zmienne dostosują ostrość obramowania.
// 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 cieniowania 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");
- Dopilnuj, aby te wartości były aktualizowane za każdym razem, gdy są tworzone, aktualizując metodę
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 w elemencie draw()
te wiersze, aby włączyć tryb mieszania w renderowaniu i zastosować przezroczystość do obiektów wirtualnych, gdy są ukryte:
// 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 wywołujące funkcję
OcclusionObjectRenderer
mogły podawać informacje o głębi:
// 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 przesłaniania obiektów
Masz już nowy dokument OcclusionObjectRenderer
, możesz go dodać do urządzenia DepthCodelabActivity
i określić, kiedy i w jaki sposób stosować renderowanie przesłonięte.
Włącz tę logikę, dodając do aktywności instancję OcclusionObjectRenderer
, tak aby zarówno ObjectRenderer
, jak i OcclusionObjectRenderer
były członkami grupy 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();
- Możesz zdecydować, kiedy ma być używany ten
occludedVirtualObject
, w zależności od tego, czy bieżące urządzenie obsługuje 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, na których głębokość nie jest obsługiwana, instancja occludedVirtualObject
jest tworzona, ale nie jest używana. W przypadku telefonów z głębokością obie wersje są inicjowane, a w czasie działania podejmujemy decyzję, która wersja mechanizmu renderowania ma zostać użyta do rysowania.
W metodzie onDrawFrame()
odszukaj istniejący kod:
virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
virtualObject.draw(viewmtx, projmtx, colorCorrectionRgba, OBJECT_COLOR);
Zastąp ten kod następującym:
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 zmapowany na renderowanie wyjściowe. Ponieważ obraz głębi ma inną rozdzielczość i może mieć inny format obrazu niż ekran, współrzędne tekstury mogą się różnić między obrazem a obrazem z aparatu.
- Dodaj metodę pomocniczą
getTextureTransformMatrix()
na końcu pliku. Ta metoda zwraca macierz przekształcenia, która po zastosowaniu sprawia, że promienie UV w przestrzeni ekranu są prawidłowo dopasowane do współrzędnych czworobocznych tekstur, które są używane do renderowania obrazu 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 na początku tego importu:
import com.google.ar.core.Coordinates2d;
Chcesz obliczyć przekształcenie między tymi współrzędnymi tekstury przy każdej zmianie tekstury ekranu (np. przy obróceniu ekranu). Ta funkcja jest zablokowana.
Na początku pliku dodaj tę flagę:
// Add this member at the top of the file.
private boolean calculateUVTransform = true;
- Sprawdź w
onDrawFrame()
, czy po utworzeniu klatki i kamery musi zostać przetworzona zapisana przekształcenie:
// 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 uruchamiać aplikację z zasłonięciem obiektów wirtualnych.
Twoja aplikacja powinna teraz płynnie działać na wszystkich telefonach i automatycznie używać funkcji głębi przesłaniania, jeśli jest obsługiwana.
Uruchamianie aplikacji z obsługą interfejsu Depth API | Aplikacja działająca bez obsługi interfejsu Depth API |
9. [Opcjonalnie] Popraw jakość przesłonięcia
Wdrożona powyżej metoda przesłaniania oparta na głębokości zapewnia okluzję z wyraźnymi granicami. W miarę jak kamera oddala się od obiektu, pomiary głębi mogą stawać się mniej dokładne, co może skutkować pojawieniem się zakłóceń wizualnych.
Aby rozwiązać ten problem, dodaj do testu przesłonięcia dodatkowe rozmycie, które zapewnia gładsze krawędzie ukrytych obiektów wirtualnych.
occlusion_object.frag
Na górze occlusion_object.frag
dodaj tę jednolitą zmienną:
uniform float u_OcclusionBlurAmount;
Dodaj tę funkcję pomocniczą tuż powyżej pola main()
do cieniowania, co spowoduje zastosowanie rozmycia jądra do próbkowania przesłonięcia:
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 tę linię w main()
:
gl_FragColor.a *= GetVisibility(depth_uvs, asset_depth_mm);
tym wierszem:
gl_FragColor.a *= GetBlurredVisibilityAroundUV(depth_uvs, asset_depth_mm);
Zaktualizuj mechanizm renderowania, aby wykorzystać jego nowe możliwości.
OcclusionObjectRenderer.java
Na początku klasy dodaj te zmienne składowe:
private int occlusionBlurUniform;
private final float occlusionsBlur = 0.01f;
Dodaj w metodzie createOnGlThread
ten kod:
// Add alongside the other calls to GLES20.glGetUniformLocation.
occlusionBlurUniform = GLES20.glGetUniformLocation(program, "u_OcclusionBlurAmount");
Dodaj w metodzie draw
ten kod:
// Add alongside the other calls to GLES20.glUniform1f.
GLES20.glUniform1f(occlusionBlurUniform, occlusionsBlur);
Porównanie wizualnePo tych zmianach granica przesłonięcia powinna być teraz płynniejsza. |
10. Build-Run-Test
Tworzenie i uruchamianie aplikacji
- Podłącz urządzenie z Androidem przez USB.
- Wybierz Plik > Kompiluj i uruchamiaj.
- Zapisz jako: ARCodeLab.apk.
- Poczekaj, aż aplikacja skompiluje się i wdroży na urządzeniu.
Przy pierwszej próbie wdrożenia aplikacji na urządzeniu:
- Musisz zezwolić na debugowanie USB na urządzeniu. Aby kontynuować, kliknij OK.
- Pojawi się pytanie, czy aplikacja ma uprawnienia do korzystania z aparatu urządzenia. Aby nadal korzystać z funkcji AR, zezwól na dostęp.
Testowanie aplikacji
Po uruchomieniu aplikacji możesz przetestować jej podstawowe działanie, przytrzymując urządzenie, poruszając się po obszarze i powolnie skanując obszar. Postaraj się zebrać co najmniej 10 sekund danych i przeskanować obszar z kilku stron, zanim przejdziesz do kolejnego kroku.
Rozwiązywanie problemów
Konfigurowanie urządzenia z Androidem na potrzeby programowania
- Podłącz urządzenie do programisty kablem USB. Jeśli programujesz w systemie Windows, konieczne może być zainstalowanie odpowiedniego sterownika USB dla Twojego 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, kliknij System. W przeciwnym razie przejdź do następnego kroku.
- Przewiń w dół i wybierz Informacje o telefonie.
- Przewiń w dół i 7 razy kliknij Numer kompilacji.
- Wróć na poprzedni ekran, przewiń w dół i kliknij Opcje programisty.
- W oknie Opcje programisty przewiń w dół, aby znaleźć i włączyć Debugowanie USB.
Bardziej szczegółowe informacje o tym procesie znajdziesz na stronie Google dla deweloperów aplikacji na Androida.
Błędy kompilacji związane z licencjami
Jeśli wystąpi błąd kompilacji związany z licencjami (Nie udało się zainstalować tych pakietów Android SDK, ponieważ niektóre licencje nie zostały zaakceptowane), możesz przejrzeć i zaakceptować te licencje, używając tych poleceń:
cd
<path to Android SDK>
tools/bin/sdkmanager --licenses
11. Gratulacje
Gratulujemy! Udało Ci się utworzyć i uruchomić pierwszą aplikację w rzeczywistości rozszerzonej, wykorzystującą interfejs ARCore Depth API od Google.