4 분 소요

모델 파일 읽기

대부분의 3D 모델은 디자이너 분들이 ‘손’으로 직접 만든 것!

대표적인 모델링 소프트웨어

  • blender, maya, 3d max 등등

Blender 사용법

블렌더는 ‘오픈 소스’이기에 무료로 사용이 가능
(https://www.blender.org/)

Image

블렌더를 킨 ‘첫 화면’
(박스는 지워주었다)

  • Modeling-> Add 를 통하여 3D 모델 추가가 가능

Image

‘원숭이’…? 도 있다

Image

(여러 사용법이 있지만 튜토리얼은 공식 페이지 등에서 찾을 수 있다)
(https://studio.blender.org/training/)

우리가 할 것은
3D 모델을 이러한 모델링 소프트웨어를 통하여
게임에서 사용할 수 있도록 편집하는 것!

Export(모델 저장 - 내보내기)

Image

  • File->Export를 통해 3D 모델을 다양한 확장자로 저장시킬 수 있다
    (언리얼 엔진 등에서는 주로 .fbx 등을 사용)
    (.gltf 는 ‘공개된’ 확장자 이기에 퀄리티가 좋아지는 중이라 카더라…)
    (.bvh : 애니메이션 데이터)

  • 여기서는 .obj를 사용
    • Wave Front?
      원래 .obj를 포맷을 만들어 제공하였던 회사
      (현재는 인수되어 해당 회사는 존재하지 않음)
  • 3D 파일이지만 ‘문서 편집기’로 열수도 있음

예시

# Blender v2.82 (sub 7) OBJ File: ''
# www.blender.org
mtllib Monkey.mtl
o Suzanne
v 0.437500 0.164062 0.765625
v -0.437500 0.164062 0.765625
v 0.500000 0.093750 0.687500
...

  • 좌표 (v), 노멀(vn), 텍스쳐 좌표(vt)
    삼각형 인덱스(f) 등이
    들어있음을 확인 가능

  • MTL 파일?
    머테리얼 파일
    Blender 에서 ‘재질’을 설정한 경우
    내용이 있음

근데 MTL 파일이 없다면…?

현재 예제의 Diff.png 파일들과
.obj 모델만 존재하는 상황…이라면?

  • 직접 머테리얼 매핑을 해주어
    MTL 파일을 만들어주자!
  1. fbx를 import로 가져오기

  2. 머테리얼(재질) 확인하기
    • Material Properties를 확인(빨간 구체 아이콘)
    • FBX에 기본 머테리얼이 있는지 확인하고 없으면 New로 새로운 머테리얼 생성
  3. Shader Editor로 이동
    • 상단 Shading 탭에서 Pricipled BSDF 노드를 찾자
  4. Diffuse 텍스쳐를 연결
    • Shift + A -> Texture > Image Texture 추가
      (3D viewport에 마우스를 대고 하면 안됨)
    • open을 통해 diff 텍스쳐 파일 선택
    • Principled BSDF의 BaseColor에 연결
  5. 뷰포트에서 확인하기
    • Material Preview 모드에서 확인가능!
  6. export로 내보내기!
    • .blender 로 저장하여 작업 재사용 가능
    • .obj 등으로 다시 내보내기

MTL 파일은 .obj 전용이다

  • obj에 지오메트리를 담고
    mtl 파일에 재질을 따로 저장하는 방식임

  • fbx는 자체적으로 Mesh,Material, Texture, 애니메이션 등을 전부 포함가능함

모델 로딩

  • 여기서는 assimp 라이브러리를 사용함

  • 역시 vcpkg 로 쉽게 다운 가능

  • .\vcpkg 를 통하여 assimp:x64를 다운

  • 이후 프로젝트에서 ‘추가 #using 라이브러리’에서 vcpkg의 include 부분을 선택하면 된다

모델 로딩 예제

struct MeshData {
    std::vector<Vertex> vertices;
    std::vector<uint32_t> indices; // uint32로 변경
    std::string textureFilename;
};
  • indices
    uint32 로 변경?
    복잡한 모델링의 경우는 int16의 범위를 넘어서는 경우가
    종종 발생할 수 있다고 하기에 안전하게 32로 사용
void ModelLoader::ProcessNode(aiNode *node, const aiScene *scene, Matrix tr) {
    Matrix m;
    ai_real *temp = &node->mTransformation.a1;
    float *mTemp = &m._11;
    for (int t = 0; t < 16; t++) {
        mTemp[t] = float(temp[t]);
    }
    m = m.Transpose() * tr;

    for (UINT i = 0; i < node->mNumMeshes; i++) {
        aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
        auto newMesh = this->ProcessMesh(mesh, scene);

        for (auto &v : newMesh.vertices) {
            v.position = DirectX::SimpleMath::Vector3::Transform(v.position, m);
        }

        meshes.push_back(newMesh);
    }

    for (UINT i = 0; i < node->mNumChildren; i++) {
        this->ProcessNode(node->mChildren[i], scene, m);
    }
}

MeshData ModelLoader::ProcessMesh(aiMesh *mesh, const aiScene *scene) {
    // Data to fill
    std::vector<Vertex> vertices;
    std::vector<uint32_t> indices;

    // Walk through each of the mesh's vertices
    for (UINT i = 0; i < mesh->mNumVertices; i++) {
        Vertex vertex;

        vertex.position.x = mesh->mVertices[i].x;
        vertex.position.y = mesh->mVertices[i].y;
        vertex.position.z = mesh->mVertices[i].z;

        vertex.normal.x = mesh->mNormals[i].x;
        vertex.normal.y = mesh->mNormals[i].y;
        vertex.normal.z = mesh->mNormals[i].z;
        vertex.normal.Normalize();

        if (mesh->mTextureCoords[0]) {
            vertex.texcoord.x = (float)mesh->mTextureCoords[0][i].x;
            vertex.texcoord.y = (float)mesh->mTextureCoords[0][i].y;
        }

        vertices.push_back(vertex);
    }

    for (UINT i = 0; i < mesh->mNumFaces; i++) {
        aiFace face = mesh->mFaces[i];
        for (UINT j = 0; j < face.mNumIndices; j++)
            indices.push_back(face.mIndices[j]);
    }

    MeshData newMesh;
    newMesh.vertices = vertices;
    newMesh.indices = indices;

    // http://assimp.sourceforge.net/lib_html/materials.html
    if (mesh->mMaterialIndex >= 0) {
        aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];

        if (material->GetTextureCount(aiTextureType_DIFFUSE) > 0) {
            aiString filepath;
            material->GetTexture(aiTextureType_DIFFUSE, 0, &filepath);

            std::string fullPath =
                this->basePath +
                std::string(std::filesystem::path(filepath.C_Str())
                                .filename()
                                .string());

            newMesh.textureFilename = fullPath;
        }
    }

    return newMesh;
}
  • 모델 구조는 보통 ‘트리’ 구조이기에
    ‘Node’나 ‘root’ 같은 표현이 사용

  • 재귀를 통하여 mesh를 읽어들인다

for (const auto &meshData : meshes) {
    auto newMesh = std::make_shared<Mesh>();
    AppBase::CreateVertexBuffer(meshData.vertices, newMesh->vertexBuffer);
    newMesh->m_indexCount = UINT(meshData.indices.size());
    AppBase::CreateIndexBuffer(meshData.indices, newMesh->indexBuffer);

    if (!meshData.textureFilename.empty()) {

        cout << meshData.textureFilename << endl;
        AppBase::CreateTexture(meshData.textureFilename, newMesh->texture,
                               newMesh->textureResourceView);
    }

    newMesh->vertexConstantBuffer = vertexConstantBuffer;
    newMesh->pixelConstantBuffer = pixelConstantBuffer;

    this->m_meshes.push_back(newMesh);
}
  • 읽어들인 데이터 중 ‘텍스쳐 파일 이름’을 이용하여
    텍스쳐 파일을 만들어준다
    (그리고 그것을 textureResourceView라는
    SRV에 저장하여 ‘텍스쳐’로 사용하겠다는 용도)

//Render()

// 버텍스/인덱스 버퍼 설정
for (const auto &mesh : m_meshes) {
    m_context->VSSetConstantBuffers(
        0, 1, mesh->vertexConstantBuffer.GetAddressOf());

    m_context->PSSetShaderResources(
        0, 1, mesh->textureResourceView.GetAddressOf());

    m_context->PSSetConstantBuffers(
        0, 1, mesh->pixelConstantBuffer.GetAddressOf());

    m_context->IASetInputLayout(m_basicInputLayout.Get());
    m_context->IASetVertexBuffers(0, 1, mesh->vertexBuffer.GetAddressOf(),
                                  &stride, &offset);
    m_context->IASetIndexBuffer(mesh->indexBuffer.Get(),
                                DXGI_FORMAT_R32_UINT, 0);
    m_context->IASetPrimitiveTopology(
        D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    m_context->DrawIndexed(mesh->m_indexCount, 0, 0);
}
  • 여러 Mesh에 대하여
    각각 draw를 해주는 모습

노멀이 없는 모델…??

// 노멀 벡터가 없는 경우를 대비하여 다시 계산
// 한 위치에는 한 버텍스만 있어야 연결 관계를 찾을 수 있음
for (auto &m : this->meshes) {

    vector<Vector3> normalsTemp(m.vertices.size(), Vector3(0.0f));
    vector<float> weightsTemp(m.vertices.size(), 0.0f);

    for (int i = 0; i < m.indices.size(); i += 3) {

        int idx0 = m.indices[i];
        int idx1 = m.indices[i + 1];
        int idx2 = m.indices[i + 2];

        auto v0 = m.vertices[idx0];
        auto v1 = m.vertices[idx1];
        auto v2 = m.vertices[idx2];

        auto faceNormal =
            (v1.position - v0.position).Cross(v2.position - v0.position);

        normalsTemp[idx0] += faceNormal;
        normalsTemp[idx1] += faceNormal;
        normalsTemp[idx2] += faceNormal;
        weightsTemp[idx0] += 1.0f;
        weightsTemp[idx1] += 1.0f;
        weightsTemp[idx2] += 1.0f;
    }

    for (int i = 0; i < m.vertices.size(); i++) {
        if (weightsTemp[i] > 0.0f) {
            m.vertices[i].normal = normalsTemp[i] / weightsTemp[i];
            m.vertices[i].normal.Normalize();
        }
    }
}
  • faceNormal을 통하여 Normal을 직접 계산하여 저장
    (각 점들간의 외적을 이용)

  • 다만 요새는 모델링 소프트웨어가 대부분 normal 값을 계산해준다

gltf 샘플 모델들

샘플 모델 링크

git clone 한 후,
원하는 모델을 읽을 수 있음!

ps) obj 파일은 vs로 열 수 있음!
(시간은 조금 걸리지만…)

예제에서 .fbx 파일이 ‘텍스쳐를 못 찾는 경우?’

현 예제는
fbx 파일이 있는 폴더 안에 ‘텍스쳐 이미지’ 파일들이
‘들어있다 가정하고 구현’한 것이기 때문

정리

Image

  • 지금껏 해온 다양한 DX 예제들이
    디자이너들이 열심히 만든 모델과 결합할때
    더 훌륭한 결과를 낼 수 있다는 점을 확인 가능

  • 모델링 엔진을 만들어 보려 한다면
    해당 부분의 ‘Model Loader’ 클래스를 조금 더 자세히 보는 것도 방법

  • 실제로 게임 엔진 등을 사용할때는
    이러한 과정이 많이 스킵되지만
    내부에서 ‘어떠한 일’이 일어나는지 안다면
    구조의 이해, 응용, 문제의 원인과 해결책 등에 대한
    다양한 이해도가 생기게 된다

댓글남기기