AR.jsとthree.jsを使ったMarker Based ARでマーカーをタップできるようにしてみます。
作成したサンプルはマーカーを認識すると、「Tap Marker!」という文字が表示されます。映像からはわからないですが、マーカーをタップするとタップ位置にランダムな高さのキューブを作成します。マーカー外をタップした場合は何も起きません。
コード全文は以下のようになっています。AR.jsをthree.jsで使用する基本的な方法は過去に記事を書いたので、そちらを参照してください。このサンプルを実行するにはdata
というディレクトリを作成して、その中にcamera_para.dat、patt.hiro、helvetiker_bold.typeface.jsonを配置する必要があります。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>Tappable Marker with AR.js and Three.js</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.js"></script>
<script src="https://raw.githack.com/AR-js-org/AR.js/3.1.0/three.js/build/ar.js"></script>
</head>
<body style='margin: 0px; overflow: hidden;'>
<script>
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setClearColor(new THREE.Color(), 0);
renderer.setSize(640, 480);
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = '0px';
renderer.domElement.style.left = '0px';
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera();
scene.add(camera);
const arToolkitSource = new THREEx.ArToolkitSource({
sourceType: 'webcam'
});
arToolkitSource.init(() => {
setTimeout(() => {
onResize();
}, 2000);
});
addEventListener('resize', () => {
onResize();
});
function onResize() {
arToolkitSource.onResizeElement();
arToolkitSource.copyElementSizeTo(renderer.domElement);
if (arToolkitContext.arController !== null) {
arToolkitSource.copyElementSizeTo(arToolkitContext.arController.canvas);
}
};
const arToolkitContext = new THREEx.ArToolkitContext({
cameraParametersUrl: 'data/camera_para.dat',
detectionMode: 'mono'
});
arToolkitContext.init(() => {
camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix());
});
const marker = new THREE.Group();
scene.add(marker);
const arMarkerControls = new THREEx.ArMarkerControls(arToolkitContext, marker, {
type: 'pattern',
patternUrl: 'data/patt.hiro',
changeMatrixMode: 'modelViewMatrix',
});
const markerPlane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1),
new THREE.MeshBasicMaterial({
colorWrite: false,
depthWrite: false,
})
);
markerPlane.rotation.x = -0.5 * Math.PI;
marker.add(markerPlane);
const raycaster = new THREE.Raycaster();
renderer.domElement.addEventListener('click', (event) => {
const element = event.currentTarget;
const x = event.clientX - element.offsetLeft;
const y = event.clientY - element.offsetTop;
const w = element.offsetWidth;
const h = element.offsetHeight;
const mouse = new THREE.Vector2((x / w) * 2 - 1, -(y / h) * 2 + 1);
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(markerPlane);
if (intersects.length !== 0) {
const intersect = intersects[0];
const height = 0.1 + Math.random() * 0.4;
const cube = new THREE.Mesh(
new THREE.BoxBufferGeometry(0.15, height, 0.15),
new THREE.MeshNormalMaterial()
);
cube.position.copy(marker.worldToLocal(intersect.point));
cube.position.y += 0.5 * height;
marker.add(cube);
}
});
const loader = new THREE.FontLoader();
loader.load('data/helvetiker_bold.typeface.json', (font) => {
const textGeom = new THREE.TextBufferGeometry('Tap Marker!', {
font: font,
size: 0.2,
height: 0.04,
});
textGeom.center();
const text = new THREE.Mesh(
textGeom,
new THREE.MeshNormalMaterial()
);
text.position.set(0, 0.75, 0);
marker.add(text);
});
requestAnimationFrame(function animate(){
requestAnimationFrame(animate);
if (arToolkitSource.ready) {
arToolkitContext.update(arToolkitSource.domElement);
}
renderer.render(scene, camera);
});
</script>
</body>
</html>
サンプルコードについて、マーカーのタップに関連する箇所を説明していきます。基本的な考え方としてはマーカーと同じ位置・大きさに見えない平面を作成して、その平面とタップ位置との交差判定をTHREE.Raycasterで行うという流れになります。
まずカメラです。AR.jsではカメラの射影行列をAR.js側が設定するため基底クラスのTHREE.Camera
を使用すればいいのですが、THREE.Raycaster
のサポートチェックを回避するためにTHREE.PerspectiveCamera
を使用します。
const camera = new THREE.PerspectiveCamera();
マーカーと同じ位置・大きさに見えない平面を追加します。colorWrite
とdepthWrite
をfalse
にすることで色も深度も描画されなくなります。
const markerPlane = new THREE.Mesh(
new THREE.PlaneBufferGeometry(1, 1),
new THREE.MeshBasicMaterial({
colorWrite: false,
depthWrite: false,
})
);
markerPlane.rotation.x = -0.5 * Math.PI;
marker.add(markerPlane);
THREE.Raycaster
を生成して、canvasがタップされたら先程作成した平面とタップ位置の交差判定を行います。交差している場合にはキューブを追加します。intersect.point
には交差位置がワールド座標系で含まれているので、ローカル座標系に変換してからmarker
に追加しています。THREE.Raycaster
を使った交差判定については以下の記事を参考にしました。
Three.jsでオブジェクトとの交差を調べる - ICS MEDIA
const raycaster = new THREE.Raycaster();
renderer.domElement.addEventListener('click', (event) => {
const element = event.currentTarget;
const x = event.clientX - element.offsetLeft;
const y = event.clientY - element.offsetTop;
const w = element.offsetWidth;
const h = element.offsetHeight;
const mouse = new THREE.Vector2((x / w) * 2 - 1, -(y / h) * 2 + 1);
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(markerPlane);
if (intersects.length !== 0) {
const intersect = intersects[0];
const height = 0.1 + Math.random() * 0.4;
const cube = new THREE.Mesh(
new THREE.BoxBufferGeometry(0.15, height, 0.15),
new THREE.MeshNormalMaterial()
);
cube.position.copy(marker.worldToLocal(intersect.point));
cube.position.y += 0.5 * height;
marker.add(cube);
}
});