실제 뷰에서 가상 객체 배치

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

조 메들리
조 메들리

WebXR Device API는 지난 가을 Chrome 79에서 출시되었습니다. 앞서 언급했듯이 Chrome의 API 구현은 진행 중인 작업입니다. Chrome은 일부 작업이 완료되었다는 소식을 전해드립니다 Chrome 81에는 두 가지 새로운 기능이 도입되었습니다.

이 문서에서는 실제 카메라 뷰에 가상 객체를 배치하는 방법인 WebXR Hit Test API를 다룹니다.

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

몰입형 AR 세션 샘플

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

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

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

컨텍스트에 따라 벽, Lax 또는 Strict에 렌더링된 레티클
레티클은 증강 현실에서 객체를 배치하는 데 도움이 되는 임시 이미지입니다.

레티클 만들기

레티클 이미지는 브라우저나 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을 사용해 보는 것입니다. 두 가지를 모두 이해하실 수 있을 만큼 배경 정보를 충분히 얻으셨기를 바랍니다.

몰입형 Web API를 개발했다고 해서 길게 끝난 것이 아닙니다. 개선 작업이 진행됨에 따라 여기에 새로운 도움말을 게시할 예정입니다.

사진: 다니엘 프랭크, Unsplash