히트 테스트 API를 사용하면 실제 환경 뷰에 가상 항목을 배치할 수 있습니다.
WebXR Device API는 지난 가을 Chrome 79에서 출시되었습니다. 당시 언급한 바와 같이 Chrome의 API 구현은 개발 중입니다. Chrome에서 일부 작업이 완료되었음을 알려드립니다. Chrome 81에는 다음과 같은 두 가지 새로운 기능이 도입되었습니다.
이 도움말에서는 실제 카메라 뷰에 가상 객체를 배치하는 방법인 WebXR Hit Test 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로 무언가를 그리려면 시청자가 어디에 있고 어디를 보고 있는지 알아야 합니다. 따라서 hitTestSource과 xrViewerPose이 여전히 유효한지 테스트합니다.
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);
}
}
결론
이 문제를 해결하는 가장 좋은 방법은 샘플 코드를 단계별로 실행하거나 코드랩을 사용해 보는 것입니다. 두 가지를 모두 이해할 수 있도록 충분한 배경 정보를 제공해 드렸기를 바랍니다.
몰입형 웹 API는 아직 완성되지 않았습니다. 문제가 해결되면 여기에 새 도움말을 게시할 예정입니다.
사진: Daniel Frank(Unsplash 제공)