So apparently
clipping masks have been a requested feature for a while. I don't know if I have
the solution, but I think I might have
a solution.
But first, pretty picture time!
As you can see, the background is only being drawn over certain parts of the screen. First we drew a sprite and a circle as stencils, then we drew the background "tracing" over that stencil, then we drew the sprite and circle again in different locations, completely ignoring the existence of stencils. In this implementation there's only one new class, the
StencilSettings class, that is passed along with
sf::RenderStates. This implementation also works on anything that can be drawn, including textured sprites that use the alpha channel and arbitrarily complex polygons, and is respected by view transformations.
Basically, my implementation takes inspiration from the implementation of blending through
sf::BlendMode. In the same way you simply pass an
sf::BlendMode to a
sf::RenderStates' constructor to change how that object is drawn, you can pass an
sf::StencilSettings to change how this object affects the stencil buffer or is affected by the stencil buffer.
The two main
StencilSettings defined are
sf::StencilCreate and
sf::StencilTrace (of course you can make your own like you can with sf::BlendMode). Basically:
window.draw(someObject, sf::RenderStates(sf::StencilCreate)) will draw
someObject as a stencil.
window.draw(anotherObject, sf::RenderStates(sf::StencilTrace)) will then draw
anotherObject only where it overlaps the area
someObject drew.
More specifically,
sf::StencilCreate draws this object to the stencil buffer instead of the color buffer.
sf::StencilTrace makes it so that the object is only drawn when it overlaps an object drawn with
sf::StencilTrace. (There is also a third pre-defined
sf::StencilDisable that is just constructed with the default
sf::StencilSettings--it's only there for completeness and is not strictly necessary.)
Here's the example code that generated the above image. Nothing of note happens until the event loop.
int main(int argc, char* argv[])
{
sf::RenderWindow window;
window.create(sf::VideoMode(640, 480), "Title", sf::Style::Default);
window.setFramerateLimit(60);
// Boring initialization stuff.
// Nothing interesting here--you can just skip past it.
sf::Sprite sprite;
sf::Sprite backgroundSprite;
sf::Texture texture;
sf::Texture backgroundTexture;
sf::CircleShape shape;
texture.loadFromFile("TestSprite.png");
sprite.setTexture(texture);
backgroundTexture.loadFromFile("Background.png");
backgroundSprite.setTexture(backgroundTexture);
shape.setOutlineColor(sf::Color::Black);
shape.setOutlineThickness(3.5f);
shape.setFillColor(sf::Color::White);
shape.setRadius(40);
shape.setOrigin(40, 40);
int animationCounter = 0;
while (window.isOpen())
{
handleInput(window);
animationCounter++;
window.clear(sf::Color::Blue);
// Move the sprite and circle around a bit
sprite.setPosition(0 + std::sinf(animationCounter / 100.f) * 100, 0);
shape.setPosition(0 + std::cosf(animationCounter / 100.f) * 100 + 100, 256.f);
// Draw our sprite as a stencil.
window.draw(sprite, sf::RenderStates(sf::StencilCreate));
// Draw our circle shape as a stencil.
window.draw(shape, sf::RenderStates(sf::StencilCreate));
// Draw our background only where we've previously drawn a stencil
window.draw(backgroundSprite, sf::RenderStates(sf::StencilTrace));
// Draw normal sprites and shapes unrelated to the stencil.
sprite.move(static_cast<sf::Vector2f>(texture.getSize()));
shape.move({ 0.f, shape.getRadius() * 2.f });
window.draw(sprite);
window.draw(shape);
window.display();
}
return 0;
}
For reference, three's
already a pull request on GitHub that offers an alternative solution. I'm mostly offering this as a different take on the same idea as the current pull request takes a completely different design approach than I took here. Competition's always good, right?
The implementation fork is
here. The changes are:
- Added extra constructors to RenderStates to take StencilSettings, as well as a stencilSettings member variable.
- Added void applyStencilSettings(const StencilSettings&) to RenderTarget
- Added a lastStencilSettings member variable to RenderTarget
- Changed ContextSettings' default constructor to request one stencil bit
- Changed RenderStates::Default to include the default StencilSettings
- Changed RenderTarget::clear to clear the stencil buffer as well
- RenderTarget::draw calls applyStencilSettings
- RenderTarget::resetGLStates calls applyStencilSettings(StencilSettings())
These should all be backwards-compatible unless you were doing stuff with the stencil buffer before this without calling
pushGLStates/
popGLStates.
So I'm just going to finish this off by saying that I'm totally more than welcome to comments/criticisms/concerns! ...but I don't really know OpenGL. I basically made this through sheer unbridled determination to get my unmoving plaid effect working after trying and failing to implement it with shaders and then with fancy blending abuse. In other words, I'll do my best to answer any questions, but at the same time if it gets too technical in terms of OpenGL-related jargon it's probably going to fly right over my head.
(Incidentally I wasn't sure if it would have been better to simply do a pull request or not instead of creating this post, but I sort of got the impression from here that it'd be better to create a forum post first.)