ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Three.js 입문기
    Three.js 2022. 9. 26. 11:10

     

    해당 게시글은 2022.05.11에 깃허브로 작성되었습니다.

     

     여느때와 같이 다양한 레퍼런스 사이트를 보던 중 Three.js를 활용한 사이트들을 많이 접하게 되었고 볼 때마다 나도 구현해보고 싶다는 생각이 들었다.

     

    그래서 미루지만 말고 조금이라도 공부해보자는 생각으로 좋은 양질의 블로그와 강의 영상을 찾아 들으며 내용을 정리해보았다.

     


     

    1. Three.js란?

    Three.js는 WebGL을 이용해 웹페이지에 3D 객체를 쉽게 렌더링하도록 도와주는 3D Javascript 라이브러리이다.

    여기서 WebGL이란?

    html의 canvas 요소를 이용하여 웹브라우저에서 인터렉티브한 3D 그래픽을 사용할 수 있도록 하는 도구.
    CPU가 아닌 GPU를 사용하여 화면 렌더링이 굉장히 빠르다.
    이를 이용해 웹 게임, 인터렉티브 페이지, VR 콘텐츠 등 여러 3D 작업물을 만들곤 한다.

     

    웹 상에서 3D 그래픽을 활용하기 위해서는 HTML5, Canvas, WebGL, SVG 등의 다양한 수단을 사용할 수 있는데

    Three.js는 이런 여러 primitive를 사용한 3D 그래픽을 좀 더 쉽게 구현하기 위해 한 단계를 감싸놓은 Javascript Wrapper 역할을 하는 라이브러리이다.

     


     

    (1) 사전 준비

    간단한 예제를 직접 구현해나가며 방법을 터득해볼텐데

    우선 Three.js 예제를 진행할 폴더를 생성하고 본인이 사용하는 코드 에디터에서 열어보자.

     

    그 후 index.html 파일을 생성하고 아래 링크에서 Three.min.js를 가져와서 html에 연결해준다.

    https://github.com/rlacodud/UID/blob/mit/Research/Three.js/example/js/Three.min.js

     

    GitHub - rlacodud/UID

    Contribute to rlacodud/UID development by creating an account on GitHub.

    github.com

     

    또는 Three.js에 접속하여 download 메뉴를 클릭하면 압축폴더를 다운받게 되는데

    이를 통해 html과 필요 파일을 연결할 수도 있다.

    https://threejs.org/

     

    Three.js – JavaScript 3D Library

     

    threejs.org

    위 링크에서는 Three.js 라이브러리 파일 외에도 다양한 sample과 API 문서들도 포함되어 있으므로 처음 배우는 사람들에게 유용하다.

    위 압축폴더 속 파일들을 이용한 예제는 다음 시간에 활용해보도록 하자.

     

    아래에서 진행하는 코드들은 모두 html 내부 <script> 태그 안에 작성한다.

     


     

    2. 3D 그래픽의 구성요소

    그럼 우선 3D 그래픽의 구성요소를 하나하나 살펴보며 이해해보자.

    (1) 공간 - Scene

    가장 먼저 우리의 Object들을 놓을 공간 이 필요하다.

    Three.js에서는 이러한 공간을 Scene이라고 부른다.

    const scene = new THREE.Scene();

    Scene은 말 그대로 우리가 화면에 그리고자 하는 어떤 장면에 해당한다.

    정확히는 그 장면에 대한 정보(카메라는 어디서 어떤 방향으로 바라보고 있고, 광원은 어디에 존재하고, 어떤 물체들이 있고 등)를 모두 담고 있는 무언가라고 할 수 있다.

     

    필요한 정보를 갖고 있어도 이러한 정보를 사람이 보는 화면에 그리기 위해서는 이 정보를 화면에 그려내는 작업이 필요한데

    이러한 작업을 하는 녀석을 Renderer라고 부른다.

    const renderer = new THREE.WebGLRenderer({
      alpha: true,
      antialias: true
    })

    WebGLRenderer Constructor는 Object의 option을 인자로 받는데

    위 코드를 보면 투명한 배경(alpha: true)에 안티얼라이어싱(깨지는 것을 막는/antialias: true)이 적용되게 설정했다.

     

    (2) 피사체: 부피, 질감 - Mesh: Geometry, Material

    우리는 간단한 팔면체를 그리고자 한다.

    이 팔면체는 두 층위로 나눠서 생각해볼 수 있다.

    1. 8개의 면, 6개의 꼭짓점, 8개의 간선을 갖는 기하학적 형태
    2. 일종의 뼈대로서 기능하는, 그 기하학적 형태 위에 덧씌워져 실제로 우리 눈에 보여지는 
    질감을 가진 표면

     

    이렇게 두 층위로 나누는 이유는

    1번은 말 그대로 형태, 뼈대이다.

    2번은 1번 위에 덧씌워지는 표면이다.

     

    이렇게 1번에는 다양한 질감의 표면을 덧씌우고 다양한 형태에는 2번을 덧씌움으로써

    다양한 Object가 효율적으로 생성될 수 있다.

    기하학적 형태, 뼈대를 담당하는 부분(1)을 Geometry라 부른다.
    특정한 질감, 색, 반사율 등을 갖는 물체의 표면(2)을 Material이라 부른다.

     

    이렇게 Geometry에 Material이 입혀진 Object를 Mesh라 부른다.

    물체(Mesh) = 뼈대(Geometry) + 표면(Material)

     

    그럼 팔면체 Mesh를 만들기 위해 우선 뼈대인 Geometry가 필요하다.

    3D 모델링의 기본 단위는 삼각형이다. 즉, 모든 면은 삼각형의 합으로 표현된다.

    수학시간에 배웠듯이 점점 꼭짓점의 개수를 늘려가다보면 결국 원에 가까워지는 원리와 같다.

     

    그럼 3D 모델의 뼈대를 만들어내는 가장 기본적인 방법은 다음과 같을 것이다.

    꼭짓점(vertex)을 정의한다.

    어떤 세 꼭짓점이 이어져서 삼각형 면(Face)을 이루는지를 정의한다.

    const geometry = new THREE.Geometry();
    geometry.vertices.push(
      new THREE.Vector3(-10, 10, 0),
      new THREE.Vector3(-10, -10, 0),
      new THREE.Vector3(10, -10, 0)
    );
    geometry.faces.push(new THREE.Face3(0, 1, 2));

    위 코드는 x-y 평면에 세 점(x= -10, y=10 | x=-10, y=-10 | x=10, y=-10)을 찍은 후

    Geometry의 첫번째, 두번째, 세번째 점을 잇는 면을 추가하는 코드다.

     

    이해하기 어려운 개념은 아니지만 이런 식으로 모든 모델링을 해야 한다면

    복잡한 물체를 그리려 할 때 코드의 양이 급격히 늘어나고 의도를 파악하기 힘들어질 것이다.

     

    그런 사태를 방지하기 위해 Three.js에서는 미리 정의된 다양한 형태의 Geometry를 제공하고 있다.

    이에 대한 전체 목록은 아래 공식 가이드에 들어가서 확인할 수 있다.

    https://threejs.org/docs/#api/en/geometries/BoxGeometry

     

    three.js docs

     

    threejs.org

     

    우리가 필요로 하는 Geometry인 Octahedron Geometry(팔면체)는 다음과 같이 생성할 수 있다.

    const RADIUS = 40
    const geometry = new THREE.OctahedronGeometry(RADIUS, 0);

     

    Geometry가 준비되었으니 Material을 준비할 시간이다.

    일단 이 파트에선 빛과 상호작용하지 않는, 가장 기본적인 표면인 MeshBasicMaterial을 사용하도록 한다.

    const material = new THREE.MeshBasicMaterial({ color: '#ff3030' })

    마지막으로 만들어낸 Geometry와 Material을 이용해 Mesh를 만들어보자.

    Mesh의 생성자는 GeometryMaterial의 두 인자를 받는다.

    const mesh  = new THREE.Mesh(geometry, material)

    이제 그리고자 하는 물체의 준비가 끝났다.

    마지막으로 앞서 준비한 공간에 이 물체를 놓아보자.

     

    기본적으로 scene.add 함수를 통해 공간에 추가한 물체는 (0, 0, 0) 위치에 놓인다.

    원활한 관찰을 위해 z축으로 약간 밀어두자.

    scene.add(mesh)
    mesh.position.z = -RADIUS * 10

     

    (3) 카메라 - Camera

    같은 공간에 같은 물체들이 배치되어 있어도,

    어디에 서서 어떤 시선으로 바라보느냐에 따라 보이는 풍경이 다른데 이 시선에 해당하는 것이 camera다.

     

    실제 사람의 눈 또는 카메라 렌즈와 비슷하게 투시 투영을 사용하는 PerspectiveCamera를 사용해보자.

    이외에도 Scene을 바라보는 형태에는 다양한 Camera가 있다.

    const WIDTH = 400
    const HEIGHT = WIDTH
    
    const Field_of_View = 20;
    const ASPECT = WIDTH / HEIGHT;
    const NEAR = 0.1;
    const FAR = 10000;
    
    const camera = new THREE.PerspectiveCamera(
      FIELD_OF_VIEW,
      ASPECT,
      NEAR,
      FAR
    )

    생성자가 받는 네개의 인자는 각각 다음과 같은 의미를 갖는다.

    - Field of View
     카메라의 시야각을 의미한다. 커질수록 카메라가 바라보는 시야각이 넓어짐을 의미한다. 단위는 degree.

    - ASPECT
     시야의 가로세로비를 의미한다. container의 가로세로비와 동일한 값 을 넣어주는 게 좋다. 단위는 없다.

    - NEAR
     렌더링할 물체 거리의 하한값으로, 너무 가까이 있는 물체를 그리는 것을 막기 위해 사용한다.

    - FAR
     렌더링할 물체 거리의 상한값으로, 너무 멀리 있는 물체를 그리는 것을 막기 위해 사용한다.
     카메라로부터의 거리가 이 값보다 큰 물체는 화면에 그리지 않는다.

     


     

    3. Renderer(그려내기)

    앞서 언급했듯이 이 모든 정보를 화면에 그려내는 일은 renderer의 일이다.

    여기서는 #three라는 id를 갖는 <div>를 container로 사용하기로 하고 <body> 내부에 추가한 뒤

    위에서 정의한 가로, 세로를 renderer에 적용해보자.

    renderer.setSize(WIDTH, HEIGHT)

     

    그 후 renderer가 그려낸 장면을 담을 <canvas> element를 DOM트리에서 container의 자식으로 추가한다.

    해당 element는 renderer.domElement 프로퍼티를 통해 접근할 수 있다.

    const container = document.querySelector('#three')
    container.appendChild(renderer.domElement)

     

    마지막으로 우리가 지금까지 만들어놓은 장면과 camera를 이용해 화면을 실제로 그리라는 명령을 내린다.

    이 명령은 renderer.render 메소드 를 사용한다.

    renderer.render(scene, camera)

     

    여기까지 모든 과정을 잘 따라왔다면 아래와 같은 화면을 볼 수 있을 것이다.

    놀랍게도 이건 마름모로 보이지만 팔면체가 맞다.

    그러나 마름모처럼 보이는 이유는 다음과 같다.

    팔면체의 중심은 (0, 0, -400)에 놓여있고, 카메라는 (0, 0, 0)에 놓여있다.
    우리가 정의한 카메라의 시점은 z축을 따라 팔면체의 정중앙을 뚫고 지나가고 있다.

    MeshBasicMaterial은 빛과 상호작용을 하지 않는 Material이라고 했다.
    실제로 우리는 공간에 빛을 정의조차 하지 않았다.

     

    빛의 부재는 곧 공간에서 심도의 부재를 의미한다.

     

    즉, 한 축이 무의미해진 3D는 2D로 나타난다.

    이를 해결하기 위해서는 당연하게도 빛(심도)을 추가하면 된다.

     


     

    4. 빛과 질감

    이 작업은 두 단계로 나눌 수 있다.

    1. 공간에 빛을 추가한다.
    2. 팔면체가 빛과 상호작용하도록 한다.

     

    먼저 빛을 추가해보자.

    모든 광원의 생성자는 기본적으로 색깔(color)과 세기(intensity)의 두 인자를 받는다.

    Three.js는 공간 전체를 밝히는 AmbientLight, 특정 방향으로 뻗어나가는 DirectionalLight 등 다양한 종류의 광원을 제공한다.

     

    이 글에서는 가장 기본적인 광원 중 하나인 PointLight를 사용하겠다.

    PointLight는 마치 전구처럼 한 점에서 시작해 모든 방향으로 뻗어나가는 광원을 표현하기 위해 사용한다.

    const pointLight = new THREE.PointLight(0xFFFFFF, 0.5)
    
    pointLight.position.x = 100
    pointLight.position.y = 100
    pointLight.position.z = 30
    
    scene.add(pointLight)

    백색 광(0xFFFFFF)을 정의하고 위치를 잡아준 뒤(pointLight.position) 공간에 빛을 더한다.(scene.add(pointLight))

     

    아직 팔면체의 표면이 빛과 상호작용을 하지 않기 때문에 화면에 아무런 변화가 없는데

    이제 Material을 변경해보자.

     

    Three.js에서는 빛과 상호작용하는 표면 중 자주 쓰이는 표면 모델 몇가지를 기본적으로 제공한다.

    그중 여기에서는 람베르드 반사율(관찰자가 바라보는 각도와 관계없이 같은 겉보기 밝기)을 가지며 물체의 표면을 나타내는 MashLambertMaterial을 이용해보겠다.

    기존에 작성된 코드에서 material의 정의를 아래와 같이 변경해보자.

    const material = new THREE.MeshLambertMaterial({ color: '0xFF3030 ' })

    이제 아래와 같이 변경됐을 것이다.

    앞서 생성한 빛을 (100, 100, 30)에 두었는데

    이는 우측상단에 위치하는 점이고 그렇기에 실제로 더 많은 빛을 받는 우측 상단은 더 밝은 색을 갖는 반면,

    좌측 하단은 빛을 거의 받지 못해 까맣게 보이는 것 을 확인할 수 있다.

     

    그럼 마지막으로 이 팔면체가 3D로 그려졌다는 것을 보다 명확하게 하기 위해 회전시켜보자.

     


     

    5. 움직임

    브라우저에서는 requestAnimationFrame 함수를 사용해 매끄러운 애니메이션을 그려낼 수 있다.

    이 함수는 콜백 함수를 인자로 받고 한 프레임을 할당받아서 인자로 받은 콜백 함수를 실행한다.

     

    앞서 작성했던 renderer.render(scene, camera)를 다음과 같이 수정하자.

    function update () {
      const speed = Math.random() / 20
      octahedron.rotation.x += speed
      octahedron.rotation.y += speed
      octahedron.rotation.z += speed
    
      renderer.render(scene, camera)
      requestAnimationFrame(update)
    }
    
    requestAnimationFrame(update)

    매 프레임마다 0 ~ 0.05 사이의 값을 임의로 정한 뒤 x, y, z축마다 해당 값만큼의 회전 을 준다.

    그 뒤에 scene을 다시 그리고 자기 자신을 requestAnimationFrame 함수의 인자로 다시 넘겨 호출 하는 내용이다.

    그럼 이제 성공적으로 위와 같이 팔면체가 여러 방향으로 회전할 것이다.

     

    이렇게 간단한 예제를 진행해보며 Three.js에 대해 알아봤다.

    위 코드들은 아래 example 폴더에서 확인 가능하다.

    https://github.com/rlacodud/UID/tree/mit/Research/Three.js/example

     

    GitHub - rlacodud/UID

    Contribute to rlacodud/UID development by creating an account on GitHub.

    github.com

     


    [참고 사이트]

    https://ahnheejong.name/articles/my-first-octahedron/

     

    나의 버건디 팔면체 : Three.js를 사용한 3D 그래픽스 입문기

    자바스크립트를 이용한 손쉬운 3D 그래픽스 프로그래밍 입문을 도와주는 라이브러리 three.js 사용기.

    ahnheejong.name

     

Designed by Tistory.