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

230516 자체 엔진 개발 : Batch Rendering (Texture)

mrawesome 2023. 6. 4. 22:42

https://github.com/ohbumjun/GameEngineTutorial/commit/b7eb5cc28c61701e257141b5725f3d0121696372

 

feat(Engine) Batch Rendering Applied to Texture also · ohbumjun/GameEngineTutorial@b7eb5cc

ohbumjun committed Jun 7, 2023

github.com

각 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 를 사용하는 경우는 드물다.