BabylonJS 기초 10일차 : 입력/인터랙션/충돌 / Picking(클릭 선택) 기본
한눈에 보는 요약
3D 씬에서 “클릭으로 오브젝트를 선택”하는 기능은 에디터, 인벤토리/배치 UI, 상호작용(문 열기, 아이템 줍기), 디버깅 도구 등 거의 모든 프로젝트에서 핵심이 됩니다. BabylonJS는 이 기능을 Picking(피킹)이라고 부르며, 대표적으로 scene.pick로 구현합니다.
오늘 목표는 아주 명확합니다. 마우스로 Mesh를 클릭해 선택하고, 하이라이트로 선택 상태를 표시하며, 선택된 Mesh 이름을 화면에 출력하는 “오브젝트 선택기”를 완성합니다. 입력 처리의 기반이 되는 pointer events(pointerdown/move 등) 흐름까지 함께 익혀두면, 다음 단계(드래그, 이동, 충돌 반응)로 넘어갈 때 훨씬 수월해집니다.
목차
- 핵심 포인트
- 상세 설명
- 1) Picking이란 무엇인가
- 2) scene.pick의 동작 방식
- 3) PickResult(피킹 결과)에서 꼭 보는 값
- 4) Pointer Events: 클릭/이동 이벤트 연결하기
- 5) 클릭한 Mesh 하이라이트 처리 방식
- 6) 실습 설계: “오브젝트 선택기” 구조
- 코드 예시
- 따라하기
- 비교 표
- 블로그 최적화 정보
핵심 포인트
-
Picking = 화면 좌표(마우스/터치) → 3D 레이(ray) → 어떤 Mesh가 맞았는지 찾는 과정
BabylonJS는 scene.pick로 현재 포인터 위치에서 피킹을 수행해 pickResult를 반환합니다.
-
입력 처리는 pointer events로 시작
scene.onPointerObservable 또는 canvas 이벤트로 pointerdown/move를 받고, 그 시점에 scene.pick를 호출하는 흐름이 가장 기본입니다.
-
PickResult에서 중요한 것은 pickedMesh, hit, pickedPoint
hit이 false면 아무 것도 선택되지 않은 상황이므로 선택 해제 로직이 필요합니다.
-
선택 상태는 “하나의 변수(selectedMesh)”로 관리
새로 클릭하면 이전 하이라이트를 끄고, 새 대상에 하이라이트를 켜는 식으로 상태를 정리하면 버그가 줄어듭니다.
-
피킹 대상은 predicate(필터)로 제한
ground나 배경까지 선택되는 것이 싫다면, “선택 가능한 Mesh만” 피킹하도록 조건 함수를 두는 편이 깔끔합니다.
상세 설명
1) Picking이란 무엇인가
3D에서 클릭은 2D 화면 좌표(예: (320, 180))로 시작합니다. 그런데 씬 안의 오브젝트는 3D 공간 좌표에 있습니다. 따라서 “클릭 위치가 어떤 3D 물체를 가리키는지”를 알기 위해서는, 카메라에서 화면 방향으로 레이(ray)를 쏘고, 그 레이가 어떤 Mesh와 교차(충돌)하는지 검사해야 합니다.
이 과정을 BabylonJS가 쉽게 해주는 API가 scene.pick입니다. 가장 단순하게는 “현재 포인터 위치(scene.pointerX/Y)에서 피킹”을 수행해, 맞은 Mesh와 교차 지점 등을 한 번에 얻을 수 있습니다.
2) scene.pick의 동작 방식
scene.pick는 내부적으로 대략 다음 일을 합니다.
- 현재 포인터 위치(또는 전달받은 x, y)를 기준으로 레이를 구성
- 씬의 Mesh들을 대상으로 교차 검사
- 가장 먼저(가장 가까이) 맞은 Mesh를 PickResult로 반환
초보자가 가장 많이 헷갈리는 부분은 “왜 아무 것도 선택이 안 되지?”입니다. 그럴 때는 아래를 먼저 확인하시면 좋습니다.
- mesh.isPickable이 false로 되어 있지 않은지
- 카메라 컨트롤(ArcRotateCamera)이 포인터 이벤트를 먹고 있어도, scene.onPointerObservable은 정상 동작하는지
- GUI/HTML 오버레이가 캔버스를 덮어서 클릭이 전달되지 않는지
3) PickResult(피킹 결과)에서 꼭 보는 값
scene.pick는 PickResult를 반환하며, 초보 단계에서는 아래 3가지를 우선적으로 보시면 됩니다.
- pickResult.hit: 무엇인가를 맞췄는지(true/false)
- pickResult.pickedMesh: 맞은 Mesh(없으면 null)
- pickResult.pickedPoint: 교차한 3D 좌표(Vector3)
그리고 실습 목표처럼 “이름 표시”를 하려면 pickedMesh.name이 아주 유용합니다. 습관적으로 Mesh에 의미 있는 name을 붙여두면, 디버깅과 인터랙션 설계가 쉬워집니다.
4) Pointer Events: 클릭/이동 이벤트 연결하기
입력 처리에는 크게 두 가지 스타일이 있습니다.
- Babylon 방식: scene.onPointerObservable로 pointerdown/move 등을 처리
- 브라우저 방식: canvas.addEventListener("pointerdown", ...)로 처리
초보자에게는 Babylon 방식(scene.onPointerObservable)이 실수 확률이 낮습니다. 이벤트 타입이 정리되어 있고, scene.pointerX/Y 같은 값도 함께 쓰기 편하기 때문입니다. 오늘 실습에서는 pointerdown(클릭)에서 선택을 확정하고, pointermove(마우스 이동)에서 “커서 변경(선택 가능 표시)”을 적용해 사용감을 올려보겠습니다.
5) 클릭한 Mesh 하이라이트 처리 방식
선택된 Mesh를 강조하는 방법은 여러 가지가 있지만, 초보자에게 가장 직관적인 방식 중 하나는 HighlightLayer입니다. 선택 시 highlightLayer.addMesh(mesh, color)로 하이라이트를 켜고, 선택 해제 시 removeMesh로 끄면 됩니다.
중요한 것은 “항상 하나만 선택”이라면 하이라이트 대상도 하나만 유지되게 만드는 것입니다. 선택 로직에서 이전 selectedMesh를 정리해주면, 하이라이트가 여러 개 켜져 혼란스러운 상태를 예방할 수 있습니다.
6) 실습 설계: “오브젝트 선택기” 구조
산출물인 “오브젝트 선택기”는 기능이 간단하지만, 인터랙션 설계의 기본 패턴을 많이 담고 있습니다. 추천 구조는 다음과 같습니다.
- 선택 상태: selectedMesh 변수(현재 선택된 Mesh) 1개로 관리
- 하이라이트: HighlightLayer 1개를 만들고, 선택/해제 시 add/remove
- UI 표시: Babylon GUI 텍스트로 “선택된 Mesh 이름”과 간단한 안내(클릭/빈 공간 클릭 시 해제)
- 피킹 필터: 선택 가능한 Mesh만 pick predicate로 통과시키기(ground 제외 등)
코드 예시
// BabylonJS Day 10 (3-1): Picking(클릭 선택) 기본
// 목표: 오브젝트를 클릭해서 선택 + 하이라이트 + 이름 표시
// 산출물: "오브젝트 선택기"
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas, true);
const createScene = () => {
const scene = new BABYLON.Scene(engine);
// 카메라
const camera = new BABYLON.ArcRotateCamera(
"cam",
-Math.PI / 2,
Math.PI / 2.5,
16,
new BABYLON.Vector3(0, 2, 0),
scene
);
camera.attachControl(canvas, true);
// 라이트
const hemi = new BABYLON.HemisphericLight("hemi", new BABYLON.Vector3(0, 1, 0), scene);
hemi.intensity = 0.95;
// 바닥(선택 대상에서 제외할 예정)
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 30, height: 30 }, scene);
const groundMat = new BABYLON.StandardMaterial("groundMat", scene);
groundMat.diffuseColor = new BABYLON.Color3(0.18, 0.18, 0.2);
ground.material = groundMat;
ground.isPickable = false; // 바닥은 선택되지 않게
// 데모 오브젝트 생성(이름을 명확히 붙여두면 UI 표시/디버깅에 유리)
const meshes = [];
const makeBox = (name, x, z) => {
const b = BABYLON.MeshBuilder.CreateBox(name, { size: 1.4 }, scene);
b.position = new BABYLON.Vector3(x, 0.7, z);
b.isPickable = true;
const m = new BABYLON.StandardMaterial(name + "_mat", scene);
m.diffuseColor = new BABYLON.Color3(Math.random(), Math.random(), Math.random());
b.material = m;
meshes.push(b);
};
const makeSphere = (name, x, z) => {
const s = BABYLON.MeshBuilder.CreateSphere(name, { diameter: 1.6 }, scene);
s.position = new BABYLON.Vector3(x, 0.8, z);
s.isPickable = true;
const m = new BABYLON.StandardMaterial(name + "_mat", scene);
m.diffuseColor = new BABYLON.Color3(Math.random(), Math.random(), Math.random());
s.material = m;
meshes.push(s);
};
makeBox("Box_A", -5, -3);
makeBox("Box_B", -2, 2);
makeBox("Box_C", 2, -1);
makeSphere("Sphere_D", 5, 3);
makeSphere("Sphere_E", 0, 6);
// 하이라이트 레이어
const hl = new BABYLON.HighlightLayer("hl", scene);
// UI(선택된 Mesh 이름 표시)
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("ui", true, scene);
const title = new BABYLON.GUI.TextBlock("title", "Object Picker");
title.color = "white";
title.fontSize = 22;
title.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
title.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
title.paddingLeft = 14;
title.paddingTop = 12;
ui.addControl(title);
const info = new BABYLON.GUI.TextBlock("info", "");
info.color = "white";
info.fontSize = 16;
info.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
info.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
info.paddingLeft = 14;
info.paddingTop = 44;
ui.addControl(info);
const help = new BABYLON.GUI.TextBlock("help", "좌클릭: 선택 / 빈 공간 클릭: 선택 해제");
help.color = "#dddddd";
help.fontSize = 14;
help.textHorizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
help.textVerticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
help.paddingLeft = 14;
help.paddingTop = 74;
ui.addControl(help);
// 선택 상태
let selectedMesh = null;
const clearSelection = () => {
if (selectedMesh) {
hl.removeMesh(selectedMesh);
selectedMesh = null;
}
info.text = "선택됨: (없음)";
};
const setSelection = (mesh, pickedPoint) => {
if (selectedMesh === mesh) {
// 같은 걸 다시 클릭하면 유지(원하면 토글 해제 로직으로 바꿔도 됩니다)
return;
}
// 이전 선택 해제
if (selectedMesh) hl.removeMesh(selectedMesh);
```
selectedMesh = mesh;
hl.addMesh(selectedMesh, new BABYLON.Color3(1, 0.9, 0.2)); // 하이라이트 색
const p = pickedPoint
? ` (${pickedPoint.x.toFixed(2)}, ${pickedPoint.y.toFixed(2)}, ${pickedPoint.z.toFixed(2)})`
: "";
info.text = "선택됨: " + selectedMesh.name + p;
```
};
// 피킹 필터(선택 가능한 Mesh만)
const pickPredicate = (mesh) => {
return mesh && mesh.isPickable === true && mesh !== ground;
};
// 클릭 선택(pointerdown)
scene.onPointerObservable.add((pointerInfo) => {
if (pointerInfo.type !== BABYLON.PointerEventTypes.POINTERDOWN) return;
```
// 좌클릭만 처리(0: left, 1: middle, 2: right)
const evt = pointerInfo.event;
if (evt && evt.button !== 0) return;
const pickResult = scene.pick(scene.pointerX, scene.pointerY, pickPredicate);
if (pickResult && pickResult.hit && pickResult.pickedMesh) {
setSelection(pickResult.pickedMesh, pickResult.pickedPoint);
} else {
clearSelection();
}
```
});
// 마우스 이동(pointermove): 선택 가능한 대상 위에 있으면 커서를 pointer로 변경
scene.onPointerObservable.add((pointerInfo) => {
if (pointerInfo.type !== BABYLON.PointerEventTypes.POINTERMOVE) return;
```
const hover = scene.pick(scene.pointerX, scene.pointerY, pickPredicate, false);
if (hover && hover.hit && hover.pickedMesh) {
canvas.style.cursor = "pointer";
} else {
canvas.style.cursor = "default";
}
```
});
// 초기 UI
clearSelection();
return scene;
};
const scene = createScene();
engine.runRenderLoop(() => scene.render());
window.addEventListener("resize", () => engine.resize());
이 코드는 “오브젝트 선택기”의 가장 기본 형태입니다. pointerdown에서 scene.pick를 호출해 PickResult를 받고, hit 여부에 따라 선택/해제를 처리합니다. 선택 시에는 HighlightLayer로 Mesh를 강조하고, UI 텍스트에 Mesh 이름과 클릭 지점 좌표를 함께 표시합니다. 또한 pointermove에서 “선택 가능한 Mesh 위에 있을 때 커서를 pointer로 변경”해 사용성을 조금 더 좋게 만들었습니다.
따라하기
-
선택 대상이 될 Mesh에 이름(name)을 명확히 부여합니다.
실습에서는 Box_A, Sphere_D 같은 이름을 사용했습니다. 나중에 “문(Door_01)”, “아이템(Potion_Red)”처럼 의미 있는 이름이 있으면 상호작용 설계가 훨씬 쉬워집니다.
-
선택 대상만 isPickable=true로 두고, 바닥/배경은 제외합니다.
ground.isPickable=false로 두면 “빈 공간 클릭 시 선택 해제”가 자연스럽습니다. 바닥까지 선택되면 hit이 항상 true가 되어 UX가 나빠지기 쉽습니다.
-
scene.onPointerObservable로 pointerdown 이벤트를 받아 scene.pick를 호출합니다.
핵심 흐름은 “입력 이벤트 발생 → scene.pick → hit이면 선택 / 아니면 해제”입니다. 이 패턴을 정확히 익히면 이후 드래그 이동이나 충돌 반응도 같은 방식으로 확장할 수 있습니다.
-
선택 상태(selectedMesh)를 하나로 관리하고, 이전 하이라이트를 정리합니다.
선택이 바뀔 때마다 이전 선택의 하이라이트를 끄는 정리 로직을 꼭 넣어두시면, “하이라이트가 여러 개 남는 문제”를 예방할 수 있습니다.
-
(선택) pointermove에서 커서/호버 피드백을 넣습니다.
사용자가 “여기가 클릭 가능한 대상인지”를 미리 알 수 있어 인터랙션 품질이 좋아집니다. 프로젝트가 커질수록 이런 작은 피드백이 체감 차이를 만듭니다.
비교 표
아래 표는 피킹/입력 처리에서 초보자가 가장 자주 쓰는 API/옵션을 정리한 것입니다. 당장 외우기보다, “필요할 때 찾아 쓸 수 있는 기준표”로 두시면 좋습니다.
| 항목 | 무엇을 하나요 | 언제 쓰나요 | 초보자 주의사항 |
|---|---|---|---|
| scene.pick(x, y, predicate, fastCheck) | 화면 좌표에서 피킹(가장 가까운 1개 결과) | 클릭 선택, 호버 체크 | ground까지 선택되면 항상 hit=true가 될 수 있어 predicate/ isPickable 관리가 중요합니다. |
| pickResult.hit | 맞춘 대상이 있는지 여부 | 선택/해제 분기 | hit=false일 때 해제 로직이 없으면 “마지막 선택이 계속 남는” UX가 됩니다. |
| pickResult.pickedMesh | 선택된 Mesh | 하이라이트, 이름 표시, 상호작용 트리거 | pickedMesh가 null일 수 있으므로 hit와 함께 확인하는 습관이 좋습니다. |
| pickResult.pickedPoint | 레이가 교차한 3D 지점(Vector3) | 클릭 위치로 오브젝트 이동/이펙트 생성 | 값을 표시하거나 디버깅할 때 좌표를 소수점 제한(toFixed)하면 보기 좋습니다. |
| scene.onPointerObservable | Babylon 방식 포인터 이벤트 처리 | pointerdown/move/up, 휠 등 입력 처리 | 이벤트 타입(POINTERDOWN 등)을 분기해 처리하면 코드가 안정적입니다. |
| mesh.isPickable | 해당 Mesh가 피킹 대상인지 여부 | 선택 가능한 오브젝트만 제한 | 기본값이 true인 경우가 많아 “원치 않는 Mesh까지 선택되는” 상황이 생길 수 있습니다. |
추가로 생각해볼 점
- “선택된 오브젝트를 이동/회전”까지 하고 싶다면, 다음 단계에서 Gizmo를 붙이는 방식이 매우 유용합니다. 오늘 만든 selectedMesh 변수를 그대로 활용하실 수 있습니다.
- 오브젝트가 아주 많아질 때는 피킹도 비용이 될 수 있습니다. 그럴 때는 predicate로 대상 축소, “호버 피킹의 빈도 제한(예: 일정 ms마다)” 같은 방식으로 부담을 줄일 수 있습니다.
- 이후 충돌(콜리전) 파트에서는 “레이/교차 검사” 개념이 다시 등장합니다. 오늘의 “hit/교차 지점/대상” 개념을 확실히 잡아두시면 다음 단원이 훨씬 편해집니다.
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

0 댓글