2022. 12. 19. 19:17ใ์น/Three.js
๐ฆraycaster์ ์ญํ ๊ณผ ๋ฐฐ๊ฒฝ
three.js๋ฅผ ์ฌ์ฉํ๋ค๋ณด๋ฉด ๊ฐ Mesh์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ๋ฃ์ด์ฃผ๊ณ ์ถ์ ์ผ๋ค์ด ์๊ธด๋ค.
(๋๋ฅด๋ฉด ๊ด๋ จ ์นํ์ด์ง๋ก ๋๊ธฐ๋๊ฐ, ์๋๋ฉด ๋ด๋ถ ๋ก์ง์ ์ํด id ๊ฐ์ key๊ฐ์ ๋ฆฌํดํด์ฃผ๋๊ฐ...)
์ด๋ฐ ์ฒ๋ฆฌ๋ฅผ raycaster ์์ ํด์ฃผ๋๋ฐ ์ ์ฉํ๊ณ ๋๋ฉด onClickEvent๋ onMouseOverEvent๋ฅผ ๊ตฌํํ ์ ์๋ค.
raycaster๋ ๋ป ๊ทธ๋๋ก ๊ด์ ์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค. ๋ง์ฐ์ค๊ฐ ์๋ ๊ฐ ๊ธฐ์ค์ผ๋ก ๊ด์ ์ ์ญ ์์ ๋, ๊ทธ ๊ด์ ์ ๋ง์ Mesh์ ๋ํ ์ ๋ณด๋ฅผ ์ป์ด๋ผ ์ ์๋ค.
์๋ฅผ ๋ค๋ฉด ์ด๋ฐ ์์ผ๋ก ๊ตฌํ์ด ๊ฐ๋ฅํ๋ค.
๊ตฌํ์ ์ํด์ ๋ช ๊ฐ์ง์ ํธ๋ฆญ(?)์ด ์กด์ฌํ๋๋ฐ ์ฃผ๋ก ์ฌ์ฉ์ ์ธํฐ๋ ์ ๊ด๋ จ๋ ๋ด์ฉ์ด๋ค.
๊ตฌํ ๋ชฉํ๋ ๋ค์๊ณผ ๊ฐ๋ค.
1. raycaster ์์ฑ in React.
2. ray๋ฅผ ๋ง์ฐ์ค์ ๋ง์ถฐ์ ์๋ ๋ฐฉ๋ฒ.
3. ๊ฐ mesh์ ๋ฉํ๋ฐ์ดํฐ ์ฝ์ ํ๋ ๋ฐฉ๋ฒ.
ETC) UI ๊ด๋ จ (๋ง์ฐ์ค ์ปค์ ๋ณ๊ฒฝ์ด๋ผ๋๊ฐ...)
1. raycaster ์์ฑ in React
raycaster ๋ฟ๋ง ์๋๋ผ, React์์ three.js์ ํด๋์ค๋ค์ ๋ค๋ฃฐ ๋ ์ค์ํ๊ฒ ๋ฆฌํ์ธํ ์ด ์๋๊ฒ ์กฐ์ฌํ๋ ๊ฒ์ด๋ค.
๋๋ถ๋ถ ํฐ ์ฉ๋์ ๋ฆฌ์์ค ํ์ผ๋ค์ ๋ ๋๋ง ํ ๋ ์๊ฐ์ด ๋ง์ด ํ์ํ๋ฐ, ๋ณ ์๊ฐ์์ด state๋ก ๊ด๋ฆฌ๋๋ค๋ฉด init์ ๋์์ฃผ๋ ํจ์๊ฐ ์คํ๋๋ค๋ฉด Mesh๊ฐ ๋ช ๋ฒ ์ฉ ๋ฆฌํ์ธํ ๋ ์ ์๋ค. (CRA์ StrictMode๋ผ๋๊ฐ..)
raycaster๋ ์์ฑํ๊ณ ๋์ ์ ์ญ์ ์ธ EventListener๋ก ํธ๋ค๋ฌ๋ฅผ ๊ณ์ ํธ์ถํด์ผํ๋ useRef๋ก ์ค์ ํด์ฃผ์.
const FrontEndPF: React.FC<FrontEndPFProps> = (props) => {
const camera = useRef<THREE.PerspectiveCamera>(new THREE.PerspectiveCamera(80, window.innerWidth / window.innerHeight, 0.1, 10000));
const renderer = useRef<THREE.WebGLRenderer>(new THREE.WebGLRenderer({ antialias: false, alpha: true }));
const raycaster = useRef<THREE.Raycaster>(new THREE.Raycaster());
//...
}
๋ง์ฝ fog๋ฅผ ๋ฐ๋ก ์ค์ ํด์คฌ๋ค๋ฉด, raycaster๋ ์ค์ ํด์ค ๊ฐ๋ค์ด ์๋ค.
์๋ฅผ ๋ค์ด ์ด๋ฐ ์ํฉ์ด ์๋ค,
๊ด์ ์ ์์ ๋, ๋งจ ์์ object๊ฐ fog ๋ฐ๊นฅ์ ํ์ object๋ผ๊ณ ํด๋ณด์.
๋ณ ์ค์ ์ด ์๋ค๋ฉด, ์๋ฌด๋ฐ๋ ํด๋ฆญํ์ ๋ ์ ํ์ object ๊ด๋ จ๋ ๊ฐ์ด ๋์ด๊ฐ ์ ์๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ fog ๋ฐ๊นฅ์ ์๋ ์์๋ค์ raycaster์ ์์กํ๋๋ก ํ๋ ์ด๊ธฐ๊ฐ(near, far)์ ์ค์ ํด์ค์ผํ๋ค.
const init = useCallback(() => {
//...
//part2: fog generator
const near = 40;
const far = 100;
const color = "#ffffff";
scene.current.fog = new THREE.Fog(color, near, far);
// set boundary in raycaster same as fog.
raycaster.current.near = near;
raycaster.current.far = far;
}, [])
2. ๐๏ธray๋ฅผ ๋ง์ฐ์ค์ ๋ง์ถฐ์ ์๋ ๋ฐฉ๋ฒ.
๋ชจ๋ object๊ฐ ๊ฐ์ x, y ๊ฐ์ ๊ฐ์ง๊ณ , z ๊ฐ๋ง ๋ค๋ฅด๋ค๋ฉด ๊ธฐ์ค ๋ฒกํฐ๋ฅผ ๊ณ ์ ๊ฐ์ผ๋ก ์ค์ ํด๋์ด๋ ๋ฌธ์ ์์ง๋ง, ์นด๋ฉ๋ผ ์์ญ ์ฌ๊ธฐ์ ๊ธฐ์ object๊ฐ ํผ์ ธ์๋ค๋ฉด, ๋ง์ฐ์ค์ ๋ง์ถฐ์ raycater์ ๊ธฐ์ค ๋ฒกํฐ๋ฅผ ๋ฐ๊ฟ์ค์ผํ๋ค.
element๋ฅผ ๋ฑ ์ง์ด๋ผ ์ ์๊ธฐ ๋๋ฌธ์, ์๋์ฐ ์ ์ฒด์ mouseEvent๋ฅผ ๊ฑธ์ด์ ๋ฐ๊ฟ์ค์ผํ๋ค.
mousemove: ๋ง์ฐ์ค์ x์ขํ์ y์ขํ๋ก vector๋ฅผ ๋ง๋ ๋ค.
click: ์์์ ๋ง๋ ์ขํ๋ฅผ ๊ธฐ์ค์ผ๋ก ๊ด์ ์ ์๋ค.
์ด๋ฐ ๋ ๊ฐ์ง ์๊ตฌ์ฌํญ์ผ๋ก ์ธํด useEffect์์ ์ด๋ฒคํธ๋ฆฌ์ค๋๋ฅผ ๋ง๋ค์ด์ค์ผํ๋ค.
(์ด๋ฒคํธ ๋ฆฌ์ค๋๋ ์ปดํฌ๋ํธ destroy ์ ์ญ์ ๋ ์ ์๊ฒ cleanup ํจ์๋ก ๊ผญ ์ง์์ฃผ์!)
useEffect(() => {
init();
animate();
window.addEventListener("mousemove", setMouseMoveAxis, false);
window.addEventListener("click", onClickObjectHandler, false);
return () => {
window.removeEventListener("mousemove", setMouseMoveAxis);
window.removeEventListener("click", onClickObjectHandler);
}
}, [animate, init, onClickObjectHandler, setMouseMoveAxis]);
mousemove์ callback.
๋ชฉ์ ์ url์ด ์๋ ์ค๋ธ์ ํธ๋ฅผ ๋ง๋ฌ์ ๋ ์ปค์๋ฅผ pointer๋ก ๋ณ๊ฒฝํ๊ธฐ ์ํจ์ด๋ค.
const setMouseMoveAxis = useCallback((e: MouseEvent) => {
mouseX.current = e.clientX;
mouseY.current = e.clientY;
// ํ๋ฉด์ ์ค์์ (0, 0)์ผ๋ก ์ค์ .
pointer.current.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
raycaster.current.setFromCamera(pointer.current, camera.current);
// ๊ด์ ๊ณผ ๊ต์ฐจํ๋ ๋ถ๋ถ์ ๋ฝ์๋. userData์ url์ด ์๋ค๋ฉด cursor: pointer๋ก ๋ณ๊ฒฝ.
const intersects = raycaster.current.intersectObjects(pngGroup.current.children);
if (intersects.length > 0 && intersects.reduce((acc, val) => acc || val.object.userData?.url, false)) document.body.style.cursor = "pointer";
else document.body.style.cursor = "auto";
}, []);
click์ callback.
cursor: pointer์ผ ๋ ๋๋ฅด๋ฉด ์์ฐฝ์ ์นํ์ด์ง๋ฅผ ๋์ด๋ค.
const onClickObjectHandler = useCallback((e: MouseEvent) => {
pointer.current.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
raycaster.current.setFromCamera(pointer.current, camera.current);
const intersects = raycaster.current.intersectObjects(pngGroup.current.children);
for (const intersect of intersects) {
if (intersect.object.userData?.url) {
window.open(intersect.object.userData?.url, "_blank");
break;
}
}
}, []);
3. Mesh์ ๋ฉํ๋ฐ์ดํฐ ์ฝ์
๋ฉํ ๋ฐ์ดํฐ ์ฝ์ ์ mesh์ userData ์์ฑ์ผ๋ก ์ฝ์ ํ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ ์ด๊ฑด ํ๋ค๊ฐ ์๊ฒ ๋ ํ์ธ๋ฐ, ์ด 3d ์์ ์ ๊ฐ์ฅ ๊ณ ๋(?) ๋ถ๋ถ์ ์ผ์ผํ 3์ฐจ์ ๊ณต๊ฐ์ ์ขํ๋ฅผ ๋ค ๊ฐ mesh๋ง๋ค ๋ฃ์ด์ค์ผํ๋ค๋ ์ ์ด๋ค.
๊ฐ๋ ์ฑ์ ๋จ์ด์ง์ง๋ง, ํ ๋งฅ๋ฝ์์์ ๋ฌถ์ด์ ๋ณด๋ผ ์ ์๋ ํจ์๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด๋๋ฉด ์ ์ ๊ฑด๊ฐ์ ์ข๋ค.
๋๋ถ๋ถ์ ํ๋ก์ ํธ์์๋ ์ง์ object ๋ชจ์์ ๋ฐ๊ฟ์ ๋ญ๊ฐ๋ฅผ ํ์ง๋ ์๋ ๊ฒ ๊ฐ๊ณ , object์ ์ฌ์ง์ ๋ง์์ฐ๊ฑฐ๋, ๋์์์ ์น๋๋ค๋๊ฐ, 3d model์ ์์ด๋ค๋๊ฐ ํ๋ ์ผ๋ค์ ํ๋ค.
์ด๋ ๋ค texture๊ฐ load๋์ง ์์ ์ํ์์ mesh๋ฅผ ๋ง๋ค๋ฉด ๊ทธ๋ฅ ๊น๋ง ๋ธ๋ญ๋ง ๋ณด์ด๊ฒ ๋๋ค.
(์ฝ๊ฒ ๋งํด์ new THREE.textureLoader().load() ํจ์๊ฐ ๋น๋๊ธฐ๋ผ์ ์ด ๋ฌธ์ ๊ฐ ์ผ์ด๋๋ค.)
๋คํํ ์์ texture์ ๋ํ ์ฝ๋ฐฑ ํจ์๊ฐ ์กด์ฌํด์ ๊ทธ ์์์ ์ฒ๋ฆฌํด์ฃผ๋ฉด๋๋ค. (๊ผญ promise ํด์ฒด๊ฐ๋ค..)
export type positionProps = {
x: number;
y: number;
z: number;
ratioScale?: number;
userData?: ThreeObjectUserDataProps;
}
export interface GroupedImageRenderProps {
srcs: string[];
eachPosition: positionProps[];
ratioScale?: number;
}
const renderLayerGroupedImage = useCallback((arg: GroupedImageRenderProps) => {
const { srcs, eachPosition, ratioScale } = args;
srcs.forEach((src, i) => {
const imageMap = new THREE.TextureLoader().load(src, (tex) => {
// 3d ๊ณต๊ฐ ์์น ๊ด๋ จ ๊ฐ๋ค
const { x, y, z, ratioScale: _ratioScale, userData } = eachPosition[i];
const width = Math.floor(tex.image.width / (_ratioScale || ratioScale || 10));
const height = Math.floor(tex.image.height / (_ratioScale || ratioScale || 10));
// geometry + material => mesh ์์ฑ
const geometry = new THREE.BoxGeometry(width, height, 0);
const material = new THREE.MeshBasicMaterial({ map: imageMap, transparent: true, color: 0xffffff });
const boxMesh = new THREE.Mesh(geometry, material);
// userData ์ฝ์
if (userData) boxMesh.userData = userData;
boxMesh.position.set(x, y, z);
pngGroup.current.add(boxMesh);
});
})
}, []);
// ๋ง๋ค๋๋ ์ด๋ ๊ฒ ๋ฌถ์ด์ ๋ณด๋ด๋ฉด ํธํ๋ค.
renderLayerGroupedImage({ srcs: introduction, eachPosition: [
{ x: -10, y: 10, z: 0, userData: { url: "https://dev-russel.tistory.com" } },
{ x: 30, y: -7, z: 2 },
{ x: -55, y: -28, z: 4 }
]});
(์ถ๊ฐ) react portal๊ณผ ์ด๋ฒคํธ ๋ฆฌ์ค๋
์ด๊ฑด webGL๋ณด๋ค๋ ์ด๋ฒคํธ๋ฆฌ์ค๋์ ๋ํด์ ์์์ผํ ์ ์ธ๋ฐ,
์ด raycaster๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด ์ค์ ํ๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ cleanup(ํด๋น ์ปดํฌ๋ํธ๊ฐ destroy)๋๊ธฐ ์ ๊น์ง๋ ๊ณ์ ์ ์ง๋๋ค๋ ์ ์ ์ ์ํด์ผํ๋ค.
๊ทธ๋ฌ๋๊น ๋ชจ๋ฌ(react-portal)๊ฐ์ ๊ฑธ ๋ค๋ฃฌ๋ค๊ณ ํ์ ๋, ์์ ๋ค๋ฅธ id์์ ๊ทธ๋ ค์ง์ง๋ง, ์์ง webGL์ ํฌํจํ๋ ์ปดํฌ๋ํธ๊ฐ ์ด์์๊ธฐ ๋๋ฌธ์ ๋ชจ๋ฌ์ ๋ง์ฐ์ค ์์ง์์์๋ ๊ณ์ raycasting์ด ๋๊ณ ์๊ณ , ํด๋ฆญํ์ ๋ ๋ฌ๊ธ์๋ ์ฌ์ดํธ๋ค์ด ๋์ฌ ์ ์๋ค๋ ์ ์ด๋ค.
ํด๊ฒฐ๋ฐฉ๋ฒ์ ์์ธ๋ก ๊ฐ๋จํ๋ฐ, ๋ชจ๋ฌ์ ์ด๊ธฐ ์ํด ์ฌ์ฉํ๋ state๋ก raycasting์ ๋ง์์ฃผ๋ฉด ๋๋ค.
const onClickObjectHandler = useCallback((e: MouseEvent) => {
pointer.current.set((e.clientX / window.innerWidth) * 2 - 1, -(e.clientY / window.innerHeight) * 2 + 1);
raycaster.current.setFromCamera(pointer.current, camera.current);
// modal์ด ์ด๋ ค์์ผ๋ฉด casting x.
if (isModalOpened) return;
const intersects = raycaster.current.intersectObjects(pngGroup.current.children);
for (const intersect of intersects) {
if (intersect.object.userData?.url) {
window.open(intersect.object.userData?.url, "_blank");
break;
}
}
}, [isModalOpened]);
'์น > Three.js' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[three.js + React] "deployํ๋๋ฐ ์๋ฌด๊ฒ๋ ์๋ณด์ด๋๋ฐ์?" ๋์ฒ๋ฒ (React Portal๊ณผ ๋งค์ฐ ๊น์ ์ฐ๊ด ์์) (0) | 2022.11.19 |
---|