threejs๋กœ ๋ˆˆ๐Ÿ‘€ ๋งŒ๋“ค๊ธฐ

Sydneyยท2024๋…„ 12์›” 14์ผ
post-thumbnail

๐Ÿ’ก threejs์— ๋Œ€ํ•œ ๊ธฐ์ดˆ์ ์ธ ์ง€์‹์ด ์žˆ์œผ์‹  ๋ถ„๋“ค์ด ์ฝ์–ด๋ณด๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค!

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), ์˜ค๋ฅธ์ชฝ์ด 3D
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 ์ขŒํ‘œ๋กœ ๋ฐ”๊พธ๋Š” ์ž‘์—…์ด ํ•„์š”ํ•˜๋‹ค.

  1. ๋ธŒ๋ผ์šฐ์ € ์ฐฝ ๊ธฐ์ค€์˜ ๋งˆ์šฐ์Šค ์ขŒํ‘œ๋ฅผ DOM ์š”์†Œ ๊ธฐ์ค€์˜ ์ƒ๋Œ€ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜
  2. canvas ๋„ˆ๋น„/๋†’์ด๋กœ ๋‚˜๋ˆ  0์—์„œ 1 ๋ฒ”์œ„๋กœ ์ •๊ทœํ™”
  3. NDC๋ฒ”์œ„(โˆ’1์—์„œ +1 ๋ฒ”์œ„)๋กœ ๋ฐ”๊พธ๊ธฐ ์œ„ํ•œ ๊ณ„์‚ฐ
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์„ ์ƒ๋‹˜์„ ์„ค๋“์ค‘์ด์ง€๋งŒ ๊ตฌํ˜„์ด ์ž˜ ๋˜์ง€์•Š๊ณ ์žˆ๋‹ค..
๋‚ฉ๋“ํ• ๋งŒํ•œ ๊ตฌํ˜„์„ ํ•˜๊ฒŒ ๋˜๋ฉด ์ถ”๊ฐ€์ ์œผ๋กœ ์—…๋กœ๋“œํ•˜๊ฒ ๋‹ค.

+์ข‹์€ ์•„์ด๋””์–ด๋‚˜ ์กฐ์–ธ์€ ์–ธ์ œ๋“ ์ง€ ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์„ธ์š”๐Ÿ‘€

reference

threejs๊ณต์‹๋ฌธ์„œ
NDC
Typescript๋กœ ์ฆ๊ธฐ๋Š” Threejs

profile
์ˆฒ์„ ๋ณด๋ฉฐ ๋‚˜๋ฌด๋ฅผ ์‹ฌ๋Š” ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๐ŸŒณ

0๊ฐœ์˜ ๋Œ“๊ธ€