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

230514 자체 엔진 개발 : Batch Rendering

mrawesome 2023. 5. 31. 23:45

https://github.com/ohbumjun/GameEngineTutorial/commit/1a20054f6c80599bd41c8c12bfe3a11cbdcf1a8a

 

feat(Engine) Draw Normal Quad With Batch Rendering · ohbumjun/GameEngineTutorial@1a20054

ohbumjun committed Jun 4, 2023

github.com

 

	struct Renderer2DData
	{
		static const uint32_t MaxQuads    = 10000;
		static const uint32_t MaxVertices = MaxQuads * 4;
		static const uint32_t MaxIndices  = MaxQuads * 6;
		static const uint32_t MaxTextureSlots = 32; 

		Ref<VertexArray> QuadVertexArray;
		Ref<VertexBuffer> QuadVertexBuffer;
		Ref<Shader> TextureShader;
		Ref<Texture2D> WhiteTexture;

		// 총 몇개의 quad indice 가 그려지고 있는가
		// Quad 를 그릴 때마다 + 6 해줄 것이다.
		uint32_t QuadIndexCount = 0;

		// QuadVertex 들을 담은 배열.을 가리키는 포인터
		QuadVertex* QuadVertexBufferBase = nullptr;

		// QuadVertexBufferBase 라는 배열 내에서 각 원소를 순회하기 위한 포인터
		QuadVertex* QuadVertexBufferPtr = nullptr;

		std::array<Ref<Texture2D>, MaxTextureSlots> TextureSlots;
		uint32_t TextureSlotIndex = 1; // 0 : Default White Texture 로 세팅

		// mesh local pos
		glm::vec4 QuadVertexPositions[4];

		Renderer2D::Statistics stats;
	};

위와 같이 Renderer2DData 라는 구조체의 구조를 변경한다.

 

Batch Rendering 이란, 여러 번의 DrawCall 을 통해 여러 개의 Mesh 를 그리는 것을

단 한번의 DrawCall 로 처리하기 위한 기법 중 하나이다.

 

이를 위해 그리고자 하는 모든 Mesh 의 Vertex, Index Buffer 를 하나의 Buffer 로 모아서

GPU 측에 넘겨주는 방법을 취해야 한다.

 

즉, VertexBuffer, IndexBuffer 의 크기가 그만큼 커야 한다는 것을 의미하고

그렇기 때문에 위와 같이 MaxQuad, MaxVertices, MaxIndices 라는 변수가 존재하는 것을 알 수 있다.

 

// 최대 크기의 Vertex Buffer 메모리 공간을 GPU 측에 생성
s_Data.QuadVertexBuffer = VertexBuffer::Create(s_Data.MaxVertices * sizeof(QuadVertex));

// 마찬가지로 최대 크기의 Index Buffer 메모리 공간을 GPU 측에 생성
uint32_t* quadIndices = new uint32_t[s_Data.MaxIndices];

uint32_t offset = 0;

for (uint32_t i = 0; i < s_Data.MaxIndices; i += 6)
{
    quadIndices[i + 0] = offset + 0;
    quadIndices[i + 1] = offset + 1;
    quadIndices[i + 2] = offset + 2;

    quadIndices[i + 3] = offset + 2;
    quadIndices[i + 4] = offset + 3;
    quadIndices[i + 5] = offset + 0;

    offset += 4;
}
Ref<IndexBuffer> quadIdxB = IndexBuffer::Create(quadIndices, s_Data.MaxIndices);
s_Data.QuadVertexArray->SetIndexBuffer(quadIdxB);
delete [] quadIndices;

위의 코드는, Batch Rendering 을 위해 쓰일, 거대한 하나의 단일 VertexBuffer, IndexBuffer 를 생성하는 코드이다.

 

// 모든 Vertex 를 담을 수 있는 충분한 크기만큼 메모리를 할당한다.
s_Data.QuadVertexBufferBase = new QuadVertex[s_Data.MaxVertices];

자, 위의 코드는, GPU 상에 Batch Rendering 을 위한 메모리 공간을 만들어주는 코드였다.

매 Frame 마다 해당 GPU 메모리 공간에, 내가 그리고자 하는 Mesh 정보를 담아 넘겨줘야 한다.

즉, CPU 측에서도 Batch Rendering 을 위한 거대한 Buffer 가 필요하고 여기에 데이터를 채워주면

GPU 가, 해당 메모리를 넘겨받아 Rendering 을 해주는 것이다.

 

void Renderer2D::BeginScene(const OrthographicCamera& camera)
{
    s_Data.TextureShader->Bind();
    s_Data.TextureShader->SetMat4(
        "u_ViewProjection", const_cast<OrthographicCamera&>(camera).GetViewProjectionMatrix());

    s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;
    s_Data.QuadIndexCount = 0;

    // 0 
    s_Data.TextureSlotIndex = 1;
}

void Renderer2D::EndScene()
{
    // EndScene 에서 s_Data.QuadVertexBufferPtr 을 이용하여 쌓아놓은 정점 정보들을
    // 이용하여 한번에 그려낼 것이다.
    // - 포인터를 숫자 형태로 형변환하기 위해  (uint8_t*) 로 캐스팅한다.
    uint32_t dataSize = (uint8_t*)s_Data.QuadVertexBufferPtr - (uint8_t*)s_Data.QuadVertexBufferBase;
    s_Data.QuadVertexBuffer->SetData(s_Data.QuadVertexBufferBase, dataSize);

    Flush();
}

자. QuadVetexBufferBase 라는 CPU 데이터를 매번 GPU 측에 넘겨줘야 한다.

이를 위해서는 

1) BeginScene : CPU 데이터를 모으기 전에 초기화를 해줘야 하고

2) EndScene    : 채워준 데이터를 GPU 측에 넘겨줘야 한다.

 

 

struct Renderer2DData
{
    ....
    ....
    
    // QuadVertex 들을 담은 배열.을 가리키는 포인터
    QuadVertex* QuadVertexBufferBase = nullptr;

    // QuadVertexBufferBase 라는 배열 내에서 각 원소를 순회하기 위한 포인터
    QuadVertex* QuadVertexBufferPtr = nullptr;
    
    // Index Buffer 에 들어가는 Index 정보의 개수
    // - 사각형 기준으로 매 사각형을 그릴 때마다, + 6을 해줄 것이다.
    // - 사각형은 삼각형 2개로 구성된 형태이기 때문이다.
    uint32_t QuadIndexCount = 0;
    
	....
    ....
};

s_Data.QuadVertexBufferPtr = s_Data.QuadVertexBufferBase;

자. 그러면 QuadVertexBufferBase 라는 배열에 데이터를 채워준다고 했다.

어떻게 채워줄 것인가 ?

해당 배열을 순회하는 별도의 포인터를 이용해서, Draw 함수 내에서 해당 포인터를 이용해서

배열에 원소를 채워줄 것이다.

 

그래서 우선 위와 같이 BeginScene 에서 포인터 위치를 배열 첫번째 원소 위치로 초기화 해준다.

 

// 각 정점이 가지고 있어야할 정보
struct QuadVertex
{
    glm::vec3 Position;
    glm::vec4 Color;
    glm::vec2 TexCoord;
    float TexIndex;			 // Texture Slot 상 mapping 된 index
    float TilingFactor;
};

void Renderer2D::DrawQuad(const glm::vec3& pos, const glm::vec2& size, const glm::vec4& color)
{
    const float texIndex = 0.f; // white texture
    const float tilingFactor = 1.f;

    // 시계 방향으로 4개의 정점 정보를 모두 세팅한다.
    // 왼쪽 아래
    s_Data.QuadVertexBufferPtr->Position = pos;
    s_Data.QuadVertexBufferPtr->Color = color;
    s_Data.QuadVertexBufferPtr->TexCoord = {0.f, 0.f}; 
    s_Data.QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data.QuadVertexBufferPtr++;

    // 오른쪽 아래
    s_Data.QuadVertexBufferPtr->Position = { pos.x + size.x, pos.y, 0.f };
    s_Data.QuadVertexBufferPtr->Color = color;
    s_Data.QuadVertexBufferPtr->TexCoord = { 1.f, 0.f };
    s_Data.QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data.QuadVertexBufferPtr++;

    // 오른쪽 위
    s_Data.QuadVertexBufferPtr->Position = { pos.x + size.x, pos.y + size.y, 0.f };
    s_Data.QuadVertexBufferPtr->Color = color;
    s_Data.QuadVertexBufferPtr->TexCoord = { 1.f, 1.f };
    s_Data.QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data.QuadVertexBufferPtr++;

    // 왼쪽 위
    s_Data.QuadVertexBufferPtr->Position = { pos.x, pos.y + size.y, 0.f };
    s_Data.QuadVertexBufferPtr->Color = color;
    s_Data.QuadVertexBufferPtr->TexCoord = { 0.f, 1.f };
    s_Data.QuadVertexBufferPtr->TexIndex = texIndex;
    s_Data.QuadVertexBufferPtr->TilingFactor = tilingFactor;
    s_Data.QuadVertexBufferPtr++;

    s_Data.QuadIndexCount += 6;
}

 

위와 같이. 매 정점 정보에 대해 QuadVertexBufferPtr 이라는 포인터로 정보를 초기화 해주고 있는 것을 확인할 수 있다.

또한 사각형을 그릴 때마다 QuadIndexCount 도 +6 씩 해주는 것을 알 수 있다.

 

uint32_t dataSize = (uint8_t*)s_Data.QuadVertexBufferPtr - (uint8_t*)s_Data.QuadVertexBufferBase;
s_Data.QuadVertexBuffer->SetData(s_Data.QuadVertexBufferBase, dataSize);

자. 그러면 EndScene 에서 위와 같이 그리고자 하는 Buffer 의 메모리를 채워줬다고 해보자.

이제 Rendering 을 실제 한번만 수행해줘야 한다.

 

void Renderer2D::EndScene()
{
    // EndScene 에서 s_Data.QuadVertexBufferPtr 을 이용하여 쌓아놓은 정점 정보들을
    // 이용하여 한번에 그려낼 것이다.
    // - 포인터를 숫자 형태로 형변환하기 위해  (uint8_t*) 로 캐스팅한다.
    uint32_t dataSize = (uint8_t*)s_Data.QuadVertexBufferPtr - (uint8_t*)s_Data.QuadVertexBufferBase;
    s_Data.QuadVertexBuffer->SetData(s_Data.QuadVertexBufferBase, dataSize);

    Flush();
}
void Renderer2D::Flush()
{
    // Batch Rendering 의 경우, 한번의 DrawCall 을 한다.
    RenderCommand::DrawIndexed(s_Data.QuadVertexArray, s_Data.QuadIndexCount);
}

void OpenGLRendererAPI::DrawIndexed(const Ref<VertexArray>& vertexArray,
		uint32_t indexCount)
{
    uint32_t count = (indexCount == 0) ? vertexArray->GetIndexBuffer()->GetCount() : indexCount;

    glDrawElements(GL_TRIANGLES, 
        count,
        GL_UNSIGNED_INT,
        nullptr);

    // Open GL 에 Bind 된 Texture Unbind 시키기
    glBindTexture(GL_TEXTURE_2D, 0);
}

위와 같이, EndScene 마지막 부분에 Flush 함수를 통해서 실제 DrawCall 을 호출한다.

 

VertexArray->VertexBuffer 는 QuadVertexBufferPtr 이라는 포인터를 통해, 정점 버퍼 정보가 채워진 상태이다.

그리고 Index 의 Count 에 해당하는 QuadIndexCount 를 넘겨줌으로써, 비로소 Batch Rendering 을 적용할 수 있다.