WEB/THREE

Three.js 시작하기

비너발트 2025. 3. 12. 10:58

웹 브라우저에서 3D 환경을 구현할 수 있는 기술에 대해 알아보고자 작성하게되었습니다

웹 환경에서 3D 환경을 구현을 하려면 WebGL 이라는 기술을 사용해야합니다 WebGL은 JavaScript API 이면서 HTML5 의 캔버스 태그를 사용하여 출력할 수 있습니다

 

WebGL의 특징

  • 브라우저 기반 동작! (크로스 플랫폼 지원), 플러그인이 별도로 필요없음!
  • GPU 가속지원 CPU 대신 GPU를 활용하여 빠르게 처리할 수 있습니다
  • OpenGL 2.0 기반이라고 합니다 OpenGL은 그래픽을 사용하기 위한 API 이라고 하네요

WebGL의 활용 분야

  • 게임 개발
  • 데이터 시각화
  • VR/AR 서비스
  • 시뮬레이션 및 AI 시각화

WebGL은 저수준 API입니다

저수준 API란 프로세서와 직접적으로 상호작용하는 기능을 제공하는데 사용방법이 상당히 복잡한편에 속합니다 그리고 세부적인 코드 최적화를 직접해야하며 사용할때 정점 데이터, 버퍼, 텍스처, 셰이더를 직접 관리해야합니다

 

WebGL에서 삼각형 렌더링 과정

  1. WebGL Context 생성
  2. 정점(Vertex) 데이터 생성 및 버퍼(Buffer) 전송
  3. Vertex Shader 작성 및 컴파일
  4. Fragment Shader 작성 및 컴파일
  5. Shader 프로그램을 연결 (Linking)
  6. 정점 속성(Attribute)과 유니폼(Uniform) 설정
  7. gl.drawArrays() 호출로 최종 렌더링

의 과정을 거친다고합니다 그러면 이제 Three.js 에 대해 알아보겠습니다

 

Three.js 시작해보기

three.js는 WebGL과 같은 Javscript로 3D 그래픽을 쉽게 구현할 수 있도록 도와주는 오픈소스 라이브러리입니다

쉽게 말하자면 사용하기 복잡한 WebGL을 간단한 코드로 3D 객체, 조명 등을 만들 수 있습니다

 

Three.js 의 특징

  • WebGL을 쉽게 다룰 수 있도록 추상화 하였습니다
  • GPU 가속을 통한 고성능 렌더링 지원
  • 애니메이션 및 물리엔진, VR/AR을 지원합니다

Three.js의 기본 개념

  • Scene: 3D 객체들이 배치될 공간
  • Camera: 장면을 바라보는 시점
  • Renderer: 3D 장면을 HTML 캔버스에 그리는 역할
  • Mesh: 3D 객체
  • Light: 3D 장면에 빛 효과

 

그럼 이제 설치와 간단한 사각형 객체를 회전시키는 코드를 작성해보겠습니다

 

//three.js 설치하기
npm install express three

//app.js 서버코드 작성하기
const express = require('express');
const path = require('path');

const app = express();
const PORT = 3000;

// 정적 파일사용을 위한 경로 설정
app.use(express.static(path.join(__dirname, 'public')));

app.listen(PORT, () => {
    console.log(`Server running at http://localhost:${PORT}`);
});

//index.html three.js가 렌더링될 페이지
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>박스객체 예제</title>
    <script type="module" src="script.js"></script>
    <style>
        body { margin: 0; overflow: hidden; }
        canvas { display: block; }
    </style>
</head>
<body>
</body>
</html>

//script.js three.js 코드
import * as THREE from 'three';

// 1. 씬(Scene) 생성하기
const scene = new THREE.Scene();

// 2. 카메라 생성 (FOV, 종횡비, near, far 순서로)
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;

// 3. 렌더러 생성 (렌더링할 Front 요소를 지정)
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 4. 큐브 생성 (큐브 객체를 만들기)
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({ color: 0x0077ff });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// 5. 조명 추가 (라이팅 설정)
const light = new THREE.PointLight(0xffffff, 1, 100);
light.position.set(10, 10, 10);
scene.add(light);

// 6. 애니메이션
function animate() {
    requestAnimationFrame(animate);
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
    renderer.render(scene, camera);
}
animate();

// 7. 창 크기 변경 대응되도록!
window.addEventListener('resize', () => {
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
});

 

이제 이 코드를 한번 분해하면서 이해를 해보도록 하겠습니다

const scene = new THREE.Scene();

장면(scene) 생성하기

앞에서 scene는 장면이라고 이야기 했었습니다 이 장면은 여러개가 있을 수 있습니다 한 장면에는 여러개의 객체들이 존재하고 어떤 장면에 어떤 객체를 넣느냐에 따라 장면을 렌더링 할때마다 다른 객체가 나오게 됩니다

 

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
camera.position.set(0, 0, 5);
camera.lookAt(0, 0, 0);

 

카메라 생성하기

scene는 하나의 3차원 공간입니다 이전에 하나의 장면을 생성하여 객체를 담을 공간을 만들었습니다

공간을 만들었지만 브라우저에서는 아무것도 나타나지 않습니다 왜냐하면 이 공간에서 무언가를 볼 수 있는

눈을 만들어 주지않았습니다 이 눈 역할을 하는것이 바로 카메라객체입니다

 

PerspectiveCamera(FOV, aspect, near, far)

PerspectiveCamera는 원근 투영 카메라입니다 자연스러운 원근감 있는 카메라가 생성됩니다

FOV는  시야각을 뜻하며 평균적으로 75를 사용합니다 높일수록 더 넓게 보입니다

aspect는 화면의 비율값입니다

near는 거리를 뜻하는데 어느정도 거리 이상인 객체만 보여줄것인지를 뜻합니다 이 값보다 가까이 있으면 보이지않게됩니다

far는 보이는 거리를 입력하는데 여기서 입력한 값 이상의 거리에 객체가 있을 경우 보이지않게됩니다

 

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

시각화 렌더러생성하기

Renderer는 장면에 있는 객체를 그려주는(시각화) 역할을 합니다

antialias는 안티앨리어싱 기능이며 객체들의 테두리처리를 어떻게 할 것인지에 관한 설정입니다 true/fasle 값을 갖으며 true일 경우 테두리를 부드럽게 표현합니다

 

setSize(width, height)는 렌더링 크기를 조정하는 역할을 합니다

document.body.appendChild(renderer.domElement)는

렌더링한 canvas를 doc객체 body에 추가하여 화면에 렌더링합니다

 

const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial({ color: 0x0077ff });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

 

장면에 형상객체 생성하기

BoxGeometry는 정육면체(큐브)를 생성합니다

MeshStandardMaterial는 재질을 설정합니다 ({})를 통해 상세한 설정이 가능합니다 본 코드 에서는

color를 파란색으로 변경하였습니다

Mesh(geometry, material)는 생성한 형상객체와 재질을 합쳐 하나의 3D 객체로 만드는 메서드입니다

scene.add()는 해당 장면에 합친 3D 객체(큐브)를 추가하는 메서드입니다

 

지금까지 장면을 만들고 무언가를 볼 수 있는 카메라를 만들고 장면에 정의된 객체들을 시각화시켜주는 렌더러를 만들고

객체를 만들고 재질과 하쳐 장면에 추가하는 코드까지 작성하였습니다

 

하지만 아직까지 화면에 보이지않는게 정상입니다 세상은 빛이 없으면 아무것도 볼 수 없기 때문이죠

그럼 이제 빛이 보이도록 추가해보겠습니다

 

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

const pointLight = new THREE.PointLight(0xffffff, 1, 100);
pointLight.position.set(10, 10, 10);
scene.add(pointLight);

 

장면에 조명(light) 추가하기

AmbientLight(0xffffff, 0.5)는 장면 전체의 조명의 색상, 밝기를 매개변수로 받습니다 쉽게 태양과 같은 역할입니다

PointLight(0xffffff, 1, 100)는 점광원이라고 하며 색사으 밝기, 범위 3가지를 매개변수로 받습니다 쉽게 스포트라이트 라고 보면 쉽습니다

pointLight.position.set(10, 10, 10)는 빛의 위치를 결정하는 코드입니다 X, Y, Z 매개변수로 받습니다

 

renderer.render(scene, camera);

화면에 장면을 그리기

그리고 최종적으로 렌더러를 렌더링 하도록 위 코드를 작성해주면 화면에 큐브가 정상적으로 보이는걸 알 수 있습니다

이제 이 코드를 작성하고 나면 화면에 큐브가 생성된걸 볼 수 있습니다 하지만 이 상태에선 이 큐브가 2차원 객체인지 3차원 객체인지 알아보기가 어렵습니다 애니메이션을 통해 회전시켜 3D 객체라는걸 확인해보도록합시다

 

function animate() {
    requestAnimationFrame(animate);
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;
    renderer.render(scene, camera);
}
animate();

형상 객체에 애니메이션 적용하기

requestAnimationFrame(animate) 저는 이 코드를 처음보고 저는 사실 이해가 잘안됐습니다 함수를 내부에 선언된 animate가 없는데 어디서 가져와서 사용한다는 건지??🤔 함수 내부에서 함수를 다시 호출한다? 이러면 무한루프가 아닌가.. 하다 0.01; 만큼 회전 시키는 코드이니 계속 반복을 시키려면 무한 루프를 만들어야하고 여기선 함수를 다시 호출하는 방식으로 무한루프를 발생시키는구나! 라고 이해가 됐습니다 근데 좀 편법같아서 찾아보니

 

웹 브라우저 기반에서 애니메이션을 사용할땐 requestAnimationFrame를 사용해 반복시키는게 정석적이고 성능에 최적화된 방법이라고 합니다

 

과거엔 setInterval() 을 통해서 했었지만 CPU의 부하가 높아져 프레임이 낮아지면 버벅거리는 문제가 발생했습니다 하지만 requestAnimationFrame()를 사용하면 브라우저 자체에서 최적의 FPS를 자동으로 조정하며 CPU 부하를 컨트롤합니다 다만 프레임 속도를 따로 지정할 수 없습니다 기본값은 60프레임으로 동작합니다

 

requestAnimationFrame는 브라우저가 비활성화되면 자동으로 동작을 멈추는 특징이 있습니다 즉 동작이 불필요한 상황에서는 동작하지않습니다

 

객체.rotation.x += 값; 구문 그대로 x축의 회전값을 지정하는 코드입니다 해당 애니메이션이 호출될때마 해당 축으로 0.01만큼 회전하겠다는 구문입니다

 

그 외에도 다양한 속성을 지원합니다 position, rotation, scale, material.color, material.opacity, visible, castShadow, receiveShadow 등 다양한 속성이 지원되고있으며 공식문서에서 추가적인 속성을 확인할 수 있습니다!

 

window.addEventListener('resize', () => {
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
});

 

브라우저 크기 변경 대응하기

브라우저 크기를 줄이거나 키울때  장면이 가운데있지않고 가려지게 됩니다 브라우저 크기를 줄이거나 키워도 가운데로 보이게끔 하려면 위 코드를 잘성해줘야합니다

renderer.setSize(window.innerWidth, window.innerHeight) 변경된 창 크기에 마춰 조정

camera.aspect = window.innerWidth / window.innerHeight 카메라 비율도 맞춰 조정

camera.updateProjectionMatrix(); 변경된 정보를 카메라에 반영하도록합니다