230501 자체 엔진 개발 : Texture System
https://github.com/ohbumjun/GameEngineTutorial/commit/78b8a6bc619757867e8fd1b1c0997972fe40eccb
https://www.youtube.com/watch?v=gtuvxBaefHs&list=PLkaVDtEaS2nYqfACk9Cx4JgP6nW0kY5o-&index=4
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);
}
*/