diff --git a/examples/shader/CMakeLists.txt b/examples/shader/CMakeLists.txt index 0cdb7c4c2..301dbd0eb 100644 --- a/examples/shader/CMakeLists.txt +++ b/examples/shader/CMakeLists.txt @@ -1,6 +1,5 @@ # all source files -set(SRC Effect.hpp - Shader.cpp) +set(SRC Shader.cpp) # define the shader target sfml_add_example(shader GUI_APP diff --git a/examples/shader/Effect.hpp b/examples/shader/Effect.hpp deleted file mode 100644 index 9cebc6bb3..000000000 --- a/examples/shader/Effect.hpp +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -//////////////////////////////////////////////////////////// -// Headers -//////////////////////////////////////////////////////////// -#include - -#include -#include - -#include - - -//////////////////////////////////////////////////////////// -// Base class for effects -//////////////////////////////////////////////////////////// -class Effect : public sf::Drawable -{ -public: - static void setFont(const sf::Font& font) - { - s_font = &font; - } - - const std::string& getName() const - { - return m_name; - } - - void load() - { - m_isLoaded = sf::Shader::isAvailable() && onLoad(); - } - - void update(float time, float x, float y) - { - if (m_isLoaded) - onUpdate(time, x, y); - } - - void draw(sf::RenderTarget& target, sf::RenderStates states) const override - { - if (m_isLoaded) - { - onDraw(target, states); - } - else - { - // Clear the target to grey to make sure the text is always readable - target.clear(sf::Color(50, 50, 50)); - sf::Text error(getFont(), "Shader not\nsupported"); - error.setPosition({320.f, 200.f}); - error.setCharacterSize(36); - target.draw(error, states); - } - } - -protected: - Effect(std::string name) : m_name(std::move(name)) - { - } - - static const sf::Font& getFont() - { - assert(s_font != nullptr && "Cannot get font until setFont() is called"); - return *s_font; - } - -private: - // Virtual functions to be implemented in derived effects - virtual bool onLoad() = 0; - virtual void onUpdate(float time, float x, float y) = 0; - virtual void onDraw(sf::RenderTarget& target, sf::RenderStates states) const = 0; - - std::string m_name; - bool m_isLoaded{}; - - // NOLINTNEXTLINE(readability-identifier-naming) - static inline const sf::Font* s_font{}; -}; diff --git a/examples/shader/Shader.cpp b/examples/shader/Shader.cpp index b90886842..9be6dcc94 100644 --- a/examples/shader/Shader.cpp +++ b/examples/shader/Shader.cpp @@ -1,15 +1,14 @@ //////////////////////////////////////////////////////////// // Headers //////////////////////////////////////////////////////////// -#include "Effect.hpp" #include #include +#include #include #include #include -#include #include #include @@ -21,46 +20,56 @@ std::random_device rd; std::mt19937 rng(rd()); } // namespace + +//////////////////////////////////////////////////////////// +// Base class for effects +//////////////////////////////////////////////////////////// +struct Effect : sf::Drawable +{ + virtual void update(float time, float x, float y) = 0; +}; + + //////////////////////////////////////////////////////////// // "Pixelate" fragment shader //////////////////////////////////////////////////////////// class Pixelate : public Effect { public: - Pixelate() : Effect("Pixelate") + static std::optional tryLoad() { - } + auto texture = sf::Texture::loadFromFile("resources/background.jpg"); + if (!texture.has_value()) + return std::nullopt; - bool onLoad() override - { - // Load the texture and initialize the sprite - if (!(m_texture = sf::Texture::loadFromFile("resources/background.jpg"))) - return false; - m_sprite.emplace(*m_texture); + auto shader = sf::Shader::loadFromFile("resources/pixelate.frag", sf::Shader::Type::Fragment); + if (!shader.has_value()) + return std::nullopt; - // Load the shader - if (!(m_shader = sf::Shader::loadFromFile("resources/pixelate.frag", sf::Shader::Type::Fragment))) - return false; - m_shader->setUniform("texture", sf::Shader::CurrentTexture); - - return true; - } - - void onUpdate(float, float x, float y) override - { - m_shader->setUniform("pixel_threshold", (x + y) / 30); - } - - void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override - { - states.shader = &*m_shader; - target.draw(*m_sprite, states); + return Pixelate{std::move(*texture), std::move(*shader)}; } private: - std::optional m_texture; - std::optional m_sprite; - std::optional m_shader; + explicit Pixelate(sf::Texture&& texture, sf::Shader&& shader) : + m_texture(std::move(texture)), + m_shader(std::move(shader)) + { + m_shader.setUniform("texture", sf::Shader::CurrentTexture); + } + + void update(float /* time */, float x, float y) override + { + m_shader.setUniform("pixel_threshold", (x + y) / 30); + } + + void draw(sf::RenderTarget& target, sf::RenderStates states) const override + { + states.shader = &m_shader; + target.draw(sf::Sprite{m_texture}, states); + } + + sf::Texture m_texture; + sf::Shader m_shader; }; @@ -70,55 +79,57 @@ private: class WaveBlur : public Effect { public: - WaveBlur() : Effect("Wave + Blur"), m_text(getFont()) + static std::optional tryLoad(const sf::Font& font) { + auto shader = sf::Shader::loadFromFile("resources/wave.vert", "resources/blur.frag"); + if (!shader.has_value()) + return std::nullopt; + + return WaveBlur{font, std::move(*shader)}; } - bool onLoad() override + void update(float time, float x, float y) override { - // Create the text - m_text.setString( - "Praesent suscipit augue in velit pulvinar hendrerit varius purus aliquam.\n" - "Mauris mi odio, bibendum quis fringilla a, laoreet vel orci. Proin vitae vulputate tortor.\n" - "Praesent cursus ultrices justo, ut feugiat ante vehicula quis.\n" - "Donec fringilla scelerisque mauris et viverra.\n" - "Maecenas adipiscing ornare scelerisque. Nullam at libero elit.\n" - "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.\n" - "Nullam leo urna, tincidunt id semper eget, ultricies sed mi.\n" - "Morbi mauris massa, commodo id dignissim vel, lobortis et elit.\n" - "Fusce vel libero sed neque scelerisque venenatis.\n" - "Integer mattis tincidunt quam vitae iaculis.\n" - "Vivamus fringilla sem non velit venenatis fermentum.\n" - "Vivamus varius tincidunt nisi id vehicula.\n" - "Integer ullamcorper, enim vitae euismod rutrum, massa nisl semper ipsum,\n" - "vestibulum sodales sem ante in massa.\n" - "Vestibulum in augue non felis convallis viverra.\n" - "Mauris ultricies dolor sed massa convallis sed aliquet augue fringilla.\n" - "Duis erat eros, porta in accumsan in, blandit quis sem.\n" - "In hac habitasse platea dictumst. Etiam fringilla est id odio dapibus sit amet semper dui laoreet.\n"); - m_text.setCharacterSize(22); - m_text.setPosition({30.f, 20.f}); - - // Load the shader - return (m_shader = sf::Shader::loadFromFile("resources/wave.vert", "resources/blur.frag")).has_value(); + m_shader.setUniform("wave_phase", time); + m_shader.setUniform("wave_amplitude", sf::Vector2f(x * 40, y * 40)); + m_shader.setUniform("blur_radius", (x + y) * 0.008f); } - void onUpdate(float time, float x, float y) override + void draw(sf::RenderTarget& target, sf::RenderStates states) const override { - m_shader->setUniform("wave_phase", time); - m_shader->setUniform("wave_amplitude", sf::Vector2f(x * 40, y * 40)); - m_shader->setUniform("blur_radius", (x + y) * 0.008f); - } - - void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override - { - states.shader = &*m_shader; + states.shader = &m_shader; target.draw(m_text, states); } private: - sf::Text m_text; - std::optional m_shader; + explicit WaveBlur(const sf::Font& font, sf::Shader&& shader) : + m_text(font, + "Praesent suscipit augue in velit pulvinar hendrerit varius purus aliquam.\n" + "Mauris mi odio, bibendum quis fringilla a, laoreet vel orci. Proin vitae vulputate tortor.\n" + "Praesent cursus ultrices justo, ut feugiat ante vehicula quis.\n" + "Donec fringilla scelerisque mauris et viverra.\n" + "Maecenas adipiscing ornare scelerisque. Nullam at libero elit.\n" + "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.\n" + "Nullam leo urna, tincidunt id semper eget, ultricies sed mi.\n" + "Morbi mauris massa, commodo id dignissim vel, lobortis et elit.\n" + "Fusce vel libero sed neque scelerisque venenatis.\n" + "Integer mattis tincidunt quam vitae iaculis.\n" + "Vivamus fringilla sem non velit venenatis fermentum.\n" + "Vivamus varius tincidunt nisi id vehicula.\n" + "Integer ullamcorper, enim vitae euismod rutrum, massa nisl semper ipsum,\n" + "vestibulum sodales sem ante in massa.\n" + "Vestibulum in augue non felis convallis viverra.\n" + "Mauris ultricies dolor sed massa convallis sed aliquet augue fringilla.\n" + "Duis erat eros, porta in accumsan in, blandit quis sem.\n" + "In hac habitasse platea dictumst. Etiam fringilla est id odio dapibus sit amet semper dui laoreet.\n", + 22), + m_shader(std::move(shader)) + { + m_text.setPosition({30.f, 20.f}); + } + + sf::Text m_text; + sf::Shader m_shader; }; @@ -128,11 +139,33 @@ private: class StormBlink : public Effect { public: - StormBlink() : Effect("Storm + Blink") + static std::optional tryLoad() { + auto shader = sf::Shader::loadFromFile("resources/storm.vert", "resources/blink.frag"); + if (!shader.has_value()) + return std::nullopt; + + return StormBlink{std::move(*shader)}; } - bool onLoad() override + void update(float time, float x, float y) override + { + const float radius = 200 + std::cos(time) * 150; + + m_shader.setUniform("storm_position", sf::Vector2f(x * 800, y * 600)); + m_shader.setUniform("storm_inner_radius", radius / 3); + m_shader.setUniform("storm_total_radius", radius); + m_shader.setUniform("blink_alpha", 0.5f + std::cos(time * 3) * 0.25f); + } + + void draw(sf::RenderTarget& target, sf::RenderStates states) const override + { + states.shader = &m_shader; + target.draw(m_points, states); + } + +private: + explicit StormBlink(sf::Shader&& shader) : m_shader(std::move(shader)) { std::uniform_real_distribution xDistribution(0, 800); std::uniform_real_distribution yDistribution(0, 600); @@ -140,38 +173,22 @@ public: // Create the points m_points.setPrimitiveType(sf::PrimitiveType::Points); + for (int i = 0; i < 40000; ++i) { const auto x = xDistribution(rng); const auto y = yDistribution(rng); + const auto r = static_cast(colorDistribution(rng)); const auto g = static_cast(colorDistribution(rng)); const auto b = static_cast(colorDistribution(rng)); + m_points.append({{x, y}, {r, g, b}}); } - - // Load the shader - return (m_shader = sf::Shader::loadFromFile("resources/storm.vert", "resources/blink.frag")).has_value(); } - void onUpdate(float time, float x, float y) override - { - const float radius = 200 + std::cos(time) * 150; - m_shader->setUniform("storm_position", sf::Vector2f(x * 800, y * 600)); - m_shader->setUniform("storm_inner_radius", radius / 3); - m_shader->setUniform("storm_total_radius", radius); - m_shader->setUniform("blink_alpha", 0.5f + std::cos(time * 3) * 0.25f); - } - - void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override - { - states.shader = &*m_shader; - target.draw(m_points, states); - } - -private: - sf::VertexArray m_points; - std::optional m_shader; + sf::VertexArray m_points; + sf::Shader m_shader; }; @@ -181,80 +198,86 @@ private: class Edge : public Effect { public: - Edge() : Effect("Edge Post-effect") - { - } - - bool onLoad() override + static std::optional tryLoad() { // Create the off-screen surface - if (!(m_surface = sf::RenderTexture::create({800, 600}))) - return false; - m_surface->setSmooth(true); + auto surface = sf::RenderTexture::create({800, 600}); + if (!surface.has_value()) + return std::nullopt; - // Load the textures - if (!(m_backgroundTexture = sf::Texture::loadFromFile("resources/sfml.png"))) - return false; - m_backgroundTexture->setSmooth(true); - if (!(m_entityTexture = sf::Texture::loadFromFile("resources/devices.png"))) - return false; - m_entityTexture->setSmooth(true); + surface->setSmooth(true); - // Initialize the background sprite - m_backgroundSprite.emplace(*m_backgroundTexture); - m_backgroundSprite->setPosition({135.f, 100.f}); + // Load the background texture + auto backgroundTexture = sf::Texture::loadFromFile("resources/sfml.png"); + if (!backgroundTexture.has_value()) + return std::nullopt; - // Load the moving entities - for (int i = 0; i < 6; ++i) - { - const sf::Sprite entity(*m_entityTexture, sf::IntRect({96 * i, 0}, {96, 96})); - m_entities.push_back(entity); - } + backgroundTexture->setSmooth(true); + + // Load the entity texture + auto entityTexture = sf::Texture::loadFromFile("resources/devices.png"); + if (!entityTexture.has_value()) + return std::nullopt; + + entityTexture->setSmooth(true); // Load the shader - if (!(m_shader = sf::Shader::loadFromFile("resources/edge.frag", sf::Shader::Type::Fragment))) - return false; - m_shader->setUniform("texture", sf::Shader::CurrentTexture); + auto shader = sf::Shader::loadFromFile("resources/edge.frag", sf::Shader::Type::Fragment); + if (!shader.has_value()) + return std::nullopt; - return true; + shader->setUniform("texture", sf::Shader::CurrentTexture); + + return Edge{std::move(*surface), std::move(*backgroundTexture), std::move(*entityTexture), std::move(*shader)}; } - void onUpdate(float time, float x, float y) override + void update(float time, float x, float y) override { - m_shader->setUniform("edge_threshold", 1 - (x + y) / 2); - - // Update the position of the moving entities - for (std::size_t i = 0; i < m_entities.size(); ++i) - { - sf::Vector2f position; - position.x = std::cos(0.25f * (time * static_cast(i) + static_cast(m_entities.size() - i))) * 300 + - 350; - position.y = std::sin(0.25f * (time * static_cast(m_entities.size() - i) + static_cast(i))) * 200 + - 250; - m_entities[i].setPosition(position); - } + m_shader.setUniform("edge_threshold", 1 - (x + y) / 2); // Render the updated scene to the off-screen surface - m_surface->clear(sf::Color::White); - m_surface->draw(*m_backgroundSprite); - for (const sf::Sprite& entity : m_entities) - m_surface->draw(entity); - m_surface->display(); + m_surface.clear(sf::Color::White); + + sf::Sprite backgroundSprite{m_backgroundTexture}; + backgroundSprite.setPosition({135.f, 100.f}); + m_surface.draw(backgroundSprite); + + // Update the position of the moving entities + constexpr int numEntities = 6; + + for (int i = 0; i < 6; ++i) + { + sf::Sprite entity{m_entityTexture, sf::IntRect({96 * i, 0}, {96, 96})}; + + entity.setPosition( + {std::cos(0.25f * (time * static_cast(i) + static_cast(numEntities - i))) * 300 + 350, + std::sin(0.25f * (time * static_cast(numEntities - i) + static_cast(i))) * 200 + 250}); + + m_surface.draw(entity); + } + + m_surface.display(); } - void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override + void draw(sf::RenderTarget& target, sf::RenderStates states) const override { - states.shader = &*m_shader; - target.draw(sf::Sprite(m_surface->getTexture()), states); + states.shader = &m_shader; + target.draw(sf::Sprite{m_surface.getTexture()}, states); } private: - std::optional m_surface; - std::optional m_backgroundTexture; - std::optional m_entityTexture; - std::optional m_backgroundSprite; - std::vector m_entities; - std::optional m_shader; + explicit Edge(sf::RenderTexture&& surface, sf::Texture&& backgroundTexture, sf::Texture&& entityTexture, sf::Shader&& shader) : + m_surface(std::move(surface)), + m_backgroundTexture(std::move(backgroundTexture)), + m_entityTexture(std::move(entityTexture)), + m_shader(std::move(shader)) + { + } + + sf::RenderTexture m_surface; + sf::Texture m_backgroundTexture; + sf::Texture m_entityTexture; + sf::Shader m_shader; }; @@ -264,48 +287,42 @@ private: class Geometry : public Effect { public: - Geometry() : Effect("Geometry Shader Billboards"), m_pointCloud(sf::PrimitiveType::Points, 10000) - { - } - - bool onLoad() override + static std::optional tryLoad() { // Check if geometry shaders are supported if (!sf::Shader::isGeometryAvailable()) - return false; + return std::nullopt; - // Move the points in the point cloud to random positions - for (std::size_t i = 0; i < 10000; ++i) - { - // Spread the coordinates from -480 to +480 - // So they'll always fill the viewport at 800x600 - std::uniform_real_distribution positionDistribution(-480, 480); - m_pointCloud[i].position = {positionDistribution(rng), positionDistribution(rng)}; - } + // Load the logo texture + auto logoTexture = sf::Texture::loadFromFile("resources/logo.png"); + if (!logoTexture.has_value()) + return std::nullopt; - // Load the texture - if (!(m_logoTexture = sf::Texture::loadFromFile("resources/logo.png"))) - return false; + logoTexture->setSmooth(true); // Load the shader - if (!(m_shader = sf::Shader::loadFromFile("resources/billboard.vert", - "resources/billboard.geom", - "resources/billboard.frag"))) - return false; - m_shader->setUniform("texture", sf::Shader::CurrentTexture); + auto shader = sf::Shader::loadFromFile("resources/billboard.vert", + "resources/billboard.geom", + "resources/billboard.frag"); + if (!shader.has_value()) + return std::nullopt; + + shader->setUniform("texture", sf::Shader::CurrentTexture); // Set the render resolution (used for proper scaling) - m_shader->setUniform("resolution", sf::Vector2f(800, 600)); + shader->setUniform("resolution", sf::Vector2f(800, 600)); - return true; + return Geometry{std::move(*logoTexture), std::move(*shader)}; } - void onUpdate(float /*time*/, float x, float y) override + void update(float /* time */, float x, float y) override { // Reset our transformation matrix m_transform = sf::Transform::Identity; + // Move to the center of the window m_transform.translate({400.f, 300.f}); + // Rotate everything based on cursor position m_transform.rotate(sf::degrees(x * 360.f)); @@ -313,14 +330,14 @@ public: const float size = 25 + std::abs(y) * 50; // Update the shader parameter - m_shader->setUniform("size", sf::Vector2f(size, size)); + m_shader.setUniform("size", sf::Vector2f{size, size}); } - void onDraw(sf::RenderTarget& target, sf::RenderStates states) const override + void draw(sf::RenderTarget& target, sf::RenderStates states) const override { // Prepare the render state - states.shader = &*m_shader; - states.texture = &*m_logoTexture; + states.shader = &m_shader; + states.texture = &m_logoTexture; states.transform = m_transform; // Draw the point cloud @@ -328,10 +345,24 @@ public: } private: - std::optional m_logoTexture; - sf::Transform m_transform; - std::optional m_shader; - sf::VertexArray m_pointCloud; + explicit Geometry(sf::Texture&& logoTexture, sf::Shader&& shader) : + m_logoTexture(std::move(logoTexture)), + m_shader(std::move(shader)), + m_pointCloud(sf::PrimitiveType::Points, 10000) + { + // Move the points in the point cloud to random positions + for (std::size_t i = 0; i < 10000; ++i) + { + // Spread the coordinates from -480 to +480 so they'll always fill the viewport at 800x600 + std::uniform_real_distribution positionDistribution(-480, 480); + m_pointCloud[i].position = {positionDistribution(rng), positionDistribution(rng)}; + } + } + + sf::Texture m_logoTexture; + sf::Transform m_transform; + sf::Shader m_shader; + sf::VertexArray m_pointCloud; }; @@ -343,29 +374,41 @@ private: //////////////////////////////////////////////////////////// int main() { + // Exit early if shaders are not available + if (!sf::Shader::isAvailable()) + { + std::cerr << "Shaders not supported on current system, aborting" << std::endl; + return EXIT_FAILURE; + } + // Create the main window sf::RenderWindow window(sf::VideoMode({800, 600}), "SFML Shader", sf::Style::Titlebar | sf::Style::Close); window.setVerticalSyncEnabled(true); - // Load the application font and pass it to the Effect class + // Load the application font const auto font = sf::Font::loadFromFile("resources/tuffy.ttf").value(); - Effect::setFont(font); // Create the effects - Pixelate pixelateEffect; - WaveBlur waveBlurEffect; - StormBlink stormBlinkEffect; - Edge edgeEffect; - Geometry geometryEffect; + std::optional pixelateEffect = Pixelate::tryLoad(); + std::optional waveBlurEffect = WaveBlur::tryLoad(font); + std::optional stormBlinkEffect = StormBlink::tryLoad(); + std::optional edgeEffect = Edge::tryLoad(); + std::optional geometryEffect = Geometry::tryLoad(); - const std::array effects{&pixelateEffect, &waveBlurEffect, &stormBlinkEffect, &edgeEffect, &geometryEffect}; + const auto optionalToPtr = [&](auto& effect) -> Effect* { return effect.has_value() ? &*effect : nullptr; }; + const std::array effects{optionalToPtr(pixelateEffect), + optionalToPtr(waveBlurEffect), + optionalToPtr(stormBlinkEffect), + optionalToPtr(edgeEffect), + optionalToPtr(geometryEffect)}; + + const std::array + effectNames{"Pixelate", "Wave + Blur", "Storm + Blink", "Edge Post-effect", "Geometry Shader Billboards"}; + + // Index of currently selected effect std::size_t current = 0; - // Initialize them - for (Effect* effect : effects) - effect->load(); - // Create the messages background const auto textBackgroundTexture = sf::Texture::loadFromFile("resources/text-background.png").value(); sf::Sprite textBackground(textBackgroundTexture); @@ -373,7 +416,7 @@ int main() textBackground.setColor(sf::Color(255, 255, 255, 200)); // Create the description text - sf::Text description(font, "Current effect: " + effects[current]->getName(), 20); + sf::Text description(font, "Current effect: " + effectNames[current], 20); description.setPosition({10.f, 530.f}); description.setFillColor(sf::Color(80, 80, 80)); @@ -391,7 +434,10 @@ int main() { // Close window: exit if (event.is()) + { window.close(); + break; + } if (const auto* keyPressed = event.getIf()) { @@ -399,49 +445,65 @@ int main() { // Escape key: exit case sf::Keyboard::Key::Escape: + { window.close(); break; + } // Left arrow key: previous shader case sf::Keyboard::Key::Left: + { if (current == 0) current = effects.size() - 1; else --current; - description.setString("Current effect: " + effects[current]->getName()); + break; + } // Right arrow key: next shader case sf::Keyboard::Key::Right: + { if (current == effects.size() - 1) current = 0; else ++current; - description.setString("Current effect: " + effects[current]->getName()); + break; + } default: break; } + + description.setString("Current effect: " + effectNames[current]); } } - // Update the current example - const auto [x, y] = sf::Vector2f(sf::Mouse::getPosition(window)).cwiseDiv(sf::Vector2f(window.getSize())); - effects[current]->update(clock.getElapsedTime().asSeconds(), x, y); - - // Clear the window - if (effects[current]->getName() == "Edge Post-effect") + // If the current example was loaded successfully... + if (effects[current] != nullptr) { - window.clear(sf::Color::White); + // Update the current example + const auto [x, y] = sf::Vector2f(sf::Mouse::getPosition(window)).cwiseDiv(sf::Vector2f(window.getSize())); + effects[current]->update(clock.getElapsedTime().asSeconds(), x, y); + + // Clear the window + window.clear(effectNames[current] == "Edge Post-effect" ? sf::Color::White : sf::Color(50, 50, 50)); + + // Draw the current example + window.draw(*effects[current]); } else { + // Clear the window to grey to make sure the text is always readable window.clear(sf::Color(50, 50, 50)); - } - // Draw the current example - window.draw(*effects[current]); + sf::Text error(font, "Shader not\nsupported"); + error.setPosition({320.f, 200.f}); + error.setCharacterSize(36); + + window.draw(error); + } // Draw the text window.draw(textBackground);