ARCore 깊이

ARCore는 휴대기기에서 증강 현실(AR) 앱을 빌드할 수 있는 플랫폼입니다. Google의 ARCore Depth API를 통해 ARCore 세션에서 제공하는 각 프레임과 일치하는 깊이 이미지에 액세스할 수 있습니다. 각 픽셀은 카메라에서 환경까지의 거리 측정값을 제공하며, 이는 이 Codelab에서 AR 애플리케이션의 향상된 현실감을 제공하는 데 사용됩니다.

Depth API는 ARCore 지원 기기의 하위 집합에서만 지원됩니다. 깊이 호출을 지원하는 휴대전화는 이 목록을 참고하세요. Depth API는 Android에서만 사용할 수 있습니다.

이 Codelab에서는 깊이 이미지를 사용하여 실제 표면 뒤에 있는 가상 작품의 오클루전을 실행하고 이 감지된 세계의 지오메트리를 시각화하는 간단한 AR 지원 앱을 빌드하는 프로세스를 안내합니다.

빌드할 항목

d9bd5136c54ce47a.gif

이 Codelab에서는 각 프레임의 깊이 이미지를 사용하여 장면의 지오메트리를 시각화하고 배치된 가상 작품에서 오클루전을 실행하는 앱을 빌드합니다. 이 Codelab에서는 다음과 같은 구체적인 단계를 거칩니다.

  1. 휴대전화에서 Depth API가 지원되는지 확인하기
  2. 각 프레임의 깊이 이미지 가져오기
  3. 깊이 정보를 시각화하는 다양한 방법(위의 애니메이션 참고)
  4. 깊이를 사용하여 오클루전으로 앱의 현실감을 높이는 방법
  5. Depth API를 지원하지 않는 휴대전화를 원활하게 처리하는 방법

참고: 도중에 문제가 발생하면 마지막 섹션으로 이동하여 몇 가지 문제 해결 도움말을 확인하세요.

이 Codelab을 완료하려면 특정 하드웨어와 소프트웨어가 필요합니다.

하드웨어 요구사항

소프트웨어 요구사항

개발 머신 설정하기

USB 케이블을 통해 ARCore 기기를 컴퓨터에 연결합니다. 기기에서 USB 디버깅을 허용하는지 확인합니다. 터미널을 열고 아래와 같이 adb devices를 실행합니다.

adb devices

List of devices attached
<DEVICE_SERIAL_NUMBER>    device

<DEVICE_SERIAL_NUMBER>은 기기의 고유한 문자열입니다. 계속하기 전에 정확히 하나의 기기가 표시되는지 확인하세요.

코드 다운로드 및 설치하기

저장소를 클론할 수 있습니다.

git clone https://github.com/googlecodelabs/arcore-depth

또는 ZIP 파일을 다운로드하고 압축을 풉니다.

ZIP 파일 다운로드

Android 스튜디오를 실행합니다. Open an existing Android Studio project를 클릭합니다. 그런 다음 위에서 다운로드한 ZIP 파일의 압축을 푼 디렉터리로 이동하여 depth_codelab_io2020 디렉터리를 더블클릭합니다.

여러 모듈이 있는 단일 Gradle 프로젝트입니다. Android 스튜디오의 왼쪽 상단에 있는 프로젝트 창에 아직 프로젝트가 표시되지 않으면 드롭다운 메뉴에서 Projects를 클릭합니다. 다음과 같은 결과가 표시됩니다.

이 프로젝트에 있는 모듈은 다음과 같습니다.

  • part0_work: 시작 앱입니다. 이 Codelab을 진행할 때 이 모듈을 수정해야 합니다.
  • part1: 1부를 완료했을 때 수정 내용이 어떻게 표시되는지에 대한 참고 코드입니다.
  • part2: 2부를 완료했을 때의 참고 코드입니다.
  • part3: 3부를 완료했을 때의 참고 코드입니다.
  • part4_completed: 앱의 최종 버전입니다. 4부 및 이 Codelab을 완료했을 때의 참고 코드입니다.

part0_work 모듈에서 작업하게 됩니다. Codelab의 각 부분에 관한 완전한 해결책도 있습니다. 각 모듈은 빌드 가능한 앱입니다.

Run > Run… > ‘part0_work'를 클릭합니다. 표시되는 Select Deployment Target 대화상자에서 기기가 Connected Devices 아래에 표시되어야 합니다. 기기를 선택하고 OK를 클릭합니다. Android 스튜디오가 기기에서 초기 앱을 빌드하고 실행합니다.

앱을 처음 실행하면 카메라 권한을 요청하는 메시지가 나타납니다. 계속하려면 Allow를 탭합니다.

앱 사용 방법

  1. 앱이 평면을 찾을 수 있도록 기기를 움직입니다. 하단에 있는 메시지는 계속 움직여야 함을 나타냅니다.
  2. 평면의 아무 곳이나 탭하여 앵커를 배치합니다. 앵커가 배치된 곳에 Android 그림이 그려집니다. 이 앱에서는 한 번에 하나의 앵커만 배치할 수 있습니다.
  3. 기기를 움직입니다. 기기가 움직이더라도 그림은 같은 위치에 있는 것처럼 표시되어야 합니다.

현재는 애플리케이션이 매우 간단하며 실제 장면의 지오메트리에 관한 정보가 많지 않습니다. 예를 들어 Android 그림을 의자 뒤에 배치하는 경우 애플리케이션은 의자가 있다는 사실을 모르고 Android를 숨겨야 하기 때문에 렌더링이 앞에서 호버링하는 것처럼 보입니다.

5e8f5fe9098d316e.png 76f41b692224801b.png 3f320c851d1903d.png

이 문제를 해결하기 위해 Depth API를 사용하여 이 앱의 몰입감과 현실감을 개선할 것입니다.

ARCore Depth API는 지원되는 기기의 하위 집합에서 실행됩니다. 이러한 깊이 이미지를 사용하는 기능을 앱에 통합하려면 먼저 앱이 지원되는 기기에서 실행되는지 확인해야 합니다.

현재 기기가 깊이를 지원하는지를 저장하는 플래그의 역할을 하는 새 비공개 구성원을 DepthCodelabActivity에 추가합니다.

private boolean isDepthSupported;

새 세션이 만들어지는 onResume() 함수 내부에서 이 플래그를 채울 수 있습니다. 기존 코드를 찾습니다.

// Creates the ARCore session.
session = new Session(/* context= */ this);

코드를 다음과 같이 업데이트합니다.

// Creates the ARCore session.
session = new Session(/* context= */ this);
Config config = session.getConfig();
isDepthSupported = session.isDepthModeSupported(Config.DepthMode.AUTOMATIC);
if (isDepthSupported) {
  config.setDepthMode(Config.DepthMode.AUTOMATIC);
} else {
  config.setDepthMode(Config.DepthMode.DISABLED);
}
session.configure(config);

이제 AR 세션이 적절하게 구성되어 앱은 깊이 기반 기능을 사용할 수 있는지를 알 수 있습니다.

또한 이 세션에 깊이가 사용되는지를 사용자에게 알려야 합니다. 화면 하단에 표시되는 스낵바에 또 다른 메시지를 추가할 수 있습니다.

// Add this line at the top of the file, with the other messages.
private static final String DEPTH_NOT_AVAILABLE_MESSAGE = "[Depth not supported on this device]";

그리고 필요에 따라 이 메시지를 onDrawFrame() 내부에 표시할 수 있습니다.

// Add this if-statement above messageSnackbarHelper.showMessage(this, messageToShow).
if (!isDepthSupported) {
  messageToShow += "\n" + DEPTH_NOT_AVAILABLE_MESSAGE;
}

깊이를 지원하지 않는 기기에서 이 앱을 실행하면 방금 추가한 메시지가 하단에 나타납니다.

feb1c9f42f3cf396.png

다음으로 Depth API를 호출하고 프레임마다 깊이 이미지를 가져오도록 앱을 업데이트합니다.

Depth API는 환경의 3D 관찰값을 캡처하고 이 데이터가 포함된 깊이 이미지를 애플리케이션에 제공합니다. 이 이미지의 각 픽셀은 카메라에서 실제 환경까지의 거리 측정값을 나타냅니다.

이 Codelab에서는 이러한 깊이 이미지를 사용하여 앱의 렌더링 및 시각화를 개선합니다. 첫 번째 단계는 각 프레임의 깊이 이미지를 가져와 GPU에서 사용할 텍스처에 결합하는 것입니다.

먼저 프로젝트에 새 클래스를 추가합니다. DepthTextureHandler는 주어진 ARCore 프레임의 깊이 이미지를 가져옵니다. 다음 파일을 추가합니다.

41c3889f2bbc8345.png

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 acquireDepthImage().
   * This method needs to be called on a thread with an EGL context attached.
   */
  public void update(final Frame frame) {
    try {
      Image depthImage = frame.acquireDepthImage();
      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() 메서드를 업데이트하여 이 텍스처를 GPU 셰이더에서 사용할 수 있도록 초기화합니다.

// Put this at the top of the "try" block in onSurfaceCreated().
depthTexture.createOnGlThread();

마지막으로 모든 프레임마다 이 텍스처를 최신 깊이 이미지로 채웁니다. 이 작업은 session에서 가져온 최신 프레임에서 위에서 만든 update() 메서드를 호출하여 완료할 수 있습니다. 이 앱에서는 깊이 지원이 선택사항이므로 깊이를 사용하는 경우에만 이 호출을 실행할 수 있습니다.

// Add this just after "frame" is created inside onDrawFrame().
if (isDepthSupported) {
  depthTexture.update(frame);
}

이제 프레임마다 업데이트되는 깊이 이미지가 있습니다. 셰이더에서 사용할 준비가 되었습니다. 하지만 앱의 동작은 아직 변경되지 않았습니다. 깊이 이미지를 사용하여 앱을 개선해 보겠습니다.

가지고 놀 수 있는 깊이 이미지가 어떻게 표시되는지 살펴보겠습니다. 이 섹션에서는 각 프레임의 깊이를 렌더링하는 버튼을 앱에 추가합니다.

새 셰이더 추가하기

깊이 이미지를 보는 방법에는 여러 가지가 있습니다. 다음 셰이더는 간단한 색상 매핑 시각화를 제공합니다.

먼저 src/main/assets/shaders/ 디렉터리에 새 .vert 셰이더 및 .frag 셰이더를 추가합니다.

새 .vert 셰이더 추가하기

Android 스튜디오에서:

  1. 셰이더 디렉터리를 마우스 오른쪽 버튼으로 클릭합니다.
  2. New -> File을 선택합니다.
  3. 이름을 background_show_depth_map.vert로 지정합니다.
  4. 텍스트 파일로 설정합니다.

새 파일에 다음 코드를 추가합니다.

src/main/assets/shaders/background_show_depth_map.vert

attribute vec4 a_Position;
attribute vec2 a_TexCoord;

varying vec2 v_TexCoord;

void main() {
   v_TexCoord = a_TexCoord;
   gl_Position = a_Position;
}

위와 동일한 단계를 반복하여 background_show_depth_map.frag라는 동일한 디렉터리에 프래그먼트 셰이더를 만듭니다. 그런 다음 이 새 파일에 다음 코드를 추가합니다.

src/main/assets/shaders/background_show_depth_map.frag

precision mediump float;
uniform sampler2D u_Depth;
varying vec2 v_TexCoord;
const highp float kMaxDepth = 8000.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;
}

다음으로 src/main/java/com/google/ar/core/codelab/common/rendering/BackgroundRenderer.java에 있는 이러한 새 셰이더를 사용하도록 BackgroundRenderer 클래스를 업데이트합니다.

클래스 상단에 있는 셰이더에 파일 경로를 추가합니다.

// Add these under the other shader names at the top of the class.
private static final String DEPTH_VERTEX_SHADER_NAME = "shaders/background_show_depth_map.vert";
private static final String DEPTH_FRAGMENT_SHADER_NAME = "shaders/background_show_depth_map.frag";

두 개의 셰이더를 실행할 것이므로 BackgroundRenderer 클래스에 구성원 변수를 더 추가합니다.

// Add to the top of file with the rest of the member variables.
private int depthProgram;
private int depthTextureParam;
private int depthTextureId = -1;
private int depthQuadPositionParam;
private int depthQuadTexCoordParam;

새 메서드를 추가하여 다음 필드를 채웁니다.

// Add this method below createOnGlThread().
public void createDepthShaders(Context context, int depthTextureId) throws IOException {
  int vertexShader =
      ShaderUtil.loadGLShader(
          TAG, context, GLES20.GL_VERTEX_SHADER, DEPTH_VERTEX_SHADER_NAME);
  int fragmentShader =
      ShaderUtil.loadGLShader(
          TAG, context, GLES20.GL_FRAGMENT_SHADER, DEPTH_FRAGMENT_SHADER_NAME);

  depthProgram = GLES20.glCreateProgram();
  GLES20.glAttachShader(depthProgram, vertexShader);
  GLES20.glAttachShader(depthProgram, fragmentShader);
  GLES20.glLinkProgram(depthProgram);
  GLES20.glUseProgram(depthProgram);
  ShaderUtil.checkGLError(TAG, "Program creation");

  depthTextureParam = GLES20.glGetUniformLocation(depthProgram, "u_Depth");
  ShaderUtil.checkGLError(TAG, "Program parameters");

  depthQuadPositionParam = GLES20.glGetAttribLocation(depthProgram, "a_Position");
  depthQuadTexCoordParam = GLES20.glGetAttribLocation(depthProgram, "a_TexCoord");

  this.depthTextureId = depthTextureId;
}

각 프레임에서 다음 셰이더로 그리는 데 사용되는 이 메서드를 추가합니다.

// Put this at the bottom of the file.
public void drawDepth(@NonNull Frame frame) {
  if (frame.hasDisplayGeometryChanged()) {
    frame.transformCoordinates2d(
        Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
        quadCoords,
        Coordinates2d.TEXTURE_NORMALIZED,
        quadTexCoords);
  }

  if (frame.getTimestamp() == 0 || depthTextureId == -1) {
    return;
  }

  // Ensure position is rewound before use.
  quadTexCoords.position(0);

  // No need to test or write depth, the screen quad has arbitrary depth, and is expected
  // to be drawn first.
  GLES20.glDisable(GLES20.GL_DEPTH_TEST);
  GLES20.glDepthMask(false);

  GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
  GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, depthTextureId);
  GLES20.glUseProgram(depthProgram);
  GLES20.glUniform1i(depthTextureParam, 0);

  // Set the vertex positions and texture coordinates.
  GLES20.glVertexAttribPointer(
        depthQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadCoords);
  GLES20.glVertexAttribPointer(
        depthQuadTexCoordParam, TEXCOORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, quadTexCoords);

  // Draws the quad.
  GLES20.glEnableVertexAttribArray(depthQuadPositionParam);
  GLES20.glEnableVertexAttribArray(depthQuadTexCoordParam);
  GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
  GLES20.glDisableVertexAttribArray(depthQuadPositionParam);
  GLES20.glDisableVertexAttribArray(depthQuadTexCoordParam);

  // Restore the depth state for further drawing.
  GLES20.glDepthMask(true);
  GLES20.glEnable(GLES20.GL_DEPTH_TEST);

  ShaderUtil.checkGLError(TAG, "BackgroundRendererDraw");
}

전환 버튼 추가하기

이제 깊이 맵을 렌더링할 수 있는 기능이 생겼으니 사용해 보겠습니다. DepthCodelabActivity에서 이 렌더링을 사용 설정하고 사용 중지하는 버튼을 추가할 수 있습니다.

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의 값을 제어합니다. 이 플래그를 사용하여 깊이 맵이 렌더링되는지를 제어합니다. DepthCodelabActivity에 있는 메서드 onDrawFrame()으로 돌아가서 다음을 추가합니다.

// Add this snippet just under backgroundRenderer.draw(frame);
if (showDepthMap) {
  backgroundRenderer.drawDepth(frame);
}

onSurfaceCreated()에 다음 줄을 추가하여 깊이 텍스처를 backgroundRenderer에 전달합니다.

// Add to onSurfaceCreated() after backgroundRenderer.createonGlThread(/*context=*/ this);
backgroundRenderer.createDepthShaders(/*context=*/ this, depthTexture.getDepthTexture());

이제 화면 오른쪽 상단의 버튼을 눌러 각 프레임의 깊이 이미지를 볼 수 있습니다.

Depth API 지원 없이 실행

Depth API 지원으로 실행

[선택사항] 멋진 깊이 애니메이션

앱은 현재 깊이 맵을 직접 표시합니다. 빨간색 픽셀은 가까운 영역을, 파란색 픽셀은 멀리 있는 영역을 나타냅니다.

깊이 정보를 전달하는 방법에는 여러 가지가 있습니다. 이 하위 섹션에서는 카메라에서 반복적으로 멀어지는 대역 내에서 깊이만 표시하도록 셰이더를 주기적으로 '파동' 깊이로 수정합니다.

다음 변수를 background_show_depth_map.frag 상단에 추가하여 시작합니다.

uniform float u_DepthRangeToRenderMm;
const float kDepthWidthToRenderMm = 350.0;

그런 다음 이 값들을 사용하여 셰이더의 main() 함수에서 깊이 값으로 처리할 픽셀을 필터링합니다.

// Add this line at the end of main().
gl_FragColor.a = clamp(1.0 - abs((depth_mm - u_DepthRangeToRenderMm) / kDepthWidthToRenderMm), 0.0, 1.0);

다음으로 이러한 셰이더 매개변수를 유지하도록 BackgroundRenderer.java를 업데이트합니다. 클래스 상단에 다음 필드를 추가합니다.

private static final float MAX_DEPTH_RANGE_TO_RENDER_MM = 10000.0f;
private float depthRangeToRenderMm = 0.0f;
private int depthRangeToRenderMmParam;

createDepthShaders() 메서드 내에 다음을 추가하여 이러한 매개변수를 셰이더 프로그램과 일치시킵니다.

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);

이제 깊이가 장면 전체에 흐르는 애니메이션 파동으로 시각화됩니다.

37e2a86b833150f8.gif

여기에서 제공된 값을 자유롭게 변경하여 파동을 더 느리거나 더 빠르게, 더 넓거나 더 좁게 만드는 등의 작업을 할 수 있습니다. 셰이더를 변경하여 깊이 정보를 표시하는 새로운 방법을 탐색할 수도 있습니다.

오클루전은 가상 객체와 카메라 간에 실제 표면이 있기 때문에 가상 객체가 완전히 렌더링되지 않은 경우입니다.

가상 객체를 제자리에 적절하게 렌더링할 수 있으면 증강된 장면의 현실감과 신뢰성이 향상됩니다. 더 많은 예를 보려면 Depth API와 현실을 혼합하는 방법에 관한 동영상을 참고하세요.

이 섹션에서는 깊이를 사용할 수 있는 경우 가상 객체를 포함하도록 앱을 업데이트합니다.

새 객체 셰이더 추가하기

이전 섹션과 마찬가지로 깊이 정보를 지원하기 위해 새 셰이더를 추가합니다. 이번에는 기존의 객체 셰이더를 복사하고 오클루전 기능을 추가합니다. 애플리케이션이 깊이를 지원할지를 런타임에 결정할 수 있도록 두 버전의 객체 셰이더를 모두 유지하는 것이 중요합니다.

src/main/assets/shaders 디렉터리에 object.vert 셰이더 파일과 object.frag 셰이더 파일의 사본을 만듭니다.

  1. object.vert를 대상 파일 src/main/assets/shaders/occlusion_object.vert에 복사합니다.
  2. object.frag를 대상 파일 src/main/assets/shaders/occlusion_object.frag에 복사합니다.

occlusion_object.vert 내에서 main() 위에 다음 변수를 추가합니다.

varying vec3 v_ScreenSpacePosition;

이 변수를 main() 하단에 설정합니다.

v_ScreenSpacePosition = gl_Position.xyz / gl_Position.w;

파일 상단에서 main() 위에 다음 변수를 추가하여 occlusion_object.frag를 업데이트합니다.

varying vec3 v_ScreenSpacePosition;

uniform sampler2D u_Depth;
uniform mat3 u_UvTransform;
uniform float u_DepthTolerancePerMm;
uniform float u_OcclusionAlpha;
uniform float u_DepthAspectRatio;

셰이더의 main() 위에 이러한 도우미 함수를 추가하여 깊이 정보를 더 쉽게 처리할 수 있도록 만듭니다.

float GetDepthMillimeters(in vec2 depth_uv) {
  // Depth is packed into the red and green components of its texture.
  // The texture is a normalized format, storing millimeters.
  vec3 packedDepthAndVisibility = texture2D(u_Depth, depth_uv).xyz;
  return dot(packedDepthAndVisibility.xy, vec2(255.0, 256.0 * 255.0));
}

// Returns linear interpolation position of value between min and max bounds.
// E.g., InverseLerp(1100, 1000, 2000) returns 0.1.
float InverseLerp(in float value, in float min_bound, in float max_bound) {
  return clamp((value - min_bound) / (max_bound - min_bound), 0.0, 1.0);
}

// Returns a value between 0.0 (not visible) and 1.0 (completely visible)
// Which represents how visible or occluded is the pixel in relation to the
// depth map.
float GetVisibility(in vec2 depth_uv, in float asset_depth_mm) {
  float depth_mm = GetDepthMillimeters(depth_uv);

  // Instead of a hard z-buffer test, allow the asset to fade into the
  // background along a 2 * u_DepthTolerancePerMm * asset_depth_mm
  // range centered on the background depth.
  float visibility_occlusion = clamp(0.5 * (depth_mm - asset_depth_mm) /
    (u_DepthTolerancePerMm * asset_depth_mm) + 0.5, 0.0, 1.0);

  // Depth close to zero is most likely invalid, do not use it for occlusions.
  float visibility_depth_near = 1.0 - InverseLerp(
      depth_mm, /*min_depth_mm=*/150.0, /*max_depth_mm=*/200.0);

  // Same for very high depth values.
  float visibility_depth_far = InverseLerp(
      depth_mm, /*min_depth_mm=*/7500.0, /*max_depth_mm=*/8000.0);

  float visibility =
    max(max(visibility_occlusion, u_OcclusionAlpha),
      max(visibility_depth_near, visibility_depth_far));

  return visibility;
}

이제 occlusion_object.fragmain()을 업데이트하여 깊이를 인식하고 오클루전을 적용합니다. 파일 하단에 다음 줄을 추가합니다.

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);

이제 객체 셰이더의 새 버전이 있으므로 렌더러 코드를 수정할 수 있습니다.

객체 오클루전 렌더링하기

다음으로 src/main/java/com/google/ar/core/codelab/common/rendering/ObjectRenderer.java에 있는 ObjectRenderer 클래스의 사본을 만듭니다.

ObjectRenderer 클래스를 선택합니다. 마우스 오른쪽 버튼을 클릭한 후 'Copy'를 클릭합니다. rendering 폴더를 선택합니다. 오른쪽 버튼을 클릭한 후 'Paste'를 클릭합니다.

클래스 이름을 OcclusionObjectRenderer로 바꿉니다.

이제 이름을 바꾼 새 클래스가 동일한 폴더에 나타납니다.

새로 만든 OcclusionObjectRenderer.java를 열고 파일 상단에서 셰이더 경로를 변경합니다.

private static final String VERTEX_SHADER_NAME = "shaders/occlusion_object.vert";
private static final String FRAGMENT_SHADER_NAME = "shaders/occlusion_object.frag";

이러한 깊이 관련 구성원 변수를 다른 변수와 함께 클래스 상단에 추가합니다.

// Shader location: depth texture
private int depthTextureUniform;

// Shader location: transform to depth uvs
private int depthUvTransformUniform;

// Shader location: depth tolerance property
private int depthToleranceUniform;

// Shader location: maximum transparency for the occluded part.
private int occlusionAlphaUniform;

private int depthAspectRatioUniform;

private float[] uvTransform = null;
private int depthTextureId;

위의 변수는 오클루전 테두리의 선명도를 조정합니다. 먼저 클래스 상단에서 기본값을 사용하여 다음 구성원 변수를 만듭니다.

// These values will be changed each frame based on the distance to the object.
private float depthAspectRatio = 0.0f;
private final float depthTolerancePerMm = 0.015f;
private final float occlusionsAlpha = 0.0f;

createOnGlThread() 메서드에서 셰이더의 균일 매개변수를 초기화합니다.

// Occlusions Uniforms.  Add these lines before the first call to ShaderUtil.checkGLError
// inside the createOnGlThread() method.
depthTextureUniform = GLES20.glGetUniformLocation(program, "u_Depth");
depthUvTransformUniform = GLES20.glGetUniformLocation(program, "u_UvTransform");
depthToleranceUniform = GLES20.glGetUniformLocation(program, "u_DepthTolerancePerMm");
occlusionAlphaUniform = GLES20.glGetUniformLocation(program, "u_OcclusionAlpha");
depthAspectRatioUniform = GLES20.glGetUniformLocation(program, "u_DepthAspectRatio");

다음을 포함하도록 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의 인스턴스를 활동에 추가하여 사용 설정되므로 ObjectRendererOcclusionObjectRenderer가 모두 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();

다음으로 현재 기기가 Depth API를 지원하는지에 따라 이 occludedVirtualObject가 사용되는 시기를 제어할 수 있습니다. virtualObject가 구성된 아래의 onSurfaceCreated 메서드 내에 다음 줄을 추가합니다.

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 지원 없이 앱 실행

[선택사항] 오클루전 품질 개선하기

위에서 구현된 깊이 기반 오클루전 메서드는 경계가 선명한 오클루전을 제공합니다. 카메라가 객체에서 멀어질수록 깊이 측정의 정확도가 떨어져 시각적 아티팩트가 발생할 수 있습니다. 오클루전 테스트에 블러를 추가하여 숨겨진 가상 객체에 더 부드러운 가장자리를 생성함으로써 이 문제를 완화할 수 있습니다.

occlusion_object.frag

occlusion_object.frag 상단에 다음 균일 변수를 추가합니다.

uniform float u_OcclusionBlurAmount;

이 도우미 함수를 셰이더의 main() 바로 위에 추가하여 오클루전 샘플링에 커널 블러를 적용합니다.

float GetBlurredVisibilityAroundUV(in vec2 uv, in float asset_depth_mm) {
  // Kernel used:
  // 0   4   7   4   0
  // 4   16  26  16  4
  // 7   26  41  26  7
  // 4   16  26  16  4
  // 0   4   7   4   0
  const float kKernelTotalWeights = 269.0;
  float sum = 0.0;

  vec2 blurriness = vec2(u_OcclusionBlurAmount,
                         u_OcclusionBlurAmount * u_DepthAspectRatio);

  float current = 0.0;

  current += GetVisibility(uv + vec2(-1.0, -2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, -2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-1.0, +2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, +2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-2.0, +1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+2.0, +1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-2.0, -1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+2.0, -1.0) * blurriness, asset_depth_mm);
  sum += current * 4.0;

  current = 0.0;
  current += GetVisibility(uv + vec2(-2.0, -0.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+2.0, +0.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+0.0, +2.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-0.0, -2.0) * blurriness, asset_depth_mm);
  sum += current * 7.0;

  current = 0.0;
  current += GetVisibility(uv + vec2(-1.0, -1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, -1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-1.0, +1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, +1.0) * blurriness, asset_depth_mm);
  sum += current * 16.0;

  current = 0.0;
  current += GetVisibility(uv + vec2(+0.0, +1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-0.0, -1.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(-1.0, -0.0) * blurriness, asset_depth_mm);
  current += GetVisibility(uv + vec2(+1.0, +0.0) * blurriness, asset_depth_mm);
  sum += current * 26.0;

  sum += GetVisibility(uv , asset_depth_mm) * 41.0;

  return sum / kKernelTotalWeights;
}

main()의 기존 줄을

gl_FragColor.a *= GetVisibility(depth_uvs, asset_depth_mm);

다음 줄로 바꿉니다.

gl_FragColor.a *= GetBlurredVisibilityAroundUV(depth_uvs, asset_depth_mm);

이 새로운 셰이더 기능을 활용하려면 렌더러를 업데이트합니다.

OcclusionObjectRenderer.java

클래스 상단에 다음 구성원 변수를 추가합니다.

private int occlusionBlurUniform;
private final float occlusionsBlur = 0.01f;

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);

시각적 비교

이러한 변경으로 이제 오클루전 경계가 더 매끄럽습니다.

앱 빌드하고 실행하기

앱을 빌드하고 실행하려면 다음 단계를 따르세요.

  1. USB를 통해 Android 기기를 연결합니다.
  2. File > Build and Run을 선택합니다.
  3. ARCodeLab.apk로 저장합니다.
  4. 앱이 빌드되고 기기에 배포될 때까지 기다립니다.

기기에 앱을 처음 배포하려고 할 때 기기에서

Allow USB debugging

메시지가 표시됩니다. 계속하려면 OK를 선택합니다.

기기에서 앱을 처음 실행하면 앱에 기기 카메라 사용 권한이 있는지 묻는 메시지가 표시됩니다. AR 기능을 계속 사용하려면 액세스를 허용해야 합니다.

앱 테스트하기

앱을 실행할 때 기기를 들고 공간을 돌아다니며 천천히 영역을 스캔하여 기본 동작을 테스트할 수 있습니다. 최소 10초 이상 데이터를 수집하고 여러 방향에서 영역을 스캔한 후에 다음 단계로 진행합니다.

축하합니다. Google의 ARCore Depth API를 사용하여 첫 번째 깊이 기반 증강 현실 앱을 성공적으로 빌드하고 실행했습니다.

개발용 Android 기기 설정하기

  1. USB 케이블을 사용하여 기기를 개발 머신에 연결합니다. Windows를 사용하여 개발하는 경우 기기에 맞는 USB 드라이버를 설치해야 할 수도 있습니다.
  2. 다음 단계를 실행하여 개발자 옵션 창에서 USB 디버깅을 사용 설정합니다.
  • 설정 앱을 엽니다.
  • 기기에서 Android v8.0 이상을 사용한다면 System을 선택합니다. 그렇지 않은 경우 다음 단계로 진행합니다.
  • 하단으로 스크롤하여 휴대전화 정보를 선택합니다.
  • 하단으로 스크롤하여 빌드 번호를 7번 탭합니다.
  • 이전 화면으로 돌아가서 하단으로 스크롤한 다음 개발자 옵션을 탭합니다.
  • 개발자 옵션 창에서 하단으로 스크롤하여 USB 디버깅을 찾아 사용 설정합니다.

이 프로세스에 관한 세부정보는 Google의 Android 개발자 웹사이트를 참고하세요.

1480e83e227b94f1.png

라이선스와 관련된 빌드 실패(일부 라이선스가 허용되지 않아 다음 Android SDK 패키지를 설치할 수 없음)가 발생하는 경우 다음 명령어를 사용하여 이러한 라이선스를 검토하고 수락할 수 있습니다.

cd <path to Android SDK>

tools/bin/sdkmanager --licenses

자주 묻는 질문(FAQ)