실제 뷰에서 가상 객체 배치

Hit Test API를 사용하면 실제 뷰에 가상 항목을 배치할 수 있습니다.

Joe Medley
Joe Medley

WebXR Device API는 작년 가을 Chrome 79에서 출시되었습니다. 당시 언급했듯이 Chrome의 API 구현은 아직 진행 중입니다. Chrome은 일부 작업이 완료되었음을 알려드립니다. Chrome 81에는 다음과 같은 두 가지 새로운 기능이 도입되었습니다.

이 도움말에서는 가상 객체를 실제 카메라 뷰에 배치하는 수단인 WebXR 히트 테스트 API를 설명합니다.

이 도움말에서는 이미 증강 현실 세션을 만드는 방법과 프레임 루프를 실행하는 방법을 알고 있다고 가정합니다. 이러한 개념에 익숙하지 않다면 이 시리즈의 이전 도움말을 읽어보세요.

몰입형 AR 세션 샘플

이 도움말의 코드는 몰입형 웹 작업 그룹의 히트 테스트 샘플(데모, 소스)에 있는 코드를 기반으로 하지만 동일하지는 않습니다. 이 예에서는 실제 표면에 가상 해바라기를 배치할 수 있습니다.

앱을 처음 열면 가운데에 점이 있는 파란색 원이 표시됩니다. 점은 기기에서 환경의 지점까지의 가상 선이 교차하는 지점입니다. 기기를 움직이면 이동합니다. 교차점을 찾으면 바닥, 테이블 상판, 벽과 같은 표면에 스냅되는 것처럼 보입니다. 충돌 테스트는 교차점의 위치와 방향을 제공하지만 노출 영역 자체에 관한 정보는 제공하지 않기 때문입니다.

이 원을 레티클이라고 하며 객체를 증강 현실에 배치하는 데 도움이 되는 임시 이미지입니다. 화면을 탭하면 화면을 탭한 위치와 관계없이 레티클 위치와 레티클 지점의 방향에 따라 해바라기가 표면에 배치됩니다. 레티클은 기기와 함께 계속 이동합니다.

맥락에 따라 벽에 렌더링된 레티클(완화됨 또는 엄격함)
레티클은 증강 현실에 객체를 배치하는 데 도움이 되는 임시 이미지입니다.

레티클 만들기

레티클 이미지는 브라우저나 API에서 제공되지 않으므로 직접 만들어야 합니다. 로드 및 그리기 방법은 프레임워크마다 다릅니다. WebGL 또는 WebGL2를 사용하여 직접 그리지 않는 경우 프레임워크 문서를 참고하세요. 따라서 샘플에서 레티클을 그리는 방법은 자세히 설명하지 않겠습니다. 아래에는 한 가지 이유만으로 한 줄이 나와 있습니다. 따라서 이후 코드 샘플에서는 reticle 변수를 사용할 때 무엇을 의미하는지 알 수 있습니다.

let reticle = new Gltf2Node({url: 'media/gltf/reticle/reticle.gltf'});

세션 요청

세션을 요청할 때는 아래와 같이 requiredFeatures 배열에서 'hit-test'를 요청해야 합니다.

navigator.xr.requestSession('immersive-ar', {
  requiredFeatures: ['local', 'hit-test']
})
.then((session) => {
  // Do something with the session
});

세션 시작

이전 기사에서는 XR 세션을 시작하는 코드를 설명했습니다. 아래에 추가된 버전을 보여드립니다. 먼저 select 이벤트 리스너를 추가했습니다. 사용자가 화면을 탭하면 레티클의 포즈에 따라 꽃이 카메라 뷰에 배치됩니다. 이 이벤트 리스너는 나중에 설명하겠습니다.

function onSessionStarted(xrSession) {
  xrSession.addEventListener('end', onSessionEnded);
  xrSession.addEventListener('select', onSelect);

  let canvas = document.createElement('canvas');
  gl = canvas.getContext('webgl', { xrCompatible: true });

  xrSession.updateRenderState({
    baseLayer: new XRWebGLLayer(session, gl)
  });

  xrSession.requestReferenceSpace('viewer').then((refSpace) => {
    xrViewerSpace = refSpace;
    xrSession.requestHitTestSource({ space: xrViewerSpace })
    .then((hitTestSource) => {
      xrHitTestSource = hitTestSource;
    });
  });

  xrSession.requestReferenceSpace('local').then((refSpace) => {
    xrRefSpace = refSpace;
    xrSession.requestAnimationFrame(onXRFrame);
  });
}

여러 참조 공간

강조 표시된 코드는 XRSession.requestReferenceSpace()를 두 번 호출합니다. 처음에는 혼란스러웠습니다. 히트 테스트 코드가 애니메이션 프레임을 요청하지 않는 이유(프레임 루프 시작)와 프레임 루프에 히트 테스트가 포함되지 않는 것처럼 보이는 이유를 물었습니다. 혼란의 원인은 참조 공간에 대한 오해였습니다. 참조 공간은 출처와 세계 사이의 관계를 표현합니다.

이 코드의 역할을 이해하려면 독립형 장비를 사용하여 이 샘플을 보고 있으며 헤드셋과 컨트롤러가 모두 있다고 가정해 보겠습니다. 컨트롤러로부터의 거리를 측정하려면 컨트롤러 중심 기준 프레임을 사용합니다. 하지만 화면에 무언가를 그리려면 사용자 중심 좌표를 사용해야 합니다.

이 샘플에서 뷰어와 컨트롤러는 동일한 기기입니다. 하지만 문제가 있습니다. 그리는 대상은 환경과 관련하여 안정적이어야 하지만 그리는 '컨트롤러'는 움직입니다.

이미지 그리기에는 환경 측면에서 안정성을 제공하는 local 참조 공간을 사용합니다. 이를 얻은 후 requestAnimationFrame()를 호출하여 프레임 루프를 시작합니다.

히트 테스트의 경우 히트 테스트 시 기기의 포즈를 기반으로 하는 viewer 참조 공간을 사용합니다. 컨트롤러에 관해 이야기하고 있으므로 이 맥락에서 '시청자' 라벨은 다소 혼란스러울 수 있습니다. 컨트롤러를 전자 뷰어로 생각하면 이해하기 쉽습니다. 이를 가져온 후 xrSession.requestHitTestSource()를 호출합니다. 그러면 그리기에 사용할 히트 테스트 데이터의 소스가 생성됩니다.

프레임 루프 실행

requestAnimationFrame() 콜백은 또한 조회 테스트를 처리하는 새 코드를 가져옵니다.

기기를 움직일 때 레티클은 표면을 찾으려고 할 때 기기와 함께 움직여야 합니다. 움직이는 것처럼 보이게 하려면 프레임마다 레티클을 다시 그립니다. 그러나 히트 테스트에 실패하면 레티클을 표시하지 마세요. 따라서 앞서 만든 레티클의 visible 속성을 false로 설정했습니다.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

AR에서 무언가를 그리려면 시청자가 어디에 있고 어디를 보고 있는지 알아야 합니다. 따라서 hitTestSourcexrViewerPose가 여전히 유효한지 테스트합니다.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

이제 getHitTestResults()를 호출합니다. hitTestSource를 인수로 사용하고 HitTestResult 인스턴스의 배열을 반환합니다. 조회 테스트에서는 여러 표면을 찾을 수 있습니다. 배열의 첫 번째 항목이 카메라에 가장 가까운 값입니다. 대부분 사용하긴 하지만 고급 사용 사례에서는 배열이 반환됩니다. 예를 들어 카메라가 바닥에 있는 테이블 위의 상자를 향하고 있다고 가정해 보겠습니다. 히트 테스트에서 배열의 세 표시 경로를 모두 반환할 수 있습니다. 대부분의 경우 내가 중요하게 생각하는 상자입니다. 반환된 배열의 길이가 0이면, 즉 히트 테스트가 반환되지 않은 경우 계속 진행합니다. 다음 프레임에서 다시 시도해 보세요.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.visible = true;
      reticle.matrix = pose.transform.matrix;
    }
  }

  // Draw to the screen
}

마지막으로 히트 테스트 결과를 처리해야 합니다. 기본 프로세스는 다음과 같습니다. 히트 테스트 결과에서 포즈를 가져오고 레티클 이미지를 히트 테스트 위치로 변환 (이동)한 다음 visible 속성을 true로 설정합니다. 포즈는 표면의 한 지점의 포즈를 나타냅니다.

function onXRFrame(hrTime, xrFrame) {
  let xrSession = xrFrame.session;
  xrSession.requestAnimationFrame(onXRFrame);
  let xrViewerPose = xrFrame.getViewerPose(xrRefSpace);

  reticle.visible = false;

  // Reminder: the hitTestSource was acquired during onSessionStart()
  if (xrHitTestSource && xrViewerPose) {
    let hitTestResults = xrFrame.getHitTestResults(xrHitTestSource);
    if (hitTestResults.length > 0) {
      let pose = hitTestResults[0].getPose(xrRefSpace);
      reticle.matrix = pose.transform.matrix;
      reticle.visible = true;

    }
  }

  // Draw to the screen
}

객체 배치

사용자가 화면을 탭하면 객체가 AR에 배치됩니다. 세션에 select 이벤트 핸들러를 이미 추가했습니다. (위 참고)

이 단계에서 중요한 것은 배치할 위치를 아는 것입니다. 움직이는 레티클은 일정한 히트 테스트 소스를 제공하므로 가장 간단한 방법은 마지막 적중 테스트에서 레티클 위치에 객체를 그리는 것입니다.

function onSelect(event) {
  if (reticle.visible) {
    // The reticle should already be positioned at the latest hit point,
    // so we can just use its matrix to save an unnecessary call to
    // event.frame.getHitTestResults.
    addARObjectAt(reticle.matrix);
  }
}

결론

이를 이해하는 가장 좋은 방법은 샘플 코드를 단계별로 살펴보거나 codelab을 사용해 보는 것입니다. 이 두 가지를 모두 이해하실 수 있도록 충분한 배경지식을 드렸기를 바랍니다.

몰입형 웹 API를 빌드하는 작업은 아직 멀었습니다. 진행 상황을 업데이트할 때마다 새로운 도움말을 게시할 예정입니다.

사진: Unsplash다니엘 프랭크