230514 자체 엔진 개발 : Batch Rendering
https://github.com/ohbumjun/GameEngineTutorial/commit/1a20054f6c80599bd41c8c12bfe3a11cbdcf1a8a
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 을 적용할 수 있다.