diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 67b4a7f32..701226141 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -26,6 +26,7 @@ if(SFML_BUILD_GRAPHICS) add_subdirectory(shader) add_subdirectory(island) add_subdirectory(vulkan) + add_subdirectory(sprite_batch) endif() if(SFML_OS_WINDOWS) diff --git a/examples/sprite_batch/Batching.cpp b/examples/sprite_batch/Batching.cpp new file mode 100644 index 000000000..6f480cfa8 --- /dev/null +++ b/examples/sprite_batch/Batching.cpp @@ -0,0 +1,435 @@ + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef SFML_SYSTEM_IOS +#include +#endif + +std::filesystem::path resourcesDir() +{ +#ifdef SFML_SYSTEM_IOS + return ""; +#else + return "resources"; +#endif +} + +enum class BatchDrawables +{ + Sprite, + Text, + Shape, + ShapeNoTexture +}; + +std::string getRandomString(std::mt19937& mt) +{ + static const std::array texts = + {"", + "Hello World!", + "London is the capital \nof Great Britain.", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut augue in dui \n" + "posuere sodales nec a orci. Phasellus sit amet tellus lobortis, scelerisque \n" + "dui dictum, posuere risus. Nam pellentesque gravida elit, id convallis tortor \n" + "aliquet sed. Duis vitae cursus risus, fermentum condimentum ex. Proin mattis \n" + "orci a malesuada commodo. Nunc egestas erat non volutpat elementum. Vivamus \n" + "pulvinar tortor eleifend libero convallis, at volutpat turpis lobortis. \n", + "'Tis but a flesh wound.", + "SFML Test Demo Thingy Stuff (Epic)", + "?!?!?!111(one)"}; + + return texts[std::uniform_int_distribution(0, texts.size() - 1)(mt)]; +} + +std::string getDisplayString(sf::SpriteBatch::BatchMode batchMode) +{ + static const std::map modeDisplay = + {{sf::SpriteBatch::BatchMode::Deferred, "Deferred"}, + {sf::SpriteBatch::BatchMode::TextureSort, "TextureSort"}, + {sf::SpriteBatch::BatchMode::DepthSort, "DepthSort"}}; + + if (modeDisplay.find(batchMode) != modeDisplay.end()) + return modeDisplay.at(batchMode); + + return "Unknown"; +} + +std::string getDisplayString(BatchDrawables drawableMode) +{ + static const std::map modeDisplay = + {{BatchDrawables::Sprite, "Sprite"}, + {BatchDrawables::Text, "Text"}, + {BatchDrawables::Shape, "Shape"}, + {BatchDrawables::ShapeNoTexture, "Shape (No Texture)"}}; + + if (modeDisplay.find(drawableMode) != modeDisplay.end()) + return modeDisplay.at(drawableMode); + + return "Unknown"; +} + +//////////////////////////////////////////////////////////// +/// Entry point of application +/// +/// \return Application exit code +/// +//////////////////////////////////////////////////////////// +int main() +{ + std::random_device rd; + std::mt19937 mt(rd()); + + // Define some constants used within the demo + const int windowWidth = 1600; + const int windowHeight = 900; + const int sameTextureChance = 85; + const std::size_t drawableCountChange = 100; + const std::size_t maxTimesSaved = 60; + const std::size_t drawablesPerLayer = 250; + + // Create the window of the application + sf::VideoMode videMode({windowWidth, windowHeight}, 32); + sf::RenderWindow window(videMode, "SFML SpriteBatch", sf::Style::Titlebar | sf::Style::Close); + window.setVerticalSyncEnabled(true); + + // Load texture assets + std::vector textures; + sf::Texture tempTexture; + + int currentTex = 0; + while (tempTexture.loadFromFile(resourcesDir() / ("Tex" + std::to_string(currentTex) + ".png"))) + { + textures.push_back(std::move(tempTexture)); + tempTexture = sf::Texture(); + ++currentTex; + } + + // Load font for debug text + sf::Font font; + if (!font.loadFromFile(resourcesDir() / "tuffy.ttf")) + return EXIT_FAILURE; + + // Different types of drawables + // For simplicity, we keep N objects of each type + std::vector sprites; + std::vector shapes; + std::vector shapesNoTexture; + std::vector texts; + + // Text used for debug info + sf::Text debugInfoText("", font); + debugInfoText.setOutlineColor(sf::Color::Blue); + debugInfoText.setOutlineThickness(4); + debugInfoText.setFillColor(sf::Color::White); + + // Define some state used in the demo + std::size_t drawableCount = 100; + sf::SpriteBatch batch; + sf::SpriteBatch::BatchMode batchMode = sf::SpriteBatch::BatchMode::Deferred; + bool batchEnabled = false; + bool useStaticBatch = false; + std::vector timeHistory; + sf::Time timeReference; + std::size_t framesTillRefUpdate = 0; + sf::Clock clock; + BatchDrawables benchmarkedDrawable = BatchDrawables::Sprite; + + while (window.isOpen()) + { + // Handle events + for (sf::Event event; window.pollEvent(event);) + { + // Window closed or escape key pressed: exit + if (event.type == sf::Event::Closed || + (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Escape)) + { + window.close(); + break; + } + + // W key pressed: increase drawable count + if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Up) + { + drawableCount += drawableCountChange; + } + + // S key pressed: decrease drawable count + if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Down) + { + drawableCount -= std::min(drawableCount, drawableCountChange); + } + + // R key pressed: cycle through the drawables to render + if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::D) + { + switch (benchmarkedDrawable) + { + case BatchDrawables::Sprite: + benchmarkedDrawable = BatchDrawables::Text; + break; + + case BatchDrawables::Text: + benchmarkedDrawable = BatchDrawables::Shape; + break; + + case BatchDrawables::Shape: + benchmarkedDrawable = BatchDrawables::ShapeNoTexture; + break; + + case BatchDrawables::ShapeNoTexture: + benchmarkedDrawable = BatchDrawables::Sprite; + break; + } + } + + // F or G key pressed: cycle through batch modes + if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::F) + { + switch (batchMode) + { + case sf::SpriteBatch::BatchMode::Deferred: + batchMode = sf::SpriteBatch::BatchMode::TextureSort; + break; + + case sf::SpriteBatch::BatchMode::TextureSort: + batchMode = sf::SpriteBatch::BatchMode::DepthSort; + break; + + case sf::SpriteBatch::BatchMode::DepthSort: + batchMode = sf::SpriteBatch::BatchMode::Deferred; + break; + } + } + + // G key pressed: toggle batching + if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::G) + { + batchEnabled = !batchEnabled; + } + + // H key pressed: toggle static batch + if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::H) + { + useStaticBatch = !useStaticBatch; + } + + // Window size changed, adjust view appropriately + if (event.type == sf::Event::Resized) + { + sf::View view; + view.setSize({windowWidth, windowHeight}); + view.setCenter({windowWidth / 2.f, windowHeight / 2.f}); + window.setView(view); + } + } + + // Clear the window + window.clear(sf::Color(50, 50, 50)); + + // Delete excess drawables + while (drawableCount < sprites.size()) + { + sprites.pop_back(); + texts.pop_back(); + shapes.pop_back(); + shapesNoTexture.pop_back(); + } + + // Add new drawables + while (drawableCount > sprites.size()) + { + float x = std::uniform_real_distribution(0, windowWidth)(mt); + float y = std::uniform_real_distribution(0, windowHeight)(mt); + float degrees = std::uniform_real_distribution(0, 360)(mt); + + const sf::Texture* texture = &textures[std::uniform_int_distribution(0, textures.size() - 1)(mt)]; + + // X% chance to use the previous texture instead + // The ordered batcher performs badly when textures change extremely often. + if (!sprites.empty() && std::uniform_int_distribution(0, 99)(mt) < sameTextureChance) + texture = (sprites.end() - 1)->getTexture(); + + auto& sprite = sprites.emplace_back(); + + sprite.setPosition(sf::Vector2f(x, y)); + sprite.setRotation(sf::degrees(degrees)); + sprite.setTexture(*texture); + + auto& shape = shapes.emplace_back(60.0f, 30); + + shape.setPosition(sf::Vector2f(x, y)); + shape.setRotation(sf::degrees(degrees)); + shape.setTexture(texture); + shape.setOutlineColor(sf::Color::Blue); + shape.setOutlineThickness(2); + + auto& shapeNoTexture = shapesNoTexture.emplace_back(60.0f, 30); + + shapeNoTexture.setPosition(sf::Vector2f(x, y)); + shapeNoTexture.setRotation(sf::degrees(degrees)); + shapeNoTexture.setOutlineColor(sf::Color::Blue); + shapeNoTexture.setOutlineThickness(2); + + auto& text = texts.emplace_back(getRandomString(mt), font, 60); + + text.setPosition(sf::Vector2f(x, y)); + text.setRotation(sf::degrees(degrees)); + text.setFillColor(sf::Color::Black); + text.setOutlineColor(sf::Color::White); + text.setOutlineThickness(8); + text.setStyle(sf::Text::Bold); + } + + // Update drawables + for (std::size_t i = 0; i < sprites.size(); i++) + { + const sf::Angle amount = sf::degrees(2); + + sprites[i].setRotation(sprites[i].getRotation() + amount); + texts[i].setRotation(texts[i].getRotation() + amount / 8); + shapes[i].setRotation(shapes[i].getRotation() + amount); + shapesNoTexture[i].setRotation(shapesNoTexture[i].getRotation() + amount); + } + + // Batch drawables + clock.restart(); + + float depth = 1.f; + std::size_t count = 0; + + if (batchEnabled && !useStaticBatch) + { + batch.clear(); + batch.setBatchMode(batchMode); + + for (std::size_t i = 0; i < sprites.size(); i++) + { + const sf::Batchable* batchable = nullptr; + + switch (benchmarkedDrawable) + { + case BatchDrawables::Sprite: + batchable = &sprites[i]; + break; + + case BatchDrawables::Shape: + batchable = &shapes[i]; + break; + + case BatchDrawables::ShapeNoTexture: + batchable = &shapesNoTexture[i]; + break; + + case BatchDrawables::Text: + batchable = &texts[i]; + break; + } + + batch.batch(*batchable, depth); + + count++; + + // Move to another layer for DepthSort case + // In a + if (count > drawablesPerLayer) + { + count -= drawablesPerLayer; + depth -= 0.01f; + } + } + } + + if (!batchEnabled) + { + for (std::size_t i = 0; i < sprites.size(); i++) + { + const sf::Drawable* drawable = nullptr; + + switch (benchmarkedDrawable) + { + case BatchDrawables::Sprite: + drawable = &sprites[i]; + break; + + case BatchDrawables::Shape: + drawable = &shapes[i]; + break; + + case BatchDrawables::ShapeNoTexture: + drawable = &shapesNoTexture[i]; + break; + + case BatchDrawables::Text: + drawable = &texts[i]; + break; + } + + window.draw(*drawable); + } + } + + if (batchEnabled) + batch.draw(window, sf::RenderStates(sf::BlendAlpha, sf::Transform::Identity, nullptr, nullptr)); + + sf::Time time = clock.restart(); + + timeHistory.push_back(time); + + if (timeHistory.size() > maxTimesSaved) + timeHistory.erase(timeHistory.begin(), timeHistory.end() - static_cast(maxTimesSaved)); + + const sf::Time average = std::accumulate(timeHistory.begin(), timeHistory.end(), sf::Time::Zero) / + static_cast(timeHistory.size()); + + if (framesTillRefUpdate <= 0) + { + framesTillRefUpdate = 18; + timeReference = time; + } + + framesTillRefUpdate--; + + // clang-format off + debugInfoText.setPosition(sf::Vector2f(0, 0)); + debugInfoText.setString( + "Batching enabled:\n" + "Current drawable:\n" + "Time:\n" + "Time (average):\n" + "Drawables:\n" + "Batch mode:\n"); + + window.draw(debugInfoText); + + debugInfoText.setPosition(sf::Vector2f(250, 0)); + debugInfoText.setString( + std::string(batchEnabled ? "true" : "false") + "\n" + + getDisplayString(benchmarkedDrawable) + "\n" + + std::to_string(timeReference.asSeconds() * 1000.0f) + "\n" + + std::to_string(average.asSeconds() * 1000.0f) + "\n" + + std::to_string(drawableCount) + "\n" + + getDisplayString(batchMode) + "\n" + + (useStaticBatch ? "Static batch is active" : "")); + // clang-format on + + window.draw(debugInfoText); + + // Display things on screen + window.display(); + } + + return EXIT_SUCCESS; +} diff --git a/examples/sprite_batch/CMakeLists.txt b/examples/sprite_batch/CMakeLists.txt new file mode 100644 index 000000000..9d9fef502 --- /dev/null +++ b/examples/sprite_batch/CMakeLists.txt @@ -0,0 +1,22 @@ +# all source files +set(SRC Batching.cpp) +if (SFML_OS_IOS) + set(RESOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/resources/Tex0.png + ${CMAKE_CURRENT_SOURCE_DIR}/resources/Tex1.png + ${CMAKE_CURRENT_SOURCE_DIR}/resources/Tex2.png + ${CMAKE_CURRENT_SOURCE_DIR}/resources/Tex3.png + ${CMAKE_CURRENT_SOURCE_DIR}/resources/Tex4.png + ${CMAKE_CURRENT_SOURCE_DIR}/resources/Tex5.png + ${CMAKE_CURRENT_SOURCE_DIR}/resources/Tex6.png + ${CMAKE_CURRENT_SOURCE_DIR}/resources/Tex7.png + ${CMAKE_CURRENT_SOURCE_DIR}/resources/Tex8.png) + set_source_files_properties(${RESOURCES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) +endif() + +# define the sprite batch demo target +sfml_add_example(sprite_batch GUI_APP + SOURCES ${SRC} + BUNDLE_RESOURCES ${RESOURCES} + DEPENDS SFML::Graphics + RESOURCES_DIR resources) diff --git a/examples/sprite_batch/resources/Tex0.png b/examples/sprite_batch/resources/Tex0.png new file mode 100644 index 000000000..9a4ec0095 Binary files /dev/null and b/examples/sprite_batch/resources/Tex0.png differ diff --git a/examples/sprite_batch/resources/Tex1.png b/examples/sprite_batch/resources/Tex1.png new file mode 100644 index 000000000..9e017d62e Binary files /dev/null and b/examples/sprite_batch/resources/Tex1.png differ diff --git a/examples/sprite_batch/resources/Tex2.png b/examples/sprite_batch/resources/Tex2.png new file mode 100644 index 000000000..62cd74a72 Binary files /dev/null and b/examples/sprite_batch/resources/Tex2.png differ diff --git a/examples/sprite_batch/resources/Tex3.png b/examples/sprite_batch/resources/Tex3.png new file mode 100644 index 000000000..f0537215c Binary files /dev/null and b/examples/sprite_batch/resources/Tex3.png differ diff --git a/examples/sprite_batch/resources/Tex4.png b/examples/sprite_batch/resources/Tex4.png new file mode 100644 index 000000000..f33d03ec8 Binary files /dev/null and b/examples/sprite_batch/resources/Tex4.png differ diff --git a/examples/sprite_batch/resources/Tex5.png b/examples/sprite_batch/resources/Tex5.png new file mode 100644 index 000000000..f4ad93b36 Binary files /dev/null and b/examples/sprite_batch/resources/Tex5.png differ diff --git a/examples/sprite_batch/resources/Tex6.png b/examples/sprite_batch/resources/Tex6.png new file mode 100644 index 000000000..83dc330c9 Binary files /dev/null and b/examples/sprite_batch/resources/Tex6.png differ diff --git a/examples/sprite_batch/resources/Tex7.png b/examples/sprite_batch/resources/Tex7.png new file mode 100644 index 000000000..5fc26f725 Binary files /dev/null and b/examples/sprite_batch/resources/Tex7.png differ diff --git a/examples/sprite_batch/resources/Tex8.png b/examples/sprite_batch/resources/Tex8.png new file mode 100644 index 000000000..7ea3f1d08 Binary files /dev/null and b/examples/sprite_batch/resources/Tex8.png differ diff --git a/examples/sprite_batch/resources/tuffy.ttf b/examples/sprite_batch/resources/tuffy.ttf new file mode 100644 index 000000000..8ea647090 Binary files /dev/null and b/examples/sprite_batch/resources/tuffy.ttf differ diff --git a/include/SFML/Graphics.hpp b/include/SFML/Graphics.hpp index 048f01e4f..a1203268a 100644 --- a/include/SFML/Graphics.hpp +++ b/include/SFML/Graphics.hpp @@ -46,6 +46,7 @@ #include #include #include +#include #include #include #include diff --git a/include/SFML/Graphics/Batchable.hpp b/include/SFML/Graphics/Batchable.hpp new file mode 100644 index 000000000..099a6c98a --- /dev/null +++ b/include/SFML/Graphics/Batchable.hpp @@ -0,0 +1,77 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2022 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +namespace sf +{ +class SpriteBatch; + +//////////////////////////////////////////////////////////// +/// \brief Abstract base class for objects that can be batched +/// to a sf::SpriteBatch +/// +//////////////////////////////////////////////////////////// +class SFML_GRAPHICS_API Batchable +{ +public: + //////////////////////////////////////////////////////////// + /// \brief Virtual destructor + /// + //////////////////////////////////////////////////////////// + virtual ~Batchable() = default; + + //////////////////////////////////////////////////////////// + /// \brief Batch the object using the given SpriteBatch + /// + /// This is a pure virtual function that has to be implemented + /// by the derived class to define how the drawable should be + /// batched. + /// + /// \param spriteBatch The SpriteBatch to use + /// \param depth The depth at which to render the object + /// + //////////////////////////////////////////////////////////// + virtual void batch(SpriteBatch& spriteBatch, float depth = 0.f) const = 0; +}; + +} // namespace sf + + +//////////////////////////////////////////////////////////// +/// \class sf::Batchable +/// \ingroup graphics +/// +/// sf::Batchable is an interface for drawables that can be +/// batched using an sf::SpriteBatch. To implement the interface, +/// you need to define a batch method that makes use of +/// sf::SpriteBatch::batch(), and supplies it the vertices, texture, +/// transform and other data for drawing. +/// +//////////////////////////////////////////////////////////// diff --git a/include/SFML/Graphics/Shape.hpp b/include/SFML/Graphics/Shape.hpp index 51686aa7f..f36462890 100644 --- a/include/SFML/Graphics/Shape.hpp +++ b/include/SFML/Graphics/Shape.hpp @@ -29,6 +29,7 @@ //////////////////////////////////////////////////////////// #include +#include #include #include #include @@ -43,7 +44,7 @@ class Texture; /// \brief Base class for textured shapes with outline /// //////////////////////////////////////////////////////////// -class SFML_GRAPHICS_API Shape : public Drawable, public Transformable +class SFML_GRAPHICS_API Shape : public Drawable, public Transformable, public Batchable { public: //////////////////////////////////////////////////////////// @@ -248,6 +249,15 @@ public: //////////////////////////////////////////////////////////// FloatRect getGlobalBounds() const; + //////////////////////////////////////////////////////////// + /// \brief Batch the Shape using the given SpriteBatch + /// + /// \param spriteBatch The SpriteBatch to use + /// \param depth The depth at which to render the shape + /// + //////////////////////////////////////////////////////////// + void batch(SpriteBatch& spriteBatch, float depth = 0.f) const override; + protected: //////////////////////////////////////////////////////////// /// \brief Default constructor diff --git a/include/SFML/Graphics/Sprite.hpp b/include/SFML/Graphics/Sprite.hpp index 85d45f8de..0dff01e2a 100644 --- a/include/SFML/Graphics/Sprite.hpp +++ b/include/SFML/Graphics/Sprite.hpp @@ -29,6 +29,7 @@ //////////////////////////////////////////////////////////// #include +#include #include #include #include @@ -44,7 +45,7 @@ class Texture; /// own transformations, color, etc. /// //////////////////////////////////////////////////////////// -class SFML_GRAPHICS_API Sprite : public Drawable, public Transformable +class SFML_GRAPHICS_API Sprite : public Drawable, public Transformable, public Batchable { public: //////////////////////////////////////////////////////////// @@ -206,6 +207,15 @@ public: //////////////////////////////////////////////////////////// FloatRect getGlobalBounds() const; + //////////////////////////////////////////////////////////// + /// \brief Batch the Sprite using the given SpriteBatch + /// + /// \param spriteBatch The SpriteBatch to use + /// \param depth The depth at which to render the sprite + /// + //////////////////////////////////////////////////////////// + void batch(SpriteBatch& spriteBatch, float depth = 0.f) const override; + private: //////////////////////////////////////////////////////////// /// \brief Draw the sprite to a render target diff --git a/include/SFML/Graphics/SpriteBatch.hpp b/include/SFML/Graphics/SpriteBatch.hpp new file mode 100644 index 000000000..a9e40cf8a --- /dev/null +++ b/include/SFML/Graphics/SpriteBatch.hpp @@ -0,0 +1,256 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2022 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace sf +{ +class RenderTarget; + +//////////////////////////////////////////////////////////// +/// \brief Batching utility for drawables, meant for increasing +/// rendering performance +/// +//////////////////////////////////////////////////////////// +class SFML_GRAPHICS_API SpriteBatch : public Drawable +{ +public: + enum class BatchMode + { + //////////////////////////////////////////////////////////// + /// \brief Creates a batch for each contiguous groups of drawables + /// that use the same texture. + /// + //////////////////////////////////////////////////////////// + Deferred, + + //////////////////////////////////////////////////////////// + /// \brief Organizes objects into a single layer, then reorders + /// them by texture before drawing. + /// + //////////////////////////////////////////////////////////// + TextureSort, + + //////////////////////////////////////////////////////////// + /// \brief Organizes objects into multiple layers. Groups objects + /// into layers based on depth, then reorders objects in + /// each layer by texture before drawing the layers from + /// furthest to closest. + /// + //////////////////////////////////////////////////////////// + DepthSort, + }; + + //////////////////////////////////////////////////////////// + /// \brief Sets the batch mode to use + /// + /// \param batchMode The new batch mode to use + /// + //////////////////////////////////////////////////////////// + void setBatchMode(BatchMode batchMode); + + //////////////////////////////////////////////////////////// + /// \brief Batches the given drawable + /// + /// \param drawable The drawable to be batched + /// \param depth The depth at which the drawable will be + /// rendererd. This value is used only when + /// BatchMode::DepthSort mode is used + /// + //////////////////////////////////////////////////////////// + void batch(const Batchable& drawable, float depth = 0.f); + + //////////////////////////////////////////////////////////// + /// \brief Batches an array of vertices + /// + /// \param vertices The array of vertices to be batched + /// \param count How many vertices to batch + /// \param type The primitive formed by the vertices. + /// Only triangle primitives are supported + /// \param texture The texture to use for rendering + /// \param transform The transform to apply on the vertices + /// \param depth The depth at which the drawable will be + /// rendererd. This value is used only when + /// BatchMode::DepthSort mode is used. + /// + //////////////////////////////////////////////////////////// + void batch(const Vertex* vertices, + std::size_t count, + PrimitiveType type, + const Texture* texture, + const Transform& transform = Transform::Identity, + float depth = 0.f); + + //////////////////////////////////////////////////////////// + /// \brief Renders the batch to the render target + /// + /// \param target The RenderTarget to draw to + /// \param states The RenderStates to use. The texture is ignored + /// + //////////////////////////////////////////////////////////// + void draw(RenderTarget& target, const RenderStates& states) const override; + + //////////////////////////////////////////////////////////// + /// \brief Clears the batch, removing all drawables that were + /// added + /// + //////////////////////////////////////////////////////////// + void clear(); + +private: + //////////////////////////////////////////////////////////// + /// \brief Pushes a triangle into internal storage + /// + /// \param a First vertex + /// \param b Second vertex + /// \param c Third vertex + /// \param transform Transform to apply before inserting + /// \param texture Texture to apply to triangle + /// \param depth The depth to use for depth sorting + /// + //////////////////////////////////////////////////////////// + void pushTriangle(const Vertex& a, + const Vertex& b, + const Vertex& c, + const Transform& transform, + const Texture* texture, + float depth); + + //////////////////////////////////////////////////////////// + /// \brief Updates the batch, preparing its internal state for + /// efficient usage + /// + //////////////////////////////////////////////////////////// + void updateBatch() const; + + //////////////////////////////////////////////////////////// + /// \brief Holds information about a batched triangle + /// + //////////////////////////////////////////////////////////// + struct TriangleInfo + { + const Texture* texture{}; //!< The triangle's texture + float depth{}; //!< The depth of the triangle + + TriangleInfo() = default; + TriangleInfo(const Texture* theTexture, float theDepth) : texture(theTexture), depth(theDepth) + { + } + }; + + //////////////////////////////////////////////////////////// + /// \brief Holds information for rendering a batch + /// + //////////////////////////////////////////////////////////// + struct BatchInfo + { + const Texture* texture{}; //!< The texture used to render the batch + std::size_t vertexCount{}; //!< The number of contiguous vertices to render + + BatchInfo() = default; + BatchInfo(const Texture* theTexture, std::size_t theVertexCount) : + texture(theTexture), + vertexCount(theVertexCount) + { + } + }; + + //////////////////////////////////////////////////////////// + // Member data + //////////////////////////////////////////////////////////// + // Batched Triangles + std::vector m_triangles; //!< Info about batched triangles + std::vector m_unsortedVertices; //!< Vertices currently batched + mutable std::vector m_batches; //!< Prepared batch information, ready for rendering + mutable std::vector m_vertices; //!< Prepared vertices, ready for rendering + + // Batch Settings + BatchMode m_batchMode{BatchMode::Deferred}; //!< The current batch strategy + mutable bool m_updateRequired{true}; //!< If true, batch must be sorted before rendering +}; + +} // namespace sf + + +//////////////////////////////////////////////////////////// +/// \class sf::SpriteBatch +/// \ingroup graphics +/// +/// sf::SpriteBatch is a batching utility for drawables. It +/// increases rendering performance by reordering and combining +/// drawables, such that they are rendered using fewer draw calls +/// and state switches. +/// +/// sf::SpriteBatch supports multiple batching strategies. Each +/// comes with its benefits and drawbacks, so the optimal choice +/// depends on your specific needs. +/// +/// \c sf::BatchMode::Deferred is best used when rendering contiguous +/// groups ofdrawables that use the same texture. This mode creates +/// batches for each such group, improving performance when many +/// drawables with few vertices are drawn (such as hundreds to +/// thousands of sf::Sprite drawables). +/// The render order of drawables is the same as the order they +/// were batched in. This is similar how rendering works with +/// sf::RenderTarget. +/// +/// \c sf::BatchMode::TextureSort is best used when drawing objects +/// that are positioned on a "layer", and do not overlap each other. +/// This mode assumes that the draw order is not important within +/// a layer, and sorts all objects by texture before batching them. +/// This provides great performance improvements, especially when +/// rendering objects using different textures (since it will reorder +/// them back in contiguous groups). +/// Care should be taken when when drawing overlapping objects, since +/// it is not guaranteed that they will be ordered correctly. +/// +/// \c sf::BatchMode::DepthSort is best used when drawing objects +/// that are positioned on multiple "layers" based on depth, and objects +/// within a layer do not overlap each other. +/// Within each layer, objects are sorted by texture before drawing, +/// similar to \c TextureSort. However, objects in a layer with a +/// higher "depth" value will be drawn behind objects with a lower +/// "depth" value. +/// You can also use this mode to render objects in an order that is +/// different from the order they were batched in, since objects +/// will be reordered by depth before rendering. +/// +/// An example of how to use sf::SpriteBatch can be found in the +/// examples/sprite_batch project. +/// +//////////////////////////////////////////////////////////// diff --git a/include/SFML/Graphics/Text.hpp b/include/SFML/Graphics/Text.hpp index ca7879a80..de6550ae6 100644 --- a/include/SFML/Graphics/Text.hpp +++ b/include/SFML/Graphics/Text.hpp @@ -29,6 +29,7 @@ //////////////////////////////////////////////////////////// #include +#include #include #include #include @@ -47,7 +48,7 @@ class Font; /// \brief Graphical text that can be drawn to a render target /// //////////////////////////////////////////////////////////// -class SFML_GRAPHICS_API Text : public Drawable, public Transformable +class SFML_GRAPHICS_API Text : public Drawable, public Transformable, public Batchable { public: //////////////////////////////////////////////////////////// @@ -417,6 +418,15 @@ public: //////////////////////////////////////////////////////////// FloatRect getGlobalBounds() const; + //////////////////////////////////////////////////////////// + /// \brief Batch the Text using the given SpriteBatch + /// + /// \param spriteBatch The SpriteBatch to use + /// \param depth The depth at which to render the text + /// + //////////////////////////////////////////////////////////// + void batch(SpriteBatch& spriteBatch, float depth = 0.f) const override; + private: //////////////////////////////////////////////////////////// /// \brief Draw the text to a render target diff --git a/src/SFML/Graphics/CMakeLists.txt b/src/SFML/Graphics/CMakeLists.txt index e595331aa..a2860ec89 100644 --- a/src/SFML/Graphics/CMakeLists.txt +++ b/src/SFML/Graphics/CMakeLists.txt @@ -4,6 +4,7 @@ set(SRCROOT ${PROJECT_SOURCE_DIR}/src/SFML/Graphics) # all source files set(SRC + ${INCROOT}/Batchable.hpp ${SRCROOT}/BlendMode.cpp ${INCROOT}/BlendMode.hpp ${INCROOT}/Color.hpp @@ -36,6 +37,8 @@ set(SRC ${INCROOT}/RenderWindow.hpp ${SRCROOT}/Shader.cpp ${INCROOT}/Shader.hpp + ${SRCROOT}/SpriteBatch.cpp + ${INCROOT}/SpriteBatch.hpp ${SRCROOT}/Texture.cpp ${INCROOT}/Texture.hpp ${SRCROOT}/TextureSaver.cpp diff --git a/src/SFML/Graphics/Shape.cpp b/src/SFML/Graphics/Shape.cpp index 94c99cd1f..6cdc6cfaa 100644 --- a/src/SFML/Graphics/Shape.cpp +++ b/src/SFML/Graphics/Shape.cpp @@ -27,6 +27,7 @@ //////////////////////////////////////////////////////////// #include #include +#include #include #include @@ -148,6 +149,28 @@ FloatRect Shape::getGlobalBounds() const } +//////////////////////////////////////////////////////////// +void Shape::batch(SpriteBatch& spriteBatch, float depth) const +{ + // Draw the shape itself + { + const std::size_t shapeCount = m_vertices.getVertexCount(); + const Vertex* shapePtr = shapeCount > 0 ? &m_vertices[0] : nullptr; + + spriteBatch.batch(shapePtr, shapeCount, m_vertices.getPrimitiveType(), m_texture, getTransform(), depth); + } + + // Draw outline + if (m_outlineThickness != 0) + { + const std::size_t outlineCount = m_outlineVertices.getVertexCount(); + const Vertex* outlinePtr = outlineCount > 0 ? &m_outlineVertices[0] : nullptr; + + spriteBatch.batch(outlinePtr, outlineCount, m_outlineVertices.getPrimitiveType(), nullptr, getTransform(), depth); + } +} + + //////////////////////////////////////////////////////////// Shape::Shape() = default; diff --git a/src/SFML/Graphics/Sprite.cpp b/src/SFML/Graphics/Sprite.cpp index eaf044c3f..2f72104a4 100644 --- a/src/SFML/Graphics/Sprite.cpp +++ b/src/SFML/Graphics/Sprite.cpp @@ -27,6 +27,7 @@ //////////////////////////////////////////////////////////// #include #include +#include #include #include @@ -130,6 +131,14 @@ FloatRect Sprite::getGlobalBounds() const } +//////////////////////////////////////////////////////////// +void Sprite::batch(SpriteBatch& spriteBatch, float depth) const +{ + if (m_texture) + spriteBatch.batch(m_vertices, 4, PrimitiveType::TriangleStrip, m_texture, getTransform(), depth); +} + + //////////////////////////////////////////////////////////// void Sprite::draw(RenderTarget& target, const RenderStates& states) const { diff --git a/src/SFML/Graphics/SpriteBatch.cpp b/src/SFML/Graphics/SpriteBatch.cpp new file mode 100644 index 000000000..323c42a03 --- /dev/null +++ b/src/SFML/Graphics/SpriteBatch.cpp @@ -0,0 +1,248 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2022 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace sf +{ +//////////////////////////////////////////////////////////// +void SpriteBatch::setBatchMode(BatchMode batchMode) +{ + m_batchMode = batchMode; + m_updateRequired = true; +} + + +//////////////////////////////////////////////////////////// +void SpriteBatch::batch(const Batchable& drawable, float depth) +{ + drawable.batch(*this, depth); +} + + +//////////////////////////////////////////////////////////// +void SpriteBatch::batch(const Vertex* vertices, + std::size_t count, + PrimitiveType type, + const Texture* texture, + const Transform& transform, + float depth) +{ + const bool unsupportedType = type != PrimitiveType::TriangleFan && type != PrimitiveType::TriangleStrip && + type != PrimitiveType::Triangles; + + if (unsupportedType) + { + err() << "SpriteBatch supports only triangle-based primitive types."; + return; + } + + // Anything to batch? + if (count == 0) + return; + + // Split into triangles + switch (type) + { + case PrimitiveType::TriangleStrip: + for (std::size_t i = 2; i < count; i++) + pushTriangle(vertices[i - 2], vertices[i - 1], vertices[i], transform, texture, depth); + break; + + case PrimitiveType::TriangleFan: + for (std::size_t i = 2; i < count; i++) + pushTriangle(vertices[0], vertices[i - 1], vertices[i], transform, texture, depth); + break; + + case PrimitiveType::Triangles: + for (std::size_t i = 2; i < count; i += 3) + pushTriangle(vertices[i - 2], vertices[i - 1], vertices[i], transform, texture, depth); + break; + + default: + err() << "A non-triangle primitive type was encountered when decomposing into triangles." << std::endl; + assert(false); + break; + } +} + + +//////////////////////////////////////////////////////////// +void SpriteBatch::draw(RenderTarget& target, const RenderStates& states) const +{ + if (m_updateRequired) + { + updateBatch(); + m_updateRequired = false; + } + + RenderStates statesCopy = states; + + std::size_t startTriangle = 0; + for (const auto& batch : m_batches) + { + statesCopy.texture = batch.texture; + target.draw(&m_vertices[startTriangle], batch.vertexCount, PrimitiveType::Triangles, statesCopy); + startTriangle += batch.vertexCount; + } +} + + +//////////////////////////////////////////////////////////// +void SpriteBatch::pushTriangle(const Vertex& a, + const Vertex& b, + const Vertex& c, + const Transform& transform, + const Texture* texture, + float depth) +{ + m_triangles.emplace_back(texture, depth); + + m_unsortedVertices.emplace_back(transform * a.position, a.color, a.texCoords); + m_unsortedVertices.emplace_back(transform * b.position, b.color, b.texCoords); + m_unsortedVertices.emplace_back(transform * c.position, c.color, c.texCoords); + + m_updateRequired = true; +} + + +//////////////////////////////////////////////////////////// +void SpriteBatch::updateBatch() const +{ + if (m_batchMode == BatchMode::Deferred) + { + // Batch based on texture change + std::size_t startIndex = 0; + std::size_t nextIndex = 0; + const Texture* lastTexture = nullptr; + + while (nextIndex < m_triangles.size()) + { + const Texture* nextTexture = m_triangles[nextIndex].texture; + if (nextTexture != lastTexture) + { + m_batches.emplace_back(lastTexture, (nextIndex - startIndex) * 3); + lastTexture = nextTexture; + startIndex = nextIndex; + } + + nextIndex++; + } + + // Deal with leftovers + if (startIndex != m_triangles.size()) + m_batches.emplace_back(lastTexture, (m_triangles.size() - startIndex) * 3); + + m_vertices = m_unsortedVertices; + } + + else if (m_batchMode == BatchMode::TextureSort || m_batchMode == BatchMode::DepthSort) + { + std::vector indices(m_triangles.size()); + + std::iota(indices.begin(), indices.end(), 0); + + if (m_batchMode == BatchMode::TextureSort) + { + const auto comp = [this](const std::size_t a, const std::size_t b) + { return m_triangles[a].texture < m_triangles[b].texture; }; + + std::stable_sort(indices.begin(), indices.end(), comp); + } + + else + { + const auto comp = [this](const std::size_t a, const std::size_t b) + { + if (m_triangles[a].depth != m_triangles[b].depth) + return m_triangles[a].depth > m_triangles[b].depth; + else + return m_triangles[a].texture < m_triangles[b].texture; + }; + + std::stable_sort(indices.begin(), indices.end(), comp); + } + + // Create the array of sorted vertices + // We need them sorted so that we feed chunks to RenderTarget + m_vertices.resize(m_unsortedVertices.size()); + + for (std::size_t i = 0; i < indices.size(); i++) + { + const std::size_t newPos = i * 3; + const std::size_t oldPos = indices[i] * 3; + m_vertices[newPos] = m_unsortedVertices[oldPos]; + m_vertices[newPos + 1] = m_unsortedVertices[oldPos + 1]; + m_vertices[newPos + 2] = m_unsortedVertices[oldPos + 2]; + } + + // Generate batches + std::size_t startIndex = 0; + std::size_t nextIndex = 0; + const Texture* lastTexture = nullptr; + + while (nextIndex < m_triangles.size()) + { + const Texture* nextTexture = m_triangles[indices[nextIndex]].texture; + + if (nextTexture != lastTexture) + { + m_batches.emplace_back(lastTexture, (nextIndex - startIndex) * 3); + lastTexture = nextTexture; + startIndex = nextIndex; + } + + nextIndex++; + } + + // Deal with leftovers + if (startIndex != m_triangles.size()) + m_batches.emplace_back(m_triangles[indices[startIndex]].texture, (m_triangles.size() - startIndex) * 3); + } +} + + +//////////////////////////////////////////////////////////// +void SpriteBatch::clear() +{ + m_unsortedVertices.clear(); + m_triangles.clear(); + m_vertices.clear(); + m_batches.clear(); + + m_updateRequired = false; +} + +} // namespace sf diff --git a/src/SFML/Graphics/Text.cpp b/src/SFML/Graphics/Text.cpp index 5f54d07d7..5abeb5c8c 100644 --- a/src/SFML/Graphics/Text.cpp +++ b/src/SFML/Graphics/Text.cpp @@ -27,6 +27,7 @@ //////////////////////////////////////////////////////////// #include #include +#include #include #include @@ -369,6 +370,33 @@ FloatRect Text::getGlobalBounds() const } +//////////////////////////////////////////////////////////// +void Text::batch(SpriteBatch& spriteBatch, float depth) const +{ + if (m_font) + { + ensureGeometryUpdate(); + const Texture* texture = &m_font->getTexture(m_characterSize); + + // Draw outline + if (m_outlineThickness != 0) + { + const std::size_t outlineCount = m_outlineVertices.getVertexCount(); + const Vertex* outlinePtr = outlineCount > 0 ? &m_outlineVertices[0] : nullptr; + + spriteBatch.batch(outlinePtr, outlineCount, m_outlineVertices.getPrimitiveType(), texture, getTransform(), depth); + } + + // Draw the text itself + { + const std::size_t textCount = m_vertices.getVertexCount(); + const Vertex* textPtr = textCount > 0 ? &m_vertices[0] : nullptr; + + spriteBatch.batch(textPtr, textCount, m_vertices.getPrimitiveType(), texture, getTransform(), depth); + } + } +} + //////////////////////////////////////////////////////////// void Text::draw(RenderTarget& target, const RenderStates& states) const {