페이지

2018년 2월 28일 수요일

ASSIMP 라이브러리를 이용한 모델데이터 불러오기 (2. 모델데이터에서 필요한 데이터 추출)

[출처 : http://assimp.sourceforge.net/index.html]

 저번 게시글에 이어서 모델 데이터를 읽어들이는 법에 대해 설명하겠습니다.

if (m_pScene) 
{
m_meshes.resize(m_pScene->mNumMeshes);
m_numMaterial = m_pScene->mNumMaterials;
InitScene();
m_ModelMeshes.resize(m_meshes.size());
}
cs
 위의 코드는 저번 게시글 마지막에 언급했던 생성자의 일부입니다.

 살펴보면 벡터형태로 저장되어 있는 m_meshes를 리사이즈 시켜서 공간을 확보하는데,
이 때 m_pScene에서 가지고 있는 mNumMeshes란 변수를 통해 리사이즈를 시켜줍니다.

 해당 변수는 내가 읽어들인 모델데이터가 몇개의 매쉬를 가지고 있는지 표기하고 있는 것입니다.
 이처럼 assimp 라이브러리의 경우 모델데이터를 읽어드리고, 라이브러리 자체적으로 데이터를 변형함과 동시에 사용자가 해당 데이터를 이용하기 편하게 여러가지 변수들 또한
제공하고 있습니다.

 여러가지 변수들이 존재하지만 개인적으로 자주 사용하게 되었던 것들을 정리하였습니다.

  1. mNumMeshes        //매쉬 갯수
  2. mNumMaterials      //메터리얼 갯수
  3. mNumAnimations   //애니메이션 갯수
  4. mRootNode           //루트노드
  5. mMeshes              //실제 저장된 매쉬 데이터(포인터의 형태로 저장됨)
  6. mAnimation           //실제 저장된 애니메이션 데이터(포인터의 형태로 저장됨)
 만약 해당 모델에 애니메이션 데이터가 포함되어 있지 않을 경우 mNumAnimations에는
0이 저장되고, mAnimation은 nullptr인 것으로 처리가 됩니다.

 그러면 위에서 이어서 코드를 살펴보면 m_meshes의 공간을 확보하고, 매터리얼의 갯수를 현재 모델의 매터리얼 갯수로 초기화를 시켜줍니다.
 그 후 InitScene() 함수를 수행합니다.

 아래 코드는 InitScene() 함수 입니다.
void LoadModel::InitScene()
{
    for (UINT i = 0; i < m_meshes.size(); ++i) {
        const aiMesh* pMesh = m_pScene->mMeshes[i];
        InitMesh(i, pMesh);
        m_numVertices += (UINT)m_meshes[i].m_vertices.size();
    }
}
cs
 해당함수는 aiScene에 존재하는 모든 매쉬를 읽어와서 다시 InitMesh()란 함수를
수행합니다.

 그리고 추가로 m_numVertices변수를 계속 갱신해주는데 이는 이 변수가 해당모델의 총
버텍스 갯수를 가지기 때문입니다.

 단순히 특정 매쉬의 버텍스 갯수를 알고싶으면 m_meshes[해당인덱스].m_vertices.size()를 호출하면 됩니다. (m_vertices는 이전 포스터에서 선언한 벡터입니다.)

 그러면 InitMesh()함수를 살펴봅시다.
void LoadModel::InitMesh(UINT index, const aiMesh * pMesh)
{
    m_meshes[index].m_vertices.reserve(pMesh->mNumVertices);
    m_meshes[index].m_indices.reserve(pMesh->mNumFaces * 3); 
    //삼각형이므로 면을 이루는 꼭지점 3개
    for (UINT i = 0; i < pMesh->mNumVertices; ++i) {
        XMFLOAT3 pos(&pMesh->mVertices[i].x);
        XMFLOAT3 normal(&pMesh->mNormals[i].x);
        XMFLOAT2 tex;
        if (pMesh->HasTextureCoords(0)) 
            tex = XMFLOAT2(&pMesh->mTextureCoords[0][i].x);
        else 
            tex = XMFLOAT2(0.0f, 0.0f);
        const vertexDatas data(pos, normal, tex);
        m_meshes[index].m_vertices.push_back(data);
    }
    for (UINT i = 0; i < pMesh->mNumFaces; ++i) {
        const aiFace& face = pMesh->mFaces[i];
        m_meshes[index].m_indices.push_back(face.mIndices[0]);
        m_meshes[index].m_indices.push_back(face.mIndices[1]);
        m_meshes[index].m_indices.push_back(face.mIndices[2]);
    }
}
cs

 해당함수는 aiMesh에서 필요한 데이터를 추출하는 함수입니다.
 여기서 필요한 데이터라 함은 
버텍스의 위치, 법선벡터, 텍스쳐매핑좌표, 그리고 마지막으로 인덱스가 있습니다. (4가지)
허접한 그림 죄송합니다...
 위의 그림을 살펴보면 회색 점은 버텍스이고, 까만 면은 폴리곤입니다.
 매쉬의 경우 폴리곤으로 이루어져 있습니다. (assimp에서는 face로 표현)
 즉 육각형 큐브의 경우 6개의 면이 아닌 12개의 면을 가지는 것이지요.  (12개의 삼각형 폴리곤)

 여기서 문제가 한가지 생깁니다. 육각형의 경우 점을(버텍스를) 8개만 가지면 되는데
이를 삼각형으로 표현하려고 하니 36개의 점이 필요하게 됩니다. 또한 이 36개의 점들은 매우 많이
중복되는 점들이죠.

 이를 해결하기 위한 것이 인덱스입니다.
 버텍스는 8개만 저장하지만, 이를 조합하는 순서를 별도로 인덱싱해서 저장해두어서
쓸데없이 중복되는 데이터를 방지하는 것입니다.

 위의 함수를 간단히 살펴보면 매쉬에 버텍스 갯수만큼 for루프를 수행하면서
버텍스 좌표, 벅선벡터, 그리고 텍스쳐매핑좌표가 존재할 경우에만 이를 저장하고,
벡터에 push_back을 해주는 것을 볼 수 있습니다.
 큐브에 예를 들면 총 8개의 버텍스를 저장해둔다고 볼 수 있습니다.

 이어서 Face의 갯수만큼 for루프를 수행하는 것을 볼 수 있습니다.
앞서 언급한 것처럼 Face는 폴리곤입니다. 그리고 이것은 삼각형의 형태입니다.
정확하게 말하면 대부분의 모델데이터의 경우 삼각형의 형태입니다. 하지만 드물게
사각형인 경우도 있습니다. 그러면 어떻게 해야할까요?

 이전 게시글에서 aiImportFile 함수에서 여러가지 옵션을 선언한 것을 기억하실 것입니다.
그 때 제가 aiProcess_Triangulat를 꼭 사용하라고 언급했었습니다.
해당 플래그는 혹시나 삼각형이 아닌 형태로 폴리곤이 저장되어 있는 경우 이를 파일을 불러드릴 때
삼각형으로 나눠주는 옵션입니다. (사각형 -> 삼각형 2개)

 하지만 이는 어디까지나 보험차원이고 권장사항은 처음부터 삼각형의 폴리곤을 가진
모델데이터를 사용하는 것입니다.

 그럼 다시 Face의 갯수만큼 for루프하는 부분을 살펴봅시다.
앞서 언급한 것처럼 Face는 삼각형인 것이 보장이 되고, 그러므로 단순히 해당 Face의 0번 부터 2번
인덱스까지를 m_indices 벡터에 push_back시켜주면 됩니다.

 여기까지 완료하면 내가 불러오고자 하는 모델데이터의 모든버텍스와 이를 위한 인덱스까지 모두 추출한 것입니다. 

 이제 여러분이 미리 선언해둔 버텍스 버퍼, 인덱스 버퍼를 이 데이터를 이용해서 갱신시켜주면
됩니다.

 앞서 언급했던 것처럼 이 게시글은 다이렉트X에 대한 기본 지식을 요하기 때문에 버텍스 버퍼를
어떻게 만드는지, 인덱스 버퍼를 어떻게 만드는지에 대한 것을 게시하지 않을
예정입니다.

 다음 게시글들에서는 셰이더 코드에서 해당 데이터들을 어떻게 다루는지 간단하게 설명하고,
애니메이션 데이터를 추출하는 방법에 대해 게시하려고 합니다.

2018년 2월 27일 화요일

ASSIMP 라이브러리를 이용한 모델데이터 불러오기 (1. 기본설정 및 기초적인 클래스 설계)


 이번 게시글에서는 ASSIMP라이브러리를 이용하여 모델데이터를 불러오는 방법에 대해
게시하려고 합니다.

 포스팅하기에 앞서 이 글을 읽으실 때는 DirectX에 대한 기본적인 지식이 있으셔야
이해하실 수 있을 것입니다.

 이 글에 경우 DirectX12를 기반으로 설명할 것이고, 글을 보시는 분들이 버텍스버퍼가 무엇인지, 인덱스버퍼가 무엇인지에 대한 기본적인 지식이 있다는 가정 하에 글을 진행할
것입니다. (즉, 적어도 DirectX의 기본 세팅을 완료하고 6면체큐브를 화면에 띄우는 것
까지는 완료한 상태여야 이해를 하시기 편할 것입니다.)

 그럼 시작하겠습니다.

 먼저 ASSIMP 홈페이지에 접속하셔서 ASSIMP 라이브러리를 다운받아야 합니다.

 여러가지 버전이 존재하는데 저는 ver 3.1.1 윈도우 바이너리 데이터도 포함하는
파일을 내려받았습니다.

 이 파일을 받는 이유는 라이브러리 제작자가 직접 라이브러리를 이용하여 제작한 모델
뷰어를 제공하고, 또 여러가지 포맷의 샘플 모델 데이터를 제공하기 때문입니다.

 내려받은 파일 압축을 해제하면 많은 폴더가 존재하는데 여기서

  1. include 폴더
  2. bin32 혹은 bin64 폴더의 assimp.dll 파일
  3. lib32 혹은 lib64 폴더의 assimp.lib 파일
 이 3가지를 자신의 프로젝트로 복사해옵니다.
 32비트인지 64비트인지는 자신이 제작하는 응용프로그램에 맞추어 복사해오면 될
것입니다.

 그 후 자신의 응용프로그램에서 미리컴파일된 헤더('stdafx.h') 혹은 자신이 모델을 다룬
코드 헤더파일에
#include "assimp\Importer.hpp"
#include "assimp\cimport.h"
#include "assimp\postprocess.h"
#include "assimp\scene.h"
 
#pragma comment(lib, "assimp.lib")
cs
 위의 코드를 복사해줍니다.
(여기서 assimp\ 경로는 자신이 include폴더 내부의 소스코드를 위치시킨 경로)

 여기까지 완료하면 assimp를 사용하기 위한 기본적인 설정을 완료한 것입니다.



 위 사진은 앞선 포스터에서 언급한 ASSIMP의 모델데이터 구조입니다.

 보시는 바와 같이 최상단에 aiScene이 위치하고, 여기에 매쉬, 애니메이션, 노드가
저장되는 형태입니다.

 일단 다른 것은 무시하고 매쉬에 집중해봅시다.

 모델은 매쉬로 이루어져 있습니다. (1개 이상의 매쉬)

 또 매쉬는 매쉬의 이름, 노말정보, 버텍스정보, 뼈정보, 면(face)정보로 이루어져 있습니다.

 위의 그림에는 안나와있지만 텍스쳐 맵핑을 위한 정보 또한 가지고 있습니다.

 이 데이터들은 우리가 모델데이터를 화면상에 랜더링하기 위해 필요한 정보들입니다.

 먼저 이 정보를 저장하기 위한 구조체입니다.
struct vertexDatas
{
    XMFLOAT3    m_pos;
    XMFLOAT3    m_normal;
    XMFLOAT2    m_tex;
 
    vertexDatas() {}
    vertexDatas(XMFLOAT3& pos, XMFLOAT3& normal, XMFLOAT2& tex) : 
    m_pos(pos), m_normal(normal), m_tex(tex)
    { }
}
cs
 위의 구조체는 버텍스정보, 노말정보, 텍스쳐매핑정보를 가지는 구조체입니다.

 하지만 이것만으로는 부족하죠. 다이렉트상에서 모델을 랜더링하기 위해서는
버텍스 조합 순서, 즉 인덱스정보도 필요합니다.
struct mesh
{
    vector<vertexDatas>    m_vertices;
    vector<int>            m_indices;
    UINT                m_materialIndex;
    mesh() {
        m_materialIndex = 0;
    }
};
 
cs
위의 구조체는 매쉬의 정보를 담는 구조체입니다.
(vector의 경우 stl의 그 vector 입니다.)

 매쉬의 경우 버텍스로 이루어져 있고, 이 버텍스를 조합하기 위한 인덱스 정보 또한 가지고 있습니다.

 추가로 매쉬를 위한 매터리얼까지 저장하는 형태로 설계를 합니다.
 vector를 사용하는 이유는 단순히 다루기 편해서입니다.

class LoadModel
{
private:
    const aiScene*                m_pScene;        //모델 정보
    vector<mesh>                m_meshes;        //매쉬 정보
    vector<pair<string, Bone>>  m_Bones;        //뼈 정보
    UINT                            m_numVertices;
    UINT                            m_numMaterial;
 
public:
    LoadModel(const string& fileName);
    ~LoadModel();
    void InitScene();
    void InitMesh(UINT index, const aiMesh* pMesh);
    void SetMeshes(ID3D12Device *pd3dDevice, ID3D12GraphicsCommandList *pd3dCommandList);
    ModelMesh**    getMeshes()  { return m_ModelMeshes.data(); }
    UINT                getNumMesh() const { return (UINT)m_meshes.size(); }
};
cs
 LoadModel 클래스는 앞서 선언한 구조체를 기반으로 실제로 모델데이터를 읽어오기 위한 클래스입니다.

 클래스를 살펴보면 크게
  1. 모델의 정보를 담기 위한 m_pScene
  2. 매쉬의 정보를 담기 위한 m_meshes
 로 이루어져 있습니다.

LoadModel::LoadModel(const string& fileName)
{
    m_pScene = aiImportFile(fileName.c_str(), aiProcess_JoinIdenticalVertices |        // 동일한 꼭지점 결합, 인덱싱 최적화
        aiProcess_ValidateDataStructure |        // 로더의 출력을 검증
        aiProcess_ImproveCacheLocality |        // 출력 정점의 캐쉬위치를 개선
        aiProcess_RemoveRedundantMaterials |    // 중복된 매터리얼 제거
        aiProcess_GenUVCoords |                    // 구형, 원통형, 상자 및 평면 매핑을 적절한 UV로 변환
        aiProcess_TransformUVCoords |            // UV 변환 처리기 (스케일링, 변환...)
        aiProcess_FindInstances |                // 인스턴스된 매쉬를 검색하여 하나의 마스터에 대한 참조로 제거
        aiProcess_LimitBoneWeights |            // 정점당 뼈의 가중치를 최대 4개로 제한
        aiProcess_OptimizeMeshes |                // 가능한 경우 작은 매쉬를 조인
        aiProcess_GenSmoothNormals |            // 부드러운 노말벡터(법선벡터) 생성
        aiProcess_SplitLargeMeshes |            // 거대한 하나의 매쉬를 하위매쉬들로 분활(나눔)
        aiProcess_Triangulate |                    // 3개 이상의 모서리를 가진 다각형 면을 삼각형으로 만듬(나눔)
        aiProcess_ConvertToLeftHanded |            // D3D의 왼손좌표계로 변환
        aiProcess_SortByPType);                    // 단일타입의 프리미티브로 구성된 '깨끗한' 매쉬를 만듬
    if (m_pScene) {
        m_meshes.resize(m_pScene->mNumMeshes);
        m_numMaterial = m_pScene->mNumMaterials;
        m_numBones = 0;
        InitScene();
        m_ModelMeshes.resize(m_meshes.size());
    }
}
cs
생성자에서는 m_pScene에 실제 모델데이터를 불러와 임포트시켜줍니다.
이 때 사용하는 함수는 aiImportFile입니다.

 인자값으로 경로값을 포함한 파일이름과 설정값을 가집니다.
(설정에 대해서는 위의 코드에 주석으로 달아놓았습니다.)

 이때 중요한 것은 반드시 aiProcess_Triangulat를 사용해야 합니다.
대부분의 모델러가 모델을 제작할 때 삼각형을 기준으로 제작하기에 일반적인 경우에문제가 없으나 특수한 경우를 위해서는 꼭 처리를 해주는 것이 좋습니다.

 사용자가 DirectX 환경에서 작업을 하는 경우 aiProcess_ConvertToLeftHanded를 사용하여 왼손좌표계로 바꿔주는 것도 필요할 수 있습니다.

 위의 함수를 이용하여 모델데이터를 불러오는 데 성공하였을 경우 m_pScene에는
실제 모델의 데이터가 저장되었을 것입니다.

 혹여나 로딩을 못했을 경우 m_pScene은 nullptr일 것이고, 빈 포인터에서 의미없는 행위를 수행하지 않기 위해 if문을 통해 조건에 만족했을 경우에만 초기화작업을 수행하도록 설계를 해주었습니다.

 여기까지 수행하였으면 모델데이터를 불러오기 위한 기본설정 및 클래스설계를
마치고, 실제로 모델데이터를 불러오는 데 성공한 것입니다.

 실제로 응용프로그램에서 필요한 데이터(버텍스, 인덱스 등...)를 불러오는 방법에
대해서는 다음 게시글에서 설명하도록 하겠습니다.

2018년 2월 26일 월요일

ASSIMP 라이브러리


 위의 이미지는 ASSIMP 라이브러리에서 3D 포맷의 파일을 읽어드린 후 라이브러리
내부에서 파일을 저장하는 내부적인 구조입니다.

 ASSIMP 라이브러리의 장점으로 크게 2가지를 꼽을 수 있습니다.

  1. 어떤 포맷의 3D 파일을 로드하더라도 위의 구조로 변형해준다.
    즉 내가 로드하는 포맷의 구조가 어떻게 되있는지 몰라도 위의 구조만 숙지하고
    있으면 내가 원하는 파일을 다룰 수 있다.
  2. 구조가 직관적이다.
    즉 이해하기가 쉽습니다. 
 또 한가지 장점으로 꼽을 수 있는것은 오픈소스이므로 모든 소스코드가 공개되어있고,
관련 자료를 찾는 것도 무척 쉽습니다. 그렇기에 게임 엔진을 사용하는 것이 아닌 OpenGL이나 DirectX를 사용하시는 분께는 ASSIMP를 사용하는 것을 적극 추천드립니다.

 ASSIMP 라이브러리에 대한 소개는 이정도에서 마치고 다음 게시글부터는 실제로 파일을 불러오고 필요한 데이터를 추출하는 방법(버텍스 등...)에 대해 게시하려고 합니다.

2018년 2월 18일 일요일

DirectX 환경에서 3D 모델 랜더링을 위한 라이브러리

3D 모델 포맷으로 제일 많이 쓰이는 포맷은 FBX와 OBJ 포맷을 꼽을 수 있습니다.

이외에도 3ds, blend 등 정말 많은 포맷이 존재하지만

일반적으로 위에 언급한 FBX파일과 OBJ파일을 구하기 쉽습니다.

FBX 포맷과 OBJ 포맷은 각각 특징이 존재합니다만 여기에는 게시하지 않을 예정입니다.

사실 이 부분은 구글에 검색해보시면 정말 많은 자료를 자세히 접할 수 있습니다.

저같은 경우 OBJ 파일에는 애니메이션이 탑재되어 있지 않고, FBX 파일에는 애니메이션을 탑재시킬 수 있다 정도로 이해하고 있습니다.

FBX 포맷의 파일을 기준으로 이를 내 응용프로그램에서 사용하기 위해서는 2가지 방법이 있습니다.


  1. FBX SDK를 사용한다
  2. ASSIMP 라이브러리를 사용한다.
1번 FBX SDK는 관련 자료를 찾기가 매우 어렵습니다... 정확히는 Autodesk에서 제공하는
설명서(?)를 제외하고는 국내외에서 모두 자료를 찾기 힘든 단점이 있습니다.

또한 웹상에서 쉽게 구할수 있는 자료의 경우 응용프로그램을 실행하고 실시간으로 모델을 다루는 것이 아닌 (애니메이션 등) 별도의 익스포터 프로그램을 만들어 내 응용프로그램에서 필요한 정보들만 파싱하는 형태입니다.

예를 들어 내가 A라는 파일이 있고, 이 파일은 모델에 대한 버텍스데이터, UV데이터, 노말데이터, 그리고 30프레임의 애니메이션데이터를 가지고 있다고 가정합시다.

그렇다면 쉽게 접할 수 있는 FBX SDK 사용법에 대한 자료의 경우 나에게 필요한 정보들을 미리 파싱해서 내 응용프로그램에서 사용할 포맷으로 변환하는 방법을 제공합니다.

여기서 주목할 점은 애니메이션의 처리입니다.

애니메이션 데이터의 경우 각 노드의 매프레임 회전, 이동, 크기변화가 저장되어 있습니다.
(애니메이션 데이터의 구조에 대해서는 다른 포스터에서 더 자세히 설명하겠습니다.)

이를 처리하는 방법으로는 2가지 방법을 볼 수 있는데,

첫번째 방법으로는 이를 단순히 행렬의 형태로 가공하고, 응용프로그램에서 실시간으로 각 버택스에 곱해주는 방법이 있습니다.

두번째 방법으로는 변환행렬에 따른 각 버텍스에 대한 변화량도 미리 전부 계산해서 저장해두는 방법입니다.

첫번째 방법의 경우 셰이더 코드를 사용하는 것이 아닌 CPU상에서 이를 처리하기 때문에 응용프로그램에 걸리는 부하가 무척 심할 것입니다. 

두번째 방법의 경우는 최초에 익스포터 프로그램을 작성할 때 복잡하겠지만, 내 응용프로그램에서 데이터를 미리 계산된 데이터를 사용하기에 프로그램에 걸리는 부하가 훨씬 적을 것입니다.

이에 대한 자료는 웹 상에 FBX SDK 라고 검색하면 관련 자료를 찾을 수 있을 것입니다.

저는 이 방법을 사용하지 않았기에 자세한 정보는 제공할 수 없어 유감입니다.

자 이제 2번째 방법인 ASSIMP 라이브러리를 사용하는 방법입니다.

ASSIMP 라이브러리의 또 다른 이름은 Open Asset Import Library입니다. 

공식 사이트의 경우 http://assimp.sourceforge.net/ 로 접속하시면 됩니다.

이 라이브러리의 장점은 오픈소스라는 것입니다. github에 라이브러리가 전부 공개되어 있기에 간단히 다운받아 사용하면 됩니다.

두번째 장점은 Open Asset Import Library라는 이름의 걸맞게 정말 많은 포맷의 3D 데이터를 지원합니다.

앞서 언급한 OBJ와 FBX 포맷은 물론이고 dae, md2, 3ds 등등.. 30여가지의 3D 데이터 포맷의 임포터를 지원하고 있습니다.

저같은 경우는 3D 모델을 임포트하고, 애니메이션 처리를 할 때 ASSIMP 라이브러리를 사용하여 처리하고 있습니다.

다만 ASSIMP 라이브러리의 최대 단점이 있는데 웹 상에서 얻을 수 있는 모든 자료가 OpenGL 기반이라는 것입니다.

아시다시피 DirectX와 OpenGL의 경우 좌표계가 달라서 이를 그래도 적용하면 의도치 않은 결과가 도출되게 됩니다. (모델의 어그러지는 등...)

앞으로 DirectX 12의 환경에서 ASSIMP 라이브러리를 활용하여 3D 모델 데이터(대표적으로 FBX) 를 로드하고 애니메이션을 적용하는 방법에 대해 포스팅해보려고 합니다.