개발/js

[JS] model-viewer 사용법 4 - model AR 활성화 및 커스텀 하기

waterpole-dev 2022. 9. 15. 13:54
반응형

이전 글 보기 (더보기 클릭)

더보기

 

이번 포스팅에서는 model-viewer 사용법 마지막으로 AR 기능을 추가해 보도록 하겠습니다.

어렵지 않아서 간단한 내용의 짧은 포스팅이 될 거 같습니다!


AR 속성 정의

<model-viewer id="model-ex" camera-controls ar ar-modes="webxr scene-viewer quick-look">
    <!-- ... -->
</model-viewer>

<model-viewer> 태그에

ar, ar-modes 속성을 정의해 줍니다.

 

ar

  • AR이 지원되는 장치에서 AR 기능을 활성화합니다.

ar-modes

  • 활성화할 AR 유형의 우선순위입니다.
    • webxr (default)
    • scene-viewer (default)
    • quick-look (option)

이 외에도 여러 속성들이 있으니 공식문서를 확인해보세요.

 

 

사실 위 속성 중에 ar만 정의해줘도 ar을 지원해주는 장치에서는 ar버튼이 활성화됩니다.

여기서 만족하시는 분들은 여기서 스탑 하셔도 괜찮아요.

하지만 저는 디자인 문제로 AR 활성화 버튼을 커스텀할 예정입니다.

이전 포스팅에 올렸던 디자인
기본 AR 버튼 (출처 : https://modelviewer.dev/ 홈페이지 캡쳐)

 


AR 활성화 버튼 커스텀

<model-viewer id="model-ex" camera-controls ar ar-modes="webxr scene-viewer quick-look">
    <div slot="ar-button"></div>
    <div class="model-nav">
        <!-- ... -->
        <div class="open-ar">
            <button class="open-ar-btn">AR</button>
        </div>
    </div>
</model-viewer>

 

<div slot="ar-button">과 기본 버튼을 대체할 버튼 태그를 만들어 줍니다.

 

slot="ar-button"

  • <model-viewer> 아래에 slot="ar-button"을 가지고 있는 자식 요소를 배치하면 해당 자식 요소가 기본 AR 버튼을 대체하게 됩니다.

이렇게 설정하게 된다면 기본 AR 버튼은 사라지게 되고 (아무 내용 없는 빈 div를 ar-button으로 지정했기 때문에) 대체로 만든 AR 버튼은 노출되게 됩니다.

 

저처럼 구조상 AR 버튼을 다른 곳에 둬야 하는 게 아닌 이상 ar-button으로 설정한 태크를 css로 커스텀만 해주셔도 충분합니다.

구조가 변경되어도 그냥 태그에 slot="ar-button"을 주면 되는 거 아니냐 하실 수도 있지만, 바로 위에서 얘기했듯이 <model-viewer>의 자식이 아닌 이상 인식이 되지 않습니다.

그러니까 위 html에서는 <model-viewer>의 자식인 <div> 2개 중 하나에 설정해야 합니다.

 

지금은 아무리 대체 버튼을 눌러도 동작하지 않으니 스크립트를 추가해 봅시다.


 

대체 AR 버튼 활성화 스크립트 추가

function modelController() {
    // ...
    const openArBtn = document.querySelector('.open-ar-btn');
    
    openArBtn.addEventListener('click', () => {
        if (!MODEL_VIEWER.canActivateAR) {
            alert('AR을 지원하지 않는 기기 입니다.');
            return;
        }
        MODEL_VIEWER.activateAR();
    });
}

canActivateAR

 

  • 현재 기기 혹은 플랫폼이 AR 지원 여부를 Boolean 형태로 리턴합니다.

activateAR()

  • AR을 활성화합니다.

 

클릭 이벤트와 위 두 가지 프로퍼티와 메서드를 통해 간단하게 구현이 가능합니다.

저는 AR을 지원하지 않는 기기에서도 AR버튼은 노출되어야 하기 때문에 alert으로 처리를 해줬지만, 디자인에 따라 hide/show 시켜주시면 될 거 같습니다.

 

이게 AR을 지원하는 기기에서 테스트해보시면 AR이 정상적으로 활성화되는 것을 확인해 보실 수 있습니다

 


최종 소스

<button class="open-model-btn" data-product-name="robot">3D - 01</button>
<button class="open-model-btn" data-product-name="phone">3D - 02</button>
<button class="open-model-btn" data-product-name="car">3D - 03</button>

<div class="mv-modal-box">
    <button class="close-modal-btn">X</button>
    <model-viewer id="model-ex" camera-controls ar ar-modes="webxr scene-viewer quick-look">
        <div class="model-nav">
            <div slot="ar-button"></div>
            <div class="model-control">
                <button class="zoom-in-btn">+</button>
                <button class="zoom-out-btn">-</button>
                <button class="animation-btn">ani</button>
                <button class="fullscreen-btn">full screen</button>
            </div>
            <div class="open-ar">
                <button class="open-ar-btn">AR</button>
            </div>
        </div>
    </model-viewer>
</div>
const MV_MODAL_BOX = document.querySelector('.mv-modal-box');
const MODAL_VIEWER = document.querySelector('#model-ex');
const modelOpenBtn = document.querySelector('.open-model-btn');
const modalCloseBtn = document.querySelector('.close-modal-btn');

let animationArr = [];

MODEL_VIEWER.addEventListener('progress', (e) => {
    if (e.detail.totalProgress === 1) {
        MODEL_VIEWER.cameraOrbit = '0deg 75deg 105%';
        animationArr = MODEL_VIEWER.availableAnimations;
    }
});

modelOpenBtn.addEventListener('click', (e) => {
    const target = e.currentTarget;
    const productName = target.getAttribute('data-product-name');
    
    MV_MODAL_BOX.style.display = 'block';
    loadModel(productName)
});

modalCloseBtn.addEventListener('click', () => {
    MODEL_VIEWER.removeAttribute('src');
    MODEL_VIEWER.removeAttribute('animation-name');
    animationArr = [];
    MV_MODAL_BOX.style.display = 'none';
});

function loadModel(name) {
    const modelSrc = `./assets/model/${name}.glb`;
    MODEL_VIEWER.setAttribute('src', modelSrc);
}

function setAnimation() {
    if (animationArr.length > 0) {
        MODEL_VIEWER.setAttribute('animation-name', animationArr[0]);
    } else {
        console.log("사용 가능한 애니메이션이 없습니다.")
    }
}

function modelController() {
    const zoomInBtn = document.querySelector('.zoom-in-btn');
    const zoomOutBtn = document.querySelector('.zoom-out-btn');
    const animationBtn = document.querySelector('.animation-btn');
    const fullScreenBtn = document.querySelector('.fullscreen-btn');
    const openArBtn = document.querySelector('.open-ar-btn');
    
    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(5);
    });

    zoomInBtn.addEventListener('click', () => {
        MODEL_VIEWER.zoom(-5);
    });
    
    animationBtn.addEventListener('click', () => {
        if (animationArr.length > 0) {
            MODEL_VIEWER.classList.toggle('activeAni');
            toggleAnimation();
        }
    });
    
    fullScreenBtn.addEventListener('click', () => {
        toggleFullScreen(MODEL_VIEWER.parentElement);
    });
    
    openArBtn.addEventListener('click', () => {
        if (!MODEL_VIEWER.canActivateAR) {
            alert('AR을 지원하지 않는 기기 입니다.');
            return;
        }
        MODEL_VIEWER.activateAR();
    });
}

function toggleAnimation() {
    const isActive = MODEL_VIEWER.classList.contains('activeAni');

    if (isActive) {
        MODEL_VIEWER.setAttribute('animation-name', animationArr[animationArr.length - 1]);
    } else {
        MODEL_VIEWER.setAttribute('animation-name', animationArr[0]);
    }
}

function toggleFullScreen(element) {
    const agent = navigator.userAgent.toLowerCase();
    const target =
        agent.indexOf('safari') > -1
            ? !document.webkitFullscreenElement
            : !document.fullscreenElement;
            
    if (target) {
        if (element.requestFullscreen) return element.requestFullscreen();
        if (element.webkitRequestFullscreen) return element.webkitRequestFullscreen();
        if (element.mozRequestFullScreen) return element.mozRequestFullScreen();
        if (element.msRequestFullscreen) return element.msRequestFullscreen();
    } else {
        if (document.exitFullscreen) return document.exitFullscreen();
        if (document.webkitExitFullscreen) return document.webkitExitFullscreen();
        if (document.mozCancelFullScreen) return document.mozCancelFullScreen();
        if (document.msExitFullscreen) return document.msExitFullscreen();
    }
}

modelController()

매 포스팅마다 가독성을 개선할 수 있도록 양식? 방법?을 수정해가면서 포스팅을 해봤는데 어떠신가요?
포스팅 가독성에 대해서 조언 남겨주시면 정말 감사하겠습니다!!

util, ui 등 평소 개발 스타일대로 포스팅 소스의 파일을 나눠서 export/import 해서 진행하는 것은 포스팅 가독성을 떨어뜨린다고 판단해서 해당 포스팅은 별도의 리팩토링 없이 한 파일에 진행됩니다.


깊지 않고 간단하게 다룬 model-viewer로 web에 3d 모델을 띄우고 ar까지 활성화하는 포스팅은 끝이 났습니다.

매우 간단한 내용이라 포스팅을 할까 말까 고민했지만, 구글에 model-viewer로 검색했을 때 한국어로 된 블로그가 안 보여서 포스팅하게 되었습니다.

 

많이 부족한 실력으로 빈약한 포스팅을 하고 있지만 누군가에게는 도움이 되었으면 합니다.

더욱 자세한 내용은 공식문서 확인하시면 됩니다. 예제도 같이 있고 어렵지 않으니 누구나 가능합니다!

 

피드백, 질문, 댓글 언제나 환영입니다!

감사합니다 :)

반응형