๐ก threejs์ ๋ํ ๊ธฐ์ด์ ์ธ ์ง์์ด ์์ผ์ ๋ถ๋ค์ด ์ฝ์ด๋ณด๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค!
์ด๋ ธ์ ๋ (๋ผ๊ณ ํด๋ดค์ ๊ณ ์ ๋ํ์ ๋๊น์ง) 3D๋ก ๋ ๊ฒ์์ ์ฆ๊ฒจํ์๋ค.
์ฌ์ฆ๋ผ๋๊ฐ ํผํผ๋ ๋ ๋ผ๋๊ฐ ์๋ฐํ๋ฅผ ์ฌ์ฉํด์ ์ํธ์์ฉ์ ํ๋๊ฒ ์ฌ๋ฏธ์์๋ค. ๊ทธ๋์ ์ด ์๋ฐํ๋ ์ด๋ป๊ฒ ๋์ํ๋ ๊ฑธ๊น ๊ด์ฌ์ ๊ฐ์ง๊ฒ ๋๊ณ , ๋ธ๋ผ์ฐ์ ์์ ๋ชจ๋ธ์ ๊ตฌํํ ์ ์๋ threejs๋ฅผ ๊ณต๋ถํด๋ณด๊ธฐ๋ก ํ๋ค.
๋ฌผ๋ก ๊ฒ์์์ ์๋ฐํ๋ฅผ ๊ตฌํํ๋ ค๋ฉด ๋ชจ๋ธ์ ๊ฒ์์์ง์ผ๋ก ๊ฐ์ ธ์ ๋ ๋๋งํ๊ธฐ ๋๋ฌธ์ ๊ฒ์์์ง์ ๊ณต๋ถํด์ผํ์ง ์์๊น?์๊ฐํ์ง๋ง, ๊ฒ์ ์์ง๊น์ง ๊ณต๋ถํ๊ธฐ์๋ ์๊ฐ์ด ๋ถ์กฑํ๊ณ , ๋ธ๋ผ์ฐ์ ์์ ๊ฐ๋จํ ๋ชจ๋ธ์ ์ถฉ๋ถํ Three.js๋ก ๊ตฌํํ ์ ์๊ธฐ ๋๋ฌธ์ threejs๋ก ๊ฒฐ์ ํ๋ค. (์ฌ์ค ์ธํ ๋งฅ์ ์ฐ๊ณ ์์ด ์ ๋ํฐ๊ฐ์ ๊ฒ์์์ง์ ์กฐ๊ธ๋ง ๋๋ ค๋ ๋ฐ์ด ์ด์ ๋๋ฌธ์ threejs๋ฅผ ์ ํํ๋ค๋ ์ฌํ ์ด์ผ๊ธฐ๊ฐ ์๋ค)
๊ณต์๋ฌธ์๋ ๋์์๊ฐ์๋ก ์์๋ ์, ์๋ฟ ๋ฑ๋ฑ์ ๋ง์ด ๋ง๋ค์ด ๋ดค๊ณ ์ด์ ์ด๊ฑธ ์์ฉํด๋ณด๊ณ ์ถ์๋ค. ํ์ง๋ง ์์ฉํ๋ค๊ณ ํด์ ๋ด๊ฐ ์ ๊ตํ ์๋ฐํ๋ฅผ ๋ง๋ค ์ ์๋ ๊ฒ๋ ์๋์๊ธฐ ๋๋ฌธ์ ์ฐ์ ๊น๋ฐ์ด๋ ๋๋ถํฐ ๋ง๋ค์ด๋ณด๊ธฐ๋ก ํ๋ค.
๋ค์ ๋ ๊ฐ์ง๋ ๊ตฌํํด๋ณด๊ณ ์ถ์๋ค.
๋ ์ฌ๋ฆฐ๊ฑด ๋์ฆ๋์ ์ฌ๋ผํ ๋์ฒ๋ผ ๋์ ๋ง๋ค์ด๋ณด๊ณ ์ถ์ด, ๊ทธ๋ฆผ์ ๊ทธ๋ ค๋ณด์๋ค.
์ ํ ์ฌ๋ผํ ๊ฐ์ ๋์ด ์๋๋ผ์ ์ ์๊ถ์๋ ์ ๊ฑธ๋ฆด ๋ฏํ๋ค.
์ฌ๋์ด๋ผ๋ ๊ฐ์ฒด๊ฐ ๋จธ๋ฆฌ + ๋ชธํต + ํ + ๋ค๋ฆฌ ๊ฐ ํฉ์ณ์ ธ ๋ง๋ค์ด์ง๋ฏ, ๊น๋ฐ์ด๋ ๋์ ๊ตฌํํ๊ธฐ ์ํด์๋ ์๋์ ๊ฐ์ ๋ถํ๋ค์ด ํ์ํ๋ค.

์ด๊ฒ๋ค์ ์ด์ threejs + typescript๋ก ๊ตฌํํด๋ณด๋ ค ํ๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก threejs์์ ๋ฌผ์ฒด๋ฅผ ํํํ๊ธฐ ์ํด์๋ ์ฅ๋ฉด(Scene), ๋ชจ๋ธ(Model), ์กฐ๋ช
(Light), ์นด๋ฉ๋ผ(Camera), ๋ ๋๋ฌ(Renderer)๊ฐ ํ์ํ๋ค.
์คํ๋์ค์์ ์ฌ์ง์ ์ฐ๊ณ , ํ์ํ๋ ๊ฒ๊ณผ ๋น์ทํ๋ค๊ณ ๋ ํ ๊น?
์ฌ๊ธฐ์๋ class๋ก ๊ตฌํํ์ผ๋ฉฐ ์ต์ข ์ฝ๋๋ ๋งจ ์๋๋ฅผ ์ฐธ์กฐํ๋ฉด ๋๋ค.
๋จผ์ class์ ์์ฑ์๋ฅผ ํตํด ๋ ๋๋ฌ, ์ฅ๋ฉด, ์นด๋ฉ๋ผ, ์กฐ๋ช , ๋ชจ๋ธ์ ์ค์ ํด์ค๋ค.
class App {
private domApp: Element;
private renderer: THREE.WebGLRenderer;
private scene: THREE.Scene;
private camera?: THREE.PerspectiveCamera;
private light?: THREE.Light;
/**
* ๋ ๋๋ฌ, ์ฅ๋ฉด, ์นด๋ฉ๋ผ, ์กฐ๋ช
, ๋ชจ๋ธ ์ค์
*/
constructor() {
this.domApp = document.querySelector("#app")!;
this.renderer = new THREE.WebGLRenderer();
this.domApp.appendChild(this.renderer.domElement);
this.scene = new THREE.Scene();
this.setupCamera();
this.setupLight();
this.setupModels();
}
}
new App();
์นด๋ฉ๋ผ๋ฅผ ์ค์ ํ๋ค.
PerspectiveCamera๋ฅผ ์ฌ์ฉํด 3D์ด๋ฏธ์ง์ ์๊ทผ๊ฐ์ ๋ํ๋ผ ์ ์๋๋ก ํด์ค๋ค.
private setupCamera() {
const domApp = this.domApp;
const width = domApp.clientWidth;
const height = domApp.clientHeight;
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100);
this.camera.position.set(0, 0, 4.5);
}
์กฐ๋ช
์ ์ค์ ํ๋ค.
AmbientLight๋ก ์ ์ฒด์ ์ธ ์กฐ๋ช
์ ์ค์ ํ๋๋ ์ฌ์ฌํด๋ณด์ฌ์, DirectionalLight๋ก ์์์ ์กฐ๋ช
์ ์ผ์ ๋ฌผ์ฒด์ ๊ทธ๋ฆผ์๊ฐ ๋ณด์ด๋๋ก ์ค์ ํ๋ค.
// ํ๊ฒฝ๊ด (์ ์ฒด์ ์ธ ์กฐ๋ช
)
const ambientLight = new THREE.AmbientLight(0xffffff, 1); // ์ฝ๊ฐ์ ์กฐ๋ช
this.scene.add(ambientLight);
}
// ๋ฐฉํฅ์ฑ ์กฐ๋ช
(๊ทธ๋ฆผ์๋ฅผ ์์ฑ)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(0, 5, 0);
this.scene.add(directionalLight);
๋ ๋๋ฌ๋ ์นด๋ฉ๋ผ๋ ์ฐฝ ํฌ๊ธฐ๊ฐ ๋ณ๊ฒฝ๋ ๋๋ง๋ค ํฌ๊ธฐ์ ๋ง์ถฐ ์์ฑ๊ฐ์ ์ฌ์ค์ ํด์ค์ผ ํ๋ฏ๋ก, ์ด๋ฒคํธ ๋ฆฌ์ค๋์ resize์ด๋ฒคํธ๋ฅผ ๋ฑ๋กํ๋ค.
์ฒ์ ์์ํ ๋๋ ํ๋ฉด ์์น์ ๋ง๊ฒ ๋ฆฌ์ฌ์ด์ฆ๋๋๋ก, resize๋ฉ์๋๋ฅผ ํ๋ฒ ๋ ์คํํด์ค๋ค.
๋ง์ง๋ง์ผ๋ก setAnimationLoop๋ฅผ ์ฌ์ฉํด ์ฐ์ํด์ render๋ฉ์๋๋ฅผ ์คํํ์ฌ ์ ๋๋ฉ์ด์
์ ์ ์ฉํ๋ค.
๐ก setAnimationLoop vs requestAnimationFrame
setAnimationLoop์ threejs์ ๋ด์ฅํจ์๋ก ์ ๋๋ฉ์ด์ ์ ์ฒ๋ฆฌํ๋๋ฐ ์ฐ์ธ๋ค. setAnimationLoop๋ ๋ด๋ถ์ ์ผ๋ก๋ requestAnimationFrame์ ์ด์ฉํด์ ๊ตฌํ๋์ด ์๊ธฐ ๋๋ฌธ์ ์ด๋์ชฝ์ด๋ ์ฌ์ฉํด๋ ์๊ด์์ง๋ง, WebXR์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ๋ฐ๋์ setAnimationLoop๋ฅผ ์ฌ์ฉํด์ผํ๋ค.
์ธ๋ถ ๊ตฌํ์ ๋ํ ์ค๋ช ์ ์ฃผ์์ฒ๋ฆฌ ํ ๊ณณ์์ ํ์ธํ ์ ์๋ค.
constructor() {
...,
this.setupEvents();
}
private setupEvents() {
// resize์์์ this๊ฐ ๊ฐ๋ฆฌํค๋ ๊ฐ์ฒด๊ฐ ์ด๋ฒคํธ ๊ฐ์ฒด๊ฐ ์๋ appํด๋์ค์ ๊ฐ์ฒด๊ฐ ๋๋๋ก ํด์ค๋ค.
window.addEventListener("resize", this.resize.bind(this));
this.resize();
this.renderer.setAnimationLoop(this.render.bind(this));
}
private resize() {
// 1. DOM ์์ ํฌ๊ธฐ ๊ฐ์ ธ์ค๊ธฐ
const domApp = this.domApp;
const width = domApp.clientWidth;
const height = domApp.clientHeight;
// 2. ์นด๋ฉ๋ผ์ aspect ๋น์จ์ ์๋ก ๊ณ์ฐ
const camera = this.camera;
if (camera) {
camera.aspect = width / height;
// ๋ณ๊ฒฝ๋ ๋น์จ์ ์
๋ฐ์ดํธ
camera.updateProjectionMatrix();
}
// ๋ ๋๋งํ ์์ญ์ ๋์ด์ ๋๋น๋ฅผ ๋ณ๊ฒฝ๋ ํฌ๊ธฐ์ ๋ง๊ฒ ์ค์
this.renderer.setSize(width, height);
}
private render() {
// ๋๋๋ฌ๊ฐ ์ฅ๋ฉด์ ์นด๋ฉ๋ผ์ ์์ ์ผ๋ก ๋ ๋๋งํ๋ค.
this.renderer.render(this.scene, this.camera!);
}
์ฌ๊ธฐ๊น์ง ํ๊ณ ์คํํ๊ฒ๋๋ฉด, ํ๋ฉด์๋ ๊ฒ์ ํ๋ฉด๋ง ๋ณด์ด๊ฒ ๋๋ค.
์๋๋ฉด ์ฅ๋ฉด ์์ ๋ชจ๋ธ์ด ์๊ธฐ ๋๋ฌธ์ด๋ค.
์ด์ ๋ถํฐ ๋ณธ๊ฒฉ์ ์ผ๋ก '๋'๋ง๋ค๊ธฐ์ ๋ค์ด๊ฐ๋ณด์
๋์ ๊ตฌํํ๊ธฐ ์ํด์๋ ๋์ ๊ตฌ์ฑํ๋ ์์์ธ ํฐ์, ๊ฒ์์(๋๊ณต), ๋ฐ์ฌ๊ด, ๋์ปคํ์ด ํ์ํ๋ค.
์ด๊ฒ๋ค์ ํ๋ํ๋ ๊ตฌํํด ๋ณด๊ฒ ๋ค.
์ฐ์ ์์น ์ค์ ์ ๋์์ค ํฌํผ๋ฅผ ํ๋ ๋ง๋ค์ด์ค๋ค.
private setupModels() {
const axisHelper = new THREE.AxesHelper(10);
this.scene.add(axisHelper);
}
์ฌ๊ธฐ์ ์นด๋ฉ๋ผ ์์น๊ฐ z์ถ๊ณผ ํํํ๊ฒ ์์นํด ์์ผ๋ฏ๋ก ํ๋ฉด์ ๋ณด์ด๋ ๊ฒ์ x์ถ, y์ถ์ด๋ค.

๊ทธ๋ฆฌ๊ณ ๊ฐ ์์๋ค์ ๊ทธ๋ฃนํ ํด์ฃผ๊ธฐ ์ํด์ Group์ธ์คํด์ค๋ฅผ ์ ์ธํ๋ค.
private setupModels() {
...
this.eyeGroup = new THREE.Group();
}
ํฐ์๋ ์ฐ์ SphereGeometry๋ฅผ ์ด์ฉํด์ ๊ตฌ ํํ๋ฅผ ๋ฒ ์ด์ค๋ก ์ก๊ณ , y์ถ์ ํฌ๊ธฐ๋ฅผ ๋๋ ค์ ํ์ํํ๋ฅผ ๋๋๋ก ๋ง๋ค์๋ค.
MeshStandardMaterial์ ์ฌ์ฉํด์ ์กฐ๋ช
์ ์ํฅ์ ๋ฐ๋ ์ง๊ฐ์ ์ ํํ๊ณ , ์๋ ์ค์ ํด์คฌ๋ค.
๋ง์ง๋ง์ผ๋ก ์์ ๋ง๋ค์ด ๋๋ ๊ทธ๋ฃน์ ํฐ์๋ฅผ ์ถ๊ฐํ๊ณ , ์ฅ๋ฉด์ ๊ทธ๋ฃน์ ์ถ๊ฐํ๋ค.
// ํฐ์ (ํ์)
const whiteGeometry = new THREE.SphereGeometry(1, 32, 32);
whiteGeometry.scale(1, 1.2, 1);// ํ์ ๋ง๋ค๊ธฐ
const whiteMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff, // ํฐ์
});
const whiteMesh = new THREE.Mesh(whiteGeometry, whiteMaterial);
this.eyeGroup.add(whiteMesh);
this.scene.add(this.eyeGroup);
์ด๋ ๊ฒ ๋ง๋ค๊ณ ์คํํ๋ฉด, ํ๋ฉด์ ๋๋์ด ๊ณ๋ ํํ์ ํฐ์ ๋ชจ๋ธ์ด ๋ณด์ฌ์ง๊ฒ๋๋ค.

๋ค์์ผ๋ก ๊ฒ์์๋ฅผ ๊ตฌํํด๋ณด๊ฒ ๋ค.
ํฐ์๋ฅผ 3D๋ก ํํํ๋๊ฑฐ์ ๋ฌ๋ฆฌ ๊ฒ์์๋ ํฐ์ ์์ ๋ฎ์ฌ์๋ ๋ง ๊ฐ์ ๊ฒ์ด๋ผ ์๊ฐํ๊ธฐ ๋๋ฌธ์ 2D๋ก ๊ตฌํํ๋ค.
CircleGeometry๋ก ์์ ๋ฒ ์ด์ค๋ก ์ก์ ๋ค์ ํ์ํ์ผ๋ก ๋ง๋ค๊ณ ,MeshBasicMaterial๋ก ๊ฒ์์์ ๋ฃ์ด์คฌ๋ค.
๋ง์ง๋ง์ผ๋ก ๋๊ณต์ด ์ฝ๊ฐ ์์ชฝ์ ์๋๋ก ์์น๋ฅผ ์กฐ์ ํด์ฃผ๊ณ , ๊ทธ๋ฃน์์ ๋ฃ์ด์ฃผ์๋ค.
// ๊ฒ์์ (ํฐ ๊ตฌ)
const pupilGeometry = new THREE.CircleGeometry(0.5, 32, 32);
pupilGeometry.scale(1, 1.2, 1);
const pupilMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const pupilMesh = new THREE.Mesh(pupilGeometry, pupilMaterial);
pupilMesh.position.z = 1; // ๋๊ณต์ด ์ฝ๊ฐ ์์ชฝ์ ์๋๋ก ์์น ์กฐ์
this.eyeGroup.add(pupilMesh);
์ฌ๊ธฐ๊น์ง ํ๋ฉด ๊ฒ์์๊ฐ ํํ๋๋ค. (์กฐ๊ธ ๋ฌด์ญ๊ฒ!)

๋ค์์ผ๋ก ๋ฐ์ฌ๊ด์ด๋ค.
์๋๋ ๋๊ณต๋ง์ ๋์์ผ๋ก Raycaster๋ฅผ ์ค์ ํด์ PointLight๋ฅผ ์ฃผ๊ณ ์ถ์์ง๋ง, ์ ๋์ง ์์ ์ผ๋จ์ ๊ผผ์๋ก ์๋ก์ด ์์ ํ๋ ์ถ๊ฐํ๊ธฐ๋ก ํ๋ค.
const reflectionGeometry = new THREE.CircleGeometry(
0.05,
32,
32
);
const reflectionMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff,
});
const reflectionMesh = new THREE.Mesh(
reflectionGeometry,
reflectionMaterial
);
reflectionMesh.position.set(0.1, 0.1, 2); // ์ด์ง ์๋ก ์ฌ๋ ค์ ๋ฐฐ์น
this.eyeGroup.add(reflectionMesh);
์๋์ฒ๋ผ ๋ฐ์ฌ๊ด์ด ์ถ๊ฐ๋์๋ค.

์ด์ ๋ง์ฐ์ค๋ฅผ ์์ง์ด๋ฉด ๋๋์๊ฐ ๋ฐ๋ผ๋ค๋๋ ๋์์ ๊ตฌํํด๋ณด๊ฒ ๋ค.
๋๋์๊ฐ ๋ง์ฐ์ค๋ฅผ ๋ฐ๋ผ๋ค๋๋๋ก ํ๋ ค๋ฉด,
1. ๋ง์ฐ์ค ํฌ์ธํฐ ์ขํ๊ฐ ์์ด์ผ ํ๊ณ ,
2. ๋๋์์ ์์ง์์ ๋ง์ฐ์ค ํฌ์ธํฐ ์ขํ์ ๋ง์ถฐ์ผ ํ๋ค.
1๋ฒ๋ถํฐ ์ฐจ๊ทผ์ฐจ๊ทผ ๊ตฌํํด๋ณด์.
๋ง์ฐ์ค ํฌ์ธํฐ์ ์ขํ๋ mousemove ์ด๋ฒคํธ๋ฅผ ์ฌ์ฉํด ์ฝ๊ฒ ๊ตฌํ ์ ์์ง๋ง, threejs์์ ์ฌ์ฉํ๋ ์ขํ๋ canvas๋ฑ HTML(2D)์์ ์ฌ์ฉํ๋ ์ขํ๋ ๋ค๋ฅด๋ค.
threejs์ ๊ธฐ๋ฐ์ธ WebGL์์ ํ๋ฉด์ ์ขํ๋ NDC(Normalized Device Coordinates) ์ขํ๊ณ๋ฅผ ์ฌ์ฉํ๋๋ฐ, canvas๋ฑ HTML(2D)์์ ์ฌ์ฉํ๋ ์ขํ์์ ์ฐจ์ด์ ์ ์๋์ ๊ฐ๋ค.

HTML(2D)์์ X์ขํ๋ (0,0)์์ (w,0)์ผ๋ก ๋ํ๋ด๊ณ , Y์ขํ๋ (0,0)์์ (0,h)์ผ๋ก ๋ํ๋ธ๋ค.
NDC ์ขํ์์ X ์ขํ๋ -1 (์ผ์ชฝ ๋)์์ +1(์ค๋ฅธ์ชฝ ๋)์ผ๋ก ๋ํ๋ด๋ฉฐ, Y์ขํ๋ -1(์๋ ๋)์์ +1(์์ชฝ ๋)์ผ๋ก ๋ํ๋ธ๋ค. Z ์ขํ๋ ์ผ๋ฐ์ ์ผ๋ก 0์์ 1, ๋๋ -1์์ 1๋ฒ์๋ก ์ฌ์ฉ๋์ง๋ง, ํต์ 2D ํ๋ฉด์์๋ ์๋ต๋๋ค.
๋ฐ๋ผ์ 2D์์ ์ฌ์ฉํ๋ ์ขํ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด NDC ์ขํ๋ก ๋ฐ๊พธ๋ ์์ ์ด ํ์ํ๋ค.
private mouseMove(event: MouseEvent) {
event.preventDefault();
// HTML์์ ์ฌ์ฉํ๋ ์ขํ๋ฅผ NDC์ขํ๋ก ๋ณํ
const rect = this.renderer.domElement.getBoundingClientRect();
this.mousePosition.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mousePosition.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
}
์ด์ ๋ฐ๋ ์ขํ๋ฅผ ๊ฐ์ง๊ณ , ๋๋์์ ์์น๋ฅผ ์ ๋ฐ์ดํธ ํด์ฃผ๋ฉด ๋๋ค.
private mouseMove(event: MouseEvent) {
...
this.eyeGroup!.children[1].position.x = this.mousePosition.x;
this.eyeGroup!.children[1].position.y = this.mousePosition.y;
}
๊ทธ๋ฆฌ๊ณ ํด๋น ์ด๋ฒคํธ๋ฅผ mousemove์ ๋ฐ์ธ๋ฉํด์ค๋ค.
private setupEvents() {
window.addEventListener("mousemove", this.mouseMove.bind(this));
...
}
์ฌ๊ธฐ๊น์ง ํ๋ฉด ๊ฒ์์๊ฐ ๋ง์ฐ์ค ํฌ์ธํฐ๋ฅผ ๋ฐ๋ผ๋ค๋๋ฉฐ ์์ง์ด๊ฒ ๋๋ค.
ํ์ง๋ง ๋ง์ฐ์ค๊ฐ ๋ ๋ฐ์ผ๋ก ๋๊ฐ๋ ๊ฒฝ์ฐ ๊ฒ์์๋ ๋ ๊ฒฝ๊ณ ๋ฐ์ผ๋ก ๋๊ฐ๋ฒ๋ฆฌ๋ ๋ฌด์์ด ์ด์๊ฐ ์๊ธด๋ค..

์ด์๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ๊ฒ์์์ ์์ง์์ ์ ํํ์ฌ ํฐ์ ๋ฐ์ผ๋ก ๋ฒ์ด๋์ง ์๊ฒ ํด์ผ ํ๋ค.
GPT์ ์๋๊ป ๋ฌผ์ด๋ณด๋, threejs์์๋ ๋ด์ฅ๋ MathUtils์ clamp๋ฅผ ์ฌ์ฉํด ํ์ฌ ๋ง์ฐ์ค ์ขํ ๊ฐ์ ์๋ก์ด ๋ฒ์ ๋ด์ ๊ฐ์ผ๋ก ๋งคํ์ํฌ ์ ์์๋ค.
private mouseMove(event: MouseEvent) {
...
// ํฐ์ ์์์ ๊ฒ์ ๋๋์๊ฐ ์์ง์ผ ์ ์๋ ๊ฒฝ๊ณ
// (์์) - ํ์ํ์ด๋ฏ๋ก ์ ํํ ๋ฐ์ง๋ฆ์ด ์๋๋ค.
this.pupilLimit = this.WHITE_RADIUS - this.PUPIL_RADIUS;
const pupilX = THREE.MathUtils.clamp(
this.mousePosition.x * 0.2,
-this.pupilLimit,
this.pupilLimit
);
// ํ์ํ์ด๋ฏ๋ก ์ธ๋ก๊ฐ ๊ธธ๊ธฐ ๋๋ฌธ์ 1.2๋ฅผ ๊ณฑํจ
const pupilY = THREE.MathUtils.clamp(
this.mousePosition.y * 0.2 * 1.2,
-this.reflectionLimit,
this.reflectionLimit
);
// ๋๋์ ์์น ์
๋ฐ์ดํธ
this.eyeGroup!.children[1].position.x = pupilX;
this.eyeGroup!.children[1].position.y = pupilY;
}
์ด์ ๋ง์ฐ์ค ํฌ์ธํฐ๋ฅผ ๋ธ๋ผ์ฐ์ ๊ฐ๋ก์ธ๋ก ๋๊น์ง ๊ฐ์ ธ๊ฐ๋ ๊ฒ์์๊ฐ ํฐ์ ๋ฐ์ผ๋ก ๋ฒ์ด๋์ง ์๋๋ค.

๋ง์ฐฌ๊ฐ์ง๋ก ๋ฐ์ฌ๊ด๋ ๋์ผํ๊ฒ ์ ์ฉํด์ค๋ค.
private mouseMove(event: MouseEvent) {
...
// ๊ฒ์ ๋๋์ ์์์ ๋ฐ์ฌ๊ด์ด ์์ง์ผ ์ ์๋ ๊ฒฝ๊ณ
// (์์) - ํ์ํ์ด๋ฏ๋ก ์ ํํ ๋ฐ์ง๋ฆ์ด ์๋๋ค.
this.reflectionLimit = this.PUPIL_RADIUS - this.REPLECTION_RADIUS;
const reflectionX = THREE.MathUtils.clamp(
this.mousePosition.x * 0.7,
-this.reflectionLimit,
this.reflectionLimit
);
const reflectionY = THREE.MathUtils.clamp(
this.mousePosition.y * 0.7 * 1.2,
-this.reflectionLimit,
this.reflectionLimit
);
this.eyeGroup!.children[2].position.x = reflectionX;
this.eyeGroup!.children[2].position.y = reflectionY;
}
ํ์ํ์ธ๋ฐ ๋ฐ์ง๋ฆ์ด ์ ํํ์ง ์๋ค๋์ง, ๊ทธ ์ธ ์์ํ ๋์ ์ด์๋ค์ด ์์ง๋ง, ์ผ๋จ ๋ชฉํ๋ก ํ๋ '๋ง์ฐ์ค๋ฅผ ์์ง์ด๋ฉด ๋๋์๋ ๋ฐ๋ผ์ ์์ง์ธ๋ค.'๋ ์ด์ฐ์ ์ฐ ๊ตฌํํ๋ค.
๋๋ฒ์งธ๋ก ๊ตฌํํ๊ณ ์ถ์๋ '๋์ปคํ์ ๋ง๋ค์ด ๋๋ค์ผ๋ก ๊น๋ฐ์ด๊ฒ ํ๋ค'๋ GPT์ ์๋์ ์ค๋์ค์ด์ง๋ง ๊ตฌํ์ด ์ ๋์ง์๊ณ ์๋ค..
๋ฉ๋ํ ๋งํ ๊ตฌํ์ ํ๊ฒ ๋๋ฉด ์ถ๊ฐ์ ์ผ๋ก ์
๋ก๋ํ๊ฒ ๋ค.
+์ข์ ์์ด๋์ด๋ ์กฐ์ธ์ ์ธ์ ๋ ์ง ๋๊ธ๋ก ๋จ๊ฒจ์ฃผ์ธ์๐