Library:

[Three.js] To infinity and beyond! 3D 웹 도전기 #7

칠일오.

⚠️ 1분 코딩님의 강의를 바탕으로 작성한 글입니다.

Raycaster(클릭 감지)을 이용하여 Mesh 감지하기

1. Mesh 제작

// Mesh
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const boxMaterial = new THREE.MeshStandardMaterial({ color: 'plum' });
const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);
boxMesh.name = 'box';

const torusGeometry = new THREE.TorusGeometry(2, 0.5, 16, 100);
const torusMaterial = new THREE.MeshStandardMaterial({ color: 'lime' });
const torusMesh = new THREE.Mesh(torusGeometry, torusMaterial);
torusMesh.name = 'torus';

scene.add(boxMesh, torusMesh);

const meshes = [boxMesh, torusMesh];

 

2. Raycaster 연결

const raycaster = new THREE.Raycaster();

 

3. 렌더링 함수 작성

function draw() {
    renderer.render(scene, camera);
    renderer.setAnimationLoop(draw);
}

camera 움직임이 생길 때마다, setAnimationLoop 메서드가 렌더링 루프를 생성하여 매 프레임마다 콜백 함수를 호출한다.

매 프레임마다 카메라의 위치를 변경하고 scene을 렌더링 하게 된다.

 

4. 마우스 좌표 값 생성

const mouse = new THREE.Vector2();

물체의 위치, 방향 등을 표현하는데 사용되는 벡터 클래스로, Vector3는 (x, y, z)의 세 개의 값을 가지기 때문에 3D 표현이 가능하며, Vector2는 (x, y)의 두 개의 좌표로 2D 표현이 가능하다.

마우스의 경우, 브라우저 상에서 2D 공간에서 동작하기 때문에 Vector2로 표현한다.

 

5. 마우스 좌표 값 재설정

canvas.addEventListener('click', e => {
    mouse.x = e.clientX / canvas.clientWidth * 2 - 1;
    mouse.y = -(e.clientY / canvas.clientHeight * 2 - 1);
    // console.log(mouse);
    checkIntersects();
});

브라우저에서 제공하는 clientX 객체는 좌측 상단을 (0, 0)으로 잡기 때문에 화면의 중심을 (0, 0)으로 잡는 three.js와는 다르게 동작한다. 때문에 이를 일치시켜 주기 위해 비율을 계산하여 재정의해준다.

 

6. checkIntersects 함수 정의

function checkIntersects() {
    if (preventDragClick.mouseMoved) return;

    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(meshes);
    for (const item of intersects) {
        item.object.material.color.set('red');
        break;
    }
}

마우스를 드래그 했으면 함수가 종료된다. 마우스를 클릭한 것이라면 setFormCamera 메서드를 사용하여 카메라 시점에서 광선을 쏘게 된다. 제작한 mesh들을 담은 배열을 전달하여 광선에 맞은 객체들은 intersets 변수에 저장한다. 이후, for...of 반복문을 통해 광선을 맞은 객체의 색상을 변경해 준다. break로 for 문이 종료되도록 한다.

👀 setFromCamera( coords: Vector2, camera )
해당 메서드는 카메라 시점에서 광선을 쏘도록 동작한다. 첫번째 인자로 마우스의 2D 좌표 값을 전달하며, 두 번째 인자로 광선이 시작되어야 하는 카메라의 위치를 전달한다.

👀 intersectObjects( object: Object3D, recursive: Boolean, optionalTarget: Array )
광선에 부딪치는 모든 객체를 알아낼 수 있다. 단수로 사용하면(intersectObject) 하나의 객체만 전달할 수 있고, 여러 개의 객체를 전달해야 할 때는 복수로 작성하여 배열을 전달하면 된다.

 

7. 드래그 클릭 방지 모듈 인스턴스 생성 및 draw 함수 실행

const preventDragClick = new PreventDragClick(canvas);

draw();

 

드래그 클릭 방지 기능 구현하기

재사용성을 위해 파일을 분리하였으며, 파일 명은 임의로 'PreventDragClick'으로하여 생성하였다.

export class PreventDragClick {
	constructor(elem) {
		this.mouseMoved;
		let clickStartX;
		let clickStartY;
		let clickStartTime;
		elem.addEventListener('mousedown', e => {
			clickStartX = e.clientX;
			clickStartY = e.clientY;
			clickStartTime = Date.now();
		});
		elem.addEventListener('mouseup', e => {
			const xGap = Math.abs(e.clientX - clickStartX);
			const yGap = Math.abs(e.clientY - clickStartY);
			const timeGap = Date.now() - clickStartTime;
			
			if (
				xGap > 5 ||
				yGap > 5 ||
				timeGap > 500
			) {
				this.mouseMoved = true;
			} else {
				this.mouseMoved = false;
			}
		});
	}
}

인자로 canvas를 전달받아 canvas에 이벤트가 바인딩되도록 해준다. canvas 내부에서 마우스를 드래그 여부에 따라 boolean 값으로 반환해 주는 클래스이다.

 

이동한 마우스 위치(mouseup)와 처음 클릭한 마우스 위치(mousedown)의 차이를 절대값으로 만들어서 그 차이가 마우스 5px 이하이면 클릭으로 간주하여 false를 반환한다. 드래그를 하였지만 위치가 처음 클릭한 위치로 돌아오는 예외 경우에 대비하여 시간을 통해서도 드래그 여부를 판단할 수 있도록 조건을 추가해 준다. (0.5초 이하일 경우, 클릭으로 간주)

 

‼ mouseMoved는 외부에서 인스턴스의 속성으로 사용되어야 하기 때문에 this로 작성한다.