본문은 Three.js 기초 내용을 바탕으로 하여 요약 및 정리글이다.
Three.js 는 웹페이지에 3D 객체를 쉽게 렌더링해주는 라이브러리다.
점, 선, 삼각현을 그리는 시스템인 WebGL 을 이용한다.
먼저 Three.js 구조를 살펴본다.
Render
는 Scene
과 Camera
객체를 넘겨 받아 이미지로 렌더링 한다.Scene
또는 Mesh
, Light
등으로 이루어진 트리 구조와 유사하다.Scene
에 포함된 객체도 부모/자식의 트리 구조이며, 자식 객체의 위치와 방향은 부모 기준이다.Camera
는 다른 객체와 달리 씬 그래프에 포함되지 않았다.Camera
도 다른 객체의 자식이 될 수 있다.Mash
는 Material
로 하나의 Geometry
를 그리는 객체다.Mesh
가 하나의 Material
또는 Geometry
를 동시에 참조할 수 있다.Camera
도 마찬가지다.Geometry
는 기하학 객체의 정점 데이터다.Material
은 기하학 객체를 그리는 데 사용하는 표면 속성이다.Texture
를 사용할 수 있다.Texture
는 이미지나 캔버스로 생성한 이미지, Scene
객체의 결과물에 해당한다.Light
는 여러 종류의 광원에 해당한다.먼저, Three.js 를 로드한다.
<script type="module">
import * as THREE from '../three.module.js';
</script>
위처럼 모듈을 불러오는 것을 잊지말자.
그리고 <canvas>
태그를 작성한다.
<body>
<canvas id="c"></canvas>
</body>
이제 Three.js 에 렌더링을 맡겨본다.
<script type="module">
import * as THREE from '../three.module.js';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
}
</script>
캔버스 요소를 참조 후 WebGLRenderer
를 생성했다.
렌더러 종류로 여러가지 있었지만 현재 3차원을 그리는 WebGLRenderer
를 사용한다.
Three.js 에 canvas 요소를 넘기지 않으면 자동으로 생성되므로 동적으로 삽입되어 코드를 직접 코쳐야하니 호환성 면에서 캔버스 요소를 직접 넣자.
다음으로 PerspectiveCamera(원근 카메라)
객체로 카메라를 생성한다.
const fov = 75;
const aspect = 2;
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
fov
는 field of view(시야각)
의 줄임말로, 수직면 75도로 설정했다.aspect
는 캔버스의 가로 세로 비율이다.near
와 far
는 카메라 앞에 렌더링되는 공간 범위를 지정하는 요소다.near
와 far
평면의 높이는 시야각, 너비는 시야각과 aspect
에 의해 결정된다.
카메라는 -Z 축 + Y 축, 즉 아래를 본다.
camera.position.z = 2;
z = 2
이므로 -Z 방향을 본다.
이제 Scene
을 만든다.
씬 그래프 최상단 위치한 요소므로 렌더링 시 먼저 Scene
에 추가한다.
const scene = new THREE.Scene();
다음으로 간단한 정육면체를 보자.
BoxGeometry
생성자를 호출하여 정육면체를 만든다.
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
Meterial
을 만들어 색을 지정한다.
색 지정은 CSS 처럼 hex 코드를 사용한다.
const material = new THREE.MeshBasicMaterial({color: 0x44aa88});
앞서 만든 물체와 색을 이용해 Mesh
를 만든다.
const cube = new THREE.Mesh(geometry, material);
마지막으로 Scene
에 넣는다.
scene.add(cube);
renderer
의 render
메소드로 화면을 렌더링한다.
한 면만 보이는 정육면체를 움직여보자.
애니메이션을 구현하기 위해 requestAnimationFrame
루프로 렌더링 함수를 호출한다.
function render(time) {
time *= 0.001; // convert time to seconds
cube.rotation.x = time;
cube.rotation.y = time;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
requestAnimationFrame
은 브라우저에 애니메이션 프레임을 요청하는 함수다.
페이지에 변화가 있으면 다시 렌더링한다. 위 예제는 Three.js 의 renderer.render
메소드로 씬을 렌더링한다.
requestAnimationFrame
은 매개변수로 받은 함수에 페이지가 로드 이후의 시간을 밀리초 단위로 넘겨준다.
전 초 단위가 익숙하니 밀리초 단위를 초 단위로 변경하였다.
그리고 씬을 렌더링한 후, 애니메이션 프레임을 요청해 반복한다.
마지막으로 루프 밖에 requestAnimationFrame
한 번 호출하여 루프를 시작한다.
3D 라기엔 좀 부족한데, 광원을 추가하여 그림자를 만들자.
지금은 예시로 DirectionalLight
를 사용한다.
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
DirectionalLight
에 위치(position) 와 타깃(target) 속성이 있다.
기본 값은 (0, 0, 0) 으로 position
을 (-1, 2, 4)로 설정해 약간 동서쪽으로 보낸다.
target
은 기본값 (0, 0, 0) 그대로 두어 공간 중앙에 비춘다.
material
도 바꾸는데, MeshBasicMaterial
은 광원에 반응하지 않으니 MeshPhongMaterial
로 바꾼다.
// const material = new THREE.MeshBasicMaterial({color: 0x44aa88}); // greenish blue
const material = new THREE.MeshPhongMaterial({color: 0x44aa88}); // greenish blue
다음은 현재까지의 프로그램을 도식화한 것이다.
아래는 결과다.
육면체를 2개 더 만들자.
미리 만든 Geometry
로 Material
만 바꾸어 다른 색의 큐브 2개를 만든다.
함수 하나를 만들어 함수로 받은 색상값으로 새 Material
을 만들어 넘겨받은 Geometry
와 조합해 새로운 Mesh
를 만든다.
씬에 추가 후 넘겨받은 X 축으로 이동도 시킨다.
function makeInstance(geometry, color, x) {
const material = new THREE.MeshPhongMaterial({color});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
cube.position.x = x;
return cube;
}
다음의 3가지 큐브를 만들어 배열로 저장한다.
const cubes = [
makeInstance(geometry, 0x44aa88, 0),
makeInstance(geometry, 0x8844aa, -2),
makeInstance(geometry, 0xaa8844, 2),
];
마지막으로 render
함수로 3개의 큐브를 회전시킨다.
동적인 효과로 다른 값을 준다.
X축으로 -2, +2 가 시야에서 조금 벗어났다.
그리고 가운데 육면체는 굴절되어 보이는데, 이는 시야각이 좁은 탓이다.
구조는 다음과 같다.
각 Mesh
객체는 같은 BoxGeometry
를 참조한다.
그러나 각기 다른 MeshPhongMaterial
을 참조하는 다른 색을 띈다.
이번엔 반응형으로 만들어보자.
이전 글에서 사이즈나 CSS를 정의하지 않은 캔버스를 사용했다.
캔버스 요소는 기본적으로 300x150 픽셀이다.
웹에서는 요소의 크기를 지정할 때 CSS 를 권장하는데, 페이지 전체를 차지하도록 CSS를 작성해보자.
<style>
html, body {
margin: 0;
height: 100%;
}
#c {
width: 100%;
height: 100%;
display: block;
}
<style>
아래는 이전 장에 CSS 스타일을 덧붙인 것이다.
캔버스가 창 전체를 채우긴 하지만 길이가 늘어나 줄어들면 길이에 따라 정육면체가 변하는 것을 볼 수 있다.
그리고 저화질에다 깨지고 흐릿해 보인다는 것이다. 창을 아주 크게하면 알 수 있다.
창 크기에 따라 늘어나는 문제는 카메라의 aspect(비율) 속성을 캔버스 화면에 맞추어야 한다.
이는 clientWidth
와 clientHeight
속성으로 해결할 수 있다.
그리고 렌더링 함수를 다음처럼 수정한다.
function render(time) {
time *= 0.001;
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
이제 더이상 늘어나거나 찌그러들지 않을 것이다.
이제 계단 현상을 없애보자.
캔버스 요소엔 크기와 픽셀 수에 대한 두 종류의 크기가 있다.
캔버스의 원본 크기, 해상도는 드로잉버퍼(drawingbuffer)라고 불린다.
Three.js 에선 renderer.setSize
메소드를 호출해 캔버스의 드로잉버퍼 크기를 지정할 수 있다.
캔버스의 디스플레이 크기인 clientWidth
와 clientHeight
를 이용하자.
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
캔버스를 리사이징할 필요가 있는지 검사했다는 점을 주의하자.
스펙상 리사이징은 화면을 다시 렌더링하므로, 같은 사이즈일 때는 리사이징하지 않아 불필요한 자원 낭비를 막아주는 것이 좋다.
캔버스 크기가 다르면, renderer.setSize
메소드로 새로운 width 와 height 를 넘겨준다.
renderer.setSize
메소드는 기본적으로 CSS 크기를 설정하니 마지막 인자에 false
를 잊지말자.
위 함수는 캔버스를 리사이징했다면 true
를 반환한다.
새 함수를 이용해 render
함수를 수정하자.akwsp
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
...
캔버스 비율이 변하려면 캔버스 사이즈가 변해야 하니 resizeRendererToDisplaySize
함수가 true
를 반환했을 때만 카메라 비율을 변경한다.
이제 디스플레이에 맞게 해상도로 렌더링될 것이다.
현재 코드를 별도의 js 파일로 저장하자. 다른 예시로 텍스트 사이에 끼워 넣자.
다음으론 우측 컨트롤 패널로 조정할 수 있는 곳에도 활용해보자.
HD-DPI는 고해상도 줄임말이다.
CSS 픽셀로 요소 크기를 지정하는데 이는 더 촘촘할 뿐이다.
Three.js 로 HD-DPI 를 다루는 방법은 다음과 같다.
첫 째는 아무것도 하지 않는다.
3D 렌더링은 GPU 를 소모하니 아마 가장 흔하다.
모바일은 데스크탑보다 GPU 성능이 부족해도 해상도가 높은데 9배 더 많은 렌더링 작업을 할 수도 있다.
이는 낮은 FPS, 화면을 버벅일 수 있다. 하지만 해상도에 따른 화면 렌더링 방법이 더 있다.
하나는 renderer.setPixelRatio
메소드로 해상도 배율을 알려주는 것이다.
renderer.setPixelRatio(window.devicePixelRatio);
이는 renderer.setSize
가 알아서 사이즈에 배율을 곱해 리사이징한다. 하지만 이 방법은 추천하지 않는다.
다른 방법은 캔버스를 리사이징할 때 직접 계산하는 것이다.
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const pixelRatio = window.devicePixelRatio;
const width = canvas.clientWidth * pixelRatio | 0;
const height = canvas.clientHeight * pixelRatio | 0;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
이 방법이 훨씬 나으며 직접 배율을 계산하면 어떤 값을 쓸지 확실하며 예외도 줄여준다.
HD-DPI 기기에서 보면 이전 예시보다 모서리가 좀 더 깨진 것이 보일 것이다.