I've been learning OpenGL for the past week, following tutorials here and there. I was looking to create a SpriteBatch-type of solution for 2D rendering, and for it I thought of utilizing instanced arrays, examples of which could be found in a couple of places.
To render the instances of my sprites, I'm using the function glDrawArraysInstanced, utilizing the glVertexAttribDivisor to get the correct instance in the shaders. I must be missing something however, because when ever I set my vertex attributes' divisors to 1 (to increment them each instance), nothing renders on the screen. When I don't set the divisors, only the first instance of sprites renders.
Additionally, I should probably mention that I am using OpenGL version 3.3 and SDL2.
For the relevant code, the SpriteBatch's initializer which set's the projection, creates the single VBO and VAO and whatnot:
#define BATCH_SIZE 2048
struct SpriteBatchItem
{
Texture * texture = NULL;
GLfloat position[2];
GLfloat dimensions[2];
};
struct SpriteBatch
{
bool batching_started = false;
unsigned int sprite_count = 0;
glm::mat4 projection;
GLuint shader;
GLuint vao;
GLuint vbo;
SpriteBatchItem sprite_buffer[BATCH_SIZE];
GLfloat vertex_buffer[BATCH_SIZE * 18];
static constexpr GLfloat quad_data[18] = {
//Position //ID
-1.0f, -1.0f, 0,
1.0f, -1.0, 1,
-1.0f, 1.0f, 2,
-1.0f, 1.0f, 3,
1.0f, -1.0f, 4,
1.0f, 1.0f, 5};
void init()
{
projection = glm::ortho(0.0f, (GLfloat)SCREEN_WIDTH, (GLfloat)SCREEN_HEIGHT, 0.0f, -1.0f, 1.0f);
shader = createShaderProgram("VertexShader.glsl", "FragmentShader.glsl");
glUseProgram(shader);
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//Position
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribDivisor(0, 1);
//ID, for texture positioning
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)(2 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glVertexAttribDivisor(1, 1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glUseProgram(0);
}
...
The begin() method starts the batching process:
void begin()
{
if (batching_started) return;
sprite_count = 0;
batching_started = true;
}
The draw method can be used after begin() has been called, and it's intention is to simply queue a sprite, remembering which texture it was using and what was it's position (I haven't gotten around to texturing yet, so only the position is really relevant.)
void draw(Texture& texture, GLfloat x, GLfloat y)
{
if (!batching_started) return;
SpriteBatchItem& item = sprite_buffer[sprite_count];
item.texture = &texture;
item.position[0] = x;
item.position[1] = y;
item.dimensions[0] = texture.getWidth();
item.dimensions[1] = texture.getHeight();
++sprite_count;
}
Finally, the end() method is called. It calculates the vertices' final positions and sends the data to the GPU:
void end()
{
if (!batching_started || sprite_count < 1) return;
glUseProgram(shader);
for (int i = 0; i < sprite_count; ++i)
{
SpriteBatchItem& item = sprite_buffer[i];
GLfloat tex_w = item.dimensions[0];
GLfloat tex_h = item.dimensions[1];
glm::mat4 model;
model = glm::translate(model, glm::vec3(item.position[0], item.position[1], 0.0f));
model = glm::translate(model, glm::vec3(1.0f * tex_w, 1.0f * tex_h, 0.0f));
model = glm::rotate(model, 0.0f, glm::vec3(0.0f, 0.0f, 1.0f));
model = glm::translate(model, glm::vec3(-0.5f * tex_w, -0.5f * tex_h, 0.0f));
model = glm::scale(model, glm::vec3(glm::vec2(tex_w/2, tex_h/2), 1.0f));
//For each vertex...
for (int j = 0; j < 6; ++j)
{
glm::vec4 pos = projection * model * glm::vec4(quad_data[j*3], quad_data[j*3+1], 0.0f, 1.0f);
vertex_buffer[i*18 + j*3 ] = pos.x;
vertex_buffer[i*18 + j*3 + 1] = pos.y;
vertex_buffer[i*18 + j*3 + 2] = (GLfloat)j;
}
}
//Send the data to the GPU
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sprite_count * sizeof(quad_data), vertex_buffer, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, sprite_count);
glBindVertexArray(0);
glUseProgram(0);
batching_started = false;
}
The VertexShader looks like this:
#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in float index;
out float out_index;
void main(void)
{
gl_Position = vec4(position, 0.0f, 1.0f);
out_index = index;
}
The fragment shader I don't suppose is relevant at this stage as it simply sets everything to the same colour in it's current state.
Now, to get to the crux of the problem, the above code causes no output. Nothing renders on the screen with the following render function in the main loop:
void render()
{
glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
batch.begin();
batch.draw(snow_tex, 375, 275);
batch.draw(snow_tex, 50, 45);
batch.draw(snow_tex, 155, 55);
batch.draw(snow_tex, 200, 2);
batch.end();
SDL_GL_SwapWindow(WINDOW);
}
What I would expect it to do is to render four rectangles on the screen. But this does not happen. Curiously, if I remove the following line from the batch init function...
glVertexAttribDivisor(0, 1);
...the first rectangle does render. However, the rest three do not. So, there's obviously something fundamental I am not quite understanding about instancing and how it's intended to work. Some help would be damn greatly appreciated, even if its a suggestion of a change of approach or something similar.