ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [DirectX11] obj 파일로 모델 임포트
    DirectX11 2023. 12. 7. 17:10

     

     

    만들어져있는 모델을 넣기 위해서 obj파일을 읽어서 정점 정보를 처리하는 클래스를 만들었다.

    Assimp도 써봤는데 내가 사용하려는 모델이 블렌더에서 익스포트한 모델이어서 설정이 잘못된건지 제대로 임포트하지 못했다.

    face의 경우 CCW를 CW로 바꿔주는 플래그 등 오른손 좌표계를 왼손 좌표계로 바꿔주는 플래그를 지정해줘도 삼각형이 제대로 만들어지지 않거나 원하는 형태로 임포트되지 않았다.

    Assimp의 문제가 아니라 사용자의 문제일 것이다.

     

    블렌더 Export 옵션

    블렌더의 Export 옵션에 Forward Axis와 Up Axis가 있는데 기본 설정은 저렇다.

    해당 옵션이 무엇을 기준으로 Forward이고 Up인지 검색해봐도 명확하게 알려주는 글을 찾을 수 없었다. (해당 모델을 사용하는 곳 기준으로 정해주면 되는 듯 하지만 확실하지는 않다.)

    그래서 그냥 저 상태로 익스포트 하면 블렌더에서 보이는 형태 그대로 임포트하도록 직접 만들었다.

     

     

     


     

     

     

    obj파일의 구조는 단순한 편이어서 어렵지 않게 만들 수 있다.

    60 X 60 X 1 크기인 직육면체의 모델은 아래와 같은 구조로 되어있다.

    v -30.000000 -1.000000 30.000000
    v -30.000000 0.000000 30.000000
    v -30.000000 -1.000000 -30.000000
    v -30.000000 0.000000 -30.000000
    v 30.000000 -1.000000 30.000000
    v 30.000000 0.000000 30.000000
    v 30.000000 -1.000000 -30.000000
    v 30.000000 0.000000 -30.000000
    vn -1.0000 -0.0000 -0.0000
    vn -0.0000 -0.0000 -1.0000
    vn 1.0000 -0.0000 -0.0000
    vn -0.0000 -0.0000 1.0000
    vn -0.0000 -1.0000 -0.0000
    vn -0.0000 1.0000 -0.0000
    vt 0.020000 0.000000
    vt 0.000000 1.000000
    vt 0.000000 0.000000
    vt 0.020000 0.000000
    vt 0.000000 1.000000
    vt 0.000000 0.000000
    vt 0.020000 -0.000000
    vt 0.000000 1.000000
    vt 0.000000 0.000000
    vt 0.020000 0.000000
    vt 0.000000 1.000000
    vt 0.000000 0.000000
    vt 2.000000 0.000000
    vt 0.000000 2.000000
    vt 0.000000 0.000000
    vt 0.020000 1.000000
    vt 0.020000 1.000000
    vt 0.020000 1.000000
    vt 0.020000 0.999989
    vt 2.000000 2.000000
    s 0
    f 2/1/1 3/2/1 1/3/1
    f 4/4/2 7/5/2 3/6/2
    f 8/7/3 5/8/3 7/9/3
    f 6/10/4 1/11/4 5/12/4
    f 7/13/5 1/14/5 3/15/5
    f 4/13/6 6/14/6 8/15/6
    f 2/1/1 4/16/1 3/2/1
    f 4/4/2 8/17/2 7/5/2
    f 8/7/3 6/18/3 5/8/3
    f 6/10/4 2/19/4 1/11/4
    f 7/13/5 5/20/5 1/14/5
    f 4/13/6 2/20/6 6/14/6

    v는 정점의 위치

    vn은 정점 노멀벡터의 방향

    vt는 정점의 텍스쳐 좌표(UV)

    f는 삼각형의 면을 이루는 v / vt / n 의 인덱스가 한 줄에 3개 포함되어있다. (사각형이면 한 줄에 4개)

     

    간단하게 한 줄씩 읽어서 맨 앞의 문자를 기준으로 정보를 따로 저장하면 된다.

     

    주의할 점은 obj파일은 오른손 좌표계를 기준으로 저장되어있다는 점이다.

    다이렉트X는 왼손 좌표계를 사용하므로 z 좌표는 뒤집고, 삼각형의 면을 읽는 순서도 CCW(시계 반대 방향)에서 CW(시계 방향)으로 바꿔야 한다.

    그리고 오른손 좌표계는 텍스쳐 좌표가 왼쪽 하단이 (0, 0)이다.

    왼손 좌표계에서는 왼쪽 상단이 (0, 0)이므로 y축을 뒤집어야 한다.

    // ObjReader.h
    class ObjReader
    {
    //...
    protected:
        std::vector<float*> vertices;
        std::vector<float*> normals;
        std::vector<float*> uvs;
        std::vector<int**> faces;
    //...
    }
    
    
    
    // ObjReader.cpp
    void ObjReader::read_file(std::string file_path)
    {
        file.open(file_path);
        // 파일이 제대로 열렸는지 확인.
        if (!file.is_open())
        	return;
    
        std::string line;
        while (std::getline(file, line))
        {
            std::stringstream tokenStream(line);
            std::vector<std::string> tokens;
            std::string token;
            // 한 줄의 문자열을 공백 기준으로 모두 나눠서 vector에 저장.
            while (std::getline(tokenStream, token, ' '))
            {
                tokens.push_back(token);
            }
    
            if (tokens[0] == "v")
            {
                float* vertex = new float[3];
                for (int i = 0; i < 3; i++)
                {
                    vertex[i] = std::stof(tokens[i + 1]);
                }
                vertex[0] = std::stof(tokens[1]);
                vertex[1] = std::stof(tokens[2]);
                vertex[2] = -std::stof(tokens[3]);
                vertices.push_back(vertex);
            }
            else if (tokens[0] == "vn")
            {
                float* normal = new float[3];
                for (int i = 0; i < 3; i++)
                {
                    normal[i] = std::stof(tokens[i + 1]);
                }
                normal[0] = std::stof(tokens[1]);
                normal[1] = std::stof(tokens[2]);
                normal[2] = -std::stof(tokens[3]);
                normals.push_back(normal);
            }
            else if (tokens[0] == "vt")
            {
                float* uv = new float[3];
                for (int i = 0; i < 2; i++)
                {
                    uv[i] = std::stof(tokens[i + 1]);
                }
                uv[0] = std::stof(tokens[1]);
                uv[1] = 1 - std::stof(tokens[2]);
                uvs.push_back(uv);
            }
            else if (tokens[0] == "f")
            {
                int** face = new int* [3];
                for (int i = 0; i < 3; i++)
                {
                    face[i] = new int[3];
                    std::stringstream ss(tokens[i + 1]);
                    std::string val;
                    int cnt = 0;
                    while (std::getline(ss, val, '/'))
                    {
                        face[i][cnt] = std::stoi(val) - 1;
                        cnt++;
                    }
                }
                faces.push_back(face);
            }
        }
        file.close();
    }

    단순하게 적은 코드다.

    여기까지는 단순하게 파일을 읽는 과정이고, 다음으로는 다이렉트x에서 사용할 수 있게 정보를 정리해야한다.

     


     

    다이렉트x에서는 정점 정보의 배열과 삼각형 면을 이루는 정점의 인덱스 배열을 받는다.

    예를 들어 육면체에는 6개의 사각형 면이 있고, 하나의 사각형은 두개의 삼각형으로 나눌 수 있으니, 12개의 삼각형이 존재한다.

    그리고 하나의 삼각형에는 정점이 3개씩 있으니 정점은 최대 36개 필요하고, 따라서 인덱스도 36개 필요하다.

    원래는 이렇게 GPU로 전달되어야하는데 obj파일의 경우에는 파일의 크기를 줄이기 위해서 중복되는 것들은 최대한 줄여서 저장되어있다.

    그것을 다시 풀어서 GPU에 전달해줘야 한다.

     

    따라서 face 정보를 하나씩 읽으며 정점의 위치, 노멀 방향, 텍스쳐 좌표 정보가 담긴 구조체를 순서대로 3개씩 생성해서 정점 배열에 넣고, 인덱스 배열에는 단순하게 0부터 하나씩 늘어나는 값을 넣었다.

     

    추가로 모델에 평면 사각형이 존재하면 6개의 정점중에 위치, 노멀 방향, 텍스쳐 좌표가 모두 동일한 두 정점 쌍이 2개 존재해서 4개로 줄일 수도 있다.

    Assimp는 해당 옵션을 지원하지만, 나는 여기까지는 구현하지 않고 넘어갔다.

    struct VertexInput
    {
        XMFLOAT3 Position;
        XMFLOAT3 Normal;
        XMFLOAT2 TexCoord;
        XMFLOAT3 Tangent;
    };
    
    
    
    // ...
    int vert_cnt = 0;
    for (int** f : faces)
    {
        // CCW -> CW
        int* vert_info = f[0];
        float* v0 = obj_reader.get_vertex(vert_info[0]);
        float* u0 = obj_reader.get_uv(vert_info[1]);
        float* n0 = obj_reader.get_normal(vert_info[2]);
        VertexInput vert0 = make_Vertex(v0, n0, u0);
    
        vert_info = f[2];
        float* v1 = obj_reader.get_vertex(vert_info[0]);
        float* u1 = obj_reader.get_uv(vert_info[1]);
        float* n1 = obj_reader.get_normal(vert_info[2]);
        VertexInput vert1 = make_Vertex(v1, n1, u1);
    
        vert_info = f[1];
        float* v2 = obj_reader.get_vertex(vert_info[0]);
        float* u2 = obj_reader.get_uv(vert_info[1]);
        float* n2 = obj_reader.get_normal(vert_info[2]);
        VertexInput vert2 = make_Vertex(v2, n2, u2);
    
        // Calc Tangent
        vert0.Tangent = CalcTangent(vert0, vert1, vert2);
        vert1.Tangent = CalcTangent(vert1, vert2, vert0);
        vert2.Tangent = CalcTangent(vert2, vert0, vert1);
    
        vertices[vert_cnt] = vert0;
        indices[vert_cnt] = vert_cnt;
        vert_cnt++;
        vertices[vert_cnt] = vert1;
        indices[vert_cnt] = vert_cnt;
        vert_cnt++;
        vertices[vert_cnt] = vert2;
        indices[vert_cnt] = vert_cnt;
        vert_cnt++;
    }

    VertexInput이 정점의 정보가 담긴 구조체다.

    InputLayout도 이 구조체에 맞게 설정했다.

    D3D11_INPUT_ELEMENT_DESC layout[] =
    {
        {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
        {"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0},
        {"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0},
        {"TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0}
    };

     

    그 다음에 버텍스 버퍼, 인덱스 버퍼를 모델에 맞게 설정해서 생성한 다음에 Context에 설정해주고 DrawIndexed를 하면 된다.

     

     

     


     

     

     

    테스트용으로 삼각뿔을 만들어서 방향이 맞게 임포트 되는지 확인했다.

     

    블렌더는 오른손 좌표계를 사용하고, Z가 up, -Y가 forward다.

    따라서 앞은 평면이고 뒤는 뾰족한 형태다.

     

    익스포트 옵션은 이렇게 했다.

     

    다이렉트x에서 렌더링 한 결과는 아래처럼 앞 뒤가 바뀐 상태로 임포트 되었다.

     

    알아보기는 좀 어렵지만 왼쪽 하단이 Z 방향이고, 그쪽으로 표족한 상태로 임포트 되었다.

    이 정도는 블렌더의 익스포트 옵션에서 Forward Axis만 바꾸면 될 듯 하다.

     

    Forward Axis를 -Z에서 Z로 바꾸면 아래처럼 된다.

     

     

    인터넷에 공개되어있는 무료 모델도 임포트 해봤다.

     

    샤워 헤드 위에서 봤을 때
    샤워 헤드 아래쪽에서 봤을 때

    마지막은 주방에서 사용하는 칼 종류 중 하나인듯 한데 가끔씩 이런식으로 삼각형이 나오지 않는 문제가 있다.

    아마도 폴리곤이 너무 많아서 생기는 문제일 것이다.

     

    해당 모델의 정점 개수다.

    인덱스 정보를 버퍼에 넣을때에는 unsigned short 타입인 WORD로 보내주는데 unsigned short 타입은 최대 65535까지만 저장할 수 있어서 68771까지의 인덱스값을 저장할 때 오버플로우가 발생한다.

    unsigned shortWORDunsigned intUINT로 바꾸고, 인덱스 버퍼를 설정할 때에도 DXGI_FORMAT_R16_UINT 에서 DXGI_FORMAT_R32_UINT로 바꿔서 해결했다.

     

    결과는 아래 이미지처럼 제대로 그려진다.

     

     

     

     

     

     

Designed by Tistory.