Jump to content
  • Advertisement
Sign in to follow this  
Gykonik

OpenGL Spritebatch only renders first time

This topic is 991 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Introduction
I'm developing my own C++ game (coded from a YT-Tutorial) and for that I wrote a class called SpriteBatch, which obviously renders Sprites to the screen... This class is connected to my SpriteFont class, which renders font to the screen.</p>

The Problem
I have a main menu and in this I have 3 buttons, that direct to 3 different game modes. In the "Level-Editor"-Gamemode I implemented some spritebatches and spritefonts and if I enter the gamemode everything works fine, but if I go back to the menu and again go forward to the level-editor-gamemode the spritebatch tries to draw everything and (I think) each function gets called, but nothing appears to the screen.

Like I said the game-code is mostly depreciated from a YT-Video. He also runs into the same issue and had not enough time to try to fix it, so I hope, that some of you can help me with the problem...

Here the link to the video, where he explains the bug:

 


(2:22 - 2:50) The complete source code of the game is in the description of the video, but because it's much code here I include the (I guess) relevant things:

 

Concept
In the file, where I want to load in a Spritebatch, in the .h-file I create a spritebatch with

Bengine::SpriteBatch m_spriteBatch;

and then in the .cpp-file I initialize it with the init-function.
Now if I want to render something to the screen I do it this way:

m_spriteBatch.begin();
//e.g. tmpLight.draw(m_spriteBatch);
m_spriteBatch.end();
m_spriteBatch.renderBatch();

At the end of the programm I dispose the m_spriteBatch so:

m_spriteBatch.dispose();

Code that is important (I think)
(If you need any more code: under the linked YT-Video is a link to Git-Hub where you can find all of the code, used for the porject)

SpriteBatch.h:
 

#pragma once

#include <GL/glew.h>
#include <glm/glm.hpp>
#include <vector>

#include "Vertex.h"

namespace Bengine{

	// Determines how we should sort the glyphs
	enum class GlyphSortType {
		NONE,
		FRONT_TO_BACK,
		BACK_TO_FRONT,
		TEXTURE
	};

	// A glyph is a single quad. These are added via SpriteBatch::draw
	class Glyph {
	public:
		Glyph() {};
		Glyph(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint Texture, float Depth, const ColorRGBA8& color);
		Glyph(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint Texture, float Depth, const ColorRGBA8& color, float angle);

		GLuint texture;
		float depth;
    
		Vertex topLeft;
		Vertex bottomLeft;
		Vertex topRight;
		Vertex bottomRight;
	private:
		// Rotates a point about (0,0) by angle
		glm::vec2 rotatePoint(const glm::vec2& pos, float angle);
	};

	// Each render batch is used for a single draw call
	class RenderBatch {
	public:
		RenderBatch(GLuint Offset, GLuint NumVertices, GLuint Texture) : offset(Offset),
			numVertices(NumVertices), texture(Texture) {
		}
		GLuint offset;
		GLuint numVertices;
		GLuint texture;
	};

	// The SpriteBatch class is a more efficient way of drawing sprites
	class SpriteBatch
	{
	public:
		SpriteBatch();
		~SpriteBatch();

		// Initializes the spritebatch
		void init();
		void dispose();

		// Begins the spritebatch
		void begin(GlyphSortType sortType = GlyphSortType::TEXTURE);

		// Ends the spritebatch
		void end();

		// Adds a glyph to the spritebatch
		void draw(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint texture, float depth, const ColorRGBA8& color);
		// Adds a glyph to the spritebatch with rotation
		void draw(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint texture, float depth, const ColorRGBA8& color, float angle);
		// Adds a glyph to the spritebatch with rotation
		void draw(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint texture, float depth, const ColorRGBA8& color, const glm::vec2& dir);

		// Renders the entire SpriteBatch to the screen
		void renderBatch();

	private:
		// Creates all the needed RenderBatches
		void createRenderBatches();

		// Generates our VAO and VBO
		void createVertexArray();

		// Sorts glyphs according to _sortType
		void sortGlyphs();

		// Comparators used by sortGlyphs()
		static bool compareFrontToBack(Glyph* a, Glyph* b);
		static bool compareBackToFront(Glyph* a, Glyph* b);
		static bool compareTexture(Glyph* a, Glyph* b);

		GLuint _vbo;
		GLuint _vao;

		GlyphSortType _sortType;

		std::vector<Glyph*> _glyphPointers; ///< This is for sorting
		std::vector<Glyph> _glyphs; ///< These are the actual glyphs
		std::vector<RenderBatch> _renderBatches;
	};
}

SpriteBatch.cpp:
 

#include "SpriteBatch.h"
#include <algorithm>

namespace Bengine {

		Glyph::Glyph(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint Texture, float Depth, const ColorRGBA8& color) :
			texture(Texture),
			depth(Depth) {

			topLeft.color = color;
			topLeft.setPosition(destRect.x, destRect.y + destRect.w);
			topLeft.setUV(uvRect.x, uvRect.y + uvRect.w);

			bottomLeft.color = color;
			bottomLeft.setPosition(destRect.x, destRect.y);
			bottomLeft.setUV(uvRect.x, uvRect.y);

			bottomRight.color = color;
			bottomRight.setPosition(destRect.x + destRect.z, destRect.y);
			bottomRight.setUV(uvRect.x + uvRect.z, uvRect.y);

			topRight.color = color;
			topRight.setPosition(destRect.x + destRect.z, destRect.y + destRect.w);
			topRight.setUV(uvRect.x + uvRect.z, uvRect.y + uvRect.w);
		}

		Glyph::Glyph(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint Texture, float Depth, const ColorRGBA8& color, float angle) :
			texture(Texture),
			depth(Depth) {

			glm::vec2 halfDims(destRect.z / 2.0f, destRect.w / 2.0f);

			// Get points centered at origin
			glm::vec2 tl(-halfDims.x, halfDims.y);
			glm::vec2 bl(-halfDims.x, -halfDims.y);
			glm::vec2 br(halfDims.x, -halfDims.y);
			glm::vec2 tr(halfDims.x, halfDims.y);

			// Rotate the points
			tl = rotatePoint(tl, angle) + halfDims;
			bl = rotatePoint(bl, angle) + halfDims;
			br = rotatePoint(br, angle) + halfDims;
			tr = rotatePoint(tr, angle) + halfDims;

			topLeft.color = color;
			topLeft.setPosition(destRect.x + tl.x, destRect.y + tl.y);
			topLeft.setUV(uvRect.x, uvRect.y + uvRect.w);

			bottomLeft.color = color;
			bottomLeft.setPosition(destRect.x + bl.x, destRect.y + bl.y);
			bottomLeft.setUV(uvRect.x, uvRect.y);

			bottomRight.color = color;
			bottomRight.setPosition(destRect.x + br.x, destRect.y + br.y);
			bottomRight.setUV(uvRect.x + uvRect.z, uvRect.y);

			topRight.color = color;
			topRight.setPosition(destRect.x + tr.x, destRect.y + tr.y);
			topRight.setUV(uvRect.x + uvRect.z, uvRect.y + uvRect.w);
		}

		glm::vec2 Glyph::rotatePoint(const glm::vec2& pos, float angle) {
			glm::vec2 newv;
			newv.x = pos.x * cos(angle) - pos.y * sin(angle);
			newv.y = pos.x * sin(angle) + pos.y * cos(angle);
			return newv;
		}

	SpriteBatch::SpriteBatch() : _vbo(0), _vao(0)
	{
	}

	SpriteBatch::~SpriteBatch()
	{
	}

	void SpriteBatch::init() {
		createVertexArray();
	}

	void SpriteBatch::dispose() {
		if (_vao != 0) {
			glDeleteVertexArrays(1, &_vao);
			_vao = 0;
		}
		if (_vbo != 0) {
			glDeleteBuffers(1, &_vbo);
			_vbo = 0;
		}
	}

	void SpriteBatch::begin(GlyphSortType sortType /* GlyphSortType::TEXTURE */) {
		_sortType = sortType;
		_renderBatches.clear();
   
		// Makes _glpyhs.size() == 0, however it does not free internal memory.
		// So when we later call emplace_back it doesn't need to internally call new.
		_glyphs.clear();
	}

	void SpriteBatch::end() {
		// Set up all pointers for fast sorting
		_glyphPointers.resize(_glyphs.size());
		for (size_t i = 0; i < _glyphs.size(); i++) {
			_glyphPointers[i] = &_glyphs[i];
		}

		sortGlyphs();
		createRenderBatches();
	}

	void SpriteBatch::draw(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint texture, float depth, const ColorRGBA8& color) {
		_glyphs.emplace_back(destRect, uvRect, texture, depth, color);
	}

	void SpriteBatch::draw(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint texture, float depth, const ColorRGBA8& color, float angle) {
		_glyphs.emplace_back(destRect, uvRect, texture, depth, color, angle);
	}

	void SpriteBatch::draw(const glm::vec4& destRect, const glm::vec4& uvRect, GLuint texture, float depth, const ColorRGBA8& color, const glm::vec2& dir) {
		const glm::vec2 right(1.0f, 0.0f);
		float angle = acos(glm::dot(right, dir));
		if (dir.y < 0.0f) angle = -angle;

		_glyphs.emplace_back(destRect, uvRect, texture, depth, color, angle);
	}

	void SpriteBatch::renderBatch() {

		// Bind our VAO. This sets up the opengl state we need, including the 
		// vertex attribute pointers and it binds the VBO
		glBindVertexArray(_vao);

		for (size_t i = 0; i < _renderBatches.size(); i++) {
			glBindTexture(GL_TEXTURE_2D, _renderBatches[i].texture);

			glDrawArrays(GL_TRIANGLES, _renderBatches[i].offset, _renderBatches[i].numVertices);
		}

		glBindVertexArray(0);
	}

	void SpriteBatch::createRenderBatches() {
		// This will store all the vertices that we need to upload
		std::vector <Vertex> vertices;
		// Resize the buffer to the exact size we need so we can treat
		// it like an array
		vertices.resize(_glyphPointers.size() * 6);

		if (_glyphPointers.empty()) {
			return;
		}

		int offset = 0; // current offset
		int cv = 0; // current vertex

		//Add the first batch
		_renderBatches.emplace_back(offset, 6, _glyphPointers[0]->texture);
		vertices[cv++] = _glyphPointers[0]->topLeft;
		vertices[cv++] = _glyphPointers[0]->bottomLeft;
		vertices[cv++] = _glyphPointers[0]->bottomRight;
		vertices[cv++] = _glyphPointers[0]->bottomRight;
		vertices[cv++] = _glyphPointers[0]->topRight;
		vertices[cv++] = _glyphPointers[0]->topLeft;
		offset += 6;

		//Add all the rest of the glyphs
		for (size_t cg = 1; cg < _glyphPointers.size(); cg++) {

			// Check if this glyph can be part of the current batch
			if (_glyphPointers[cg]->texture != _glyphPointers[cg - 1]->texture) {
				// Make a new batch
				_renderBatches.emplace_back(offset, 6, _glyphPointers[cg]->texture);
			} else {
				// If its part of the current batch, just increase numVertices
				_renderBatches.back().numVertices += 6;
			}
			vertices[cv++] = _glyphPointers[cg]->topLeft;
			vertices[cv++] = _glyphPointers[cg]->bottomLeft;
			vertices[cv++] = _glyphPointers[cg]->bottomRight;
			vertices[cv++] = _glyphPointers[cg]->bottomRight;
			vertices[cv++] = _glyphPointers[cg]->topRight;
			vertices[cv++] = _glyphPointers[cg]->topLeft;
			offset += 6;
		}

		// Bind our VBO
		glBindBuffer(GL_ARRAY_BUFFER, _vbo);
		// Orphan the buffer (for speed)
		glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), nullptr, GL_DYNAMIC_DRAW);
		// Upload the data
		glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.size() * sizeof(Vertex), vertices.data());

		// Unbind the VBO
		glBindBuffer(GL_ARRAY_BUFFER, 0);

	}

	void SpriteBatch::createVertexArray() {

		// Generate the VAO if it isn't already generated
		if (_vao == 0) {
			glGenVertexArrays(1, &_vao);
		}
    
		// Bind the VAO. All subsequent opengl calls will modify it's state.
		glBindVertexArray(_vao);

		//G enerate the VBO if it isn't already generated
		if (_vbo == 0) {
			glGenBuffers(1, &_vbo);
		}
		glBindBuffer(GL_ARRAY_BUFFER, _vbo);

		//Tell opengl what attribute arrays we need
		glEnableVertexAttribArray(0);
		glEnableVertexAttribArray(1);
		glEnableVertexAttribArray(2);

		//This is the position attribute pointer
		glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
		//This is the color attribute pointer
		glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex), (void*)offsetof(Vertex, color));
		//This is the UV attribute pointer
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, uv));

		glBindVertexArray(0);

	}

	void SpriteBatch::sortGlyphs() {
   
		switch (_sortType) {
			case GlyphSortType::BACK_TO_FRONT:
				std::stable_sort(_glyphPointers.begin(), _glyphPointers.end(), compareBackToFront);
				break;
			case GlyphSortType::FRONT_TO_BACK:
				std::stable_sort(_glyphPointers.begin(), _glyphPointers.end(), compareFrontToBack);
				break;
			case GlyphSortType::TEXTURE:
				std::stable_sort(_glyphPointers.begin(), _glyphPointers.end(), compareTexture);
				break;
		}
	}

	bool SpriteBatch::compareFrontToBack(Glyph* a, Glyph* b) {
		return (a->depth < b->depth);
	}

	bool SpriteBatch::compareBackToFront(Glyph* a, Glyph* b) {
		return (a->depth > b->depth);
	}

	bool SpriteBatch::compareTexture(Glyph* a, Glyph* b) {
		return (a->texture < b->texture);
	}
}

Conclusion
I know, this is a hard and tough question and if you don't have time / attitude to answere it, it's ok and I can understand this. I only try to get this working, because this would not only help me, it would also help about 1000 other people, that worked through the YT-Playlist and running into the same issue.
I hope, that someone can help me fix this weird bug...

Share this post


Link to post
Share on other sites
Advertisement

I can't see anywhere in spritebatch that this code gets called when you do a normal begin->draw->end sequence

//Tell opengl what attribute arrays we need
		glEnableVertexAttribArray(0);
		glEnableVertexAttribArray(1);
		glEnableVertexAttribArray(2);

		//This is the position attribute pointer
		glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
		//This is the color attribute pointer
		glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex), (void*)offsetof(Vertex, color));
		//This is the UV attribute pointer
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, uv));

so if you change any of those settings in the game itself, I wouldn't expect the menu to display at all.

Share this post


Link to post
Share on other sites

I can't see anywhere in spritebatch that this code gets called when you do a normal begin->draw->end sequence

//Tell opengl what attribute arrays we need
		glEnableVertexAttribArray(0);
		glEnableVertexAttribArray(1);
		glEnableVertexAttribArray(2);

		//This is the position attribute pointer
		glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
		//This is the color attribute pointer
		glVertexAttribPointer(1, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(Vertex), (void*)offsetof(Vertex, color));
		//This is the UV attribute pointer
		glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, uv));

so if you change any of those settings in the game itself, I wouldn't expect the menu to display at all.

 

Those are stored in the vertex array object, there's no need to set them again after initialization.

Share this post


Link to post
Share on other sites

As Sponji already said, the things are stored in the vertex array object and I only initialize them one time at the top of the document, where I use spritebatch.

 

So this is not the problem...

Share this post


Link to post
Share on other sites

Inside of SpriteBatch::renderBatch(), call ::wglMakeCurrent() and then call ::glIsTexture() on _renderBatches.texture.

Call ::glIsVertexArray() and ::glIsBuffer() (also after calling ::wglMakeCurrent()) where appropriate.

In other words, verify that all of your objects are still objects (and that the context is still current).

 

 

L. Spiro

Share this post


Link to post
Share on other sites

How do you mean it?

 

Like this?:

void SpriteBatch::renderBatch() {
    // Bind our VAO. This sets up the opengl state we need, including the 
    // vertex attribute pointers and it binds the VBO
    glBindVertexArray(_vao);
    glIsVertexArray(_vao);
    glIsBuffer(_vbo);

    for (size_t i = 0; i < _renderBatches.size(); i++) {
        glIsTexture(_renderBatches[i].texture);
        glBindTexture(GL_TEXTURE_2D, _renderBatches[i].texture);
        glDrawArrays(GL_TRIANGLES, _renderBatches[i].offset, _renderBatches[i].numVertices);
    }

    glBindVertexArray(0);
    glDisableVertexAttribArray(0);
    glDisableVertexAttribArray(1);
    glDisableVertexAttribArray(2);
}

 

 
I'm not sure, how to use wglMakeCurrent();
 
Maybe you can say me exactly, what I should do? I'm not sure, how you mean it :s
Edited by Gykonik

Share this post


Link to post
Share on other sites
I decided to take a look at the problem by pulling the source from github and fixing few stuff to get it compiling on Linux. The code overall seems a little bit messy, lots of manual calls to init and dispose methods for example. Why not just use constructors and destructors for this stuff?
 
Anyway, the quickest way to fix the problem is by setting _numAttributes to 0 in GLSLProgram's dispose method. But the actual problem is that the GLSLProgram is touching current vertex array object's vertex attributes, why would it even do that? So, you can comment out glEnableVertexAttribArray and glDisableVertexAttribArray calls in GLSLProgram class and it will work that way too.

Share this post


Link to post
Share on other sites

@Sponji, if I comment out glEnableVertexAttribArray & Disable but the problem was still there...

 

If I set the _numAttributes to 0 it works perfectly, but I want to get this working with your sec. solution with commenting out glEnab...

Edited by Gykonik

Share this post


Link to post
Share on other sites

Maybe I wrote in a silly way, but you need to keep the _numAttributes = 0 line in the dispose method too. That's needed for addAttribute method so that it understands to reset counting from zero.

 

//enable the shader 
void GLSLProgram::use() {
    glUseProgram(_programID);
}

//disable the shader
void GLSLProgram::unuse() {
    glUseProgram(0);
}

void GLSLProgram::dispose() {
    if (_programID) glDeleteProgram(_programID);
    _numAttributes = 0;
}

 

Share this post


Link to post
Share on other sites

Wow, thank you very very much!

You helped me and other people so much *-*

I'll post the result under the YT-Video and will link the thread.

 

Thanks!

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!