SFML community forums

General => SFML wiki => Topic started by: raycrasher on February 10, 2013, 09:29:13 am

Title: Sprite batching that works with multiple textures
Post by: raycrasher on February 10, 2013, 09:29:13 am
Here's a SpriteBatch class I created that allows you to just put in sf::Sprites without regard for texture. The class automatically constructs sf::VertexArrays for consecutive sprites with the same texture, blend mode, and shader. It's derived from sf::Drawable and sf::Transformable, so you can use it much like a sprite.

Tell me what you think. I'm sure this class still has much room for improvement. Also, feel free to use and modify it for your projects.

EDIT: I haven't posted this to the wiki yet, but I will post if this is useful.

To use:

sf::Sprite spr;

...

SpriteBatch batch;
SpriteBatchItem theSubItem = batch.AddItem(spr); // add a sprite. Differing textures are automatically handled.
SpriteBatchItem theSubItem2 = batch.AddItem(spr, sf::BlendAdd, myShader); // specify different blend mode and shader (this creates a different batch if the previous add is different in texture, blend mode or shader)

theSubItem.setPosition(50.f,50.f); // you can use sf::Transformable methods
theSubItem.setColor(sf::Color::White); // you can set the color of all 4 vertices
theSubItem.setColor(sf::Color::Red,2); // you can set the color of the third vertex (index 2)
theSubItem.UpdateSpriteBatch(); // update the sprite batch (update only if the sprite batch item is transformed

window.draw(batch); // draw the batch

batch.RemoveItem(theSubItem2); // remove a batch item
 

Requires: weak_ptr, shared_ptr, C++11

SpriteBatch.h
#pragma once

#include <list>

class SpriteBatchItem;

class SpriteBatch: public sf::Drawable, public sf::Transformable {
   friend class SpriteBatchItem;

   struct Batch {
      sf::RenderStates states;
      std::vector<sf::Vertex> vertices;
   };

   struct Quad {
      std::list<Batch>::iterator batch;
      std::list<shared_ptr<Quad>>::iterator quadIter;
      int vertexIndex;
      sf::IntRect texCoords;
      SpriteBatch *spriteBatch;
   };



public:

   SpriteBatch();
   ~SpriteBatch();

   SpriteBatchItem AddItem(const sf::Sprite &spr, sf::BlendMode blendMode=sf::BlendAlpha, sf::Shader *shader=nullptr);
   void RemoveItem(SpriteBatchItem &item);
   sf::FloatRect GetLocalBounds();
   sf::FloatRect GetGlobalBounds();
   void Clear();

protected:
   void draw(sf::RenderTarget &target, sf::RenderStates states) const;
private:
   
   sf::FloatRect bounds;
   bool boundsInvalidated;
   std::list<shared_ptr<Quad>> quads;
   std::list<Batch> batches;
};

class SpriteBatchItem: public sf::Transformable {
   friend class SpriteBatch;
public:
   void SetColor(const sf::Color &c, unsigned vtx=4);
   sf::Color GetColor(unsigned vtx=0);
   bool IsValid();
   void UpdateSpriteBatch();
   SpriteBatch &GetSpriteBatch();

   sf::FloatRect GetLocalBounds() const;
   sf::FloatRect GetGlobalBounds() const;

private:
   weak_ptr<SpriteBatch::Quad> quad;
};
 

SpriteBatch.cpp
#include "SpriteBatch.h"

SpriteBatch::SpriteBatch(){}
SpriteBatch::~SpriteBatch(){}

SpriteBatchItem SpriteBatch::AddItem(const sf::Sprite &spr, sf::BlendMode blendMode, sf::Shader *shader) {
   if(batches.empty() || (spr.getTexture() != batches.back().states.texture) || blendMode != batches.back().states.blendMode || shader != batches.back().states.shader) {
      batches.push_back(Batch());
      batches.back().states.texture=spr.getTexture();
      batches.back().states.shader=shader;
      batches.back().states.blendMode=blendMode;
   }

   shared_ptr<Quad> q(new Quad);
   q->batch = (--batches.end());
   q->spriteBatch = this;
   q->vertexIndex = batches.back().vertices.size();
   q->texCoords = spr.getTextureRect();

   batches.back().vertices.resize(batches.back().vertices.size()+4);
   q->batch->vertices[q->vertexIndex].texCoords=sf::Vector2f((float)q->texCoords.left,(float)q->texCoords.top);
   q->batch->vertices[q->vertexIndex+1].texCoords=sf::Vector2f((float)q->texCoords.left,(float)q->texCoords.top+q->texCoords.height);
   q->batch->vertices[q->vertexIndex+2].texCoords=sf::Vector2f((float)q->texCoords.left+q->texCoords.width,(float)q->texCoords.top+q->texCoords.height);
   q->batch->vertices[q->vertexIndex+3].texCoords=sf::Vector2f((float)q->texCoords.left+q->texCoords.width,(float)q->texCoords.top);
   
   

   quads.push_back(q);
   q->quadIter=(--quads.end());

   

   static SpriteBatchItem sbi;
   sbi.quad = q;
   sbi.SetColor(spr.getColor());
   sbi.setOrigin(spr.getOrigin());
   sbi.setPosition(spr.getPosition());
   sbi.setScale(spr.getScale());
   sbi.UpdateSpriteBatch();
   return sbi;
}

void SpriteBatch::RemoveItem(SpriteBatchItem &item) {
   auto q=item.quad.lock();
   if(q && q->spriteBatch==this){
      auto start=q->quadIter;
      start++;
      q->batch->vertices.erase(q->batch->vertices.begin()+q->vertexIndex,q->batch->vertices.begin()+q->vertexIndex+4);
      for(auto iter=start;iter!=quads.end();iter++){
         if(q->batch == (*iter)->batch) {
            (*iter)->vertexIndex-=4;
         }
      }
      if(q->batch->vertices.size()==0) batches.erase(q->batch);
      quads.erase(q->quadIter);
   }
}

void SpriteBatch::draw(sf::RenderTarget &target, sf::RenderStates states) const {
   states.transform*=getTransform();
   for(auto &b : batches){
      states.texture=b.states.texture;
      states.shader=b.states.shader;
      states.blendMode=b.states.blendMode;
      target.draw(&b.vertices[0],b.vertices.size(),sf::Quads,states);
   }
}

void SpriteBatch::Clear() {
   quads.clear();
   batches.clear();
}


void SpriteBatchItem::UpdateSpriteBatch() {
   auto q=quad.lock();
   if(q){
      sf::Vertex *vtx[4] = {
         &q->batch->vertices[q->vertexIndex],
         &q->batch->vertices[q->vertexIndex+1],
         &q->batch->vertices[q->vertexIndex+2],
         &q->batch->vertices[q->vertexIndex+3]
      };

      vtx[0]->position = getTransform().transformPoint(sf::Vector2f(0.f, 0.f));
      vtx[1]->position = getTransform().transformPoint(sf::Vector2f(0.f, (float)q->texCoords.height));
      vtx[2]->position = getTransform().transformPoint(sf::Vector2f((float)q->texCoords.width, (float)q->texCoords.height));
      vtx[3]->position = getTransform().transformPoint(sf::Vector2f((float)q->texCoords.width, 0.f));
   }
}

void SpriteBatchItem::SetColor( const sf::Color &c, unsigned vtx ) {
   auto q=quad.lock();
   if(q){
      if(vtx >= 4){
         q->batch->vertices[q->vertexIndex].color=c;
         q->batch->vertices[q->vertexIndex+1].color=c;
         q->batch->vertices[q->vertexIndex+2].color=c;
         q->batch->vertices[q->vertexIndex+3].color=c;
      } else {
         q->batch->vertices[q->vertexIndex+vtx].color=c;
      }
   }
}

sf::Color SpriteBatchItem::GetColor(unsigned vtx) {
   auto q=quad.lock();
   if(q){
      if(vtx >= 4) vtx=0;
      return q->batch->vertices[q->vertexIndex+vtx].color;
   }
   else return sf::Color(0.f,0.f,0.f,0.f);
}

bool SpriteBatchItem::IsValid() {
   return ((bool) quad.lock());
}

SpriteBatch &SpriteBatchItem::GetSpriteBatch() {
   return *quad.lock()->spriteBatch;
}

sf::FloatRect SpriteBatch::GetLocalBounds()
{
   if(boundsInvalidated)
   {
      sf::Vector2f topleft(0.f,0.f),bottomRight(0.f,0.f);

      for(auto &b : batches){
         for(auto &v : b.vertices){
            topleft.x = std::min(topleft.x,v.position.x);
            topleft.y = std::min(topleft.y,v.position.y);
            bottomRight.x=std::max(bottomRight.x,v.position.x);
            bottomRight.y=std::max(bottomRight.y,v.position.y);
         }
      }
      bounds=sf::FloatRect (topleft,bottomRight-topleft);
      boundsInvalidated=false;
   }
   
   return bounds;
}

sf::FloatRect SpriteBatch::GetGlobalBounds() {
   return getTransform().transformRect(GetLocalBounds());
}

sf::FloatRect SpriteBatchItem::GetLocalBounds() const
{
   auto q=quad.lock();
   if(q){
      return sf::FloatRect(0.f, 0.f, (float) q->texCoords.width, (float) q->texCoords.height);
   }
   else return sf::FloatRect();
}


////////////////////////////////////////////////////////////
sf::FloatRect SpriteBatchItem::GetGlobalBounds() const
{
   return getTransform().transformRect(GetLocalBounds());
}