[Three.js journey 강의노트] 21

9rganizedChaos·2021년 7월 10일
7
post-thumbnail

🙌🏻 해당 글은 Three.js Journey의 강의 노트입니다.

Three.js를 통해 많은 도형을 만들 수 있지만, 복잡한 도형의 경우 전용 3D 소프트웨어를 사용하는 것이 좋다. 이번 단원에서는 이미 만들어진 모델을 사용하는 방법을 배웁니다.

Formats

3D모델에는 아주 많은 포맷이 있다. 이 포맷들은 모델에 어떤 데이터가 내장되어 있는지, 무게, 압축률, 호환성, 저작권 등 다양한 문제에 대응해왔다. 물론 모든 포맷들은 저마다 다른 특성을 갖고 있다. 가볍지만, 데이터가 부족한 겅우도 있고, 많은 데이터를 포함하지만 무거운 포맷도 있다. 어떤 포맷은 오픈소스이고 어떤 소스는 아니다. 바이너리인 포맷도 있고, ASCII인 경우도 있다. 다음은 우리가 자주 접하는 포맷 목록이다.

  • OBJ
  • FBX
  • STL
  • PLY
  • COLLADA
  • 3DS
  • GLTF

GLTF

GLTF는 GL 전송 포맷이다. GLTF는 매우 다양한 데이터셋을 지원한다. Geometry와 Material을 비롯해서 camera, light, scene graph, animation, skeleton, morphing, 그리고 multiple scene등도 지원한다. 파일 형식도 json, binary, embed textures를 지원한다. 사실상 현재 GLTF는 표준이라고 할 수 있다. 대부분은 3D 소프트웨어, 게임 엔진 및 라이브러리에서 이를 지원한다. 그렇다고 모든 경우에 GLTF를 이용해야 하는 것은 아니다. Geometry만 이용하는 경우 OBJ, FBX, STL 또는 PLY와 같은 다른 형식을 사용하는 것이 좋다. 다양한 상황을 고려해 포맷을 골라야 한다.

Find a model

GLTF는 단순한 삼각형에서부터, 애니메이션, morphing, clearcoat 등과 같은 사실적인 모델들까지 제공한다. 이 레포지토리에서 찾을 수 있다. 그 모델들을 테스트하려면, 일단 전체 레포지토리를 다운로드하고 클론하고, 필요한 파일을 가져와야 한다. 우리는 일단 /static/models/에서 찾을 수 있는 간단한 오리 예에서부터 시작하도록 한다.

GLTF formats

/static/models/Duck/ 폴더에는 서로 다른 네 가지 GLTF 형식의 폴더가 있다. 이 네 가지가 일단은 가장 중요하다. OS가 파일 중 일부 확장자를 숨길 수 있기 때문에 주의해야 한다.

  • glTF: 기본형식. Duck.gltf는 editor에서 열 수 있는 JSON 파일이다. camera, light, scene, material, object transform 등의 정보가 포함되지만, geometry와 texture는 포함되지 않는다. Duck0.bin은 editor에서 쉽게 읽을 수 없는 binary파일이다. 여기에는 일반적으로 지오메트리와 같은 데이터와 UV 좌표, 법선, 정점 색상 등과 같은 정점과 관련된 모든 정보가 포함된다. DuckCM.png는 오리의 texture이다. 이 포맷을 로드할 때는 나머지 파일들을 참조하는 Duck.gltf만 로드하면, 자동으로 로드된다.
  • glTF-Binary: 이 포맷은 하나의 파일로 구성된다. 로드하기에 가볍지만, 데이터를 쉽게 변경할 수 없다. glTF 기본형식으로 이야기한 모든 데이터가 포함되어 있다. editor에서 내용을 조회할 수 없다.
  • glTF-Draco: glTF 기본형식과 비슷하지만 buffer date(일반적으로 geometry)는 Draco 알고리즘을 통해 압축된다. 일단 가볍다. 나중에 더 언급하도록 한다.
  • glTF-Embedded: glTF-Binary와 비슷하다. 하지만 editor에서 열 수 있다는 것이 결정적으로 다르다. 그것이 유일한 장점.

익스포팅한 이후에 texture를 수정하거나 빛의 좌표를 수정하기 원한다면 glTF 기본형식. 서로다른 파일을 개별적으로 로드해 로드 속도가 향상되는 장점이 있다.
모델당 하나의 파일만을 원하고 수정을 원치 않는다면 glTF-Binary.
두 경우 모두 Draco 압축을 사용할지 여부를 결정해야 하지만, 이 부분은 나중에 다룬다.

Load the model in Three.js

GLTF는 표준이기 때문에 lights를 지원한다. GLTF파일을 Three.js 프로젝트에 임포트하면, MeshStandardMaterial를 가진 Mesh를 갖게되는데, 만약 light가 scene에 없다면 materials를 볼 수 없다. 우선 AmbientLight와 DirectionalLight를 만들어준다.

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

/**
 * Models
 */
const gltfLoader = new GLTFLoader()

gltfLoader.load(
    '/models/Duck/glTF/Duck.gltf',
    (gltf) =>
    {
        console.log('success')
        console.log(gltf)
    },
    (progress) =>
    {
        console.log('progress')
        console.log(progress)
    },
    (error) =>
    {
        console.log('error')
        console.log(error)
    }
)

다음과 같이 콘솔에 찍히는 것을 확인할 수 있다.

Add the loaded model to our scene

콘솔에 출력된 값을 보면, 수많은 요소를 가진 객체를 확인할 수 있다. 가장 중요한 부분은 scene 속성이다. 우리는 익스포트된 모델에서 오직 하나의 scene을 갖는다. 이 scene에는 필요한 모든 것이 포함되어 있다.

scene > children > children > PerspectiveCamera, Mesh 이런 식으로 요소들을 확인할 수 있다! 여기서 Mesh가 우리가 출력하고자 하는 오리 모델이다. 이제 오리를 출력하기 위한 여러가지 방법이 있다.

  • model의 전체 scene을 우리의 scene에 추가한다. 왜냐하면 모델의 scene이 설령 이름이 scene이라고 지어졌더라도 사실 그것은 Group이기 때문이다.
  • scene안에 포함된 children들을 우리의 scene으로 가져온다. 대신 쓰지 않을 PerspectiveCamera는 제외해준다.
  • PerspectiveCamera와 같이 쓰지 않을 요소들을 필터해주고 children을 scene에 추가!
  • 오직 Mesh만을 가져온다. 그러나 그렇게 가져올 시, Duck이 잘못 scaled, rotated, positioned일 수 있다.
  • 3D 소프트웨어에서 모델 파일을 열어 PerspectiveCamera를 제거해주고 다시 export한다!

우선, 쓰지 않을 PerspectiveCamera를 무시하고, Object3D를 우리의 scene에 추가하는 방법을 사용해보자!

gltfLoader.load(
    '/models/Duck/glTF/Duck.gltf',
    (gltf) =>
    {
        scene.add(gltf.scene.children[0])
    }
)

아래와 같은 다양한 포맷을 이용해줄 수 있다!

gltfLoader.load(
    '/models/Duck/glTF/Duck.gltf', // Default glTF

// Or
gltfLoader.load(
    '/models/Duck/glTF-Binary/Duck.glb', // glTF-Binary

// Or
gltfLoader.load(
    '/models/Duck/glTF-Embedded/Duck.gltf', // glTF-Embedded

이번에는 FlightHelmet 모델을 가져와보자!

gltfLoader.load("/models/FlightHelmet/glTF/FlightHelmet.gltf", (gltf) => {
  console.log(gltf);
});

이번에는 약간 구조가 다르다.
scene 안에 총 개의 Mesh가 들어있는 것을 콘솔로 확인해볼 수가 있다.

그렇다면 이것들을 어떻게 우리의 scene으로 가져올 수가 있을까?
우선 첫번째 children만 가져와보자!

gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    (gltf) =>
    {
        scene.add(gltf.scene.children[0])
    }
)

헬멧이 특정 부분만 가져와진 것을 볼 수 있다. 반복문 loop을 통해 전체를 가져와보자.

        for(const child of gltf.scene.children)
        {
            scene.add(child)
        }

여전히 헬멧이라고 하기에는 무리가 있다. 더 유심히 살펴볼 것은 새로고침을 할 때마다 아래와 같이 서로 다른 모습이 출력된다는 점이다.

scene에서 다른 장면으로 child를 추가할 때 원래 있던 장면에서 자동으로 child가 제거되고 있다.
우리는 로드된 scene의 첫 번째 자식을 가져와 아무것도 남지 않을 때까지 scene에 추가하는 방법을 사용할 것이다.

        while(gltf.scene.children.length)
        {
            scene.add(gltf.scene.children[0])
        }

이제 온전한 헬멧의 모습을 확인할 수 있다. 또 다른 해결 책을 children 배열을 복제해버리는 것이다. 전개연산자를 이용한다.

        const children = [...gltf.scene.children]
        for(const child of children)
        {
            scene.add(child)
        }

마지막으로 이 모든 것들보다 더 간단한 방법을 사용해보자.
씬 전체를 가져온다.

        scene.add(gltf.scene)

Draco compression

이제 다시 Duck을 가져와 Draco version을 시도해보자.

const gltfLoader = new GLTFLoader();

gltfLoader.load("/models/Duck/glTF-Draco/Duck.gltf", (gltf) => {
  scene.add(gltf.scene);
});

이와 같이 경로를 바꿔주었지만, 콘솔에 에러만 출력되고 오리는 가져와지지 않는 모습을 볼 수 있다.

압축파일을 로드할 수 있도록 GLTFLoader에 DRACOLoader 인스턴스를 제공해주어야 한다.

Add the DRACOLoader

import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'

const dracoLoader = new DRACOLoader()

dracoLoader.setDecoderPath('/draco/')
gltfLoader.setDRACOLoader(dracoLoader)

디코더들은 가본적으로 Javascript에서 사용할 수 있지만 웹어셈블리에서도 사용할 수 있다. 우리는 Three.js에서 DRACO 폴더를 살펴볼 수 있다. 이것을 /static/ 폴더에 복사하고 dracoLoader에 경로를 제공해줄 수 있다. 이제 다시 오리를 확인할 수 있다.

When to use the Draco compression

Draco압축이 무조건 좋다고 생각할지도 모른다. 그러나 그렇지 않다. 첫째로, geometries들이 더 가볍지만, 너는 일단 DRACOLoader와 decoder를 로드해야 한다. 둘째로 우리가 worker와 웹어셈블리 코드를 사용하고 있다고 하더라도, 시작 부분에 짧은 freeze를 낼 수 있다. 컴퓨터가 압축된 파일을 디코드하는데 리소스를 소모하기 때문이다. 100kB geometry 모델이 하나만 있는 경우 딱히 Draco가 필요하지 않다. 그러나 로드할 모델이 많고, 로드 초반 순간적인 정지를 신경쓰지 않는다면 Draco 압축을 사용하면 된다.

Animations

glTF는 animation을 지원하고 Three.js는 그 애니메이션들을 컨트롤할 수 있다.

Load an animated model

일단 애니메이션이 적용된 모델을 가져오자.

gltfLoader.load("/models/Fox/glTF/Fox.gltf", (gltf) => {
  scene.add(gltf.scene);
});

여우가 너무 크다. 우선 scale을 조정해주도록 한다. 여우는 하나의 Object3D로 구성되어 있다. Bone과 SkinnedMesh로 구성되어 있다. 때문에 막연히 생각해봐도 Object3D 자체의 스케일을 건드려서는 안 된다는 것을 알 수 있다. 우리가 할 수 있는 것은 로드된 scene의 스케일을 조절하고 이 scene을 통째로 가져오는 것이다.

gltfLoader.load(
    '/models/Fox/glTF/Fox.gltf',
    (gltf) =>
    {
        gltf.scene.scale.set(0.025, 0.025, 0.025)
        scene.add(gltf.scene)
    }
)

Handle the animation

gltf를 콘솔에 찍어보면 animation이라는 속성을 찾을 수 있다. AnimationClip이 여러 개 들어있다. 이 AnimationClip들은 쉽게 사용될 수 없다. 우리는 먼저 AnimationMixer응 만들어야 한다. AnimationMixer는 많은 AnimationClips를 포함하는 객체와 연관된 플레이어 같은 것이다.

애니메이션을 재생하려면 mixer에 각 프레임에서 자체 업데이트하도록 지시해야 한다. 콜백 함수 밖에서 mixer 변수를 선언해주고 model이 로드되었을 떄 그것을 업데이트 해주자.

let mixer = null

gltfLoader.load(
    '/models/Fox/glTF/Fox.gltf',
    (gltf) =>
    {
        gltf.scene.scale.set(0.03, 0.03, 0.03)
        scene.add(gltf.scene)

        mixer = new THREE.AnimationMixer(gltf.scene)
        const action = mixer.clipAction(gltf.animations[0])
        action.play()
    }
)

이미 계산된 deltaTime으로 tick 함수의 믹서를 업데이트할 수 있다.

const tick = () =>
{
    // ...

    if(mixer)
    {
        mixer.update(deltaTime)
    }

    // ...
}

이제 움직이기 시작한다! 다른 애니메이션들도 시도해줄 수 있다.

Three.js editor

Three.js는 Three.js의 온라인 editor를 갖고 있다.
3D 소프트웨어와 비슷하지만, 기능이 더 적다. 하나 주의할 점은 하나의 파일로 구성된 모델만 테스트할 수 있다. glTF-Binary 또는 glTF-Embedded 오리로 시도할 수 있다.

add 탭에서 AmbientLight과 DirectionalLight를 추가해주면!

profile
부정확한 정보나 잘못된 정보는 댓글로 알려주시면 빠르게 수정토록 하겠습니다, 감사합니다!

0개의 댓글