Advertisement
  1. Code
  2. JavaScript
  3. Web APIs

Three.js를 이용한 WebGL: 기본

Scroll to top
Read Time: 7 min

() translation by (you can also view the original English article)

브라우저에서 구현되는 3D 그래픽은 맨 처음 소개될 때부터 화젯거리였습니다. 그러나 밋밋한 WebGL로 앱을 제작하려 한다면 아주 오랜 시간이 걸릴 겁니다. 최근에 유용한 라이브러리들이 생기고 있는 이유가 바로 이 때문이죠. 가장 대중화된 라이브러리 중의 하나가 Three.js입니다. 저는 이번 시리즈에서 사용자에게 보여줄 멋진 3D 경험을 제작하는 데 이 라이브러리를 사용할 최적의 방법을 여러분에게 보여드리겠습니다.

시작하기에 앞서 저는 여러분이 이 튜토리얼을 읽기 전에 3D 공간에 관한 기초를 알고 있다고 생각하겠습니다. 그러므로 좌표와 벡터와 같은 용어를 설명하지 않을 겁니다.


1단계: 준비

우선 파일 3개를 만드세요. index.htmlmain.js, style.css입니다. 이제 Three.js를 다운로드 하세요. (예제와 소스가 있는 압축 파일을 통째로 받거나 자바스크립트 파일 하나만 받는 것은 여러분의 선택입니다.) 자, index.html을 열고 아래 코드를 넣어줍니다.

1
2
<!DOCTYPE html>
3
<html>
4
<head>
5
  <link rel="stylesheet" href="./style.css">
6
	<script src="./three.js"></script>
7
</head>
8
<body>
9
	<script src="./main.js"></script>
10
</body>
11
</html>

위 코드가 이 파일에서 여러분이 해야 할 전부입니다. 스크립트와 스타일시트에 관한 정의뿐이죠. 모든 마술은 main.js에서 일어납니다. 다만 그 작업을 하기 전에 앱을 보기 좋게 하기 위해 마술이 한 번 더 필요합니다. style.css를 열고 아래 코드를 넣어주세요.

1
2
canvas {
3
	position: fixed;
4
	top: 0;
5
	left: 0;
6
}

이 코드로 캔버스는 좌측 상단 모서리에 있게 됩니다. 원래 body에 margin이 8px 적용되기 때문이죠. 이제 자바스크립트 코드로 이어서 진행하면 됩니다.


2단계: 장면과 렌더러

Three.js는 디스플레이 목록(display list)이란 개념을 사용합니다. 오브젝트 전체가 목록에 저장되고 나서 화면에 그려진다는 얘기죠.

Three.js는 디스플레이 목록이란 개념을 사용합니다. 오브젝트 전체가 목록에 저장되고 나서 화면에 그려진다는 얘기죠. 여기 THREE.Scene 오브젝트가 있습니다. 화면마다 그려지길 바라는 모든 오브젝트를 추가해야 해요. 원하는 만큼 많은 장면을 넣을 수 있으나 하나의 렌더러는 한 번에 한 장면만 그릴 수 있습니다. (당연히 여러분은 화면에 보일 장면을 바꿀 수 있어요.)

렌더러는 장면에 있는 모든 것을 WebGL 캔버스에 그릴뿐입니다. Three.js 역시 SVG나 2D Canvas에서 그림을 그리는 기능을 지원하지만 WebGL에 집중하겠습니다. 

시작하겠습니다. 윈도우 창의 너비와 높이를 변수에 저장합시다. 해당 변수는 나중에 쓰일 거예요.

1
2
var width = window.innerWidth;
3
var height = window.innerHeight;

이제 렌더러와 장면을 정의합니다.

1
2
var renderer = new THREE.WebGLRenderer({ antialias: true });
3
renderer.setSize(width, height);
4
document.body.appendChild(renderer.domElement);
5
6
var scene = new THREE.Scene;

첫째 줄에서 WebGL 렌더러를 정의했습니다. 여러분은 첫 번째 인수에서 렌더러의 옵션을 맵(map)처럼 통과할 수 있습니다. 여기에서는 antialias를 true로 설정했습니다. 오브젝트의 가장자리가 들쭉날쭉하지 않고 매끄럽기를 바라기 때문이죠.

둘째 줄에서 렌더러 크기를 윈도우 창 크기에 맞추었습니다. 셋째 줄에서는 렌더러의 canvas 요소를 document에 추가했고요. (여러분은 $('body').append(renderer.domElement) 식으로 jQuery와 같은 라이브러리를 써서 작업해도 됩니다.)

마지막 줄에서 장면을 정의했습니다. 여기서는 인자가 필요 없습니다.


3단계: 입방체

이제 그려질 무언가를 추가해 보죠. 입방체로 합시다. 제일 단순한 3D 오브젝트이니까요. Three.js에서는 장면에 그려질 오브젝트를 메시(meshes)라고 칭합니다. 각 메시에는 입체 형상(geometry)와 머리티얼(material)이 있어야 합니다. 입체 형상은 점의 집합으로 각각의 점들은 오브젝트를 만들기 위해 연결되어야 합니다. 머티리얼은 간단히 오브젝트에 바르는 페인트입니다(페인트 대신 그림이라고 하지만, 이 튜토리얼의 주제는 아니죠). 자, 입방체를 만들어 봅시다. 운 좋게도 Three.js에는 도형 원형(단순한 모양)을 만들어 주는 보조 기능이 있습니다.

1
2
var cubeGeometry = new THREE.CubeGeometry(100, 100, 100);
3
var cubeMaterial = new THREE.MeshLambertMaterial({ color: 0x1ec876 });
4
var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
5
6
cube.rotation.y = Math.PI * 45 / 180;
7
8
scene.add(cube);

보시다시피 맨 먼저 입체 형상(geometry)을 생성합니다. 인자에서는 입방체의 크기 즉, 너비와 높이와 깊이를 정의해 줍니다.

그다음, 입방체의 머티리얼을 정의합니다. Three.js에 몇 가지 머티리얼 종류가 있지만, 이번에는 THREE.MeshLambertMaterial을 사용할 겁니다. 나중에 조명을 어느 정도 넣고 싶기 때문이죠(이 머티리얼은 조명 계산에 관한 람베르트의 알고리즘을 이용합니다). 여러분은 렌더러에서 했듯이 map처럼 첫 번째 인자의 옵션을 그냥 넘길 수 있습니다. Three.js에서 좀 더 복잡한 오브젝트에 대한 일종의 관례인 것이죠. 여기서는 색상만 사용하고, 해당 값을 16진수로 전달합니다.

셋째 줄에서는 앞서 생성한 입체 형상과 머티리얼을 이용해 메시를 만듭니다. 그다음, 더 좋아 보이도록 입방체를 Y축으로 45도 회전시킵니다. 각도를 라디안(radians)으로 바꿔야 합니다. 여러분이 기억할지 모를, 고등학교 물리 시간에 배운 공식인 Math.PI * 45 / 180에서 변경하게 되죠. 마침내 입방체가 장면에 넣어졌네요.

이제 여러분은 결과를 보려고 브라우저에서 index.html을 열 수 있습니다. 그러나 장면이 아직 렌더링 되지 않았기에 아무것도 눈으로 확인하지 못할 겁니다.


4단계: 카메라!

무언가를 화면에 렌더링하기 위해서는 우선 카메라를 장면에 넣어야 합니다. 그렇게 해서 렌더러는 어떤 관점에서 오브젝트를 렌더링할지 알게 됩니다. Three.js에 카메라 유형이 몇 가지 있지만, 여러분은 아마 THREE.PerspectiveCamera만 사용할 겁니다. 이 카메라는 우리가 세상을 보는 것처럼 장면을 보여줍니다. 카메라를 하나 만들어 보죠.

1
2
var camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 10000);

"무언가를 화면에 렌더링하기 위해서는 우선 카메라를 장면에 넣어야 합니다. 그렇게 해서 렌더러는 어떤 관점에서 오브젝트를 렌더링할지 알게 됩니다."

카메라를 생성하는 것은 지금껏 해온 작업 중 나머지보다 조금 더 복잡합니다. 첫 번째 인자에서는 카메라의 위치에서 보여줄 각도(angle)인 FOV(시야)를 정의합니다. FOV가 45도에서 자연스럽게 보입니다. 다음에는 카메라의 화면 종횡비를 정의합니다. 여러분이 특수 효과를 원하지 않는 한, 이는 언제나 렌더러의 너비를 높이로 나눈 값입니다. 마지막 숫자 2개는 오브젝트가 카메라에 얼마나 가까워지고 멀어져서 그려질 수 있는지를 정의합니다.

Three.js에서 만들어진 오브젝트 모두를 기본적으로 장면의 중앙(x: 0, y: 0, z: 0)에 위치시켜야 하므로 지금은 카메라를 조금 더 뒤로, 조금 더 위로 이동시킵니다.

1
2
camera.position.y = 160;
3
camera.position.z = 400;

뷰어의 방향에서 z축이 양의 값이므로 이보다 높은 z 지점에 있는 오브젝트는 좀 더 가까이 보일 겁니다. (이 경우에 카메라를 이동시켰기 때문에 모든 오브젝트들은 더 멀찍이 떨어져 보이게 되고요.)

자, 카메라를 장면에 추가하고 렌더링해 봅시다.

1
2
scene.add(camera);
3
4
renderer.render(scene, camera);

입방체를 추가했듯 카메라를 추가합니다. 다음 줄에서 이 카메라로 장면을 렌더링합니다. 이제 여러분이 브라우저를 띄우면, 화면이 다음과 같이 보여야 합니다.

first_renderingfirst_renderingfirst_rendering

입방체의 상단만 볼 수 있을 겁니다. 왜냐하면, 카메라를 위로 옮기긴 했지만, 카메라는 여전히 똑바로 앞을 바라보고 있기 때문이죠. 이는 카메라에게 어느 지점을 봐야 하는지 알려주어 해결할 수 있습니다. 카메라 위치를 설정하는 줄 아래에 아래 코드를 추가해 주세요.

1
2
camera.lookAt(cube.position);

전달될 단 하나의 인자는 카메라가 볼 지점입니다. 이제 장면이 보기에 더 좋습니다. 근데 입방체를 생성했을 때 어떤 색상을 입혔든 상관없이 입방체가 여전히 검군요. 

fixed_camera_lookatfixed_camera_lookatfixed_camera_lookat

5단계: 조명!

입방체는 검습니다. 장면에 빛을 주는 조명이 없기 때문이죠. 그렇기에 완전히 어두운 방과 같습니다. 배경이 하얀 이유는 입방체로부터 그려질 게 없기 때문입니다. 그렇게 되지 않도록 skybox라 칭하는 테크닉을 쓰겠습니다. 무엇보다도 장면의 배경을 보여줄 커다란 입방체 하나를 넣을 겁니다. (일반적으로 열린 공간이라면 어느 정도 먼 영역이 되겠죠.) skybox를 만들어 봅시다. 아래 코드는 renderer.render을 호출하기 전에 있어야 합니다.

1
2
var skyboxGeometry = new THREE.CubeGeometry(10000, 10000, 10000);
3
var skyboxMaterial = new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.BackSide });
4
var skybox = new THREE.Mesh(skyboxGeometry, skyboxMaterial);
5
6
scene.add(skybox);

위의 코드는 입방체를 생성하는 코드와 유사합니다. 그런데 이번에는 입체 형상(geometry)이 더 큽니다. skybox에 빛을 비추지 않으므로 THREE.MeshBasicMaterial도 사용했습니다. 더불어 머티리얼에 전달한 추가 인자를 알려줍니다. side: THREE.BackSide 입방체가 안쪽를 보여줄 것이므로 그릴 방향을 바꿔주어야 합니다. (Three.js는 보통 바깥쪽 벽만 그립니다.)

지금은 렌더링된 장면이 완전히 검습니다. 그 문제를 해결하려면 장면에 조명을 넣어야 합니다. THREE.PointLight을 사용하겠습니다. 이는 전구와 같은 빛을 발산합니다. 아래 코드를 skybox 다음에 추가해 주세요.

1
2
var pointLight = new THREE.PointLight(0xffffff);
3
pointLight.position.set(0, 300, 200);
4
5
scene.add(pointLight);

아시다시피 포인트 광원을 흰색으로 만들고 나서 입방체의 윗면과 앞면에 빛이 비치도록 광원의 위치를 좀 더 위로, 좀 더 뒤로 잡았습니다. 마침내 다른 오브젝트처럼 조명이 장면에 들어갔습니다. 브라우저를 띄우면, 색이 있고 그늘진 입방체가 보일 겁니다.

colored_shaded_cubecolored_shaded_cubecolored_shaded_cube

아직도 입방체가 엄청 따분하게 보이네요. 입방체를 움직이게 해봅시다.


6단계: 액션!

이제 장면에 움직임을 어느 정도 넣겠습니다. 입방체를 Y축을 중심으로 회전시켜 보죠. 그런데 우선 장면을 렌더링할 방법을 변경해야 합니다. renderer.render 호출 한 번에 장면의 현재 상태를 한번 렌더링합니다. 그렇기에 입방체에 어느 정도 애니메이션을 적용해도 입방체가 움직이는 것을 알 수 없습니다. 방법을 변경하기 위해서는 애플리케이션에 렌더 루프(render loop)를 추가해야 합니다. 이는 renderAnimationFrame 함수를 써서 할 수 있습니다. 그런 목적에 특별히 쓰이도록 만들어진 함수이죠. 주요 브라우저 대부분에서 지원되며, 지원되지 않은 브라우저에 대해 Three.js 자체에 폴리필(polyfill)이 들어 있습니다. 자, 바꿔보죠.

1
2
renderer.render(scene, camera);

이렇게요.

1
2
function render() {
3
	renderer.render(scene, camera);
4
	
5
	requestAnimationFrame(render);
6
}
7
8
render();

사실 함수 안에는 루프가 없습니다. 브라우저를 정지시킬지 모르기 때문이죠. requestAnimationFrame 함수는 setTimeout 같이 동작합니다. 하지만 브라우저에 로딩되자마자 그 함수가 전달되도록 호출할 겁니다. 그러니 화면에 보인 장면에서 아무것도 변하지 않고, 입방체는 그대로 움직이지 않은 채로 있습니다. 이 문제를 해결해 봅시다. Three.js에는 오브젝트를 부드럽게 애니메이션 시킬 수 있는 THREE.Clock이 들어 있습니다. 우선 render 함수를 정의하기 전에 초기화를 해주세요.

1
2
var clock = new THREE.Clock;

자, clock.getDelta를 호출할 때마다 마지막 호출 이후의 시간을 밀리세컨드(milliseconds) 단위로 돌려줄 겁니다. 그 값은 입방체를 아래와 같이 회전하는 데 사용됩니다.

1
2
cube.rotation.y -= clock.getDelta();

아래 코드를 render 함수 안에 있는 renderer.renderrequestAnimationFrame 호출 사이에 넣어 주세요. 입방체를 시계 방향으로 회전하도록 Y축(라디안 단위라는 것을 기억하세요)에서 입방체의 회전에서 흐르는 시간을 뺀 것일 뿐입니다. 이제 브라우저를 띄우면 입방체가 시계 방향으로 부드럽게 회전하는 것이 보일 겁니다.


결론

튜토리얼 시리즈 중 여기에서 여러분은 장면을 준비하고, 오브젝트와 조명을 추가하는 방법과 그들을 애니메이션 시키는 방법을 학습했습니다. 앱으로 실험해 보고, 더 많고 다양한 오브젝트와 조명을 장면에 더해 보세요. 여러분이 하기 나름입니다. 다음 튜토리얼에서는 질감을 활용하는 방법과 파티클로 멋진 효과를 제작하는 방법을 보여드리겠습니다. 문제가 발생하면 잊지 말고 문서를 꼼꼼히 살펴보시고요.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.