본문 바로가기

게임엔진/크로스플랫폼 : HazelEngine

230501 자체 엔진 개발 : Texture System

https://github.com/ohbumjun/GameEngineTutorial/commit/78b8a6bc619757867e8fd1b1c0997972fe40eccb

 

feat(Engine) Create Texture, Texture Sub Classes · ohbumjun/GameEngineTutorial@78b8a6b

Show file tree Showing 11 changed files with 8,415 additions and 91 deletions.

github.com

 

Texture 클래스 구성

Texture
 - Texture2D
    	- OpenGLTexture2D
        - VulkanTexture2D

 - Texture3D
    	- OpenGLTexture3D
        
   ...
   ...

기본적인 클래스 구조는 위와 같다.

 

1) 형태와 관계없이 Texture 라는 공통 형태를 처리해야 하는 경우가 있으므로 최종 Base Class 로 세팅한다.

2) 2d, 3d 등 Texture의 형태에 따라 다르게 처리하는 로직이 존재할 수 있으므로, Texture2D, Texture3D 등으로 구분한다.

3) 당연히 각각의 2d, 3d, cube ... 등에 대해서 Cross Platform 을 구성하기 위해 실제 Concrete Class 에 해당하는 OpenGLTexture2D 와 같은 클래스들을 만든다.

 

Texture 코드

class Texture
{
public :
    virtual uint32_t GetWidth() const = 0;
    virtual uint32_t GetHeight() const = 0;
    virtual void Bind(uint32_t slot = 0) const = 0;
};

class Texture2D : public Texture
{
public:
    virtual ~Texture2D() = default;

    static Ref<Texture2D> Create(const std::string& path);
};
class OpenGLTexture2D : public Texture2D
{
public :
    OpenGLTexture2D(const std::string& path);
    virtual ~OpenGLTexture2D();

    virtual uint32_t GetWidth() const override;
    virtual uint32_t GetHeight() const override;
    virtual void Bind(uint32_t slot = 0) const override;

private :
    uint32_t m_Width;
    uint32_t m_Height;
    std::string m_Path;
    uint32_t m_RendererID;
};

 

OpenGL channel 에 근거한 Texture 세팅 과정

OpenGLTexture2D::OpenGLTexture2D(const std::string& path) :
		m_Path(path)
{
    int width, height, channels;

    // OPENGL 은 Texture Coord 가 아래 -> 위 방향으로 증가한다고 계산
    // 하지만 stbl 은 위에서 아래 방향으로 증가한다고 계산
    // 따라서 그 값들을 뒤집어 줘야 한다.
    stbi_set_flip_vertically_on_load(1);

    // channels : 한 픽셀에 몇개의 채널이 존재하는지 ex) rgb, rgba -> 각각 3개, 4개
    stbi_uc* data = stbi_load(path.c_str(), &width, &height, &channels, 0);

    HZ_CORE_ASSERT(data, "Failed to load image");

    m_Width = width; 
    m_Height = height;

    GLenum internalFormat = 0, dataFormat = 0;

    switch (channels)
    {
        case 3 :
        {
            // rgba : format / 8 : bits for each channel
            internalFormat = GL_RGB8;
            dataFormat     = GL_RGB;
        }
        break;
        case 4 :
        {
            internalFormat = GL_RGBA8;
            dataFormat = GL_RGBA;
        }
        break;
    }

    HZ_CORE_ASSERT(internalFormat && dataFormat, "format should not be 0");

    // buffer data 를 gpu 가 인식할 수 있는 형태로 만들기
    // + 만들어낸 Texture Object 를 가리키는 ID 리턴
    glCreateTextures(GL_TEXTURE_2D, 1, &m_RendererID);

    // gpu 쪽에 Texture Buffer 가 들어갈 메모리 할당
    glTextureStorage2D(m_RendererID, 1, internalFormat, m_Width, m_Height);

    // gpu 쪽에 넘겨주기 
    // - texture 가 원래 크기보다 smaller 하게 display 될때, Linear Interpolation 적용
    glTextureParameteri(m_RendererID, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    // - texture 가 원래 크기보다 크게 하게 display 될때,  Neareset Interpolation 적용
    glTextureParameteri(m_RendererID, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    // texture data 의 일부분을 update 하는 함수
    // - m_RendererID : Update 할 Texture Object
    // - Texture Level : 0
    // - Position where update should begin : (0,0)
    // - Region being updated : m_Width, m_Height
    // - Data Type
    glTextureSubImage2D(m_RendererID, 0, 0, 0, m_Width, m_Height, dataFormat, GL_UNSIGNED_BYTE, data);

    stbi_image_free(data);
}

 

위 코드에서 중요한 것은 channels 라는 변수를 이용하는 것이다.

channel 이란 해당 텍스쳐 파일의 각 픽셀이 몇개의 channel 을 이용하는가에 대한 변수이다.

예를 들어 RGB Format 의 텍스쳐 파일은 R,G,B 총 3개가 될 것이고

RGBA Format 은 4개가 될 것이다.

 

각 텍스쳐 파일 포멧에 따라 OpenGL Texture Data 를 생성할 때 반드시 해당 Format 에 맞게 세팅해줘야 한다.

이전에도 말했듯이 실제 GPU 상으로 넘겨지는 Texture Data 는 byte 배열일 뿐이다. 

그리고 GPU 에서는 우리가 세팅한 Texture Format 에 따라 해당 byte 배열로부터 4 * float 을 하나의 픽셀로 인식할지,

3 * float 을 하나의 픽셀로 인식할지 결정하는 것일 뿐이다. 

 

예를 들어 할 때 2가지 문제가 발생할 수 있다.

 

1) 데이터를 잘못 읽는다. 

- 텍스쳐 포멧 : RGBA Format  <--> OpenGL : RGB Format 으로 세팅

- Texture 파일의 'alpha' 채널값을, GPU 가 'R" 값으로 읽을 수 있다. 따라서 이상하게 Texture 가 보일 수 있다.

 

2) 데이터가 터질 수 있다.

- 텍스쳐 포멧 : RGB Format  <--> OpenGL : RGBA Format 으로 세팅

- 예를 들어, OpenGL 이 RGB가 아니라, RGBA 를 하나의 픽셀로 읽기 위해

한 픽셀에 대해 float 값 하나만큼 더 읽어들이는 것이다. 예를 들어 Texture 의 픽셀이 총 100개 이고, RGB Format 이면 100 * (3 * 4) = 1200 byte 가 된다. 그런데 OpenGL 은 100 * (4 * 4) = 1600 byte 만큼의 바이트를 읽어들이려고 하니 메모리가 터지는 것이다.

 

Texture Filtering

추가적으로 봐야할 사항은 

    // - texture 가 원래 크기보다 smaller 하게 display 될때, Linear Interpolation 적용
    glTextureParameteri(m_RendererID, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

    // - texture 가 원래 크기보다 크게 하게 display 될때,  Neareset Interpolation 적용
    glTextureParameteri(m_RendererID, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

위의 코드이다.

 

구체적인 개념은 다른 링크를 참조

https://m.blog.naver.com/ruvendix/221405407212

 

Texture 입히기 (코드)

이를 위해서는 Texture Coordinate 를 알아야 한다.

흔히 UV 좌표라고도 불린다.

 

참고 : https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=znfgkro1&logNo=80181835458

 

먼저 각 정점을 정의하는 데이터에 TexCoord 에 해당하는 내용도 추가한다.

float squareVertices[5 * 4] = {
    -0.5f, -0.5f, 0.0f, 0.0f, 0.0f,  /*Bottom Left  */
    0.5f, -0.5f, 0.0f, 1.0f, 0.0f,  /*Bottom Right*/
    0.5f,  0.5f, 0.0f, 1.0f, 1.0f,   /*Top Right*/
    0.5f, -0.5f, 0.0f, 1.0f, 0.0f,  /*Bottom Right*/
    0.5f,  0.5f, 0.0f, 1.0f, 1.0f,   /*Top Right*/
    -0.5f,  0.5f, 0.0f, 0.0f, 1.0f    /*Top Left*/
};

 

당연히 Layout 에도 TexCoord 에 대한 정보를 추가해서 GPU 측에 각 정점 데이터가 어떻게 구성되었는지 알려줘야 한다.

아래와 같이 Float2 에 해당하는 buffer 정보를 추가한다.

Hazel::BufferLayout squareVBLayout = {
    {Hazel::ShaderDataType::Float3, "a_Position"},
    {Hazel::ShaderDataType::Float2, "a_TexCoord"}
};

 

Texture 를 입히는 Shader 코드는 아래와 같다

#type vertex
#version 330 core
			
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec2 a_TexCoord;

uniform mat4 u_ViewProjection;
uniform mat4 u_Transform;

out vec2 v_TexCoord;

void main()
{
	v_TexCoord = a_TexCoord;
	gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0);	
}

#type fragment
#version 330 core
			
layout(location = 0) out vec4 color;

in vec2 v_TexCoord;
			
uniform sampler2D u_Texture;

void main()
{
	color = texture(u_Texture, v_TexCoord);
}

위에 sampler2D 에 해당하는 u_Texture 변수를 gpu 측에 넘겨주는 코드는 아래와 같다.

 

// Texture 에 해당하는 객체 생성
m_Texture = Hazel::Texture2D::Create("assets/textures/RandomBox.png");

// Texture 관련 Shader 객체 생성
m_TextureShader.reset(Hazel::Shader::Create("assets/shaders/Texture.glsl"));
	
// Texture Shader Bind 시키기
std::dynamic_pointer_cast<Hazel::OpenGLShader>(m_TextureShader)->Bind();

// 0번째 Slot 에 묶인 Texture 객체를 "u_Texture"라는 이름으로 사용하겠다. 라는 옵션 세팅
std::dynamic_pointer_cast<Hazel::OpenGLShader>(m_TextureShader)->UploadUniformInt("u_Texture", 0);

/*
void OpenGLShader::UploadUniformInt(const std::string& name, const int& val)
{
    GLint location = glGetUniformLocation(m_RendererID, name.c_str());
    glUniform1i(location, val);
}
*/

// 해당 Texture 를 0번째 slot 에 bind 시킨다.
m_Texture->Bind();

/*
void OpenGLTexture2D::Bind(uint32_t slot = 0) const
{
	// 0 번 슬롯에 해당 Texture Object 정보를 mapping 시킨다.
    glBindTextureUnit(slot, m_RendererID);
}
*/