I'm trying to create a shade to a text. A sharp shade is obviously no problem, just draw the text twice slightly displaced in the opposite direction of the "light" direction. I would like to blur this shadow around the edges. The answer seems to be "use shaders".
I've looked at the (SFML) shader examples and isolated the blur effect out of the wave+blur effect. This does not give a good result. That's why I'm working on a shader solution...
Other simpeler approaches (to the problem of adding feathered shade to text) are equally appreciated.
[EDIT] I've come to something I'm happy with (for now). It's still slow, but for my purposes I can work around it. I'll post the code in the top post (this one), and attach the result of running the code below it. The attached png is for some reason displayed larger (about 125%) than it is in reality (when clicked) and will therefore look better in an application.
#include <ciso646>
#include <vector>
#include <string>
#include <SFML/Graphics.hpp>
const std::string gaussian_blur_shader { "\
\
uniform sampler2D texture;\
\
uniform vec2 pixel_size;\
uniform int blur_radius;\
uniform float [ 256 ] weights;\
uniform bool horizontal;\
\
\
void main ( ) {\
\
const vec2 texture_coordinates = gl_TexCoord [ 0 ].xy;\
\
gl_FragColor = texture2D ( texture, texture_coordinates ) * weights [ 0 ];\
\
if ( horizontal ) {\
\
for ( int i = 1; i < blur_radius; ++i ) {\
\
const vec2 offset = vec2 ( float ( i ) * pixel_size.x, 0.0f );\
\
gl_FragColor += texture2D ( texture, texture_coordinates + offset ) * weights [ i ];\
gl_FragColor += texture2D ( texture, texture_coordinates - offset ) * weights [ i ];\
}\
}\
\
else {\
\
for ( int i = 1; i < blur_radius; ++i ) {\
\
const vec2 offset = vec2 ( 0.0f, float ( i ) * pixel_size.y );\
\
gl_FragColor += texture2D ( texture, texture_coordinates + offset ) * weights [ i ];\
gl_FragColor += texture2D ( texture, texture_coordinates - offset ) * weights [ i ];\
}\
}\
}\
\
" };
template<typename T>
class Shadowed : public sf::Drawable {
sf::Sprite m_object_sprite;
const std::vector<float> m_gaussian_weights;
sf::Shader m_shader;
sf::RenderTexture m_horz_render_texture;
sf::Sprite m_horz_sprite;
sf::RenderTexture m_render_texture;
sf::Sprite m_sprite;
std::vector<float> gaussianWeights ( const std::size_t size_ ) const {
// http://www.cocos2d-x.org/wiki/User_Tutorial-RenderTexture_Plus_Blur
std::vector<float> gaussian_weights ( size_ );
double nill = 1.0;
for ( std::size_t i = 1; i < size_; ++i ) {
const double x = ( double ) i / ( double ) ( size_ - 1 ), gwi = 1.196826841204297942 / ( double ) size_ * std::exp ( -x * x * 4.5 );
gaussian_weights [ i ] = gwi;
nill -= 2.0 * gwi;
}
gaussian_weights [ 0 ] = nill;
return gaussian_weights;
}
public:
Shadowed (
T &object_,
const std::size_t radius_,
const sf::Vector2f &displacement_,
const sf::Color &shadow_color = sf::Color::Black,
const sf::Color &background_color = sf::Color::Transparent // To be set for better gradient...
) :
m_gaussian_weights ( gaussianWeights ( radius_ ) ) {
// Finish setting text object...
const sf::Color original_fill_color ( object_.getFillColor ( ) );
const sf::Color original_outline_color ( object_.getOutlineColor ( ) );
const sf::Vector2f original_position ( object_.getPosition ( ) );
const sf::Vector2f original_origin ( object_.getOrigin ( ) );
object_.setFillColor ( shadow_color );
object_.setOutlineColor ( shadow_color );
object_.setOrigin ( object_.getLocalBounds ( ).left - std::max ( ( float ) radius_, displacement_.x ), object_.getLocalBounds ( ).top - std::max ( ( float ) radius_, displacement_.y ) );
object_.setPosition ( sf::Vector2f ( 0.0f, 0.0f ) );
// Init render texture...
m_render_texture.create ( object_.getLocalBounds ( ).width + ( radius_ > std::abs ( displacement_.x ) ? 2 * radius_ : radius_ + std::abs ( displacement_.x ) ), object_.getLocalBounds ( ).height + ( radius_ > std::abs ( displacement_.y ) ? 2 * radius_ : radius_ + std::abs ( displacement_.y ) ) );
m_render_texture.clear ( background_color );
m_render_texture.setSmooth ( true );
m_render_texture.draw ( object_ );
m_render_texture.display ( );
// 1. Create object sprite...
m_object_sprite.setTexture ( m_render_texture.getTexture ( ) );
// Load shader...
m_shader.loadFromMemory ( gaussian_blur_shader, sf::Shader::Fragment );
// Set shader parameters...
m_shader.setUniform ( "pixel_size", sf::Vector2f ( 1.0f / m_object_sprite.getLocalBounds ( ).width, 1.0f / m_object_sprite.getLocalBounds ( ).height ) );
m_shader.setUniform ( "blur_radius", ( int ) m_gaussian_weights.size ( ) );
m_shader.setUniformArray ( "weights", m_gaussian_weights.data ( ), m_gaussian_weights.size ( ) );
// 2. Draw it to a RenderTexture with horizontal blur shader...
m_shader.setUniform ( "horizontal", true );
m_horz_render_texture.create ( m_object_sprite.getLocalBounds ( ).width, m_object_sprite.getLocalBounds ( ).height );
m_horz_render_texture.clear ( background_color );
m_horz_render_texture.setSmooth ( true );
m_horz_render_texture.draw ( m_object_sprite, &m_shader );
m_horz_render_texture.display ( );
// 3. Create a sprite from resulting texture...
m_horz_sprite.setTexture ( m_horz_render_texture.getTexture ( ) );
// 4. Draw this sprite to a Render texture with vertical shader...
m_shader.setUniform ( "horizontal", false );
m_render_texture.clear ( background_color );
// Draw the objects's shadow...
m_render_texture.draw ( m_horz_sprite, &m_shader );
// Setup the actual object...
object_.setFillColor ( original_fill_color );
object_.setOutlineColor ( original_outline_color );
object_.setPosition ( -displacement_.x, -displacement_.y );
// Draw object...
m_render_texture.draw ( object_ );
m_render_texture.display ( );
// Create sprite with the object at it's original position, dis-regarding the shadow... (needs more work)
m_sprite.setTexture ( m_render_texture.getTexture ( ) );
m_sprite.setPosition ( original_position + displacement_ - sf::Vector2f ( ( float ) radius_, ( float ) radius_ ) );
object_.setPosition ( original_position );
object_.setOrigin ( original_origin );
}
virtual void draw ( sf::RenderTarget &target_, sf::RenderStates states_ ) const {
target_.draw ( m_sprite, states_ );
}
};
int main ( ) {
sf::ContextSettings settings;
settings.antialiasingLevel = 8;
sf::RenderWindow window ( sf::VideoMode ( 600, 800 ), "SFML add Shadow to Objects...", sf::Style::Titlebar | sf::Style::Close, settings );
window.setVerticalSyncEnabled ( true );
window.setFramerateLimit ( 60 );
sf::Font font;
if ( not ( font.loadFromFile ( "c:/windows/fonts/impact.ttf" ) ) ) {
return EXIT_FAILURE;
}
sf::Text text ( L"SFML", font, 210u );
text.setFillColor ( sf::Color::Green );
text.setOutlineColor ( sf::Color::Black );
text.setOutlineThickness ( 4.0f );
text.setPosition ( sf::Vector2f ( 100.0f, 100.0f ) );
Shadowed<sf::Text> shadowed_text (
text,
16u,
sf::Vector2f ( 8.0f, 8.0f ),
sf::Color::Black
);
sf::RectangleShape rect ( sf::Vector2f ( 300.0f, 300.0f ) );
rect.setFillColor ( sf::Color::Yellow );
rect.setOutlineColor ( sf::Color::Black );
rect.setOutlineThickness ( 1.0f );
rect.setPosition ( sf::Vector2f ( 150.0f, 400.0f ) );
Shadowed<sf::RectangleShape> shadowed_rect (
rect,
16u,
sf::Vector2f ( -16.0f, 24.0f ),
sf::Color::Black
);
while ( window.isOpen ( ) ) {
sf::Event event;
while ( window.pollEvent ( event ) ) {
if ( event.type == sf::Event::Closed ) {
window.close ( );
}
}
window.clear ( sf::Color::White );
window.draw ( shadowed_text );
window.draw ( shadowed_rect );
window.display ( );
}
return 0;
}
The code is self contained, on non-Windhoze platforms you'll have to stick in another font.
PS: What I thought would also be cool is to not just put in the displacement of the shadow, but a direction, elevation, and distance to the "base surface", so as to be able to calculate the displacement required from those three parameters. (need to brush-up my trig, or find something, help accepted :D) The objective would be to make the shadow coincide with the real shadows at the players location at the time of play.
Have been looking at this a bit lot more, and I've got something working. I would like to ask for your comments, suggestions and/or improvements.
The fragment shader is now as follows:
uniform sampler2D texture;
uniform vec2 pixel_size;
uniform int blur_radius;
uniform float[128] weights;
uniform vec2 direction;
void main ( ) {
const vec2 texture_coordinates = gl_TexCoord [ 0 ].xy;
gl_FragColor = texture2D ( texture, texture_coordinates ) * weights [ 0 ];
for ( int i = 1; i < blur_radius; ++i ) {
const vec2 offset = vec2 ( float ( i ) * pixel_size.x * direction.x, float ( i ) * pixel_size.y * direction.y );
gl_FragColor += texture2D ( texture, texture_coordinates + offset ) * weights [ i ];
gl_FragColor += texture2D ( texture, texture_coordinates - offset ) * weights [ i ];
}
}
And the ShadedText class (and a main) is like this:
class ShadedText {
sf::Text m_text;
sf::RenderTexture m_text_render_texture;
sf::Sprite m_text_sprite;
const std::vector<float> m_gaussian_weights;
sf::RenderStates m_render_state;
sf::Shader m_shader;
sf::RenderTexture m_horz_render_texture;
sf::RenderTexture m_vert_render_texture;
sf::Sprite m_horz_sprite;
sf::Sprite m_final_sprite;
std::vector<float> gaussianWeights ( const std::size_t size_ ) const {
// http://www.cocos2d-x.org/wiki/User_Tutorial-RenderTexture_Plus_Blur
std::vector<float> gaussian_weights ( size_ );
double nill = 1.0;
for ( std::size_t i = 1; i < size_; ++i ) {
const double x = ( double ) i / ( double ) ( size_ - 1 ), gwi = 1.196826841204297942 / ( double ) size_ * std::exp ( -x * x * 4.5 );
gaussian_weights [ i ] = gwi;
nill -= 2.0 * gwi;
}
gaussian_weights [ 0 ] = nill;
return gaussian_weights;
}
public:
ShadedText ( const std::wstring &text_, const sf::Font &font_, const std::size_t points_, const std::size_t radius_, const sf::Vector2f &position_ = sf::Vector2f ( 0.0f, 0.0f ) ) :
m_text ( text_, font_, points_ ),
m_gaussian_weights ( gaussianWeights ( radius_ ) ) {
const float dispx = 12.0f, dispy = 13.0f;
// Finish setting text object...
m_text.setFillColor ( sf::Color::Black );
m_text.setOrigin ( m_text.getLocalBounds ( ).left - radius_, m_text.getLocalBounds ( ).top - radius_ ); // Otherwise text is not properly positioned in the rendertexture
// Init render texture...
m_text_render_texture.create ( m_text.getLocalBounds ( ).width + 2.0f * radius_ + dispx, m_text.getLocalBounds ( ).height + 2.0f * radius_ + dispx );
m_text_render_texture.clear ( sf::Color::White );
m_text_render_texture.setSmooth ( true );
m_text.setPosition ( dispx, dispy );
m_text_render_texture.draw ( m_text );
m_text_render_texture.display ( );
// 1. Create text sprite...
m_text_sprite.setTexture ( m_text_render_texture.getTexture ( ) );
// Load shader...
if ( not ( m_shader.loadFromFile ( "c:/tmp/resources/blur.frag", sf::Shader::Fragment ) ) ) {
exit ( EXIT_FAILURE );
}
// Set shader parameters...
m_shader.setUniform ( "pixel_size", sf::Vector2f ( 2.0f / m_text_sprite.getLocalBounds ( ).width, 2.0f / m_text_sprite.getLocalBounds ( ).height ) );
m_shader.setUniform ( "blur_radius", ( int ) radius_ );
m_shader.setUniformArray ( "weights", m_gaussian_weights.data ( ), radius_ );
m_render_state.shader = &m_shader;
// 2. Draw it to a RenderTexture with horizontal blur shader...
m_shader.setUniform ( "direction", sf::Vector2f ( 1.0f, 0.0f ) );
m_horz_render_texture.create ( m_text_sprite.getLocalBounds ( ).width + 2.0f * radius_, m_text_sprite.getLocalBounds ( ).height + 2.0f * radius_ );
m_horz_render_texture.clear ( sf::Color::White );
m_horz_render_texture.setSmooth ( true );
m_horz_render_texture.draw ( m_text_sprite, m_render_state );
m_horz_render_texture.display ( );
// 3. Create a sprite from resulting texture...
m_horz_sprite.setTexture ( m_horz_render_texture.getTexture ( ) );
// 4. Draw this sprite to a Render texture with vertical shader...
m_shader.setUniform ( "direction", sf::Vector2f ( 0.0f, 1.0f ) );
m_vert_render_texture.create ( m_horz_sprite.getLocalBounds ( ).width, m_horz_sprite.getLocalBounds ( ).height );
m_vert_render_texture.clear ( sf::Color::White );
m_vert_render_texture.setSmooth ( true );
m_vert_render_texture.draw ( m_horz_sprite, m_render_state );
m_text.setFillColor ( sf::Color::Green );
m_text.setOutlineColor ( sf::Color::Black );
m_text.setOutlineThickness ( 1.0f );
m_text.setPosition ( 0.0f, 0.0f );
m_vert_render_texture.draw ( m_text );
m_vert_render_texture.display ( );
m_final_sprite.setTexture ( m_vert_render_texture.getTexture ( ) );
m_final_sprite.setPosition ( 50.0f, 50.0f );
}
void draw ( sf::RenderWindow &window_ ) {
window_.draw ( m_final_sprite );
}
};
int main ( ) {
sf::ContextSettings settings;
settings.antialiasingLevel = 8;
sf::RenderWindow window ( sf::VideoMode ( 500, 300 ), "SFML Shaded Text", sf::Style::Titlebar | sf::Style::Close, settings );
window.setVerticalSyncEnabled ( true );
window.setFramerateLimit ( 60 );
sf::Font font;
if ( not ( font.loadFromFile ( "c:/windows/fonts/impact.ttf" ) ) ) {
return EXIT_FAILURE;
}
ShadedText text ( L"Text", font, 224u, 8u, sf::Vector2f ( window.getSize ( ).x * 0.5f, window.getSize ( ).y * 0.5f ) );
while ( window.isOpen ( ) ) {
sf::Event event;
while ( window.pollEvent ( event ) ) {
if ( event.type == sf::Event::Closed ) {
window.close ( );
}
}
window.clear ( sf::Color::White );
text.draw ( window );
window.display ( );
}
return 0;
}
The image of the resulting text is attached below. The result is reasonably good I would say (but could be improved IMO)...
The constructor runs in about 90ms 80ms on my laptop. It seems long to me... In a real-time situation it might be better, but improvements (or ideas) are welcomed.
[EDIT1] I wonder whether writing the m_text_sprite to an sf::Image, apply the blur on cpu and writing it back to the sf:Image wouln't be much faster (as opposed to creating all those sf::RenderTargets adn other boilerplate). Writing the m_text to sf::Image takes about 1-2ms, so there's time left!
[EDIT2] Eliminated 1 RenderTexture by re-use... The constructor now runs in about 80ms.
[EDIT3] About a quarter of the time is spent in this call:
m_text.setOrigin ( m_text.getLocalBounds ( ).left - radius_, m_text.getLocalBounds ( ).top - radius_ );
That's expensive!
[EDIT4] updated the latest result... looks good now.