Next Broadcast

Try to build a 2D Batch renderer using OpenGL

Purilainen "Pyry" GameDev DK30 New Year 2022 2 1

Description

Hoping to work on my C++ skills and learn OpenGL at the same time. I’m choosing a rendering feature since I feel like in order to keep me motivated in a programming project I need to see actual results on my screen. So thus graphics pipeline seems like a good fit. I hope to work on this almost daily to build a routine of doing stuff.

Recent Updates

UPDATE 3

Drawing some batched shapes using SDF Functions Drawing some batched shapes using SDF functions inspired by: Inigo Quillez : 2D Distance Functions

Fragment shader used to draw borders with shapes inside them:

float Box(in vec2 pos, in vec2 bounds) {
	vec2 d = abs(pos) - bounds;
	return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
}

float Border(in vec2 pos, in vec2 bounds, in float radius) {
	return abs(Box(pos, bounds)) - radius;
}

float Circle(in vec2 pos, float radius) {
	return length(pos) - radius;
}


float RoundedX(in vec2 pos, float width, float radius) {
	pos = abs(pos);
	return length(pos-min(pos.x+pos.y, width) *0.5) - radius;
}

float Triangle(in vec2 pos, float radius) {
    const float k = sqrt(3.0);
    pos.x = abs(pos.x) - 1.0;
    pos.y = pos.y + 1.0/k;
    if( pos.x+k*pos.y>0.0 ) pos = vec2(pos.x-k*pos.y,-k*pos.x-pos.y)/2.0;
    pos.x -= clamp( pos.x, -2.0, 0.0 );
    return (-length(pos)*sign(pos.y)) + radius;
}

void main()
{
// Shifted UVs  (-1,0,-1,0) - (1.0, 1.0)
	vec2 uv = v_TexCoord * 2.0 - 1.0;
	
	vec2 bounds = vec2(0.9, 0.9);
	// Calculate border
	float border = ceil(Border(uv, bounds, 0.1));
	
	// Draw border and shape with different colors
	out_Color.rgb = border < 1.0 ?  vec3(0.75) : v_Color.rgb ;
	
	// Use tex index to select functions to draw shapes inside borders
	float shape;
	if(v_TexIndex == 0)
		shape = Circle(uv, 0.5);
	else if(v_TexIndex == 1)
		shape = RoundedX(uv, 0.75, 0.1);
	else if(v_TexIndex == 2)
		shape = Triangle(uv, 0.25);

	shape = smoothstep(0.0, 0.02, shape);

	float distance = border * shape;
	out_Color.a = 1 - distance;
}

Update 2

It’s been a while since I’ve written an update. I feel a bit ashamed of not keeping up with the updates and been kinda avoiding writing this one since I could not hold on to my own goals and schedule.

TLDR: Durings weeks 2-4 I didn’t get many hours logged on this project unfortunately due to procrastination, distractions and life basically. However I’ve got few new features implemented anyways and realized in order to build a demo I need to do a lot of abstraction of my code and OpenGL code to make it more straightforward to work with.


Week 2 Goals & Progress

  • Asset Loading (Textures, Shaders):
    • Loading of textures (image files) implemented using stb_image
    • Reading and parsing text files into strings (shaders) using c++ standard libraries (iostream, fstream)
    • Rudimentary static functions implemented below:
static GLuint LoadTexture(const std::string& pathToTexture, bool bUseAlpha = false, bool bFlipVertically = true) {

    int width, height, bits;

    if(bFlipVertically)
        stbi_set_flip_vertically_on_load(1); // Flip texture for OpenGL

    unsigned char* texturePixels;
    if (bUseAlpha)
        texturePixels = stbi_load(pathToTexture.c_str(), &width, &height, &bits, STBI_rgb_alpha);
    else
        texturePixels = stbi_load(pathToTexture.c_str(), &width, &height, &bits, STBI_rgb);

    GLuint textureID;
    glCreateTextures(GL_TEXTURE_2D, 1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    if(bUseAlpha)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, texturePixels);
    else
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, texturePixels);

    stbi_image_free(texturePixels); // unload memory

    return textureID;
}
static const std::string ReadFileAsString(const std::string& filePath) {

    std::string result;
    std::ifstream inputFile(filePath, std::ios::in | std::ios::binary);
    if (inputFile) {
        inputFile.seekg(0, std::ios::end);
        result.resize((size_t)inputFile.tellg());
        inputFile.seekg(0, std::ios::beg);
        inputFile.read(&result[0], result.size());
        inputFile.close();
    }
    else {
        std::cout << "Could not open file." << std::endl;
    }
    return result;
}
// Use case of shader loading (could probably change the load function to return GLchar* to make this cleaner)
const std::string VertexShaderStr = ReadFileAsString("assets/shaders/vertexShader.glsl");
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
const GLchar* vertexSource = VertexShaderStr.c_str();
glShaderSource(vertexShader, 1, &vertexSource, 0);
glCompileShader(vertexShader);
  • Textured primitives:

    • Managed to add textures to my batched quads with alpha blending:

    Textured Quads

    • Had to add more attributes to my vertices in vertex buffer to make it work with batching.
// One quad vertices and vertex attributes
float vertices[] = {
    -0.25f, -0.25f, 0.0f, 1.0f, 0.25f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
    0.0f, -0.25f, 0.0f, 1.0f, 0.25f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 0.0f, 1.0f, 0.25f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,
    -0.25f, 0.0f, 0.0f, 1.0f, 0.25f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f
}

So in the code snippet above is my new vertex layout for drawing batched quads with possibility of adding textures.

Vertex(vec3 location, vec4 color, vec2 textureCoordinates, float textureIndex);

Especially texture coordinates and texture index are necessary if one wants to use different textures inside a batch. Texture index is used to tell the shader the index of a texture inside an uniform texture array. The limitation here is that the amount of textures that can be binded simultanously is anywhere from ~8-32 depending on the platform. So we can setup an texture atlas system where we use larger textures with small sub-textures inside them and can assign them using texture coordinates.

In order to use the attributes in shaders it is necessary to to setup the layout for the vertex buffer based on the data on each vertex. Basically we have to tell the shader a layout location ID, the amount of elements in given attribute, the size of the whole vertex and the offset to the previous attribute.

 // a_Position
 glEnableVertexArrayAttrib(VertexBuffer.RendererID() , 0);
 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 10 * sizeof(float), 0); // no offset for first elements
 // a_Color
 glEnableVertexArrayAttrib(VertexBuffer.ID(), 1);
 glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 10 * sizeof(float), (const void*)12); // offset 3 * float
 // a_TexCoord
 glEnableVertexArrayAttrib(VertexBuffer.ID(), 2);
 glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 10 * sizeof(float), (const void*)28); // offset 7 * float
 // a_TexIndex
 glEnableVertexArrayAttrib(VertexBuffer.ID(), 3);
 glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 10 * sizeof(float), (const void*)36); // offset 9 * float

And inside the shader you can access your vertex attributes with the layout locations set previously:

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;

Renderer architecture design

After implementing textures it became pretty obvious that I will have to start abstracing a lot of code into smaller classes and objects. I haven’t got far with the implementation yet.

I would like to abstract the general update loop to something more simple:

while(running) {
    // Listen to input events and pass events forward to components which are listening to set events
    events();

    // Handle any "gameplay" related updating here
    update();

    // Handle drawing in one place
    // Go through data and sort elements into layers and draw from back to front
    draw();
}

Week 1 Update

Got some work done mostly during the week after work. Turns out thinking about programming for 12+ hours a day can be fairly exhausting. (Took a few days off for a small vacation and some quality family time.)

TLDR: A bit behind schedule, but some visible progress was made. I’m having difficulties with my MVP-approach (minimum-viable-product). I keep getting ideas of features that I “should” have and getting stuck in design instead of actually programming anything.


Week 1 Goals & Progress

  • Linking of libraries and building development environment:

    • Got: SDL, glm, glad & stb_image up and running
    • Some minor difficulties with linking the libraries in VisualStudio. Turned out to be mostly because inconsistencies in build configuration with libraries and application (Debug / Release).
  • Drawing primitives:

    • Wrote some very simple shaders to draw initial primitives (triangle, quad)
#version 450 core

layout (location = 0) in vec3 Position;

void main()
{
	gl_Position = vec3(0.5f);
}
#version 450 core

layout (location = 0) out vec4 out_Color;

uniform vec4 uniform_Color;

void main()
{
	out_Color = uniform_Color;
}

Hello World!

The theory of batch rendering

What Is batch rendering?

- A way of rendering multiple objects with single draw call
- Generally used for performance purposes. (Less draw calls = less load on CPU).
- Used in for example tile based games (Stardew Valley, Minecraft etc.)

Simply put in OpenGL terms

// Instead of calling this for every single triangle/quad
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, nullptr); // 3 = indices count (corners of triangle)

// Batch triangles into single draw call
glDrawElements(GL_TRIANGLES, 3 * batchTriangleCount, GL_UNSIGNED_INT, nullptr);

The limitation in batching is that per-instance data can be tricky to set for individual elements in the batch. (Such as location, color, texture, geometry). Since by using traditional uniforms only apply for the whole batch instead of single elements. The way we can overcome this is by packing more data into the vertex and index buffers used to draw the geometry. Also the data has to become dynamic if we want to have real-time effects.

// Buffer data used for drawing a single quad - traditional way
float vertices[] = {
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.5f, 0.5f, 0.0f,
    -0.5f, 0.5f, 0.0f
};
uint32_t indices[] = {
    0, 1, 2, 2, 3, 0
};
// ... Create arrays and buffers and bind them
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);

// Data used for drawing two quads - batched
float batchVertices[] = {
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.5f, 0.5f, 0.0f,
    -0.5f, 0.5f, 0.0f

    -1.5f, -0.5f, 0.0f,
    -0.5f, -0.5, 0.0f,
    -0.5f, 0.5f, 0.0f,
    -1.5f, 0.5f, 0.0f,
};
uint32_t batchIndices[] = {
    0, 1, 2, 2, 3, 0,
    4, 5, 6, 6, 7, 4
};
// ... Create arrays and buffers and bind them
// Draw 2 quads using a single draw call
glDrawElements(GL_TRIANGLES, 12, GL_UNSIGNED_INT, nullptr);

Additional data for each element in the batch can be added into the vertex buffer created from vertices[] and then handle the data inside the shaders.

Small update on the starting point. I’ve chosen some libraries to use and started working on the project setup to get it up and running. I consulted a colleague who has years worth of experience with OpenGL and he recommended these libraries for me to use in my project. Some of them I’ve heard of before, but most I have little to none experience with.

External libraries used:

Estimated Timeframe

Mar 18th - Apr 18th

Week 1 Goal

Setup the project and get something on the screen. I’m choosing to use few libraries instead of building everything from ground up.

  • Get libraries linked and running
  • Draw primitives on the screen (Triangle, Quad, Circle)
  • Do some additional research on batch rendering

Week 2 Goal

  • Asset loading (Textures)
  • Textured primitives
  • Design simple architecture for the renderer
  • Start implementation

Week 3 Goal

Hopefully at this point I’ve got a clear plan and got some work done.

  • Continue implementation

Week 4 Goal

Finish implementation of batch renderer. If there is time to spare work on a demo.

Tags

  • c++
  • programming
  • opengl