先日、AR.js + Three.js + Next.jsでスマートフォン向けのウェブARをつくりました。
3Dオブジェクトをブラウザ上に、3Dオブジェクトの影を現実空間に表示する、ウェブAR + プロジェクションマッピングを作りました。
— 君塚史高 (@ki_230) 2021年12月7日
名付けて「影絵R(カゲエーアール)」です。
本日20時までみどり荘にて展示中です。https://t.co/f7bA7o7ktN pic.twitter.com/NULnZtPnS9
制作している時、最後の最後に気づきました。
微妙にマーカーとオブジェクトの位置がずれていることに。
インスペクタで確認してみると。
bodyよりも中の要素の横幅の方が大きくなっています。
なぜ、こうなってしまったのかソースを見返してみると。
- スマートフォンなのでbodyにoverflow: hiddenが効かない
- AR.jsがbody直下にvideoタグをappendする
というのが原因のようです。
つまり、body直下に巨大なvideoタグがappendされているため、bodyの横幅を超えてしまい、結果、ビデオとオブジェクトの位置に微妙なずれが発生しているようです。
対策を考えたのですが、今回はNext.jsを併用しているため、body直下に#__nextがあります。
なので、
#__next { overflow: hidden; }
とした上で、AR.jsがvideoをappendする先を#__nextにすれば解決しそうです。
append先を変更できるAPIはないものかと、AR.jsのドキュメントを読んだり、ソースを読んだりしてみたのですが、
function onSourceReady() { document.body.appendChild(_this.domElement); window.dispatchEvent(new CustomEvent('arjs-video-loaded', { detail: { component: document.querySelector('#arjs-video'), }, })); _this.ready = true onReady && onReady() }
残念ながら、bodyにappendされる仕様のようでした。。
なので、
arToolkitSource.init(() => { document.getElementById('__next').appendChild(arToolkitSource.domElement); });
という形で、appendされたvideoを取得して#__nuxtにappendし直すようにしてみたところ、
無事にbody内に収まるようになりました。一件落着です。
ソースコード(抜粋)
import useResize from '../hooks/useResize'; import { useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; declare global { interface Window { THREE: any; THREEx: any; } } const Wrapper = styled.div` .overlay { position: fixed; top: 50%; left: 50%; border: solid 8000px rgba(0, 0, 0, .6); width: 200px; height: 200px; transform: translate(-50%, -50%); &:after { position: fixed; top: calc(50% + 120px); left: 0; right: 0; content: 'マーカーを探してください'; text-align: center; } } `; export default function Index() { const [ isBrowser, setIsBrowser ] = useState(false); const [ renderer, setRenderer ] = useState(null); const [ isFound, setIsFound ] = useState(false); const [ arToolkitSource, setArToolkitSource ] = useState(null); const [ arToolkitContext, setArToolkitContext ] = useState(null); const canvasRef = useRef(null); const { windowWidth, windowHeight } = useResize(); const size = 1; useEffect(() => setIsBrowser(process.browser), []); useEffect(() => { const THREE = window.THREE; const THREEx = window.THREEx; const scene = new THREE.Scene(); const renderer = new THREE.WebGLRenderer({ canvas: canvasRef.current, antialias : true, alpha: true }); renderer.outputEncoding = THREE.GammaEncoding; renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); setRenderer(renderer); const camera = new THREE.Camera(); scene.add(camera); const arToolkitContext = new THREEx.ArToolkitContext({ cameraParametersUrl: 'camera_para.dat', detectionMode: 'mono' }); arToolkitContext.init(() => { camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix()); }); setArToolkitContext(arToolkitContext); const arToolkitSource = new THREEx.ArToolkitSource({ sourceType: 'webcam' }); setArToolkitSource(arToolkitSource); arToolkitSource.init(() => { document.getElementById('__next').appendChild(arToolkitSource.domElement); // DOMを移動する setTimeout(() => { arToolkitSource.onResize(); arToolkitSource.copySizeTo(renderer.domElement); document.body.style.opacity = String(1); }, 800); }); const markerRoot = new THREE.Group(); scene.add(markerRoot); const arMarkerControl = new THREEx.ArMarkerControls(arToolkitContext, markerRoot, { type: 'pattern', patternUrl: 'pattern.patt' }); arMarkerControl.addEventListener('markerFound', () => { setIsFound(true); }); const geometry = new THREE.BoxGeometry(size, size, size); const material = new THREE.MeshBasicMaterial({ color: 0xCCCCCC, opacity: .8 }); const model = new THREE.Mesh(geometry, material); model.position.set(0, 1, 0); model.rotation.y = Math.PI / 180 * -90; markerRoot.add(model); renderer.setAnimationLoop(() => { if (arToolkitSource.ready) { arToolkitContext.update(arToolkitSource.domElement); } renderer.render(scene, camera); }); const light = new THREE.AmbientLight(); scene.add(light); return () => renderer.setAnimationLoop(null); }, [canvasRef]); const handleResize = useCallback(() => { if (arToolkitSource && renderer) { arToolkitSource.onResize(); arToolkitSource.copySizeTo(renderer.domElement); if (arToolkitContext && arToolkitContext.arController) { arToolkitSource.copySizeTo(arToolkitContext.arController.canvas); } } }, [renderer, arToolkitSource, arToolkitContext]); useEffect(handleResize, [handleResize, windowWidth, windowHeight]); return ( <Wrapper> <canvas ref={ canvasRef } /> {(() => { if (isBrowser) { return (() => !isFound && <div className="overlay" /> )(); } })()} </Wrapper> ); }
DEMO
bodyから飛び出すバージョン
bodyに収めるバージョン
マーカー