Three.js 설치 및 실행

일반적인 웹페이지는 2D 기반으로 구현합니다.

이러한 웹페이지는 서비스를 제공하거나 정보 공유를 목적으로 충분합니다.

만약 이렇게 보편적으로 사용하는 2D 웹페이지에 3D 환경을 더한다면, 서비스나 제품의 브렌딩에 차별점을 줄 수 있을 것 같습니다.


Three.js 를 스터디하며, 이 블로그의 Profile 페이지를 구현하는 것을 첫번째 목표로 하여 포스팅을 해보려 합니다.


Vite 프로젝트 생성하기

Three.js 를 사용하기 위해, VanillaJS 환경의 프로젝트를 생성해 보겠습니다.

Webpack 을 사용하여 프로젝트를 만들어도 되지만, 프로젝트 구성에 투자되는 리소스가 많아지므로, Vite 를 사용하여 프로젝트를 생성해보겠습니다.

Vite 프로젝트 생성
yarn create vite --template vanilla my-threejs
 
cd my-threejs
 
yarn install

Three.js 설치하기

프로젝트를 생성한 후, Three.js 를 설치합니다.

Three.js 설치
yarn install three

Three.js 기본 구조 구성하기

Three.js 는 WebGL 을 기반으로 동작합니다.

이는 HTML Canvas API 를 사용하여 그리는 방식입니다.

Three.js 로 화면을 그리기 위한 기본 과정을 살펴보면 다음과 같습니다.

  • <canvas /> 태그 생성
  • WebGLRenderer 객체 생성
  • 카메라 생성
  • 조명 생성
  • Scene 객체 생성
  • 위에서 생성한 객체들을 조합하여 WebGLRenderer 에 적용
  • requestAnimationFrame() 을 사용하여 <canvas /> 렌더링

위 과정은 아래와 같이 함수를 생성하여 구현해 보겠습니다.

  • init() 함수 만들기: 1번 ~ 6번 과정을 처리합니다.
  • render() 함수 만들기: 7번 과정을 처리합니다.

1. <canvas /> 태그 생성

먼저 Three.js 를 렌더링할 <canvas /> 태그를 생성하는 로직을 구현해 보겠습니다.

main.js
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
}
 
init();

2. WebGLRenderer 객체 생성

WebGLRenderer 객체는 Three.js 의 코어 역할을 합니다.

위에서 생성한 <canvas /> 를 인자로 넘겨주어 렌더링 대상을 지정해 줍니다.

main.js
import {
    WebGLRenderer,
} from 'three';
 
/** @type { WebGLRenderer } */
let renderer;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. WebGLRenderer 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
    });
}
 
init();

3. 카메라 생성

Three.js 가 실제로 화면에 렌더링하는 것은 Camera 가 비추는 곳이 됩니다.

따라서 Three.js 에 사용할 카메라 객체를 생성해 줍니다.

main.js
import {
    WebGLRenderer,
    PerspectiveCamera,
} from 'three';
 
/** @type { WebGLRenderer } */
let renderer;
 
/** @type { PerspectiveCamera } */
let camera;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. `WebGLRenderer` 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
    });
 
    // 3. 카메라 생성
    camera = new PerspectiveCamera();
}
 
init();

4. 조명 생성

이번에는 조명 객체인 DirectionalLight 객체를 생성해 보겠습니다.

현실 세계에서도 빛이 있어야 물체를 볼 수 있듯이, Three.js 에서도 조명이 있어야 물체가 렌더링 됩니다.

조명은 여러가지가 있는데, 이 중 햇빛처럼 직선의 일정한 양의 빛을 나타내는 DirectionalLight 객체를 사용해 보겠습니다.

main.js
import {
    WebGLRenderer,
    PerspectiveCamera,
    DirectionalLight,
} from 'three';
 
/** @type { WebGLRenderer } */
let renderer;
 
/** @type { PerspectiveCamera } */
let camera;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. `WebGLRenderer` 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
    });
 
    // 3. 카메라 생성
    camera = new PerspectiveCamera();
 
    // 4. 조명 생성
    const light = new DirectionalLight();
}
 
init();

5. Scene 객체 생성

무대는 Scene 객체로 만들 수 있습니다.

Scene 객체는 add() 메서드를 사용하여 위에서 만들었던 조명과 물체(Mesh)들을 적용할 수 있습니다.


Scene 객체를 생성하고 조명을 적용해 보겠습니다.

main.js
import {
    WebGLRenderer,
    PerspectiveCamera,
    DirectionalLight,
    Scene,
} from 'three';
 
/** @type { WebGLRenderer } */
let renderer;
 
/** @type { PerspectiveCamera } */
let camera;
 
/** @type { Scene } */
let scene;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. `WebGLRenderer` 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
    });
 
    // 3. 카메라 생성
    camera = new PerspectiveCamera();
 
    // 4. 조명 생성
    const light = new DirectionalLight();
 
    // 5. `Scene` 객체 생성
    scene = new Scene();
    scene.add(light);
}
 
init();

6. 위에서 생성한 객체들을 조합하여 WebGLRenderer 에 적용

이제 Three.js 를 실행하여 화면을 그릴 준비가 되었습니다.

WebGLRendererScene(무대)카메라 를 사용하여 화면을 그리는 역할을 하게 됩니다.

main.js
import {
    WebGLRenderer,
    PerspectiveCamera,
    DirectionalLight,
    Scene,
} from 'three';
 
/** @type { WebGLRenderer } */
let renderer;
 
/** @type { PerspectiveCamera } */
let camera;
 
/** @type { Scene } */
let scene;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. `WebGLRenderer` 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
    });
 
    // 3. 카메라 생성
    camera = new PerspectiveCamera();
 
    // 4. 조명 생성
    const light = new DirectionalLight();
 
    // 5. `Scene` 객체 생성
    scene = new Scene();
    scene.add(light);
 
    // 6. 위에서 생성한 객체들을 조합하여 WebGLRenderer 에 적용
    renderer.render(scene, camera);
}
 
init();

7. requestAnimationFrame() 을 사용하여 <canvas /> 렌더링

이번에는 init() 의 마지막에 호출할 render() 함수를 만들고, Three.js 를 실행하여 브라우저에서 결과를 확인해 보겠습니다.

추가할 render() 함수는 requestAnimationFrame() 을 사용하여 render() 함수를 재귀 호출하도록 하는데, 이는 브라우저에서 60fps 로 실행하며 화면을 업데이트 하게 됩니다.

main.js
import {
    WebGLRenderer,
    PerspectiveCamera,
    DirectionalLight,
    Scene,
} from 'three';
 
/** @type { WebGLRenderer } */
let renderer;
 
/** @type { PerspectiveCamera } */
let camera;
 
/** @type { Scene } */
let scene;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. `WebGLRenderer` 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
    });
 
    // 3. 카메라 생성
    camera = new PerspectiveCamera();
 
    // 4. 조명 생성
    const light = new DirectionalLight();
 
    // 5. `Scene` 객체 생성
    scene = new Scene();
    scene.add(light);
 
    // 6. 위에서 생성한 객체들을 조합하여 WebGLRenderer 에 적용
    renderer.render(scene, camera);
 
    // 7. requestAnimationFrame() 을 사용하여 <canvas /> 렌더링
    render();
}
 
function render() {
    renderer.render(scene, camera);
 
    requestAnimationFrame(render);
}
 
init();

추가: Scene 에 SphereMesh 추가하기

지금까지 작성한 코드를 실행하면, 검은색 화면만 보이게 됩니다.

이는 실제로 화면에 그릴 물체(Mesh) 가 없기 때문에 그릴 대상이 없는 현상입니다.


위에서 구성한 Three.js 가 잘 동작하는지 테스트를 위해 구형 물체(Sphere Mesh) 를 생성하고, 적용해 보겠습니다.

main.js
import {
    WebGLRenderer,
    PerspectiveCamera,
    DirectionalLight,
    Scene,
 
    SphereGeometry,
    MeshPhongMaterial,
    Mesh,
} from 'three';
 
/** @type { WebGLRenderer } */
let renderer;
 
/** @type { PerspectiveCamera } */
let camera;
 
/** @type { Scene } */
let scene;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. `WebGLRenderer` 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
    });
 
    // 3. 카메라 생성
    camera = new PerspectiveCamera();
    camera.position.set(0, 0, 10);
 
    // 4. 조명 생성
    const light = new DirectionalLight();
    light.position.set(1, 1, 1);
 
    // 5. `Scene` 객체 생성
    scene = new Scene();
    scene.add(light);
 
    // 6. 위에서 생성한 객체들을 조합하여 WebGLRenderer 에 적용
    renderer.render(scene, camera);
 
    // 추가: Schene 에 SphereMesh 추가하기
    createSphereMesh();
 
    // 7. requestAnimationFrame() 을 사용하여 <canvas /> 렌더링
    render();
}
 
function render() {
    renderer.render(scene, camera);
 
    requestAnimationFrame(render);
}
 
function createSphereMesh() {
    const sphere_geometry = new SphereGeometry();
    const sphere_material = new MeshPhongMaterial();
    const sphere_mesh = new Mesh(sphere_geometry, sphere_material);
 
    scene.add(sphere_mesh);
}
 
init();

createSphereMesh() 함수에서 3가지 객체를 생성하고 있습니다.

이는 컴퓨터 그래픽스에서 3D 물체를 표현하기 위한 요소들 입니다.

  • Geometry: 색이 없는 모델링 객체
  • Material: 모델링 객체의 색상 또는 질감
  • Mesh: Geometry 와 Material 을 합친 결과 모델링 객체

sphere_mesh 객체의 add() 메소드를 사용하여 GeometryMaterial 을 합쳐서 하나의 Mesh 를 만들 수 있게 됩니다.


지금까지 작성한 코드를 실행하면 다음과 같은 결과물을 볼 수 있습니다.

Threejs 실행 결과

추가: <canvas /> 를 전체화면으로 설정하기

위 결과물을 확인하면, <canvas /> 요소가 inline 으로 렌더링되고 있습니다.

이는 WebGLRenderer 객체의 size 를 조정하여 전체화면으로 설정할 수 있습니다.

그리고 <canvas /> 의 기본 스타일인 display: inlinedisplay: block 으로 변경합니다.

style.css
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
 
canvas {
    display: block;
}
main.js
import {
    WebGLRenderer,
    PerspectiveCamera,
    DirectionalLight,
    Scene,
 
    SphereGeometry,
    MeshPhongMaterial,
    Mesh,
} from 'three';
import './style.css';
 
/** @type { WebGLRenderer } */
let renderer;
 
/** @type { PerspectiveCamera } */
let camera;
 
/** @type { Scene } */
let scene;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. `WebGLRenderer` 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
    });
    renderer.setSize(
        window.innerWidth,
        window.innerHeight
    );
 
    // 3. 카메라 생성
    camera = new PerspectiveCamera();
    camera.position.set(0, 0, 10);
 
    // 4. 조명 생성
    const light = new DirectionalLight();
    light.position.set(1, 1, 1);
 
    // 5. `Scene` 객체 생성
    scene = new Scene();
    scene.add(light);
 
    // 6. 위에서 생성한 객체들을 조합하여 WebGLRenderer 에 적용
    renderer.render(scene, camera);
 
    // 추가: Schene 에 SphereMesh 추가하기
    createSphereMesh();
 
    // 7. requestAnimationFrame() 을 사용하여 <canvas /> 렌더링
    render();
}
 
function render() {
    renderer.render(scene, camera);
 
    requestAnimationFrame(render);
}
 
function createSphereMesh() {
    const sphere_geometry = new SphereGeometry();
    const sphere_material = new MeshPhongMaterial();
    const sphere_mesh = new Mesh(sphere_geometry, sphere_material);
 
    scene.add(sphere_mesh);
}
 
init();

이렇게 적용한 결과는 다음과 같습니다.

전체화면 적용

추가: 찌그러진 화면 보정하기

Scene 에 추가한 모델링은 구(Sphere) 입니다.

하지만 결과 화면에서 보여지는 구는 타원형으로 보여집니다.

원인은 아래와 같습니다.

  • 카메라의 종횡비(aspect radio) 설정을 하지 않았으므로, 기본값인 1 로 설정됨
  • 브라우저의 종횡비가 1:1 이 아닌 상태에서 <canvas /> 를 전체화면으로 늘리면서 발생하는 화면 늘어짐

이를 해결하기 위해 카메라의 종횡비(aspect radio) 를 설정해줍니다.

main.js
import {
    WebGLRenderer,
    PerspectiveCamera,
    DirectionalLight,
    Scene,
 
    SphereGeometry,
    MeshPhongMaterial,
    Mesh,
} from 'three';
import './style.css';
 
/** @type { WebGLRenderer } */
let renderer;
 
/** @type { PerspectiveCamera } */
let camera;
 
/** @type { Scene } */
let scene;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. `WebGLRenderer` 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
    });
    renderer.setSize(
        window.innerWidth,
        window.innerHeight
    );
 
    // 3. 카메라 생성
    camera = new PerspectiveCamera(
        45,
        window.innerWidth / window.innerHeight
    );
    camera.position.set(0, 0, 10);
 
    // 4. 조명 생성
    const light = new DirectionalLight();
    light.position.set(1, 1, 1);
 
    // 5. `Scene` 객체 생성
    scene = new Scene();
    scene.add(light);
 
    // 6. 위에서 생성한 객체들을 조합하여 WebGLRenderer 에 적용
    renderer.render(scene, camera);
 
    // 추가: Schene 에 SphereMesh 추가하기
    createSphereMesh();
 
    // 7. requestAnimationFrame() 을 사용하여 <canvas /> 렌더링
    render();
}
 
function render() {
    renderer.render(scene, camera);
 
    requestAnimationFrame(render);
}
 
function createSphereMesh() {
    const sphere_geometry = new SphereGeometry();
    const sphere_material = new MeshPhongMaterial();
    const sphere_mesh = new Mesh(sphere_geometry, sphere_material);
 
    scene.add(sphere_mesh);
}
 
init();

PerspectiveCamera 생성자에 인자로 2가지를 넘겨주었습니다.

인자 타입은 다음과 같습니다.

Perspective 생성자의 인자
class PerspectiveCamera {
    constructor(
        fov: number,
        aspect: number
    ) {
        // ...
    }
}

fov (Field of View)카메라의 왜곡 정도값을 말합니다.

이는 렌즈 배율Sensor-size 에 대한 연산값이며, 화각을 수치화 한 것입니다.

값이 클수록 멀리서 보는 느낌의 효과가 발생합니다.


두번째 인자인 aspect카메라 종횡비(aspect ratio) 에 대한 설정입니다.

화면의 가로 / 세로 비율을 나타내므로, window.innerWidth / window.innerHeight 로 값을 도출할 수 있습니다.


이렇게 설정한 결과물은 아래와 같습니다.

전체화면 적용

추가: 계단 효과 보정하기

모니터는 Pixel 에 RGB 를 발사하여 화면을 그립니다.

이 Pixel 은 정사각형 모양이기 때문에 곡선을 렌더링하게 되면 마치 계단처럼 각진 곡선으로 표현됩니다.

WebGLRenderer 객체를 생성하며 options 를 통해 계단 효과를 보정할 수 있습니다.


컴퓨터 그래픽스에서 계단 효과를 해소하는 기법을 간단하게 설명하면, 곡선이 아닌 부분을 흐림(blur) 처리하여 마치 자연스러운 곡선처럼 표현해 줍니다.

이러한 기법을 Anti-Aliasing 이라고 합니다.

main.js
import {
    WebGLRenderer,
    PerspectiveCamera,
    DirectionalLight,
    Scene,
 
    SphereGeometry,
    MeshPhongMaterial,
    Mesh,
} from 'three';
import './style.css';
 
/** @type { WebGLRenderer } */
let renderer;
 
/** @type { PerspectiveCamera } */
let camera;
 
/** @type { Scene } */
let scene;
 
function init() {
    // 1. `<canvas />` 태그 생성
    const $canvas = document.createElement('canvas');
 
    const $app = document.querySelector('#app');
    $app.appendChild($canvas);
 
    // 2. `WebGLRenderer` 객체 생성
    renderer = new WebGLRenderer({
        canvas: $canvas,
        antialias: true,
    });
    renderer.setSize(
        window.innerWidth,
        window.innerHeight
    );
 
    // 3. 카메라 생성
    camera = new PerspectiveCamera(
        45,
        window.innerWidth / window.innerHeight
    );
    camera.position.set(0, 0, 10);
 
    // 4. 조명 생성
    const light = new DirectionalLight();
    light.position.set(1, 1, 1);
 
    // 5. `Scene` 객체 생성
    scene = new Scene();
    scene.add(light);
 
    // 6. 위에서 생성한 객체들을 조합하여 WebGLRenderer 에 적용
    renderer.render(scene, camera);
 
    // 추가: Schene 에 SphereMesh 추가하기
    createSphereMesh();
 
    // 7. requestAnimationFrame() 을 사용하여 <canvas /> 렌더링
    render();
}
 
function render() {
    renderer.render(scene, camera);
 
    requestAnimationFrame(render);
}
 
function createSphereMesh() {
    const sphere_geometry = new SphereGeometry();
    const sphere_material = new MeshPhongMaterial();
    const sphere_mesh = new Mesh(sphere_geometry, sphere_material);
 
    scene.add(sphere_mesh);
}
 
init();

마치며

Three.js 를 실행하기 위한 최소 구성요소를 구현해 보았습니다.

뭔가 복잡해 보이지만 현실 세계의 무대를 만드는 것과 유사한 개념으로 만들고 있습니다.

  • WebGLRenderer: 무대를 구성할 건물
  • Scene: 무대
  • PerspectiveCamera: 카메라
  • DirectionalLight: 조명
  • Mesh: 배경, 배우, 소품

Three.js 를 처음 시작하며 제가 느낀 어려움으로는 컴퓨터 그래픽스 개념과 카메라에 대한 이해였습니다.

단순히 Three.js 사용법을 익히는 것만으로 원하는 결과물을 얻기는 어려워 보입니다.

지금까지의 코딩에 비해 학습 난이도는 높지만, 새로운 성취감과 즐거움이 기대됩니다.