Hello there,
I have tried following various OpenGL tutorials and I'm now at a point where I can render multiple 2d sprites with textures.
For that I have a sprite class:
Header:
#ifndef SPRITE_H
#define SPRITE_H
#include <GL/glew.h>
#include "Shader.h"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "Texture.h"
#include <stb_image.h>
#include "Camera.h"
class Sprite
{
public:
Sprite(GLfloat x, GLfloat y, GLfloat width, GLfloat height, Shader& shader, Texture *texture);
~Sprite();
void draw(Camera &camera);
void setPosition(float x, float y, float z);
void move(float x, float y, float z);
void setTexture(Texture *texture);
Texture getTexture();
float x, y, width, height;
private:
void init(GLfloat x, GLfloat y, GLfloat width, GLfloat height, Shader& shader, Texture *texture);
GLuint VBO = 0, VAO = 0, EBO = 0;
GLint transformShaderLocation, viewShaderLocation, projectionShaderLocation;
Shader* shader;
glm::mat4 transform, projection, view;
Texture *texture;
};
#endif
Code:
#include "Sprite.h"
Sprite::Sprite(GLfloat x, GLfloat y, GLfloat width, GLfloat height, Shader& shader, Texture *texture)
{
init(x, y, width, height, shader, texture);
}
void Sprite::init(GLfloat x, GLfloat y, GLfloat width, GLfloat height, Shader& shader, Texture *texture)
{
this->shader = &shader;
this->x = x;
this->y = y;
this->width = width;
this->height = height;
GLfloat vertices[] = {
width / 2 , height / 2, 0.0f, /* Top Right */ 1.0f, 1.0f,
width / 2 , -height / 2 , 0.0f, /* Bottom Right*/ 1.0f, 0.0f,
-width / 2 ,-height / 2 , 0.0f, /* Bottom Left */ 0.0f, 0.0f,
-width / 2 , height / 2 , 0.0f, /* Top Left */ 0.0f, 1.0f
};
GLuint indices[] = {
0, 1, 3, // 1
1, 2, 3 // 2
};
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// TexCoord
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
transformShaderLocation = glGetUniformLocation(shader.program, "transform");
viewShaderLocation = glGetUniformLocation(shader.program, "view");
projectionShaderLocation = glGetUniformLocation(shader.program, "projection");
transform = glm::translate(transform, glm::vec3(x , y , 0));
this->texture = texture;
}
Sprite::~Sprite()
{
//DELETE BUFFERS
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteBuffers(1, &VAO);
delete texture;
}
void Sprite::draw(Camera &camera) {
shader->Use();
glBindTexture(GL_TEXTURE_2D, texture->texture);
view = camera.getView();
projection = camera.getProjection();
// Pass to shaders
glUniformMatrix4fv(transformShaderLocation, 1, GL_FALSE, glm::value_ptr(transform));
glUniformMatrix4fv(viewShaderLocation, 1, GL_FALSE, glm::value_ptr(view));
// Note: currently we set the projection matrix each frame, but since the projection matrix rarely changes it's often best practice to set it outside the main loop only once.
glUniformMatrix4fv(projectionShaderLocation, 1, GL_FALSE, glm::value_ptr(projection));
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
void Sprite::setPosition(float x, float y, float z) { //Z?
transform = glm::translate(transform, glm::vec3(x - this->x , y - this->y , z));
this->x = x;
this->y = y;
}
void Sprite::move(float x, float y, float z) {
transform = glm::translate(transform, glm::vec3(x, y , z));
this->x += x;
this->y += y;
}
void Sprite::setTexture(Texture *texture) {
delete this->texture;
this->texture = texture;
}
Texture Sprite::getTexture()
{
return *texture;
}
When I want to draw something, I create an instance of the sprite class with it's own Texture and use sprite->draw(); in the draw loop for each sprite to draw it. This works perfectly fine.
To improve the performance, I now want to create a spritebatch.
As far as I understood it puts all the sprites together so it can send them all at once to the gpu.
I had no clue how to get started, so I just created a spritebatch class which put all the vertices and indices into one object every time draw() is called, and actually only draws when flush() is called. Here's the header file:
#ifndef SPRITEBATCH_H
#define SPRITEBATCH_H
#include <glm/glm.hpp>
#include "Texture.h"
#include <GL/glew.h>
#include "Camera.h"
#include "Shader.h"
#include <vector>
class SpriteBatch
{
public:
SpriteBatch(Shader& shader, Camera &camera);
~SpriteBatch();
void draw(Texture *texture, GLfloat x, GLfloat y, GLfloat width, GLfloat height);
void flush();
private:
GLfloat vertices[800];
GLuint indices[800];
int index{ 0 };
int indicesIndex{ 0 };
GLuint VBO = 0, VAO = 0, EBO = 0;
GLint transformShaderLocation, viewShaderLocation, projectionShaderLocation;
Shader *shader;
Camera *camera;
std::vector<Texture*>* textures;
glm::mat4 transform, projection, view;
};
#endif
And the class. I added some comments here:
#include "SpriteBatch.h"
SpriteBatch::SpriteBatch(Shader& shader, Camera &camera)
{
this->shader = &shader;
this->camera = &camera;
textures = new std::vector<Texture*>();
}
SpriteBatch::~SpriteBatch()
{
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glDeleteBuffers(1, &VAO);
//delete texture;
}
void SpriteBatch::draw(Texture *texture, GLfloat x, GLfloat y, GLfloat width, GLfloat height)
{
textures->push_back(texture);
vertices[index] = width/2 ;
vertices[index + 1] = height/2;
vertices[index + 2] = 0.0f;
vertices[index + 3] = 1.0f;
vertices[index + 4] = 1.0f;
vertices[index + 5] = width / 2;
vertices[index + 6] = -height / 2;
vertices[index + 7] = 0.0f;
vertices[index + 8] = 1.0f;
vertices[index + 9] = 0.0f;
vertices[index + 10] = -width / 2;
vertices[index + 11] = -height / 2;
vertices[index + 12] = 0.0f;
vertices[index + 13] = 0.0f;
vertices[index + 14] = 0.0f;
vertices[index + 15] = -width / 2;
vertices[index + 16] = height / 2;
vertices[index + 17] = 0.0f;
vertices[index + 18] = 0.0f;
vertices[index + 19] = 1.0f;
index += 20;
indices[indicesIndex] = 0;
indices[indicesIndex + 1] = 1;
indices[indicesIndex + 2] = 3;
indices[indicesIndex + 3] = 1;
indices[indicesIndex + 4] = 2;
indices[indicesIndex + 5] = 3;
indicesIndex += 6;
}
void SpriteBatch::flush()
{
if (index == 0) return; //Ensures that there are sprites added
//Debug information. This works perfectly
int spritesInBatch = index / 20;
std::cout << spritesInBatch << " I : " << index << std::endl;
int drawn = 0;
//Create Buffers
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
//Bind vertices
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//Bind indices
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// TexCoord
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
//VAO
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
//Shader locations
transformShaderLocation = glGetUniformLocation(shader->program, "transform");
viewShaderLocation = glGetUniformLocation(shader->program, "view");
projectionShaderLocation = glGetUniformLocation(shader->program, "projection");
//Draw
//So this sets the texture for each sprites and draws it afterwards with the right texture. At least that's how it should work.
for (int i = 0; i < spritesInBatch; i++) {
Texture *tex = textures->at(i);
shader->Use();
glBindTexture(GL_TEXTURE_2D, tex->texture); //?
view = camera->getView();
projection = camera->getProjection();
// Pass them to the shaders
glUniformMatrix4fv(transformShaderLocation, 1, GL_FALSE, glm::value_ptr(transform));
glUniformMatrix4fv(viewShaderLocation, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projectionShaderLocation, 1, GL_FALSE, glm::value_ptr(projection));
//Draw VAO
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indicesIndex, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
//Sets index 0 to welcome new sprites
index = 0;
}
It also puts the textures into a list.
The code to draw two sprites is this:
spriteBatch->draw(&_sprite1->getTexture(), _sprite1->x, _sprite1->y, _sprite1->width, _sprite1->height);
spriteBatch->draw(&_sprite1->getTexture(), _sprite1->x+10, _sprite1->y+10, _sprite1->width*2, _sprite1->height);
spriteBatch->flush();
but I only get one small black rectangle in the bottom left corner.
It works perfectly when I draw the sprites without the spritebatch;
_sprite1->draw(*camera);
_sprite2->draw(*camera);
I think I messed up in the flush() method, but I have no clue how to implement this. I'd be grateful if someone can help me with it.
Thank you!