230516 자체 엔진 개발 : Batch Rendering (Texture)
https://github.com/ohbumjun/GameEngineTutorial/commit/b7eb5cc28c61701e257141b5725f3d0121696372
각 GPU 마다, 최대 한번에 Bind 할 수 있는 Texture Slot 및 Texture 개수가 정해져 있다.
보통 Desktop 은 32개의 Slot 이 존재한다.즉, 0 ~ 31 번째 index 까지 Slot 에 Texture 를 Bind 할 수 있다.
glBindTextureUnit(slot, m_RendererID);
위와 같은 함수를 통해서, 내가 원하는 Texture 를 GPU 측에 Bind 시킬 수 있다.
uniform sampler2D u_Textures[32];
glsl 에는 sampler2D 라는 변수가 있다. 이는 2D Texture Sampler 인데 이것을 integer 변수 형태로 세팅한다.
int samplers[s_Data.MaxTextureSlots]; // 32
for (uint32_t i = 0; i < s_Data.MaxTextureSlots; ++i)
{
samplers[i] = i;
}
s_Data.TextureShader->SetIntArray("u_Textures", samplers, s_Data.MaxTextureSlots);
///////
void OpenGLShader::SetIntArray(const std::string& name, int* values, uint32_t count)
{
UploadUniformIntArray(name, values, count);
}
///////
void OpenGLShader::UploadUniformIntArray(const std::string& name, int* values, uint32_t count)
{
GLint location = glGetUniformLocation(m_RendererID, name.c_str());
glUniform1iv(location, count, values);
}
"uniform" 에 해당하는 변수 값을 세팅하려면 어떻게 해야 할까 ?
sampler2D 는 사실상 int 변수 형태라고 했다.
그러면 int 변수 형태의 uniform 을 세팅하는 함수를 호출하면 될 것 같다.
즉, gpu 측에 있는 uniform sampler2D u_Textures[32]; 값을 위한
1) 메모리를 할당하고
2) 해당 메모리에 값을 세팅한다.
ex) u_Textures[2] 에는 "2" 라는 값을 세팅할 것이고, 이 말은 즉슨 2번 slot 에 Texture 를 Binding 시키면, u_Textures[2] 를 통해 GPU 측에서 Binding 한 Texture 값을 얻어올 수 있다는 의미이다.
struct Renderer2DData
{
Ref<Shader> TextureShader;
Ref<Texture2D> WhiteTexture;
std::array<Ref<Texture2D>, MaxTextureSlots> TextureSlots;
uint32_t TextureSlotIndex = 1; // 0 : Default White Texture 로 세팅
};
/// Scene::Initialize
for (uint32_t i = 0; i < s_Data.TextureSlots.size(); ++i)
{
s_Data.TextureSlots[i] = 0;
}
// bind default texture
s_Data.TextureSlots[0] = s_Data.WhiteTexture;
자. 우리는 최대 32개의 Texture Slot 을 GPU 측에 Bind 시키고자 한다.
우선 맨 처음 Scene Initalize에서는 Texture Slot 이라는 CPU 측의 변수를 초기화 하고
0번째 Texture Slot 에는 Default Texture 로 사용할 WhiteTexture 를 세팅한다.
void Renderer2D::DrawRotatedQuad(const glm::vec3& pos, const glm::vec2& size,
float rotation, const Ref<Texture2D>& texture, float tilingFactor, const glm::vec4& tintColor)
{
// 현재 인자로 들어온 Texture 에 대한 s_Data.TextureSlot 내 Texture Index 를 찾아야 한다.
float textureIndex = 0.f;
for (uint32_t i = 0; i < s_Data.TextureSlotIndex; ++i)
{
if (*s_Data.TextureSlots[i].get() == *texture.get())
{
textureIndex = (float)i;
break;
}
}
// new texture
if (textureIndex == 0.f)
{
textureIndex = (float)s_Data.TextureSlotIndex;
s_Data.TextureSlots[s_Data.TextureSlotIndex] = texture;
s_Data.TextureSlotIndex += 1;
}
...
...
...
}
자. 그러면 GPU 로 넘겨줄 Texture 정보들은 우선 s_Data.TextureSlots 라는 변수에 채워주는 것 같다.
해당 변수는 우리가 Texture 를 이용한 "Renderer2D::DrawRotatedQuad" 등을 호출할 때 채워줄 것이다.
인자로 들어온 texture 가 현재 s_Data.TextureSlots 에 존재하는지 확인하고
없으면 새롭게 채워준다.
void Renderer2D::Flush()
{
// 모든 Texture 를 한꺼번에 Bind 해야 한다.
// 0 번째에 기본적으로 Binding 된 WhiteTexture 도 Bind 해줘야 한다.
for (uint32_t i = 0; i < s_Data.TextureSlotIndex; ++i)
{
s_Data.TextureSlots[i]->Bind(i);
}
// Batch Rendering 의 경우, 한번의 DrawCall 을 한다.
RenderCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);
}
void OpenGLTexture2D::Bind(uint32_t slot) const
{
// Fragment Shader 의 slot 번째에 해당 Texture 객체를 binding 한다.
HZ_PROFILE_FUNCTION();
glBindTextureUnit(slot, m_RendererID);
}
그렇게 채워준 Texture Data 는 그렇다면, 언제 GPU 측에 넘겨주는 것일까 ?
우리는 현재 Batch Rendering 을 하고 있다. 즉, 한번의 DrawCall 을 호출할 텐데
마찬가지로, 이 시점에 한번에 모든 Texture Slot 정보들을 넘겨줄 것이다.
// GPU
#type vertex
#version 330 core
layout(location = 0) in vec3 a_Position;
layout(location = 1) in vec4 a_Color;
layout(location = 2) in vec2 a_TexCoord;
layout(location = 3) in float a_TexIndex;
layout(location = 4) in float a_TilingFactor;
uniform mat4 u_ViewProjection;
uniform mat4 u_Transform;
out vec4 v_Color;
out vec2 v_TexCoord;
out float v_TexIndex;
out float v_TilingFactor;
void main()
{
v_Color = a_Color;
v_TexCoord = a_TexCoord;
v_TexIndex = a_TexIndex;
v_TilingFactor = a_TilingFactor;
// 단일 DrawCall
// gl_Position = u_ViewProjection * u_Transform * vec4(a_Position, 1.0);
gl_Position = u_ViewProjection * vec4(a_Position, 1.0);
}
#type fragment
#version 330 core
layout(location = 0) out vec4 color;
in vec4 v_Color;
in vec2 v_TexCoord;
in float v_TexIndex;
in float v_TilingFactor;
uniform sampler2D u_Textures[32];
void main()
{
// TODO : m_TilingFactor to Batch
// v_TexIndex 정보도 제대로 넘어온다. 그러면 Texture 가 Binding 이 안된다는 것이다.
// 아니면, u_Textures[32] 가 제대로 넘어오지 않거나..
// Texture 가 없는 경우 : 기본 white texture * u_Color 형태로 구현
// 구체적으로는 Texture 가 없는 경우 texIndex 가 0 이 되고, 0번째 slot 에 Binding 된 것이 WhiteTexture
// color = texture(u_Textures[int(v_TexIndex)], v_TexCoord * v_TilingFactor) * v_Color;
color = texture(u_Textures[int(v_TexIndex)], v_TexCoord * v_TilingFactor) * v_Color;
}
// CPU
glm::mat4 transform = glm::translate(glm::mat4(1.0f), pos)
* glm::rotate(glm::mat4(1.f), glm::radians(rotation), { 0.f, 0.f, 1.f }) // z 축 회전
* glm::scale(glm::mat4(1.f), { size.x, size.y, 1.f });
// 시계 방향으로 4개의 정점 정보를 모두 세팅한다.
// 왼쪽 아래
// s_Data.QuadVertexBufferPtr->Position = pos;
// transform * s_Data.QuadVertexPositions[0] : opengl 오른손 좌표계 반영 위해, mat4 를 vec4 앞에 곱한다.
s_Data.QuadVertexBufferPtr->Position = transform * s_Data.QuadVertexPositions[0];
s_Data.QuadVertexBufferPtr->Color = color;
s_Data.QuadVertexBufferPtr->TexCoord = { 0.f, 0.f };
s_Data.QuadVertexBufferPtr->TexIndex = textureIndex;
s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
s_Data.QuadVertexBufferPtr++;
이후 GPU 측 Shader 에서는 위와 같이 u_Texture[v_TexIndex] 를 통해서, 매핑시킨 특정 Texture 를 얻어올 수 있다.
그리고 특정 정점이 어떤 Texture 를 사용해야 하는가 ? 를 정해주기 위해서
각 정점마다 "v_TexIndex" 라는 값을 지니게 하여, 정점 정보와 함께 GPU 측에 넘겨주면
GPU 측에서 해당 "v_TexIndex" 라는 값을 통해서, 내가 원하는 Texture 를 GPU 측에서 찾아서 처리해줄 수 있게 할 수 있다.
만약 32개 slot 이상의 Texture 를 사용하고자 하면 어떻게 해야 하는가 ?
현재 Batch 를 flush 하고, 그 다음 32번째 slot 을 Bound 시켜서 그리는 Batch 를 새로 시작하면 되는 것이다.
하지만 보통 하나의 Draw Call 내에서 32 개 이상의 Texture 를 사용하는 경우는 드물다.