WebXR Device API를 사용하여 증강 현실(AR) 앱 빌드

1. 시작하기 전에

이 Codelab에서는 AR 웹 앱을 빌드하는 예시를 살펴봅니다. 여기서는 실제로 존재하는 것처럼 보이는 3D 모델을 렌더링하기 위해 자바스크립트를 사용합니다.

AR 및 가상 현실(VR) 기능을 결합하는 WebXR Device API를 사용합니다. 대화형 웹에서 실행되는 간단한 AR 앱을 만들기 위해 WebXR Device API의 AR 확장 프로그램에 중점을 둡니다.

AR이란 무엇인가요?

AR은 보통 컴퓨터에서 생성한 그래픽과 실제 세상을 혼합하는 것을 설명하는 데 사용되는 용어입니다. 즉, 휴대전화 기반 AR의 경우 컴퓨터 그래픽을 실시간 카메라 피드 위에 배치함으로써 실제로 존재하는 것처럼 보이게 하는 것입니다. 휴대전화가 공간에서 움직이면서 이 효과를 사실적으로 유지하려면 AR이 지원되는 기기가 주변 세계를 이해하고 3D 공간에서 포즈(위치 및 방향)를 결정해야 합니다. 여기에는 표면을 감지하고 주변 환경의 조명을 추정하는 작업이 포함될 수 있습니다.

셀카 필터 또는 AR 기반 게임에 사용되는 AR은 Google의 ARCore 및 Apple의 ARKit가 출시된 이후 앱에서 널리 사용되고 있습니다.

빌드할 항목

이 Codelab에서는 증강 현실을 사용하여 실제 세상에 모델을 배치하는 웹 앱을 빌드합니다. 앱에 다음 기능을 구현할 수 있습니다.

  1. 대상 기기의 센서를 사용하여 주변 공간에서의 위치와 방향을 확인하고 추적합니다.
  2. 실시간 카메라 뷰 위에 합성된 3D 모델을 렌더링합니다.
  3. 히트 테스트를 실행하여 실제로 감지된 표면 위에 객체를 배치합니다.

학습할 내용

  • WebXR Device API 사용 방법
  • 기본 AR 장면을 구성하는 방법
  • AR 히트 테스트를 사용하여 표면을 찾는 방법
  • 실제 카메라 피드와 동기화된 3D 모델을 로드하고 렌더링하는 방법
  • 3D 모델을 기반으로 그림자를 렌더링하는 방법

이 Codelab에서는 AR API에 중점을 둡니다. 따라서 이와 관련 없는 개념과 코드 블록은 그냥 넘어가겠습니다. 단, 필요할 때 확인할 수 있도록 해당 저장소 코드에서 제공만 해드리겠습니다.

필요한 항목

이 Codelab의 첫 번째 단계를 진행하려면 AR 기기에서 사용해 보기를 클릭합니다. '브라우저에 AR 기능이 없습니다'라는 메시지가 표시된 페이지가 나오면 Android 기기에 Google Play AR 서비스가 설치되어 있는지 확인하세요.

2. 개발 환경 설정

코드 다운로드

  1. 다음 링크를 클릭하여 워크스테이션에 이 Codelab의 모든 코드를 다운로드합니다.

  1. 다운로드한 ZIP 파일의 압축을 해제합니다. 그러면 필요한 모든 리소스와 함께 이 Codelab의 여러 단계의 디렉터리가 포함된 루트 폴더(ar-with-webxr-master)가 압축 해제됩니다.

step-03 폴더와 step-04 폴더에는 이 Codelab의 세 번째와 네 번째 단계에서 도달하고자 하는 최종 상태와 final 결과가 들어 있습니다. 참고용으로 제공됩니다.

모든 코딩 작업은 work 디렉터리에서 실행합니다.

웹 서버 설치

  1. 자체 웹 서버를 사용할 수 있습니다. 아직 설정되지 않은 경우 이 섹션에서는 Chrome용 웹 서버를 설정하는 방법을 자세히 설명합니다.
    워크스테이션에 아직 앱을 설치하지 않았다면 Chrome 웹 스토어에서 설치할 수 있습니다.

  1. Chrome용 웹 서버 앱을 설치한 후 chrome://apps로 이동하여 웹 서버 아이콘을 클릭합니다.

웹 서버 아이콘

그다음 이 대화상자가 표시되면 로컬 웹 서버를 구성할 수 있습니다.

Chrome 웹 서버 구성

  1. 폴더 선택을 클릭하고 ar-with-webxr-master 폴더를 선택합니다. 이렇게 하면 웹 서버 대화상자(웹 서버 URL 섹션)에 강조표시된 URL을 통해 진행 중인 작업을 제공할 수 있습니다.
  2. 옵션(다시 시작해야 함)에서 index.html 자동 표시 체크박스를 선택합니다.
  3. 웹 서버중지로 전환하고 다시 시작됨으로 전환합니다.Chrome 웹 서버 다시 시작
  4. 하나 이상의 웹 서버 URL이 기본 로컬 호스트 URL(http://127.0.0.1:8887)인지 확인합니다.

포트 전달 설정

localhost:8887을 방문할 때 워크스테이션에서 동일한 포트에 액세스하도록 AR 기기를 구성합니다.

  1. 개발 워크스테이션에서 chrome://inspect로 이동하여 포트 전달을 클릭합니다. chrome://inspect
  2. 포트 전달 설정 대화상자를 사용하여 포트 8887을 localhost:8887로 전달합니다.
  3. 포트 전달 사용 설정 체크박스를 선택합니다.

포트 전달 구성

설정 확인

연결을 테스트합니다.

  1. USB 케이블을 사용하여 AR 기기를 워크스테이션에 연결합니다.
  2. Chrome의 AR 기기에서 주소 표시줄에 http://localhost:8887을 입력합니다. AR 기기는 이 요청을 개발 워크스테이션의 웹 서버로 전달해야 합니다. 파일 디렉터리가 표시됩니다.
  3. AR 기기에서 step-03을 클릭하여 브라우저에 step-03/index.html 파일을 로드합니다.

증강 현실 시작 버튼이 있는 페이지가 표시됩니다.

하지만 지원되지 않는 브라우저 오류 페이지가 표시되면 기기가 호환되지 않는 것일 수 있습니다.

ARCore 지원됨

ARCore 지원되지 않음

이제 웹 서버와의 연결이 AR 기기와 작동됩니다.

  1. 증강 현실 시작을 클릭합니다. ARCore를 설치하라는 메시지가 표시될 수 있습니다.

ARCore 설치 메시지

AR 앱을 처음 실행하면 카메라 권한 메시지가 표시됩니다.

Chrome에서 카메라 권한 요청권한 대화상자

모든 준비가 완료되면 카메라 피드 위에 여러 큐브가 오버레이되는 장면이 표시됩니다. 카메라가 더 넓은 공간을 파싱함에 따라 장면 이해가 개선되므로, 여기저기 움직이면 효과가 안정화될 수 있습니다.

3. WebXR 구성

이 단계에서는 WebXR 세션과 기본 AR 장면을 설정하는 방법을 알아봅니다. HTML 페이지에는 기본 AR 기능을 사용할 수 있도록 CSS 스타일 지정 및 자바스크립트가 제공됩니다. 이렇게 하면 설정 프로세스에 소요되는 시간이 단축되어 Codelab에서 AR 기능에 집중할 수 있습니다.

HTML 페이지

기존 웹 기술을 사용하여 기존 웹페이지에 AR 환경을 빌드합니다. 이 환경에서는 전체 화면 렌더링 캔버스를 사용하므로 HTML 파일이 지나치게 복잡해질 필요가 없습니다.

AR 기능을 사용하려면 사용자 동작이 필요하므로 AR 시작 버튼 및 지원되지 않는 브라우저 메시지를 표시하기 위한 몇 가지 머티리얼 디자인 구성요소가 있습니다.

이미 work 디렉터리에 있는 index.html 파일은 다음과 같습니다. 실제 콘텐츠의 일부입니다. 이 코드를 파일에 복사하지 마세요.

<!-- Don't copy this code into your file! -->
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Building an augmented reality application with the WebXR Device API</title>
    <link rel="stylesheet" href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css">
    <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>

    <!-- three.js -->
    <script src="https://unpkg.com/three@0.123.0/build/three.js"></script>
    <script src="https://unpkg.com/three@0.123.0/examples/js/loaders/GLTFLoader.js"></script>

    <script src="../shared/utils.js"></script>
    <script src="app.js"></script>
  </head>
  <body>
  <!-- Information about AR removed for brevity. -->

  <!-- Starting an immersive WebXR session requires user interaction. Start the WebXR experience with a simple button. -->
  <a onclick="activateXR()" class="mdc-button mdc-button--raised mdc-button--accent">
    Start augmented reality
  </a>

</body>
</html>

키 자바스크립트 코드 열기

앱의 시작점은 app.js입니다. 이 파일은 AR 환경을 설정하기 위한 상용구를 제공합니다.

작업 디렉터리에는 이미 앱 코드(app.js)가 포함되어 있습니다.

WebXR 및 AR 지원 확인

사용자가 AR로 작업하기 전에 필요한 XR 기능과 navigator.xr이 있는지 확인합니다. navigator.xr 객체는 WebXR Device API의 진입점이므로 기기가 호환되는 경우 있어야 합니다. 또한 "immersive-ar" 세션 모드가 지원되는지 확인합니다.

문제가 없는 경우 증강 현실 진입 버튼을 클릭하면 XR 세션을 만들려고 합니다. 문제가 있다면 shared/utils.js에서 onNoXRDevice()가 호출되고 AR 지원 부족을 나타내는 메시지를 표시합니다.

이 코드는 이미 app.js에 있으므로 변경할 필요가 없습니다.

(async function() {
  if (navigator.xr && await navigator.xr.isSessionSupported("immersive-ar")) {
    document.getElementById("enter-ar").addEventListener("click", activateXR)
  } else {
    onNoXRDevice();
  }
})();

XRSession 요청

증강 현실 진입을 클릭하면 코드는 activateXR()을 호출합니다. 그러면 AR 환경이 시작됩니다.

  1. app.js에서 activateXR() 함수를 찾습니다. 일부 코드는 생략되었습니다.
activateXR = async () => {
  // Initialize a WebXR session using "immersive-ar".
  this.xrSession = /* TODO */;

  // Omitted for brevity
}

WebXR의 진입점은 XRSystem.requestSession()을 통해 이루어집니다. immersive-ar 모드를 사용하면 렌더링된 콘텐츠를 실제 환경에서 볼 수 있습니다.

  1. "immersive-ar" 모드를 사용하여 this.xrSession을 초기화합니다.
activateXR = async () => {
  // Initialize a WebXR session using "immersive-ar".
  this.xrSession = await navigator.xr.requestSession("immersive-ar");

  // ...
}

XRReferenceSpace 초기화

XRReferenceSpace는 가상 세계 내 객체에 사용되는 좌표계를 설명합니다. 'local' 모드는 뷰어 근처에 원점이 있고 안정적인 추적이 가능한 참조 공간이 있는 AR 환경에 가장 적합합니다.

onSessionStarted()에서 다음 코드를 사용하여 this.localReferenceSpace를 초기화합니다.

this.localReferenceSpace = await this.xrSession.requestReferenceSpace("local");

애니메이션 루프 정의

  1. window.requestAnimationFrame과 유사하게 XRSessionrequestAnimationFrame을 사용하여 렌더링 루프를 시작합니다.

모든 프레임에서 타임스탬프와 XRFrame을 사용하여 onXRFrame이 호출됩니다.

  1. onXRFrame의 구현을 완료합니다. 프레임이 그려지면 아래 코드를 추가하여 다음 요청을 큐에 추가합니다.
// Queue up the next draw request.
this.xrSession.requestAnimationFrame(this.onXRFrame);
  1. 그래픽 환경을 설정하는 코드를 추가합니다. onXRFrame의 하단에 추가합니다.
// Bind the graphics framebuffer to the baseLayer's framebuffer.
const framebuffer = this.xrSession.renderState.baseLayer.framebuffer;
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
this.renderer.setFramebuffer(framebuffer);
  1. 뷰어 포즈를 확인하려면 XRFrame.getViewerPose()를 사용하세요. 이 XRViewerPose는 공간 내 기기의 위치와 방향을 설명합니다. 또한 현재 기기에서 올바르게 표시하기 위해 장면을 렌더링해야 하는 모든 시점을 설명하는 XRView 배열도 포함됩니다. 입체 VR에는 두 개의 뷰(각 눈당 하나씩)가 있지만, AR 기기에는 한 개의 뷰만 있습니다.
    pose.views의 정보는 가상 카메라의 뷰 매트릭스와 투영 매트릭스를 구성하는 데 가장 일반적으로 사용됩니다. 장면이 3D로 표시되는 방식에 영향을 줍니다. 카메라가 구성되면 장면을 렌더링할 수 있습니다.
  2. onXRFrame의 하단에 추가합니다.
// Retrieve the pose of the device.
// XRFrame.getViewerPose can return null while the session attempts to establish tracking.
const pose = frame.getViewerPose(this.localReferenceSpace);
if (pose) {
  // In mobile AR, we only have one view.
  const view = pose.views[0];

  const viewport = this.xrSession.renderState.baseLayer.getViewport(view);
  this.renderer.setSize(viewport.width, viewport.height);

  // Use the view's transform matrix and projection matrix to configure the THREE.camera.
  this.camera.matrix.fromArray(view.transform.matrix);
  this.camera.projectionMatrix.fromArray(view.projectionMatrix);
  this.camera.updateMatrixWorld(true);

  // Render the scene with THREE.WebGLRenderer.
  this.renderer.render(this.scene, this.camera);
}

테스트

앱을 실행합니다. 개발 기기에서 work/index.html을 방문하세요. 카메라 피드에 공중에 떠있는 여러 큐브가 보이며 기기를 움직임에 따라 시점이 바뀝니다. 이동함에 따라 추적이 개선되므로 사용 중인 기기와 본인에게 맞는 방법을 찾아보세요.

앱을 실행하는 데 문제가 있는 경우 소개개발 환경 설정 섹션을 확인합니다.

4. 타겟팅 레티클 추가

기본 AR 장면을 설정했다면 히트 테스트를 사용하여 실제 세상과 상호작용해보겠습니다. 이 섹션에서는 히트 테스트를 프로그래밍하고 이를 사용하여 실제 표면을 찾을 수 있습니다.

히트 테스트 이해

히트 테스트는 일반적으로 공간상의 한 지점에서 특정 방향으로 직선을 뻗어 대상 객체와 교차하는지 확인하는 방법입니다. 이 예시에서는 기기를 실제로 있는 위치에 조준합니다. 기기의 카메라에서 나온 광선이 기기 앞의 현실 세상으로 뻗어나가는 모습을 상상해 보세요.

WebXR Device API를 사용하면 기본 AR 기능과 세상에 대한 이해를 기반으로, 이 광선이 실제 객체와 교차했는지 여부를 확인할 수 있습니다.

히트 테스트 설명

추가 기능으로 XRSession 요청

히트 테스트를 실행하려면 XRSession을 요청할 때 추가 기능이 필요합니다.

  1. app.js에서 navigator.xr.requestSession을 찾습니다.
  2. 다음과 같이 "hit-test""dom-overlay" 기능을 requiredFeature로 추가합니다.
this.xrSession = await navigator.xr.requestSession("immersive-ar", {
  requiredFeatures: ["hit-test", "dom-overlay"]
});
  1. DOM 오버레이를 구성합니다. 다음과 같이 AR 카메라 뷰 위에 document.body 요소를 적용합니다.
this.xrSession = await navigator.xr.requestSession("immersive-ar", {
  requiredFeatures: ["hit-test", "dom-overlay"],
  domOverlay: { root: document.body }
});

모션 메시지 추가

ARCore는 환경에 대한 적절한 이해가 완료되었을 때 가장 잘 작동합니다. 이를 위해서는 위치와 환경 특성의 변화를 계산하기 위해 시각적으로 고유한 특징점을 사용하는 동시 현지화 및 매핑(SLAM) 프로세스를 거쳐야 합니다.

이전 단계의 "dom-overlay"를 사용하여 카메라 스트림 상단에 모션 메시지를 표시합니다.

ID가 stabilization<div>index.html에 추가합니다. 이 <div>는 사용자에게 안정화 상태를 나타내는 애니메이션을 표시하고 SLAM 프로세스를 개선하기 위해 기기를 들고 이동하라는 메시지를 표시합니다. 이는 사용자가 AR에 있을 때 표시되며, 레티클이 <body> 클래스로 제어되는 표면을 찾으면 숨겨집니다.

  <div id="stabilization"></div>

</body>
</html>

레티클 추가

레티클을 사용하여 기기 뷰가 가리키는 위치를 나타냅니다.

  1. app.js에서 setupThreeJs()DemoUtils.createCubeScene() 호출을 빈 Three.Scene()으로 바꿉니다.
setupThreeJs() {
  // ...

  // this.scene = DemoUtils.createCubeScene();
  this.scene = DemoUtils.createLitScene();
}
  1. 충돌 지점을 나타내는 객체로 새 장면을 채웁니다. 제공된 Reticle 클래스는 shared/utils.js의 레티클 모델을 로드하는 작업을 처리합니다.
  2. setupThreeJs()의 장면에 Reticle을 추가합니다.
setupThreeJs() {
  // ...

  // this.scene = DemoUtils.createCubeScene();
  this.scene = DemoUtils.createLitScene();
  this.reticle = new Reticle();
  this.scene.add(this.reticle);
}

히트 테스트를 실행하려면 새 XRReferenceSpace를 사용합니다. 이 참조 공간은 뷰어의 관점에서 새로운 좌표계를 표시하여 보는 방향과 일치하는 광선을 만듭니다. 이 좌표계는 히트 테스트를 계산할 수 있는 XRSession.requestHitTestSource()에서 사용됩니다.

  1. app.jsonSessionStarted()에 다음을 추가합니다.
async onSessionStarted() {
  // ...

  // Setup an XRReferenceSpace using the "local" coordinate system.
  this.localReferenceSpace = await this.xrSession.requestReferenceSpace("local");

  // Add these lines:
  // Create another XRReferenceSpace that has the viewer as the origin.
  this.viewerSpace = await this.xrSession.requestReferenceSpace("viewer");
  // Perform hit testing using the viewer as origin.
  this.hitTestSource = await this.xrSession.requestHitTestSource({ space: this.viewerSpace });

  // ...
}
  1. hitTestSource를 사용하여 프레임마다 히트 테스트를 실행합니다.
    • 히트 테스트 결과가 없으면 ARCore는 환경을 이해하기 위한 시간이 충분하지 않은 것입니다. 이 경우 안정화 <div>를 사용하여 기기를 이동하라는 메시지를 사용자에게 표시합니다.
    • 결과가 나오면 레티클을 해당 위치로 이동합니다.
  2. onXRFrame을 수정하여 레티클을 이동합니다.
onXRFrame = (time, frame) => {
  // ... some code omitted ...
  this.camera.updateMatrixWorld(true);

  // Add the following:
  const hitTestResults = frame.getHitTestResults(this.hitTestSource);

  if (!this.stabilized && hitTestResults.length > 0) {
    this.stabilized = true;
    document.body.classList.add("stabilized");
  }
  if (hitTestResults.length > 0) {
    const hitPose = hitTestResults[0].getPose(this.localReferenceSpace);

    // update the reticle position
    this.reticle.visible = true;
    this.reticle.position.set(hitPose.transform.position.x, hitPose.transform.position.y, hitPose.transform.position.z)
    this.reticle.updateMatrixWorld(true);
  }
  // More code omitted.
}

화면 탭 시 동작 추가

XRSession은 기본 작업을 나타내는 select 이벤트를 통해 사용자 상호작용을 기반으로 이벤트를 내보낼 수 있습니다. 휴대기기의 WebXR에서 기본 작업은 화면 탭입니다.

  1. onSessionStarted 하단에 select 이벤트 리스너를 추가합니다.
this.xrSession.addEventListener("select", this.onSelect);

이 예시에서는 화면 탭을 통해 해바라기를 레티클에 배치합니다.

  1. App 클래스에서 onSelect의 구현을 만듭니다.
onSelect = () => {
  if (window.sunflower) {
    const clone = window.sunflower.clone();
    clone.position.copy(this.reticle.position);
    this.scene.add(clone);
  }
}

앱 테스트

히트 테스트를 사용하여 기기로 조준할 수 있는 레티클을 만들었습니다. 화면을 탭하면 레티컬이 지정하는 위치에 해바라기를 배치할 수 있어야 합니다.

  1. 앱을 실행할 때 바닥의 표면을 추적하는 레티클을 볼 수 있습니다. 제대로 작동하지 않으면 휴대전화로 천천히 주변을 둘러보세요.
  2. 레티클이 표시되면 탭합니다. 해바라기가 위에 배치되어야 합니다. 기본 AR 플랫폼이 실제 표면을 더 잘 감지할 수 있도록 조금 돌아다녀야 할 수 있습니다. 조명이 밝지 않고 특징이 없는 표면인 경우 장면 이해의 품질이 저하되고 히트가 발견되지 않을 가능성이 커집니다. 문제가 발생하면 step-04/app.js 코드를 확인하여 이 단계의 실제로 작동하는 예시를 확인하세요.

5. 그림자 추가

사실적인 장면을 만들려면 장면에 현실감과 몰입감을 더하는 디지털 개체의 적절한 조명 및 그림자와 같은 요소가 필요합니다.

three.js에서 조명과 그림자를 처리합니다. 그림자를 드리우는 조명, 이러한 그림자를 받아 렌더링해야 하는 머티리얼, 그림자를 드리우는 메시를 지정할 수 있습니다. 이 앱의 장면에는 그림자를 드리우는 조명과 그림자만 렌더링하는 평평한 표면이 포함되어 있습니다.

  1. three.js WebGLRenderer에서 그림자를 사용 설정합니다. 렌더기를 만든 후 shadowMap에서 다음 값을 설정합니다.
setupThreeJs() {
  ...
  this.renderer = new THREE.WebGLRenderer(...);
  ...
  this.renderer.shadowMap.enabled = true;
  this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  ...
}

DemoUtils.createLitScene()에서 만든 예시 장면에는 그림자만 렌더링하는 평탄한 수평면인 shadowMesh라는 객체가 포함되어 있습니다. 이 표면의 초기 Y 위치는 10,000단위입니다. 해바라기가 배치되면 꽃의 그림자가 실제 지면 위에 렌더링되도록 shadowMesh를 실제 표면과 동일한 높이로 이동합니다.

  1. onSelect에서 clone을 장면에 추가한 후 코드를 추가하여 그림자 영역의 위치를 변경합니다.
onSelect = () => {
  if (window.sunflower) {
    const clone = window.sunflower.clone();
    clone.position.copy(this.reticle.position);
    this.scene.add(clone);

    const shadowMesh = this.scene.children.find(c => c.name === "shadowMesh");
    shadowMesh.position.y = clone.position.y;
  }
}

테스트

해바라기를 배치할 때 그림자가 드리워지는 것을 볼 수 있습니다. 문제가 발생하면 final/app.js 코드를 확인하여 이 단계의 실제로 작동하는 예시를 확인하세요.

6. 추가 리소스

축하합니다. WebXR을 사용하는 AR에 관한 이 Codelab을 완료했습니다.

자세히 알아보기