Raycaster와 editor picking
Three.js에는 pointer 위치로 scene object를 찾는 Raycaster가 있습니다. 3D object picking에는 강력하지만, Figma-like editor의 선택 정책을 모두 대신해주지는 않습니다.
pointer를 normalized device coordinate로 바꾼다
const pointer = new THREE.Vector2(
(x / width) * 2 - 1,
-(y / height) * 2 + 1
);
raycaster.setFromCamera(pointer, camera);
const hits = raycaster.intersectObjects(pickableObjects);
Raycaster는 camera와 pointer에서 world ray를 만들고 mesh 교차 결과를 반환합니다.
CSS pixel, DPR, NDC를 분리한다
Raycaster에 넣는 좌표는 backing store pixel이 아니라 viewport 안의 정규화 좌표입니다. clientX/Y에서 canvas rect를 빼고, CSS pixel 기준 width/height로 나눕니다.
function pointerToNdc(event, canvas) {
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
return new THREE.Vector2(
(x / rect.width) * 2 - 1,
-(y / rect.height) * 2 + 1
);
}
function pickWithRaycaster(event, canvas, camera, objects) {
const ndc = pointerToNdc(event, canvas);
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(ndc, camera);
return raycaster.intersectObjects(objects, true);
}
DPR은 renderer size와 render target에는 필요하지만, NDC 계산에 바로 곱하면 pointer가 어긋납니다.
editor picking 정책을 다시 적용한다
교차 결과를 그대로 선택하면 안 됩니다. editor에는 lock, hidden, frame clipping, layer order, group selection 같은 정책이 있습니다.
raycast hit Object3D
-> editor node id
-> visibility / lock / layer policy
-> selection
function resolveEditorHit(hits, objectToNodeId, document) {
for (const hit of hits) {
const nodeId = objectToNodeId.get(hit.object);
if (!nodeId) continue;
const node = document.nodesById.get(nodeId);
if (!node || node.hidden || node.locked) continue;
if (isOutsideClippedFrame(node, hit.point, document)) continue;
return node;
}
return null;
}
Three.js가 반환한 것은 geometry 교차 후보이고, 최종 선택 가능 여부는 editor core가 판단합니다.
오늘의 핵심
Raycaster는 후보를 찾는 도구입니다. 최종 선택은 editor core의 정책으로 결정해야 합니다.