Three.js Raycaster 정리 및 보완 설명
1. Raycaster란?
Raycaster는 특정 위치에서 특정 방향으로 광선(ray) 을 쏘고, 그 광선과 어떤 3D 객체가 교차하는지 판단하는 도구입니다.
활용 예:
- 플레이어 앞에 벽이 있는지 감지
- 총알(광선)이 적을 맞췄는지 체크
- 마우스로 클릭하거나 호버했을 때 어떤 객체가 있는지 탐지
2. Raycaster 생성 및 기본 설정
const raycaster = new THREE.Raycaster();
const rayOrigin = new THREE.Vector3(-3, 0, 0); // 광선의 시작점
const rayDirection = new THREE.Vector3(10, 0, 0); // 광선의 방향
rayDirection.normalize(); // 방향 벡터는 반드시 정규화해야 함 (길이 1)
raycaster.set(rayOrigin, rayDirection);
💡 normalize()를 호출하면 방향 벡터의 길이를 1로 맞춰줍니다. 크기는 중요하지 않고, 방향만 중요하기 때문입니다.
3. 교차 테스트
const intersects = raycaster.intersectObjects([object1, object2, object3]);
- intersectObject(object) → 단일 객체와 교차 검사
- intersectObjects([objects]) → 다수 객체와 교차 검사
객체의 모양에 따라(예를 들어, 도넛의 중앙을 통과하는 경우 2번) 하나의 객체와 여러 번 충돌할 수 있음.
반환값: intersects (배열)

각 항목에는 다음 정보가 포함됨:
- distance: 원점에서 충돌 지점까지의 거리
- point: 교차 지점의 위치 (THREE.Vector3)
- object: 충돌한 객체
- face, faceIndex, uv 등도 포함
※ 충돌 객체를 찾지 못하는 문제 발생 시:
/* Three.js는 렌더링 직전에 객체의 좌표(행렬이라고 함)를 업데이트합니다.
레이 캐스팅을 즉시 실행하기 때문에 객체가 렌더링되지 않습니다.
레이 캐스팅 전에 수동으로 행렬을 업데이트하면 이 문제를 해결할 수 있습니다. */
object1.updateMatrixWorld();
object2.updateMatrixWorld();
object3.updateMatrixWorld();
4. 매 프레임마다 애니메이션 객체와 교차 검사
const tick = () => {
const elapsedTime = clock.getElapsedTime();
// 애니메이션: 구체들을 위아래로 이동시킴
object1.position.y = Math.sin(elapsedTime * 0.3) * 1.5;
object2.position.y = Math.sin(elapsedTime * 0.8) * 1.5;
object3.position.y = Math.sin(elapsedTime * 1.4) * 1.5;
// 교차 검사
const rayOrigin = new THREE.Vector3(-3, 0, 0);
const rayDirection = new THREE.Vector3(1, 0, 0).normalize();
raycaster.set(rayOrigin, rayDirection);
const intersects = raycaster.intersectObjects([object1, object2, object3]);
// 기본 색상 초기화
for (const obj of [object1, object2, object3]) {
obj.material.color.set('#ff0000');
}
// 교차한 객체를 파란색으로 변경
for (const intersect of intersects) {
intersect.object.material.color.set('#0000ff');
}
};
5. 마우스를 이용한 교차 검사 (setFromCamera)
setFromCamera(mouse, camera)는 "이 마우스 위치에서 시작해서 카메라 시점으로 광선을 쏴줘" 라고 명령하는 메서드
const mouse = new THREE.Vector2();
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / sizes.width) * 2 - 1;
mouse.y = -(event.clientY / sizes.height) * 2 + 1;
});
const tick = () => {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects([object1, object2, object3]);
// 호버된 객체만 파란색 처리, 나머지는 빨간색 복원
for (const obj of [object1, object2, object3]) {
obj.material.color.set('#ff0000');
}
for (const intersect of intersects) {
intersect.object.material.color.set('#0000ff');
}
};
💡 setFromCamera(mouse, camera)를 쓰면 마우스 위치 기준으로 카메라에서 광선을 쏩니다.
6. 마우스 진입(mouseenter) / 이탈(mouseleave) 이벤트 흉내내기
let currentIntersect = null;
const tick = () => {
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects([object1, object2, object3]);
if (intersects.length > 0) {
if (!currentIntersect) console.log('mouse enter');
currentIntersect = intersects[0];
} else {
if (currentIntersect) console.log('mouse leave');
currentIntersect = null;
}
};
7. 마우스 클릭 이벤트 핸들링
window.addEventListener('click', () => {
if (currentIntersect) {
switch (currentIntersect.object) {
case object1:
console.log('click on object 1');
break;
case object2:
console.log('click on object 2');
break;
case object3:
console.log('click on object 3');
break;
}
}
});
8. GLTF 모델에 레이캐스팅 적용하기
모델 로딩
const gltfLoader = new GLTFLoader();
let model = null;
// 모델 로딩
gltfLoader.load('./models/Duck/glTF-Binary/Duck.glb', (gltf) => {
model = gltf.scene;
model.position.y = -1.2;
scene.add(model);
});
교차 검사 및 상호작용
const tick = () => {
if (model) {
const modelIntersects = raycaster.intersectObject(model);
if (modelIntersects.length) {
model.scale.set(1.2, 1.2, 1.2); // 커서가 닿으면 확대
} else {
model.scale.set(1, 1, 1); // 닿지 않으면 원래 크기로
}
}
};
⚠️ 주의할 점
- model은 Group이며 내부에 실제 Mesh들이 있음
- 다행히도 Raycaster는 내부 자식까지 재귀적으로 검사하므로 model 전체에 대해 한 번만 검사해도 됨
- intersectObject(model, true) 와 같이 두 번째 인자를 명시하면 재귀 여부를 직접 설정할 수 있음 (기본값: true)
정리 요약
| Raycaster 생성 | new THREE.Raycaster() |
| 수동 설정 | raycaster.set(origin, direction) |
| 마우스 기준 설정 | raycaster.setFromCamera(mouse, camera) |
| 단일 객체 검사 | raycaster.intersectObject(object) |
| 다수 객체 검사 | raycaster.intersectObjects([objs]) |
| 반환값 | 항상 배열 형태이며, 교차 정보 포함 |
| 자식 검사 | 기본적으로 true (재귀적 검사) |

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import GUI from 'lil-gui';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
/**
* Base
*/
// Debug
const gui = new GUI(); // 디버그 UI 생성 (현재 사용은 안함)
// Canvas
const canvas = document.querySelector('canvas.webgl'); // 렌더링할 <canvas> 요소
// Scene
const scene = new THREE.Scene(); // 장면(Scene) 생성
/**
* Objects
*/
const object1 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16), // 반지름 0.5, 세그먼트 16x16의 구체
new THREE.MeshBasicMaterial({ color: '#ff0000' }), // 기본 머티리얼 (조명 영향을 받지 않음)
);
object1.position.x = -2;
const object2 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16),
new THREE.MeshBasicMaterial({ color: '#ff0000' }),
);
const object3 = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 16, 16),
new THREE.MeshBasicMaterial({ color: '#ff0000' }),
);
object3.position.x = 2;
scene.add(object1, object2, object3); // 세 개의 구체를 장면에 추가
// object1.updateMatrixWorld();
// object2.updateMatrixWorld();
// object3.updateMatrixWorld();
/**
* Raycaster
*/
const raycaster = new THREE.Raycaster(); // 광선 투사용 Raycaster 인스턴스 생성
// const rayOrigin = new THREE.Vector3(-3, 0, 0);
// const rayDirection = new THREE.Vector3(10, 0, 0); // 원한는 방향으로의 거리
// rayDirection.normalize();
// new THREE.TorusGeometry();
// raycaster.set(rayOrigin, rayDirection);
// const intersect = raycaster.intersectObject(object2);
// console.log(intersect);
// const intersects = raycaster.intersectObjects([object1, object2, object3]);
// console.log(intersects);
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
window.addEventListener('resize', () => {
// 창 크기 변경 시 사이즈 업데이트
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// 카메라 비율 조정
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
// 렌더러 사이즈 및 해상도 조정
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 성능 최적화
});
/**
* Mouse
*/
const mouse = new THREE.Vector2(); // 마우스 위치 (NDC 좌표: -1 ~ 1)
window.addEventListener('mousemove', (e) => {
// 마우스 좌표를 NDC (Normalized Device Coordinates)로 변환
mouse.x = (e.clientX / sizes.width) * 2 - 1;
mouse.y = -(e.clientY / sizes.height) * 2 + 1;
// tick() 안에서 raycaster.setFromCamera()를 통해 실제 캐스팅 수행
/**
* 콜백에서 레이를 캐스팅할 수도 있지만,
* 일부 브라우저에서는 mousemove이벤트가 프레임 속도보다 더 빠르게 트리거될 수 있으므로 권장하지 않음.
* => 그러므로 tick에서 프레임 속도로 이벤트 캐스팅
*/
});
window.addEventListener('click', () => {
// 교차된 객체가 있을 때 클릭에 반응
if (currentIntersect) {
switch (currentIntersect.object) {
case object1:
console.log('ob1');
break;
case object2:
console.log('ob2');
break;
case object3:
console.log('ob3');
break;
}
}
});
/**
* Camera
*/
// 원근 카메라 생성: (시야각, 종횡비, 근거리 클리핑, 원거리 클리핑)
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100,
);
camera.position.z = 3; // 카메라 위치 (Z축으로 멀리 떨어뜨림)
scene.add(camera);
// 궤도 카메라 컨트롤 생성 (마우스로 회전 가능)
const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true; // 관성 효과 (자연스러운 감속)
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas, // 앞서 선택한 <canvas> 요소에 렌더링
});
renderer.setSize(sizes.width, sizes.height); // 렌더러 크기 설정
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 고해상도 디바이스 대응
/**
* Models
*/
const glTFLoader = new GLTFLoader(); // GLTF 모델 로더
let model = null; // 나중에 추가할 모델 참조용
glTFLoader.load('./models/Duck/glTF-Binary/Duck.glb', (glTF) => {
model = glTF.scene; // 씬 객체 추출
glTF.scene.position.y = -1.2; // 위치 조정
scene.add(glTF.scene); // 장면에 모델 추가
});
/**
* Lights
*/
// 주변광 (전체적으로 고르게 조명)
const ambientLight = new THREE.AmbientLight('#ffffff', 0.9);
scene.add(ambientLight);
// 방향광 (태양처럼 방향을 갖는 광원)
const directionalLight = new THREE.DirectionalLight('#ffffff', 2.1);
directionalLight.position.set(1, 2, 3);
scene.add(directionalLight);
/**
* Animate
*/
const clock = new THREE.Clock(); // 경과 시간 측정용 시계
let currentIntersect = null; // 현재 광선과 교차 중인 객체
const tick = () => {
const elapsedTime = clock.getElapsedTime(); // 현재까지 경과 시간
// 각 오브젝트를 다른 속도로 상하 진동시키기
object1.position.y = Math.sin(elapsedTime * 0.3) * 1.5;
object2.position.y = Math.sin(elapsedTime * 0.8) * 1.5;
object3.position.y = Math.sin(elapsedTime * 1.4) * 1.5;
// Cast a ray
// const rayOrigin = new THREE.Vector3(-3, 0, 0);
// const rayDirection = new THREE.Vector3(1, 0, 0);
// rayDirection.normalize();
// raycaster.set(rayOrigin, rayDirection);
// const objectsToTest = [object1, object2, object3];
// const intersects = raycaster.intersectObjects(objectsToTest);
// console.log(intersects.length);
// for (const object of objectsToTest) {
// 원래 색상으로 변경
// object.material.color.set('#ff0000');
// }
// for (const intersect of intersects) {
// 색상 변경을 위해서 object의 Mesh에 접근해야한다.
// intersect.object.material.color.set('#0000ff');
// }
// 마우스 위치 기반으로 레이캐스팅 수행
raycaster.setFromCamera(mouse, camera);
const objectsToTest = [object1, object2, object3];
const intersects = raycaster.intersectObjects(objectsToTest); // 교차 판정
// 색상 초기화
for (const object of objectsToTest) {
object.material.color.set('#ff0000');
}
// 교차된 오브젝트는 색상 변경
for (const intersect of intersects) {
intersect.object.material.color.set('#0000ff');
}
// 마우스 enter/leave 감지
if (intersects.length) {
if (currentIntersect === null) {
console.log('mouse enter');
}
currentIntersect = intersects[0];
} else {
if (currentIntersect) {
console.log('mouse leave');
}
currentIntersect = null;
}
// 모델과의 교차 검사
if (model) {
const modelIntersects = raycaster.intersectObject(model);
if (modelIntersects.length > 0) {
model.scale.set(1.2, 1.2, 1.2); // 마우스 오버 시 모델 확대
} else {
model.scale.set(1, 1, 1); // 평상시 크기
}
}
controls.update(); // OrbitControls의 관성 효과 적용
renderer.render(scene, camera); // 렌더링 수행
window.requestAnimationFrame(tick); // 다음 프레임 예약
};
tick(); // 애니메이션 루프 시작'Graphic > ThreeJS' 카테고리의 다른 글
| 25 Realistic render (2) | 2025.07.31 |
|---|---|
| 24 Environment Map (3) | 2025.07.31 |
| 21 모델 불러오기(Imported models) (4) | 2025.07.29 |
| 20 물리엔진(Physics) (3) | 2025.07.29 |
| 19 스크롤 기반 애니메이션 (8) | 2025.07.28 |