web graphics

Three.js에서 박스의 색깔과 크기 변형하기

ainniz 2025. 5. 18. 19:30

간단한 박스의 색깔과 형태를 변형하는 코드를 작성해보자.

Canvas에 박스를 띄우고, 버튼을 누르면 박스가 변형되도록 만들 것이다.


1. 환경 세팅

환경은 Vite + Vanilla ts + Three.js로 구성했다.

DOM은 간단히만 다룰 거라 React는 사용하지 않았다.

npm create vite@latest

vite로 vanilla ts를 시작하기 위해서는 위 명령어를 입력한 뒤 쉘에서 typescript를 선택하면 된다.

자세한 사항은 아래 공식 문서 링크에 있다.

 

https://ko.vite.dev/guide/

 

Vite

Vite, 프런트엔드 개발의 새로운 기준

ko.vite.dev

 

Three.js 설치 및 타입 적용

npm install three @types/three

 

Typescript 환경에서 Three.js를 설치할 때 현재까지는 three와 함께 @types/three 패키지도 설치해야 한다.

three 패키지는 js 기반 라이브러리기 때문에 타입 정의가 없다.

@types/three는 타입 정의 파일(d.ts)로 이를 보완해준다.

 

빌드 툴을 사용하는 이유

빌드 툴을 사용하지 않으면 불편한 점이 많다.

빌드 툴을 쓰지 않고 직접 개발 환경을 세팅하려면 많은 일이 필요하기 때문이다.

 

html, css, js 디렉터리 구조를 직접 만들고, 필요한 패키지들을 찾아 직접 설치하고, package.json에서 필요한 스크립트를 작성하고, 타입스크립트 설정을 해야한다.

이렇게 해서 바로 작동하면 모르겠지만 CommonJS와 ESM 설정을 직접 고민해야 하고, node_modules 폴더를 해석할 수 없는 우리의 브라우저가 특정 패키지를 가져올 수 있도록 별도로 설정해야 한다.

필요한 패키지를 잘못 설치했거나 생각하지 못한 다른 패키지도 필요할 수도 있다.

그리고 VSC 확장 프로그램인 라이브 서버를 쓰거나 서버도 직접 만들어야 한다.

그 외에도 빌드 툴을 사용해야 할 이유는 많다.

 

기존에는 처음부터 프로젝트를 만들 줄 알아야 한다고 생각해서 몇 번 고생했었다.

이제는 사용하는 이유도 알았고 필요성도 느끼기 때문에 빌드 툴로 주로 시작하려 한다.

특히 이번에 느낀 점은 빌드 툴이 제공하는 보일러 플레이트 덕분에 바로 구현을 할 수 있었다는 점이다.

2. Three.js 세팅

기본 Three.js 세팅

 

기본적인 Scene, Camera, Renderer, Controls, Animation(requestAnimationFrame)을 설정한다.

width, height, depth가 각각 1이고 빨간색의 박스를 띄워주는 코드다.

import './style.css';
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/Addons.js';

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(75, 800 / 600, 0.1, 1000);
camera.position.z = 3;
scene.add(camera);

const material = new THREE.MeshBasicMaterial({ color: 'red' });
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(800, 600);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.render(scene, camera);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// renderer에서 생성된 canvas를 #app의 자식으로 붙인다.
document.querySelector<HTMLDivElement>('#app')!.appendChild(renderer.domElement);

let start = performance.now();
let current = start;
let delta = 16;
let elapsed = 0;

window.requestAnimationFrame(animate);

function animate() {
    const currentTime = performance.now();
    delta = currentTime - current;
    current = currentTime;
    elapsed = current - start;

    controls.update();
    renderer.render(scene, camera);

    window.requestAnimationFrame(() => {
        animate();
    });
}

 

3. 박스 변형 코드

색깔과 스케일 변화를 적용한 경우

 

처음에는 클릭 시 색깔과 크기를 변화시키는 코드를 작성했다.

아래 코드의 문제점은 클릭할 때마다 이전 geometry 자원을 해제해야 하고, 새로운 geometry를 그려야 한다는 것이다.

지금은 간단한 기하이므로 문제가 없어보여도, 복잡한 기하의 경우 잦은 드로우 콜로 렌더링 부하가 크게 증가하고 성능에 좋지 않다.

changeButton?.addEventListener('click', () => {
    changeColor(material);
    changeSize(mesh);
});

function changeColor(material: THREE.MeshBasicMaterial) {
    const randomColor = new THREE.Color();
    randomColor.setHSL(Math.random(), 1, 0.5);

    material.color = randomColor;
}

function changeSize(mesh: THREE.Mesh) {
    const width = Math.random() + 0.5;
    const height = Math.random() + 0.5;
    const depth = Math.random() + 0.5;

    const newGeometry = new THREE.BoxGeometry(width, height, depth);
    mesh.geometry.dispose();
    mesh.geometry = newGeometry;
}

 

Scale을 조정하는 방법으로 변경

직접 크기가 다른 기하로 교체하는 것보다 한 기하의 스케일 값을 조절하는 방법이다.

렌더링 되는 모습은 위와 같다.

changeButton?.addEventListener('click', () => {
    changeColor(material);
    changeSize(mesh);
});

function changeColor(material: THREE.MeshBasicMaterial) {
    const randomColor = new THREE.Color();
    randomColor.setHSL(Math.random(), 1, 0.5);

    material.color = randomColor;
}

function changeSize(mesh: THREE.Mesh) {
    const scaleX = Math.random() + 0.5;
    const scaleY = Math.random() + 0.5;
    const scaleZ = Math.random() + 0.5;

    mesh.scale.x = scaleX;
    mesh.scale.y = scaleY;
    mesh.scale.z = scaleZ;
}

 

부드러운 Scale 애니메이션 추가

스케일 애니메이션을 더해준 경우

 

아래 두 유틸 함수가 필요하다. (GPT의 힘을 빌렸다)

/* Utils */
function lerp(start: number, end: number, t: number): number {
    return start * (1 - t) + end * t;
}

function easeInOutCubic(t: number): number {
    return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

 

위 함수들을 이용해 애니메이션 함수에서 호출하면 부드러운 애니메이션 효과가 나타난다.

function animate() {
    const currentTime = performance.now();
    delta = currentTime - current;
    current = currentTime;
    elapsed = current - start;

    if (isAnimating) {
        const animationElapsed = currentTime - animationStart;
        const progress = Math.min(animationElapsed / animationDuration, 1);
        const easedProgress = easeInOutCubic(progress);

        material.color.lerp(targetColor, easedProgress);

        mesh.scale.x = lerp(mesh.scale.x, targetScaleX, easedProgress);
        mesh.scale.y = lerp(mesh.scale.y, targetScaleY, easedProgress);
        mesh.scale.z = lerp(mesh.scale.z, targetScaleZ, easedProgress);

        if (easedProgress >= 1) {
            mesh.scale.set(targetScaleX, targetScaleY, targetScaleZ);
            material.color.copy(targetColor);
            isAnimating = false;
        }
    }

    controls.update();
    renderer.render(scene, camera);

    window.requestAnimationFrame(() => {
        animate();
    });
}