페이지

레이블이 GamePrograming인 게시물을 표시합니다. 모든 게시물 표시
레이블이 GamePrograming인 게시물을 표시합니다. 모든 게시물 표시

2018년 3월 4일 일요일

ASSIMP 라이브러리를 이용한 모델데이터 불러오기 (3. 데이터 출력을 위한 HLSL코드)

 이번 게시글에서는 이전 게시글에서 불러왔던 데이터를 응용프로그램 상에서 랜더링하기 위한 셰이더 코드에 대해서 간단히 살펴보도록 하겠습니다.
(이 게시글에서는 버텍스셰이더에 대해서만 다룰 것입니다.)

 먼저 데이터를 불러오기 위한 버텍스 셰이더 구조체 모형입니다.
struct VS_MODEL_INPUT
{
    float3 pos : POSITION;
    float3 normal : NORMAL;
    float2 uv : TEXCOORD;
    uint4 index : BORNINDEX;
    float3 weight : WEIGHT;
};
cs
 순서대로 위치, 법선벡터, UV, 본 인덱스, 가중치입니다.

 우선 본 인덱스와 가중치는 애니메이션을 위한 변수입니다. 
 이에 대해서는 다음 애니메이션에 대한 게시글에서 좀 더 자세히 다루도록 하겠습니다.
(지금 예상으로는 애니메이션에 관한 게시글은 3~4개정도 될 것으로 예상하고 있습니다.)

 다이렉트 상에서 위 포맷에 맞추어 버텍스 셰이터를 컴파일하면 데이터가 정상적으로 로드될 것입니다.

 이번 구조체는 버텍스셰이더를 적절히 가공해서 픽실 셰이더로 출력할 구조체 모형입니다.
struct VS_TEXTURED_LIGHTING_OUTPUT
{
    float4 position : SV_POSITION;
    float3 positionW : POSITION;
    float3 normalW : NORMAL;
    float2 uv : TEXCOORD;
#ifdef _WITH_VERTEX_LIGHTING
    float4 color : COLOR;
#endif
};
cs
 여기까지 쓰고 보니 어떻게 설명해야 할지 어렵네요;
 (사실 셰이더에 관련된 부분은 저 말고 다른 친구가 주로 다뤘었습니다. 그냥 이런 포맷으로 출력했구나 정도만 보시는 것이 좋을 것 같습니다.)

 그리고 마지막으로 오브젝트의 위치, 재질, 뼈를 저장할 cbuffer입니다.
cbuffer cbGameObjectInfo : register(b2)
{
    matrix        gmtxGameObject;
    matrix        gBoneTransforms[96];

};
cs
 위에서부터 차례대로 오브젝트의 위치, 뼈, 재질입니다.

 위치는 게임월드 상에서의 오브젝트의 위치입니다.
 
 두번째로 뼈는 오브젝트의 애니메이션을 위한 변수입니다. 이에 대해서는 다른 게시글에서 더 자세히 다루도록 하겠습니다.
 
 이제 필요한 구조체 및 cbuffer의 소개는 끝났습니다.

 본격적으로 코드를 살펴보죠.

VS_TEXTURED_LIGHTING_OUTPUT VSModel(VS_MODEL_INPUT input)
{
    VS_TEXTURED_LIGHTING_OUTPUT output;
 
    output.positionW = (float3)mul(float4(input.pos, 1.0f), gmtxGameObject);
    //output.positionW = (float3)mul(float4(posL, 1.0f), gmtxGameObject);
    
    output.normalW = mul(input.normal, (float3x3)gmtxGameObject);
    //output.normalW = mul(normalL, (float3x3)gmtxGameObject);
    output.position = mul(mul(float4(output.positionW, 1.0f), gmtxView), gmtxProjection);
    output.uv = input.uv;
#ifdef _WITH_VERTEX_LIGHTING
    output.normalW = normalize(output.normalW);
    output.color = Lighting(output.positionW, output.normalW);
#endif
    return(output);
}
cs
 (주석친 부분은 애니메이션을 위한 부분이므로 지금은 무시하시면 됩니다.)

 사실 버텍스 셰이더 코드 자체도 그리 어렵지 않습니다.

 출력할 픽셀셰이더 구조체를 선언해주고,
 불러온 버텍스셰이더를 위한 데이터를 픽셀셰이더에 맞추어 적절히 가공해준 뒤 픽셀셰이더로 리턴해주면 완료입니다.

 여기서 적절한 가공이란 위치에 월드변환행렬, 카메라변환행렬, 투영변환행렬을 곱해주는 것을 뜻합니다.

 셰이더코드에 대해서는 이정도까지만 설명하고 마치도록 하겠습니다.

 다음 게시글부터는 불러온 모델데이터(FBX 등...)에서 애니메이션 데이터를 추출하고, 이를 자신의 응용프로그램에 적용하는 방법에 대해 설명해보려고 합니다.

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월 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) 를 로드하고 애니메이션을 적용하는 방법에 대해 포스팅해보려고 합니다.