게임 프로그래머의 만담, 게임코디 2nd
+2 투표
1.3k views 만담 (1.9kp)
수정됨 , 1.3k views

지난 강좌

1. OpenGL 윈도우 만들기

2. 삼각형 그리기

3. 알록달록한 삼각형 그리기

 


전체 소스 코드 : https://github.com/sunduk/opengl-3d-examples

 

4. 텍스쳐 입히기


이미지 파일 읽기

세상에는 다양한 이미지 파일 형식이 존재 합니다. 하나의 파일 형식 또한 복잡한 구조로 되어 있기 때문에 코드로 구현 하는 것 역시 간단하지 않습니다. 따라서 우리는 이 작업을 쉽게 처리 해 주는 라이브러리를 이용 해 보도록 하겠습니다. 여기서 사용 할 라이브러리는 stb_image라는 오픈 소스 입니다. stb_image는 헤더 파일 하나로 구현되어 있기 때문에 include만으로 프로젝트에 포함 시켜 빌드 할 수 있어 사용하기 매우 편리한 라이브러리 입니다. 아래 코드는 stb_image를 사용 하여 이미지 파일을 로드 한 뒤 OpenGL 텍스쳐 버퍼에 올리는 코드입니다.

#define STB_IMAGE_IMPLEMENTATION
#include "stb/stb_image.h"

unsigned int LoadTexture(const std::string& path)
{
    // (1) OpenGL 텍스쳐 버퍼 생성.
    unsigned int textureId{};
    glGenTextures(1, &textureId);
    glBindTexture(GL_TEXTURE_2D, textureId);

    // wrap type.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

    // texture sampling.
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    // (2) Load Texture.
    int width{};
    int height{};
    int channels{};
    
    // OpengL은 왼쪽 하단이 0,0 이기 때문에 y좌표를 뒤집어 줘야 합니다.
    stbi_set_flip_vertically_on_load(1);

    // 이미지 파일 로드.  
    unsigned char* imageData = stbi_load(path.c_str(), &width, &height, &channels, 0);
    
    // (3) OpenGL 텍스쳐 버퍼에 올리기.
    if (imageData)
    {
        switch (channels)
        {
        case 3:    // alpha channel이 없는 이미지.
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imageData);
            break;

        case 4:    // alpha channel이 존재 하는 이미지.
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
            break;
        }

        glGenerateMipmap(GL_TEXTURE_2D);
        
        // 메모리 해제.
        stbi_image_free(imageData);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }

    return textureId;
}

(1) 이미지 파일을 OpenGL에서 사용 하기 위해 서는 텍스쳐 라는 버퍼에 이미지 데이터를 올리는 작업이 필요 합니다. 먼저 텍스쳐 버퍼를 생성 하고 wrap mode와 sampling값을 설정 해 줍니다.

wrap mode는 텍스쳐를 폴리곤에 입힐 때 빈 공간을 어떻게 채울지에 대한 옵션 입니다. 일단 GL_REPEAT로 설정 한 뒤 나중에 다른 타입에 대해서 살펴보도록 하겠습니다.
sampling이란 텍스쳐가 원본 크기보다 크거나 작게 렌더링 되었을 때 픽셀의 색상을 결정 하는 옵션 입니다. 지금은 GL_NEAREST로 설정 한 뒤 나중에 자세히 알아보도록 하겠습니다.

(2) 다음은 stbi_load함수를 호출 하여 이미지 파일을 로드 합니다. 그 전에 stbi_set_flip_vertically_on_load(1)함수를 호출 해서 y좌표를 뒤집어 줍니다. 보통 이미지 파일은 왼쪽 위가 0,0이고 아래로 갈 수록 y좌표가 커지는 형태로 되어 있습니다. 하지만 OpenGL 텍스쳐는 왼쪽 아래가 0,0으로 되어 있고 위로 올라갈 수록 y좌표가 커지는 방식 입니다. 따라서 y좌표를 거꾸로 뒤집어 줘야 원하는 결과가 나오게 됩니다.

(3) 이미지를 로드한 뒤에는 glTexImage2D함수를 호출 하여 OpenGL 텍스쳐 버퍼에 올려야 합니다. 이 때 이미지 특성에 따라 알파 채널이 존재 하는 경우와 없는 경우를 구분 해 줘야 합니다. 알파 채널은 투명으로 렌더링 할 부분을 표시 해 놓은 공간을 의미 합니다. 투명값이 필요 없는 완전 불투명한 이미지를 사용한다면 GL_RGB옵션을 적용하고, 투명값이 일부 들어간 이미지를 사용할 때에는 GL_RGBA옵션을 적용해 줍니다. stbi_load 함수에서 리턴하는 channels변수를 통해서 알파값의 존재 유무를 확인할 수 있습니다.

이미지 데이터를 OpenGL 텍스쳐에 다 올렸다면 밉맵 설정을 해 주고 더이상 사용할 필요가 없는 메모리는 해제 해 줍니다. 밉맵은 원래 이미지 사이즈보다 작은 사이즈의 텍스쳐를 미리 생성 해 두고 화면에 렌더링 되는 크기에 따라 작은 텍스쳐를 사용하게 해 주는 기술 입니다. 우리는 특별히 설정할 필요가 없기 때문에 기본값으로 알아서 생성하도록 내버려 두겠습니다.

 

UV좌표 설정

이미지를 로드 하여 OpenGL 텍스쳐 버퍼에 올리는 것 까지 완료 하였지만 이것을 화면에 출력 하려면 UV좌표 라는 것을 설정 해 줘야합니다. 이것은 삼각형의 각 버텍스에 텍스쳐 좌표를 연결 시켜 주는 작업인데 보통 UV매핑 이라고 부릅니다. 복잡한 3D 모델의 경우 아티스트가 작업한 모델 파일에 UV좌표가 같이 익스포트 되어 나오지만 우리는 간단한 삼각형만 렌더링 할 것이므로 프로그램 코드에서 직접 입력해 보겠습니다.

// (1)
struct VertexUV
{
    glm::vec3 mPosition{ 0,0,0 };
    glm::vec3 mColor{ 0,0,0 };
    glm::vec2 mUV{ 0,0 };

    VertexUV()
    {
    }

    VertexUV(const glm::vec3& position, const glm::vec3& color, const glm::vec2& uv)
    {
        mPosition = position;
        mColor = color;
        mUV = uv;
    }
};

// (2)
void CreateRectangle()
{
        glm::vec2 uvLeftTop = glm::vec2(0.0f, 1.0f);
        glm::vec2 uvLeftBottom = glm::vec2(0.0f, 0.0f);
        glm::vec2 uvRightBottom = glm::vec2(1.0f, 0.0f);
        glm::vec2 uvRightTop = glm::vec2(1.0f, 1.0f);

        float scale = 0.8f;
        // left top
        mVertices.push_back(VertexUV(scale * glm::vec3(-1.0f, 1.0f, 0.0f), glm::vec3(1, 0, 0), uvLeftTop));

        // left bottom
        mVertices.push_back(VertexUV(scale * glm::vec3(-1.0f, -1.0f, 0.0f), glm::vec3(0, 1, 0), uvLeftBottom));

         // right bottom
        mVertices.push_back(VertexUV(scale * glm::vec3(1.0f, -1.0f, 0.0f), glm::vec3(0, 0, 1), uvRightBottom));

          // right top
        mVertices.push_back(VertexUV(scale * glm::vec3(1.0f, 1.0f, 0.0f), glm::vec3(0, 0, 1), uvRightTop));

        mIndices.push_back(0);  // first vertex.
        mIndices.push_back(1);  // second vertex.
        mIndices.push_back(2);  // third vertex.

        mIndices.push_back(0);  // first vertex.
        mIndices.push_back(2);  // second vertex.
        mIndices.push_back(3);  // third vertex.
}

(1) 각 버텍스 마다 UV좌표를 갖고 있어야 하므로 기존의 Vertex구조체에 glm::vec2 mUV 변수를 추가 하였습니다. 여기서는 기존 코드와 헷갈리지 않도록 VertexUV라는 구조체를 새로 생성 하였습니다. 물론 기존 구조체에 UV변수만 추가 하여 사용 하셔도 무방 합니다.

(2) 사각형을 생성 하고 UV좌표를 매핑 하는 코드 입니다. 네 개의 버텍스에 해당하는 UV좌표를 설정 하여 버텍스 데이터 생성시 파라미터로 같이 넣어 줍니다. 여기서 독특한 점은 UV좌표는 0~1 사이의 값만 갖고 있다는 것입니다. 이미지가 아무리 커도 혹은 아주 작아도 UV좌표는 항상 0~1만 취급한다고 생각하면 됩니다.

 

void CreateVertexBuffer()
{
    ...
    // Bind VAO.
    glBindVertexArray(mVertexArrayObjectId);
    {
        ...
        // 버텍스 속성(attribute)을 설정한다.
        glEnableVertexAttribArray(0);
        {
            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexUV), nullptr);
        }

        glEnableVertexAttribArray(1);
        {
            glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexUV), (void*)offsetof(VertexUV, mColor));
        }
        
        // 추가된 부분
        // UV 속성을 설정한다.
        glEnableVertexAttribArray(2);
        {
            glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(VertexUV), (void*)offsetof(VertexUV, mUV));
        }
    }

    glBindVertexArray(0);
}

이전 글에서 버텍스 컬러 속성을 설정 했던 것 처럼 UV속성이 추가 되었으니 이것도 마찬가지로 버텍스 버 퍼의 레이아웃에 설정 해 줘야 합니다. 이렇게 되면 버텍스는 다음과 같은 모습으로 메모리에 배치 됩니다.

 

 

셰이더 코드 수정

아직은 셰이더에 대해서 정확한 실체를 모르기 때문에 셰이더 코드를 수정 하는 일이 따분할 수 있겠지만 일단 흐름대로 따라가 보도록 하겠습니다. 나중에 셰이더를 본격적으로 배우게 되면 3D 프로그래밍에서 필수적이고 강력한 도구 라는 것을 알게 될 것입니다.

먼저 셰이더에서 우리가 로딩한 텍스쳐를 사용할 수 있도록 Render함수에서 텍스쳐ID를 넘겨 주는 부분을 작성 합니다.

void Render()
{
    // 렌더링에 적용할 셰이더 프로그램을 설정한다.
    glUseProgram(mDefaultShaderID);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, mTextureId0);
    glUniform1i(glGetUniformLocation(mDefaultShaderID, "sample0"), 0);
    ...
}

 

이전 글에서도 등장했던 CreateDefaultShader함수를 아래 내용 처럼 수정 합니다.

void CreateDefaultShader()
{
    // 버텍스 셰이더 코드.
    std::string vertexCode = "#version 330 core\n"
        "layout(location = 0) in vec3 position;\n"
        "layout(location = 1) in vec3 vertex_color;\n"

        // 추가된 부분.
        "layout(location = 2) in vec2 texture_uv;\n"
        
        "out vec3 VertexColor;\n"
        
        // 추가된 부분.
        "out vec2 TextureUV;\n"
        
        "void main()\n"
        "{\n"
        "   gl_Position = vec4(position, 1.0f);\n"
        "   VertexColor = vertex_color;\n"
        
        // 추가된 부분.
        "   TextureUV = texture_uv;\n"
        "}\n";

    // 프래그먼트 셰이더 코드.
    std::string fragmentCode = "#version 330 core\n"
        "in vec3 VertexColor;\n"

        // 추가된 부분.
        "in vec2 TextureUV;\n"
        
        "out vec4 Color;\n"
        
        // 추가된 부분.
        "uniform sampler2D sample0;\n"

        "void main()\n"
        "{\n"
        // 텍스쳐에서 컬러값을 읽어 적용 시킨다.
        "    Color = texture(sample0, TextureUV);\n"
        "}\n";
    ...
}

 

에러 없이 잘 실행 되었다면 이렇게 이미지가 입혀진 폴리곤이 출력 될 것입니다.

 

 

다양한 텍스쳐 속성들

Wrap Mode

UV좌표를 설정 할 때 각 버텍스 네 귀퉁이에 맞게 0 또는 1을 입력 하면 사각형에 딱 맞게 출력 됩니다. 하지만 1보다 큰 값 이나 0보다 작은 값을 넣으면 폴리곤의 일부분에만 이미지가 출력 되고 나머지 공간은 비어 있게 됩니다. 이 남는 공간을 어떤 방식으로 채울지 텍스쳐 속성값을 통해서 결정할 수 있습니다.

UV좌표를 다음과 같이 수정 하고 옵션들을 바꿔 보면서 어떻게 렌더링 되는지 확인 해 보겠습니다.

void CreateRectangle()
{
    ...
    uvLeftTop = glm::vec2(0.0f, 1.0f);
    
    // 원래 좌표는 0,0 이지만 아래로 두배만큼 더 내려 가도록 설정한다.
    uvLeftBottom = glm::vec2(0.0f, -2.0f);
    
    // 원래 좌표는 1,0 이지만 오른쪽으로 두배, 아래로 두배만큼 더 내려가도록 설정한다.
    uvRightBottom = glm::vec2(3.0f, -2.0f);
    
    // 원래 좌표는 1,1 이지만 오른쪽으로 두배 더 가도록 설정한다.
    uvRightTop = glm::vec2(3.0f, 1.0f);
    ...
}

unsigned int LoadTexture(const std::string& path)
{
    ...
    // GL_REPEAT, GL_MIRRORED_REPEAT, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_BORDER
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    
    ...
    
    // GL_CLAMP_TO_BORDER일 경우 아래 지정된 색상으로 빈 공간을 출력 하도록 합니다.
    const float borderColor[] = { 1.0f, 0.0f, 1.0f, 1.0f };
    glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
    ...
}

 

GL_REPEAT

이미지를 반복 합니다.

 

GL_MIRRORED_REPEAT

이미지를 반복 할 대 마다 뒤집어서 출력 합니다.

 

GL_CLAMP_TO_EDGE

이미지의 끝 엣지 부분만 반복 하여 쭉 늘어난 모습입니다.

 

GL_CLAMP_TO_BORDER

빈 공간을 지정된 컬러값으로 채웁니다.

 

전체 소스 코드 : Example04.h / cpp (https://github.com/sunduk/opengl-3d-examples)

이번 시간에는 텍스쳐를 입혀봤습니다. 다음에는 3D 프로그래밍에서 아주 아주 중요한 '변환(Transform)' 이라는 것에 대해 배워보겠습니다.

>>> 다음 강좌 : 5. 변환

감사합니다.

 

만담에 대한 답글을 하려면 로그인 또는 가입해야합니다.

게임 프로그래머의 만담 커뮤니티, 게임코디 입니다

온종일 코드와 씨름하는 우리들의 외로움을 삭이고자 합니다