Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Font Outlines #745

Open
atom0s opened this issue Jul 21, 2016 · 14 comments
Open

Suggestion: Font Outlines #745

atom0s opened this issue Jul 21, 2016 · 14 comments

Comments

@atom0s
Copy link

atom0s commented Jul 21, 2016

In some cases with certain controls, the text can become hard to read as colors can blend into each other.
A good example of this is the progress bar control where text can blend into the bar color.

It would be nice to be able to implement a font outline / shadow so things are easier to read on certain controls. Perhaps as a flag passed to a controls creation?

For example:
imgur

This is my current theme, a slider bar or progress bar has the same issue with being a little hard to read under certain conditions.

@ocornut
Copy link
Owner

ocornut commented Jul 21, 2016

The problem are

  • How to implement quality outline, stb_truetype.h doesn't do it yet and it's not a trivial problem for that sort of font.
  • Have two colors per glyph means we can't color multiply easily, or shaders have to be changed.
  • It would also possibly break the fact that the atlas texture can be alpha only.
    If you have an idea of how to implement it on a good way, go for it, but at the moment I don't know.

Note that you can also render fonts yourself following e.g. what #618 does and then you are free to render with outlines.

Easier to do some of that:

  • have thicker font and choose colors accordingly.
  • redesign widgets layout
  • redesign widget to be able to draw a background behind text blocks, introduce a TextBg color (generally all zero) for that sort of sort.

@r-lyeh-archived
Copy link

@ocornut
Copy link
Owner

ocornut commented Jul 22, 2016

Interesting. Still too much work to take any action now, but if you want to integrate a way to do the outlining easily within ImGui be my guest :)
(we have specific performances and API requirements for which FontStash as-is won't work as a trivial drop-in, but intend to implement a more dynamic atlasing system eventually).

@r-lyeh-archived
Copy link

I've used fontstash-es in the past. Quite good with SDF and outlines, and gives you a standarised dual stb/freetype interface for free. It will add some complexity to the library though. Might try to merge everything but then I think DearImgui wont be so compact anymore :) On the other hand, I know there is a single-header freetype distribution lying around.

@powof2
Copy link

powof2 commented Jul 26, 2016

here is a simple way to solve the problem : draw your info twice, first one is black, another one is drawn with normal color and offset a little, this way it will be much easier to read.

cheap outline effect would be like this : draw your info 9 times, first 4 times are outer shadows, then 4 times to draw inner shadows, the final draw would be your normal info placed in the shadow center. this is more expensive so i prefer first one.

@atom0s
Copy link
Author

atom0s commented Jul 26, 2016

Rendering things multiple times like that is too much overhead on intensive UIs that have a lot of text to render. A system similar to how FreeType works would be much more efficient.

@MrSapps
Copy link

MrSapps commented Jul 27, 2016

if the font is just textured quads would it really be so bad?

@abbaswasim
Copy link

abbaswasim commented Dec 31, 2019

I was missing text shadows so I have looked into this. Here is my probably hacky way of achieving something that works and looks nice. No ImGui changes required, you can do all of this in your application. I see @ocornut's answer above saying there isn't a clean way of doing so I though how about pre-multiplied alpha.

First step is to generate shadows in your glyph atlas. One could use freetype or what not to generate high quality shadows but I chose to just edit the glyph atlas before submitting to GPU as texture. The glyph atlas used in demos is 32-bit RGBA texture but only A has data in it. So I chose to use the blue component to save the shadow. Thats how it looks. with a zoomed view of one glyph.

atlas_with_zoomed_p

Thats how you generate it.

// First lets make a copy
unsigned char *pixels_shadow = new unsigned char[width * height * 4];
memcpy(pixels_shadow, pixels, width * height * 4);

// Clear the buffers' blue component, its set to white in the RGBA texture
for (int i = 0; i < height * width * 4; i+=4)
    pixels_shadow[i + 2] = 0;

// Create shadow at offset
// this depends on how far you want your shadows 
// but make sure the glyph atlas has padding around glyphs to accommodate for this
const int offset = 2;
for (int i = 0; i < height; i++)
{
    for (int j = 0; j < width; j++)
    {
        unsigned int current_index = ((j * 4) + (width * 4 * i));
        unsigned int write_shadow_index = (((j + offset - 1) * 4) + (width * 4 * (i + offset)));
        
        unsigned char current_pixel = pixels[current_index + 3];
        unsigned char current_pixel_shadow = pixels[write_shadow_index + 3];

        // Only write shadow pixels into empty areas
        if (current_pixel != 0 && current_pixel_shadow == 0)
                pixels_shadow[write_shadow_index + 2] = pixels[current_index + 3];
    }
}

Step 2 is to change shader to read this blue channel for shadows.

Change the following line:

    "    return half4(in.color) * texColor;\n"

to:

    "    float texShadow = texColor.b;\n"
    "    half4 shadowColor = half4(0.0f, 0.0f, 0.0f, 1.0f);\n" // You can use any color but need to use pre-multiplied alpha
    "    texColor.b = 1.0f;\n"
    "    texColor.rgb *= texColor.a;\n" // Use pre-multiplied alpha
    "    in.color.rgb *= in.color.a;\n" // Use pre-multiplied alpha
    "    return half4(in.color) * texColor + shadowColor * texShadow;\n"

Step 3 is to enable pre-multiplied alpha in your setup.
For metal change:

    pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;

to

    pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne;

Thats it, now time for some pretty screenshots.

No shadows:
no-shadows

With shadows:
shadows

I need to workout how to increase the actual renderer area of the glyph to accommodate for some shadows missing (look at the bottom of "L"). It might already be possible with ImGui's settings.

Inspired by beautiful colours and text in blender
blender_beauty

[edit]: fixed a bug in the shadow generator. Haven't updated the screenshots they looks ok.

@abbaswasim
Copy link

abbaswasim commented Dec 31, 2019

with some padding hack it will look like:

with-padding

sweet!

For those interested peeps, here is the patch.

diff --git a/imgui.h b/imgui.h
index 83bb2558..6d469f76 100644
--- a/imgui.h
+++ b/imgui.h
@@ -2173,6 +2173,7 @@ struct ImFontAtlas
     ImTextureID                 TexID;              // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure.
     int                         TexDesiredWidth;    // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height.
     int                         TexGlyphPadding;    // Padding between glyphs within texture in pixels. Defaults to 1. If your rendering method doesn't rely on bilinear filtering you may set this to 0.
+    ImVec2                      TexGlyphShadowOffset;  // If you would like to use shadows with your text use this. Defaults to (0, 0). Defines horizontal and vertical shadows. Can only be positive at the moment.
 
     // [Internal]
     // NB: Access texture data via GetTexData*() calls! Which will setup a default font for you.
diff --git a/imgui_draw.cpp b/imgui_draw.cpp
index 5a8477fc..d8ccb3f6 100644
--- a/imgui_draw.cpp
+++ b/imgui_draw.cpp
@@ -1532,6 +1532,7 @@ ImFontAtlas::ImFontAtlas()
     TexID = (ImTextureID)NULL;
     TexDesiredWidth = 0;
     TexGlyphPadding = 1;
+    TexGlyphShadowOffset = ImVec2(0.0f, 0.0f);
 
     TexPixelsAlpha8 = NULL;
     TexPixelsRGBA32 = NULL;
@@ -2012,14 +2013,15 @@ bool    ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas)
         // Gather the sizes of all rectangles we will need to pack (this loop is based on stbtt_PackFontRangesGatherRects)
         const float scale = (cfg.SizePixels > 0) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -cfg.SizePixels);
         const int padding = atlas->TexGlyphPadding;
+        const ImVec2 shadow_offset = atlas->TexGlyphShadowOffset;
         for (int glyph_i = 0; glyph_i < src_tmp.GlyphsList.Size; glyph_i++)
         {
             int x0, y0, x1, y1;
             const int glyph_index_in_font = stbtt_FindGlyphIndex(&src_tmp.FontInfo, src_tmp.GlyphsList[glyph_i]);
             IM_ASSERT(glyph_index_in_font != 0);
             stbtt_GetGlyphBitmapBoxSubpixel(&src_tmp.FontInfo, glyph_index_in_font, scale * cfg.OversampleH, scale * cfg.OversampleV, 0, 0, &x0, &y0, &x1, &y1);
-            src_tmp.Rects[glyph_i].w = (stbrp_coord)(x1 - x0 + padding + cfg.OversampleH - 1);
-            src_tmp.Rects[glyph_i].h = (stbrp_coord)(y1 - y0 + padding + cfg.OversampleV - 1);
+            src_tmp.Rects[glyph_i].w = (stbrp_coord)(x1 - x0 + padding + cfg.OversampleH - 1 + shadow_offset.x);
+            src_tmp.Rects[glyph_i].h = (stbrp_coord)(y1 - y0 + padding + cfg.OversampleV - 1 + shadow_offset.y);
             total_surface += src_tmp.Rects[glyph_i].w * src_tmp.Rects[glyph_i].h;
         }
     }

diff --git a/misc/freetype/imgui_freetype.cpp b/misc/freetype/imgui_freetype.cpp
index 6695fb65..85f1008b 100644
--- a/misc/freetype/imgui_freetype.cpp
+++ b/misc/freetype/imgui_freetype.cpp
@@ -456,6 +511,7 @@ bool ImFontAtlasBuildWithFreeType(FT_Library ft_library, ImFontAtlas* atlas, uns
 
         // Gather the sizes of all rectangles we will need to pack
         const int padding = atlas->TexGlyphPadding;
+        const ImVec2 shadow_offset = atlas->TexGlyphShadowOffset;
         for (int glyph_i = 0; glyph_i < src_tmp.GlyphsList.Size; glyph_i++)
         {
             ImFontBuildSrcGlyphFT& src_glyph = src_tmp.GlyphsList[glyph_i];
@@ -482,8 +538,8 @@ bool ImFontAtlasBuildWithFreeType(FT_Library ft_library, ImFontAtlas* atlas, uns
             buf_bitmap_current_used_bytes += bitmap_size_in_bytes;
             src_tmp.Font.BlitGlyph(ft_bitmap, src_glyph.BitmapData, src_glyph.Info.Width * 1, multiply_enabled ? multiply_table : NULL);
 
-            src_tmp.Rects[glyph_i].w = (stbrp_coord)(src_glyph.Info.Width + padding);
-            src_tmp.Rects[glyph_i].h = (stbrp_coord)(src_glyph.Info.Height + padding);
+            src_tmp.Rects[glyph_i].w = (stbrp_coord)(src_glyph.Info.Width + padding + shadow_offset.x);
+            src_tmp.Rects[glyph_i].h = (stbrp_coord)(src_glyph.Info.Height + padding + shadow_offset.y);
             total_surface += src_tmp.Rects[glyph_i].w * src_tmp.Rects[glyph_i].h;
         }
     }

For that screenshot TexGlyphShadowOffset = ImVec2(1.0f, 2.0f);

@ocornut could something like this be upstreamed? or do I still don't know how to use padding correctly?

@rlalance
Copy link

Oh Sexy!

@atom0s
Copy link
Author

atom0s commented Jan 1, 2020

Not recommended for heavy use, but another means of adding font outlines I made for fun.

In imconfig.h, add to the custom namespace code:

namespace ImGui
{
    inline bool custom_UseFontShadow;
    inline unsigned int custom_FontShadowColor;

    inline static void PushFontShadow(unsigned int col)
    {
        custom_UseFontShadow   = true;
        custom_FontShadowColor = col;
    }

    inline static void PopFontShadow(void)
    {
        custom_UseFontShadow = false;
    }
}; // namespace ImGui

In imgui_draw.cpp find ImDrawList::AddText and the following before the font->RenderText call:

    if (ImGui::custom_UseFontShadow)
    {
        ImVec2 shadowPos1(pos);
        ImVec2 shadowPos2(pos);
        ImVec2 shadowPos3(pos);
        ImVec2 shadowPos4(pos);

        shadowPos1.x -= 1;
        shadowPos1.y -= 1;
        font->RenderText(this, font_size, shadowPos1, ImGui::custom_FontShadowColor, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL);
        shadowPos2.x -= 1;
        shadowPos2.y += 1;
        font->RenderText(this, font_size, shadowPos2, ImGui::custom_FontShadowColor, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL);
        shadowPos3.x += 1;
        shadowPos3.y -= 1;
        font->RenderText(this, font_size, shadowPos3, ImGui::custom_FontShadowColor, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL);
        shadowPos4.x += 1;
        shadowPos4.y += 1;
        font->RenderText(this, font_size, shadowPos4, ImGui::custom_FontShadowColor, clip_rect, text_begin, text_end, wrap_width, cpu_fine_clip_rect != NULL);
    }

Example usage would be like this, (modded the demo):

    ImGui::PushFontShadow(0xFF000000);
    ShowDemoWindowPopups();
    ImGui::PopFontShadow();

    ImGui::PushFontShadow(0xFFFF00FF);
    ShowDemoWindowColumns();
    ImGui::PopFontShadow();

example_outlines

Because of how this works, it is NOT ideal for actual usage in production. It's also not ideal for adding outlines to all text, you should not do it like this if you wish to outline everything ImGui draws text wise. This will heavily impact performance if used on a lot of text. Not recommended for anything other than debug setups and simple/light usage.

@abbaswasim
Copy link

abbaswasim commented Jan 1, 2020

@atom0s you are right this won’t be performant at all.

For outline use the same trick and putting the outline in Green or Red channel should work.
A simple outline convolution filter like int outline_kernel[3][3] = {{-1, -1, -1}, {-1, 8, -1}, {-1, -1, -1}}; works good enough. You need to increase the rectangle of glyph to accommodate for the outline at the top-left though.

@ocornut
Copy link
Owner

ocornut commented Jan 2, 2020

@abbaswasim

could something like this be upstreamed? or do I still don't know how to use padding correctly?

Yes we could! But I think that exact patch is not ideal. Opening a new issue/PR specifically for it would be good so we can discuss that in a separate that. Your patch assume that: padding is only desired from two sides (bottom and right) and that ShadowPadding <= TexGlyphPadding which is not guaranteed. We can find out a solution.

@abbaswasim
Copy link

cool, I will create a PR. I am sure there must be other things wrong with that. Could also add some controls in the demo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants