[Three.js] Three 기초

Study·2021년 8월 11일
11

Three.js

목록 보기
2/8
post-thumbnail

본문은 Three.js 기초 내용을 바탕으로 하여 요약 및 정리글이다.

Three.js 란?

Three.js 는 웹페이지에 3D 객체를 쉽게 렌더링해주는 라이브러리다.

점, 선, 삼각현을 그리는 시스템인 WebGL 을 이용한다.

먼저 Three.js 구조를 살펴본다.

  • Three.js의 핵심 객체인 RenderSceneCamera 객체를 넘겨 받아 이미지로 렌더링 한다.
  • 씬 그래프(Scene Graph)는 Scene 또는 Mesh, Light 등으로 이루어진 트리 구조와 유사하다.
    최상위 노드로서 배경색, 안개 등의 요소를 포함한다.
    Scene 에 포함된 객체도 부모/자식의 트리 구조이며, 자식 객체의 위치와 방향은 부모 기준이다.
    Camera 는 다른 객체와 달리 씬 그래프에 포함되지 않았다.
    물론 Camera 도 다른 객체의 자식이 될 수 있다.
  • MashMaterial 로 하나의 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);
  • fovfield of view(시야각) 의 줄임말로, 수직면 75도로 설정했다.
  • aspect 는 캔버스의 가로 세로 비율이다.
    기본 설정은 300x150 이니 비율 300/150, 2로 설정한다.
  • nearfar 는 카메라 앞에 렌더링되는 공간 범위를 지정하는 요소다.
    이 공간 밖 요소는 화면에서 잘려나가 렌더링되지 않는다.

nearfar 평면의 높이는 시야각, 너비는 시야각과 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);

rendererrender 메소드로 화면을 렌더링한다.

한 면만 보이는 정육면체를 움직여보자.

애니메이션을 구현하기 위해 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개 더 만들자.

미리 만든 GeometryMaterial 만 바꾸어 다른 색의 큐브 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 을 참조하는 다른 색을 띈다.

Three.js 반응형 디자인

이번엔 반응형으로 만들어보자.

이전 글에서 사이즈나 CSS를 정의하지 않은 캔버스를 사용했다.
캔버스 요소는 기본적으로 300x150 픽셀이다.

웹에서는 요소의 크기를 지정할 때 CSS 를 권장하는데, 페이지 전체를 차지하도록 CSS를 작성해보자.

<style>
  html, body {
    margin: 0;
    height: 100%;
  }
  #c {
    width: 100%;
    height: 100%;
    display: block;
  }
<style>

아래는 이전 장에 CSS 스타일을 덧붙인 것이다.

캔버스가 창 전체를 채우긴 하지만 길이가 늘어나 줄어들면 길이에 따라 정육면체가 변하는 것을 볼 수 있다.

그리고 저화질에다 깨지고 흐릿해 보인다는 것이다. 창을 아주 크게하면 알 수 있다.

창 크기에 따라 늘어나는 문제는 카메라의 aspect(비율) 속성을 캔버스 화면에 맞추어야 한다.
이는 clientWidthclientHeight 속성으로 해결할 수 있다.

그리고 렌더링 함수를 다음처럼 수정한다.

function render(time) {
  time *= 0.001;
  
  const canvas = renderer.domElement;
  camera.aspect = canvas.clientWidth / canvas.clientHeight;
  camera.updateProjectionMatrix();
}

이제 더이상 늘어나거나 찌그러들지 않을 것이다.

이제 계단 현상을 없애보자.

캔버스 요소엔 크기와 픽셀 수에 대한 두 종류의 크기가 있다.

캔버스의 원본 크기, 해상도는 드로잉버퍼(drawingbuffer)라고 불린다.
Three.js 에선 renderer.setSize 메소드를 호출해 캔버스의 드로잉버퍼 크기를 지정할 수 있다.

캔버스의 디스플레이 크기인 clientWidthclientHeight 를 이용하자.

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 디스플레이 다루기

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 기기에서 보면 이전 예시보다 모서리가 좀 더 깨진 것이 보일 것이다.

profile
Study

0개의 댓글