Browse Source

Add configurable oversampling to improve subpixel font rendering (#1953)

Subpixel-positioned text looks blurry, due to bilinear filtering of the
underlying texture. The basic idea here is to stretch the font glyphs
horizontally to increase the sharpness at subpixel positions. The
stretched images need to be smoothed to avoid aliasing artifacts; this
is done in FontFaceFreeType::BoxFilter().

Glyphs are always pixel-aligned vertically, so no vertical oversampling
is needed.

To make this feature comprehensible (I hope!) I've removed the
'subpixelGlyphPositions' flag and replaced it with a couple of values:
'fontSubpixelThreshold' sets the point at which subpixel positioning
kicks on, and 'fontOversampling' controls the amount of stretching.

The default values are 12pt text and 2x oversampling. These are fairly
conservative settings, which should improve small text without wasting
a lot of memory.

Note that when the font hint level is NORMAL (the default), subpixel
positioning and oversampling are both disabled. So, this feature doesn't
change any default behavior, and applies some hopefully sensible values
if the hint level is set to LIGHT or NONE.
Iain Merrick 8 years ago
parent
commit
0450bcd11e

+ 47 - 14
Source/Samples/47_Typography/Typography.cpp

@@ -71,7 +71,7 @@ void Typography::Start()
     // (Don't modify the root directly, as the base Sample class uses it)
     uielement_ = new UIElement(context_);
     uielement_->SetAlignment(HA_CENTER, VA_CENTER);
-    uielement_->SetLayout(LM_VERTICAL, 20, IntRect(20, 40, 20, 40));
+    uielement_->SetLayout(LM_VERTICAL, 10, IntRect(20, 40, 20, 40));
     root->AddChild(uielement_);
 
     // Add some sample text.
@@ -92,20 +92,46 @@ void Typography::Start()
     CreateCheckbox("UI::SetForceAutoHint", URHO3D_HANDLER(Typography, HandleForceAutoHint))
         ->SetChecked(ui->GetForceAutoHint());
 
-    // Add a checkbox for the global SubpixelGlyphPositions setting. This affects character spacing.
-    CreateCheckbox("UI::SetSubpixelGlyphPositions", URHO3D_HANDLER(Typography, HandleSubpixelGlyphPositions))
-        ->SetChecked(ui->GetSubpixelGlyphPositions());
-
     // Add a drop-down menu to control the font hinting level.
-    const char* items[] = {
+    const char* levels[] = {
         "FONT_HINT_LEVEL_NONE",
         "FONT_HINT_LEVEL_LIGHT",
         "FONT_HINT_LEVEL_NORMAL",
         NULL
     };
-    CreateMenu("UI::SetFontHintLevel", items, URHO3D_HANDLER(Typography, HandleFontHintLevel))
+    CreateMenu("UI::SetFontHintLevel", levels, URHO3D_HANDLER(Typography, HandleFontHintLevel))
         ->SetSelection(ui->GetFontHintLevel());
 
+    // Add a drop-down menu to control the subpixel threshold.
+    const char* thresholds[] = {
+        "0",
+        "3",
+        "6",
+        "9",
+        "12",
+        "15",
+        "18",
+        "21",
+        NULL
+    };
+    CreateMenu("UI::SetFontSubpixelThreshold", thresholds, URHO3D_HANDLER(Typography, HandleFontSubpixel))
+        ->SetSelection(ui->GetFontSubpixelThreshold() / 3);
+
+    // Add a drop-down menu to control oversampling.
+    const char* limits[] = {
+        "1",
+        "2",
+        "3",
+        "4",
+        "5",
+        "6",
+        "7",
+        "8",
+        NULL
+    };
+    CreateMenu("UI::SetFontOversampling", limits, URHO3D_HANDLER(Typography, HandleFontOversampling))
+        ->SetSelection(ui->GetFontOversampling() - 1);
+
     // Set the mouse mode to use in the sample
     Sample::InitMouseMode(MM_FREE);
 }
@@ -174,11 +200,11 @@ SharedPtr<DropDownList> Typography::CreateMenu(const String& label, const char**
         list->AddItem(item);
         item->SetText(items[i]);
         item->SetStyleAuto();
+        item->SetMinWidth(item->GetRowWidth(0) + 10);
         item->AddTag(TEXT_TAG);
     }
 
     text->SetMaxWidth(text->GetRowWidth(0));
-    list->SetMaxWidth(text->GetRowWidth(0) * 1.5);
 
     SubscribeToEvent(list, E_ITEMSELECTED, handler);
     return list;
@@ -229,16 +255,23 @@ void Typography::HandleSRGB(StringHash eventType, VariantMap& eventData)
     }
 }
 
-void Typography::HandleSubpixelGlyphPositions(StringHash eventType, VariantMap& eventData)
+void Typography::HandleFontHintLevel(StringHash eventType, VariantMap& eventData)
 {
-    CheckBox* box = static_cast<CheckBox*>(eventData[Toggled::P_ELEMENT].GetPtr());
-    bool checked = box->IsChecked();
-    GetSubsystem<UI>()->SetSubpixelGlyphPositions(checked);
+    DropDownList* list = static_cast<DropDownList*>(eventData[Toggled::P_ELEMENT].GetPtr());
+    unsigned i = list->GetSelection();
+    GetSubsystem<UI>()->SetFontHintLevel((FontHintLevel)i);
 }
 
-void Typography::HandleFontHintLevel(StringHash eventType, VariantMap& eventData)
+void Typography::HandleFontSubpixel(StringHash eventType, VariantMap& eventData)
 {
     DropDownList* list = static_cast<DropDownList*>(eventData[Toggled::P_ELEMENT].GetPtr());
     unsigned i = list->GetSelection();
-    GetSubsystem<UI>()->SetFontHintLevel((FontHintLevel)i);
+    GetSubsystem<UI>()->SetFontSubpixelThreshold(i * 3);
+}
+
+void Typography::HandleFontOversampling(StringHash eventType, VariantMap& eventData)
+{
+    DropDownList* list = static_cast<DropDownList*>(eventData[Toggled::P_ELEMENT].GetPtr());
+    unsigned i = list->GetSelection();
+    GetSubsystem<UI>()->SetFontOversampling(i + 1);
 }

+ 2 - 1
Source/Samples/47_Typography/Typography.h

@@ -58,5 +58,6 @@ private:
     void HandleSRGB(StringHash eventType, VariantMap& eventData);
     void HandleForceAutoHint(StringHash eventType, VariantMap& eventData);
     void HandleFontHintLevel(StringHash eventType, VariantMap& eventData);
-    void HandleSubpixelGlyphPositions(StringHash eventType, VariantMap& eventData);
+    void HandleFontSubpixel(StringHash eventType, VariantMap& eventData);
+    void HandleFontOversampling(StringHash eventType, VariantMap& eventData);
 };

+ 4 - 2
Source/Urho3D/AngelScript/UIAPI.cpp

@@ -789,8 +789,10 @@ static void RegisterUI(asIScriptEngine* engine)
     engine->RegisterObjectMethod("UI", "bool get_forceAutoHint() const", asMETHOD(UI, GetForceAutoHint), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "void set_fontHintLevel(FontHintLevel)", asMETHOD(UI, SetFontHintLevel), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "FontHintLevel get_fontHintLevel() const", asMETHOD(UI, GetFontHintLevel), asCALL_THISCALL);
-    engine->RegisterObjectMethod("UI", "void set_subpixelGlyphPositions(bool)", asMETHOD(UI, SetSubpixelGlyphPositions), asCALL_THISCALL);
-    engine->RegisterObjectMethod("UI", "bool get_subpixelGlyphPositions() const", asMETHOD(UI, GetSubpixelGlyphPositions), asCALL_THISCALL);
+    engine->RegisterObjectMethod("UI", "void set_fontSubpixelThreshold(float)", asMETHOD(UI, SetFontSubpixelThreshold), asCALL_THISCALL);
+    engine->RegisterObjectMethod("UI", "float get_fontSubpixelThreshold() const", asMETHOD(UI, GetFontSubpixelThreshold), asCALL_THISCALL);
+    engine->RegisterObjectMethod("UI", "void set_fontOversampling(int)", asMETHOD(UI, SetFontOversampling), asCALL_THISCALL);
+    engine->RegisterObjectMethod("UI", "int get_fontOversampling() const", asMETHOD(UI, GetFontOversampling), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "void set_scale(float value)", asMETHOD(UI, SetScale), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "float get_scale() const", asMETHOD(UI, GetScale), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "void set_customSize(const IntVector2&in)", asMETHODPR(UI, SetCustomSize, (const IntVector2&), void), asCALL_THISCALL);

+ 6 - 3
Source/Urho3D/LuaScript/pkgs/UI/UI.pkg

@@ -31,7 +31,8 @@ class UI : public Object
     void SetUseMutableGlyphs(bool enable);
     void SetForceAutoHint(bool enable);
     void SetFontHintLevel(FontHintLevel level);
-    void SetSubpixelGlyphPositions(bool enable);
+    void SetFontSubpixelThreshold(float threshold);
+    void SetFontOversampling(int limit);
     void SetScale(float scale);
     void SetWidth(float width);
     void SetHeight(float height);
@@ -60,7 +61,8 @@ class UI : public Object
     bool GetUseMutableGlyphs() const;
     bool GetForceAutoHint() const;
     FontHintLevel GetFontHintLevel() const;
-    bool GetSubpixelGlyphPositions() const;
+    float GetFontSubpixelThreshold() const;
+    int GetFontOversampling() const;
     bool HasModalElement() const;
     bool IsDragging() const;
     float GetScale() const;
@@ -85,7 +87,8 @@ class UI : public Object
     tolua_property__get_set bool useMutableGlyphs;
     tolua_property__get_set bool forceAutoHint;
     tolua_property__get_set FontHintLevel fontHintLevel;
-    tolua_property__get_set bool subpixelGlyphPositions;
+    tolua_property__get_set float fontSubpixelThreshold;
+    tolua_property__get_set int fontOversampling;
     tolua_readonly tolua_property__has_set bool modalElement;
     tolua_property__get_set float scale;
     tolua_property__get_set IntVector2& customSize;

+ 10 - 6
Source/Urho3D/UI/FontFace.h

@@ -43,14 +43,18 @@ struct URHO3D_API FontGlyph
     short x_;
     /// Y position in texture.
     short y_;
-    /// Width.
-    short width_;
-    /// Height.
-    short height_;
+    /// X position in texture.
+    short texWidth_;
+    /// Y position in texture.
+    short texHeight_;
+    /// Width on screen.
+    float width_;
+    /// Height on screen.
+    float height_;
     /// Glyph X offset from origin.
-    short offsetX_;
+    float offsetX_;
     /// Glyph Y offset from origin.
-    short offsetY_;
+    float offsetY_;
     /// Horizontal advance.
     float advanceX_;
     /// Texture page. M_MAX_UNSIGNED if not yet resident on any texture.

+ 90 - 20
Source/Urho3D/UI/FontFaceFreeType.cpp

@@ -32,6 +32,8 @@
 #include "../UI/FontFaceFreeType.h"
 #include "../UI/UI.h"
 
+#include <assert.h>
+
 #include <ft2build.h>
 #include FT_FREETYPE_H
 #include FT_TRUETYPE_TABLES_H
@@ -104,7 +106,12 @@ bool FontFaceFreeType::Load(const unsigned char* fontData, unsigned fontDataSize
     freeType_ = freeType;
 
     UI* ui = font_->GetSubsystem<UI>();
-    int maxTextureSize = ui->GetMaxFontTextureSize();
+    const int maxTextureSize = ui->GetMaxFontTextureSize();
+    const FontHintLevel hintLevel = ui->GetFontHintLevel();
+    const float subpixelThreshold = ui->GetFontSubpixelThreshold();
+
+    subpixel_ = (hintLevel <= FONT_HINT_LEVEL_LIGHT) && (pointSize <= subpixelThreshold);
+    oversampling_ = subpixel_ ? ui->GetFontOversampling() : 1;
 
     FT_Face face;
     FT_Error error;
@@ -128,7 +135,7 @@ bool FontFaceFreeType::Load(const unsigned char* fontData, unsigned fontDataSize
         URHO3D_LOGERROR("Could not create font face");
         return false;
     }
-    error = FT_Set_Char_Size(face, 0, pointSize * 64, FONT_DPI, FONT_DPI);
+    error = FT_Set_Char_Size(face, 0, pointSize * 64, oversampling_ * FONT_DPI, FONT_DPI);
     if (error)
     {
         FT_Done_Face(face);
@@ -340,6 +347,65 @@ bool FontFaceFreeType::SetupNextTexture(int textureWidth, int textureHeight)
     return true;
 }
 
+void FontFaceFreeType::BoxFilter(unsigned char* dest, size_t destSize, const unsigned char* src, size_t srcSize)
+{
+    const int filterSize = oversampling_;
+
+    assert(filterSize > 0);
+    assert(destSize == srcSize + filterSize - 1);
+
+    if (filterSize == 1)
+    {
+        memcpy(dest, src, srcSize);
+        return;
+    }
+
+    // "accumulator" holds the total value of filterSize samples. We add one sample
+    // and remove one sample per step (with special cases for left and right edges).
+    int accumulator = 0;
+
+    // The divide might make these inner loops slow. If so, some possible optimizations:
+    // a) Turn it into a fixed-point multiply-and-shift rather than an integer divide;
+    // b) Make this function a template, with the filter size a compile-time constant.
+
+    int i = 0;
+
+    if (srcSize < filterSize)
+    {
+        for (; i < srcSize; ++i)
+        {
+            accumulator += src[i];
+            dest[i] = accumulator / filterSize;
+        }
+
+        for (; i < filterSize; ++i)
+        {
+            dest[i] = accumulator / filterSize;
+        }
+    }
+    else
+    {
+        for ( ; i < filterSize; ++i)
+        {
+            accumulator += src[i];
+            dest[i] = accumulator / filterSize;
+        }
+
+        for (; i < srcSize; ++i)
+        {
+            accumulator += src[i];
+            accumulator -= src[i - filterSize];
+            dest[i] = accumulator / filterSize;
+        }
+    }
+
+    for (; i < srcSize + filterSize - 1; ++i)
+    {
+        accumulator -= src[i - filterSize];
+        dest[i] = accumulator / filterSize;
+    }
+}
+
 bool FontFaceFreeType::LoadCharGlyph(unsigned charCode, Image* image)
 {
     if (!face_)
@@ -354,6 +420,8 @@ bool FontFaceFreeType::LoadCharGlyph(unsigned charCode, Image* image)
     {
         const char* family = face->family_name ? face->family_name : "NULL";
         URHO3D_LOGERRORF("FT_Load_Char failed (family: %s, char code: %u)", family, charCode);
+        fontGlyph.texWidth_ = 0;
+        fontGlyph.texHeight_ = 0;
         fontGlyph.width_ = 0;
         fontGlyph.height_ = 0;
         fontGlyph.offsetX_ = 0;
@@ -364,15 +432,14 @@ bool FontFaceFreeType::LoadCharGlyph(unsigned charCode, Image* image)
     else
     {
         // Note: position within texture will be filled later
-        fontGlyph.width_ = slot->bitmap.width;
+        fontGlyph.texWidth_ = slot->bitmap.width + oversampling_ - 1;
+        fontGlyph.texHeight_ = slot->bitmap.rows;
+        fontGlyph.width_ = slot->bitmap.width + oversampling_ - 1;
         fontGlyph.height_ = slot->bitmap.rows;
-        fontGlyph.offsetX_ = slot->bitmap_left;
-        fontGlyph.offsetY_ = ascender_ - slot->bitmap_top;
+        fontGlyph.offsetX_ = slot->bitmap_left - (oversampling_ - 1) / 2.0f;
+        fontGlyph.offsetY_ = floorf(ascender_ + 0.5f) - slot->bitmap_top;
 
-        UI* ui = font_->GetSubsystem<UI>();
-        FontHintLevel level = ui->GetFontHintLevel();
-        bool subpixel = ui->GetSubpixelGlyphPositions();
-        if (level <= FONT_HINT_LEVEL_LIGHT && subpixel && slot->linearHoriAdvance)
+        if (subpixel_ && slot->linearHoriAdvance)
         {
             // linearHoriAdvance is stored in 16.16 fixed point, not the usual 26.6
             fontGlyph.advanceX_ = slot->linearHoriAdvance / 65536.0;
@@ -380,14 +447,18 @@ bool FontFaceFreeType::LoadCharGlyph(unsigned charCode, Image* image)
         else
         {
             // Round to nearest pixel (only necessary when hinting is disabled)
-            fontGlyph.advanceX_ = floor(FixedToFloat(slot->metrics.horiAdvance) + 0.5f);
+            fontGlyph.advanceX_ = floorf(FixedToFloat(slot->metrics.horiAdvance) + 0.5f);
         }
+
+        fontGlyph.width_ /= oversampling_;
+        fontGlyph.offsetX_ /= oversampling_;
+        fontGlyph.advanceX_ /= oversampling_;
     }
 
     int x = 0, y = 0;
-    if (fontGlyph.width_ > 0 && fontGlyph.height_ > 0)
+    if (fontGlyph.texWidth_ > 0 && fontGlyph.texHeight_ > 0)
     {
-        if (!allocator_.Allocate(fontGlyph.width_ + 1, fontGlyph.height_ + 1, x, y))
+        if (!allocator_.Allocate(fontGlyph.texWidth_ + 1, fontGlyph.texHeight_ + 1, x, y))
         {
             if (image)
             {
@@ -403,7 +474,7 @@ bool FontFaceFreeType::LoadCharGlyph(unsigned charCode, Image* image)
                 return false;
             }
 
-            if (!allocator_.Allocate(fontGlyph.width_ + 1, fontGlyph.height_ + 1, x, y))
+            if (!allocator_.Allocate(fontGlyph.texWidth_ + 1, fontGlyph.texHeight_ + 1, x, y))
             {
                 URHO3D_LOGWARNINGF("FontFaceFreeType::LoadCharGlyph: failed to position char code %u in blank page", charCode);
                 return false;
@@ -424,8 +495,8 @@ bool FontFaceFreeType::LoadCharGlyph(unsigned charCode, Image* image)
         else
         {
             fontGlyph.page_ = textures_.Size() - 1;
-            dest = new unsigned char[fontGlyph.width_ * fontGlyph.height_];
-            pitch = (unsigned)fontGlyph.width_;
+            dest = new unsigned char[fontGlyph.texWidth_ * fontGlyph.texHeight_];
+            pitch = (unsigned)fontGlyph.texWidth_;
         }
 
         if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO)
@@ -433,8 +504,9 @@ bool FontFaceFreeType::LoadCharGlyph(unsigned charCode, Image* image)
             for (unsigned y = 0; y < (unsigned)slot->bitmap.rows; ++y)
             {
                 unsigned char* src = slot->bitmap.buffer + slot->bitmap.pitch * y;
-                unsigned char* rowDest = dest + y * pitch;
+                unsigned char* rowDest = dest + (oversampling_ - 1)/2 + y * pitch;
 
+                // Don't do any oversampling, just unpack the bits directly.
                 for (unsigned x = 0; x < (unsigned)slot->bitmap.width; ++x)
                     rowDest[x] = (unsigned char)((src[x >> 3] & (0x80 >> (x & 7))) ? 255 : 0);
             }
@@ -445,15 +517,13 @@ bool FontFaceFreeType::LoadCharGlyph(unsigned charCode, Image* image)
             {
                 unsigned char* src = slot->bitmap.buffer + slot->bitmap.pitch * y;
                 unsigned char* rowDest = dest + y * pitch;
-
-                for (unsigned x = 0; x < (unsigned)slot->bitmap.width; ++x)
-                    rowDest[x] = src[x];
+                BoxFilter(rowDest, fontGlyph.texWidth_, src, slot->bitmap.width);
             }
         }
 
         if (!image)
         {
-            textures_.Back()->SetData(0, fontGlyph.x_, fontGlyph.y_, fontGlyph.width_, fontGlyph.height_, dest);
+            textures_.Back()->SetData(0, fontGlyph.x_, fontGlyph.y_, fontGlyph.texWidth_, fontGlyph.texHeight_, dest);
             delete[] dest;
         }
     }

+ 6 - 0
Source/Urho3D/UI/FontFaceFreeType.h

@@ -52,6 +52,8 @@ private:
     bool SetupNextTexture(int textureWidth, int textureHeight);
     /// Load char glyph.
     bool LoadCharGlyph(unsigned charCode, Image* image = 0);
+    /// Smooth one row of a horizontally oversampled glyph image.
+    void BoxFilter(unsigned char* dest, size_t destSize, const unsigned char* src, size_t srcSize);
 
     /// FreeType library.
     SharedPtr<FreeTypeLibrary> freeType_;
@@ -59,6 +61,10 @@ private:
     void* face_;
     /// Load mode.
     int loadMode_;
+    /// Use subpixel glyph positioning?
+    bool subpixel_;
+    /// Oversampling level.
+    int oversampling_;
     /// Ascender.
     float ascender_;
     /// Has mutable glyph.

+ 1 - 1
Source/Urho3D/UI/Text.cpp

@@ -821,7 +821,7 @@ void Text::ConstructBatch(UIBatch& pageBatch, const PODVector<GlyphLocation>& pa
         const GlyphLocation& glyphLocation = pageGlyphLocation[i];
         const FontGlyph& glyph = *glyphLocation.glyph_;
         pageBatch.AddQuad(dx + glyphLocation.x_ + glyph.offsetX_, dy + glyphLocation.y_ + glyph.offsetY_, glyph.width_,
-            glyph.height_, glyph.x_, glyph.y_);
+            glyph.height_, glyph.x_, glyph.y_, glyph.texWidth_, glyph.texHeight_);
     }
 
     if (depthBias != 0.0f)

+ 18 - 4
Source/Urho3D/UI/UI.cpp

@@ -57,6 +57,7 @@
 #include "../UI/Window.h"
 #include "../UI/View3D.h"
 
+#include <assert.h>
 #include <SDL/SDL.h>
 
 #include "../DebugNew.h"
@@ -107,7 +108,8 @@ UI::UI(Context* context) :
     useMutableGlyphs_(false),
     forceAutoHint_(false),
     fontHintLevel_(FONT_HINT_LEVEL_NORMAL),
-    subpixelGlyphPositions_(false),
+    fontSubpixelThreshold_(12),
+    fontOversampling_(2),
     uiRendered_(false),
     nonModalBatchSize_(0),
     dragElementsCount_(0),
@@ -616,11 +618,23 @@ void UI::SetFontHintLevel(FontHintLevel level)
     }
 }
 
-void UI::SetSubpixelGlyphPositions(bool enable)
+void UI::SetFontSubpixelThreshold(float threshold)
 {
-    if (enable != subpixelGlyphPositions_)
+    assert(threshold >= 0);
+    if (threshold != fontSubpixelThreshold_)
     {
-        subpixelGlyphPositions_ = enable;
+        fontSubpixelThreshold_ = threshold;
+        ReleaseFontFaces();
+    }
+}
+
+void UI::SetFontOversampling(int oversampling)
+{
+    assert(oversampling >= 1);
+    oversampling = Clamp(oversampling, 1, 8);
+    if (oversampling != fontOversampling_)
+    {
+        fontOversampling_ = oversampling;
         ReleaseFontFaces();
     }
 }

+ 13 - 6
Source/Urho3D/UI/UI.h

@@ -110,8 +110,10 @@ public:
     void SetForceAutoHint(bool enable);
     /// Set the hinting level used by FreeType fonts.
     void SetFontHintLevel(FontHintLevel level);
-    /// Set whether text glyphs can have fractional positions. Default is false (pixel-aligned).
-    void SetSubpixelGlyphPositions(bool enable);
+    /// Set the font subpixel threshold. Below this size, if the hint level is LIGHT or NONE, fonts will use subpixel positioning plus oversampling for higher-quality rendering. Has no effect at hint level NORMAL.
+    void SetFontSubpixelThreshold(float threshold);
+    /// Set the oversampling (horizonal stretching) used to improve subpixel font rendering. Only affects fonts smaller than the subpixel limit.
+    void SetFontOversampling(int oversampling);
     /// Set %UI scale. 1.0 is default (pixel perfect). Resize the root element to match.
     void SetScale(float scale);
     /// Scale %UI to the specified width in pixels.
@@ -188,8 +190,11 @@ public:
     /// Return the current FreeType font hinting level.
     FontHintLevel GetFontHintLevel() const { return fontHintLevel_; }
 
-    // Return whether text glyphs can have fractional positions.
-    bool GetSubpixelGlyphPositions() const { return subpixelGlyphPositions_; }
+    /// Get the font subpixel threshold. Below this size, if the hint level is LIGHT or NONE, fonts will use subpixel positioning plus oversampling for higher-quality rendering. Has no effect at hint level NORMAL.
+    float GetFontSubpixelThreshold() const { return fontSubpixelThreshold_; }
+
+    /// Get the oversampling (horizonal stretching) used to improve subpixel font rendering. Only affects fonts smaller than the subpixel limit.
+    int GetFontOversampling() const { return fontOversampling_; }
 
     /// Return true when UI has modal element(s).
     bool HasModalElement() const;
@@ -356,8 +361,10 @@ private:
     bool forceAutoHint_;
     /// FreeType hinting level (default is FONT_HINT_LEVEL_NORMAL).
     FontHintLevel fontHintLevel_;
-    /// Flag for subpixel text glyph positions.
-    bool subpixelGlyphPositions_;
+    /// Maxmimum font size for subpixel glyph positioning and oversampling (default is 12).
+    float fontSubpixelThreshold_;
+    /// Horizontal oversampling for subpixel fonts (default is 2).
+    int fontOversampling_;
     /// Flag for UI already being rendered this frame.
     bool uiRendered_;
     /// Non-modal batch size (used internally for rendering).

+ 45 - 14
bin/Data/LuaScripts/47_Typography.lua

@@ -26,7 +26,7 @@ function Start()
     -- (Don't modify the root directly, as the base Sample class uses it)
     uielement = UIElement:new()
     uielement:SetAlignment(HA_CENTER, VA_CENTER)
-    uielement:SetLayout(LM_VERTICAL, 20, IntRect(20, 40, 20, 40))
+    uielement:SetLayout(LM_VERTICAL, 10, IntRect(20, 40, 20, 40))
     ui.root:AddChild(uielement)
 
     -- Add some sample text
@@ -47,19 +47,43 @@ function Start()
     CreateCheckbox("UI::SetForceAutoHint", "HandleForceAutoHint")
         :SetChecked(ui:GetForceAutoHint())
 
-    -- Add a checkbox for the global SubpixelGlyphPositions setting. This affects character spacing.
-    CreateCheckbox("UI::SetSubpixelGlyphPositions", "HandleSubpixelGlyphPositions")
-        :SetChecked(ui:GetSubpixelGlyphPositions())
-
     -- Add a drop-down menu to control the font hinting level.
-    local items = {
+    local levels = {
         "FONT_HINT_LEVEL_NONE",
         "FONT_HINT_LEVEL_LIGHT",
         "FONT_HINT_LEVEL_NORMAL"
     }
-    CreateMenu("UI::SetFontHintLevel", items, "HandleFontHintLevel")
+    CreateMenu("UI::SetFontHintLevel", levels, "HandleFontHintLevel")
         :SetSelection(ui:GetFontHintLevel())
     
+    -- Add a drop-down menu to control the subpixel threshold.
+    local thresholds = {
+        "0",
+        "3",
+        "6",
+        "9",
+        "12",
+        "15",
+        "18",
+        "21"
+    }
+    CreateMenu("UI::SetFontSubpixelThreshold", thresholds, "HandleFontSubpixel")
+        :SetSelection(ui:GetFontSubpixelThreshold() / 3)
+
+    -- Add a drop-down menu to control oversampling.
+    local limits = {
+        "1",
+        "2",
+        "3",
+        "4",
+        "5",
+        "6",
+        "7",
+        "8"
+    }
+    CreateMenu("UI::SetFontOversampling", limits, "HandleFontOversampling")
+        :SetSelection(ui:GetFontOversampling() - 1)
+
     -- Set the mouse mode to use in the sample
     SampleInitMouseMode(MM_FREE)
 end
@@ -122,11 +146,11 @@ function CreateMenu(label, items, handler)
         list:AddItem(t)
         t.text = item
         t:SetStyleAuto()
+        t:SetMinWidth(t:GetRowWidth(0) + 10);
         t:AddTag(TEXT_TAG) 
     end
 
     text:SetMaxWidth(text:GetRowWidth(0))
-    list:SetMaxWidth(text:GetRowWidth(0) * 1.5)
     
     SubscribeToEvent(list, "ItemSelected", handler)
     return list
@@ -165,18 +189,25 @@ function HandleSRGB(eventType, eventData)
     end
 end
 
-function HandleSubpixelGlyphPositions(eventType, eventData)
-    local box = eventData["Element"]:GetPtr("CheckBox")
-    local checked = box:IsChecked()
+function HandleFontHintLevel(eventType, eventData)
+    local list = eventData["Element"]:GetPtr("DropDownList")
+    local i = list:GetSelection()
 
-    ui:SetSubpixelGlyphPositions(checked)
+    ui:SetFontHintLevel(i)
 end
 
-function HandleFontHintLevel(eventType, eventData)
+function HandleFontSubpixel(eventType, eventData)
     local list = eventData["Element"]:GetPtr("DropDownList")
     local i = list:GetSelection()
 
-    ui:SetFontHintLevel(i)
+    ui:SetFontSubpixelThreshold(i * 3)
+end
+
+function HandleFontOversampling(eventType, eventData)
+    local list = eventData["Element"]:GetPtr("DropDownList")
+    local i = list:GetSelection()
+
+    ui:SetFontOversampling(i + 1)
 end
 
 -- Create XML patch instructions for screen joystick layout specific to this sample app

+ 45 - 14
bin/Data/Scripts/47_Typography.as

@@ -27,7 +27,7 @@ void Start()
     // (Don't modify the root directly, as the base Sample class uses it)
     uielement = UIElement();
     uielement.SetAlignment(HA_CENTER, VA_CENTER);
-    uielement.SetLayout(LM_VERTICAL, 20, IntRect(20, 40, 20, 40));
+    uielement.SetLayout(LM_VERTICAL, 10, IntRect(20, 40, 20, 40));
     ui.root.AddChild(uielement);
     
     // Add some sample text
@@ -48,19 +48,43 @@ void Start()
     CreateCheckbox("UI::SetForceAutoHint", "HandleForceAutoHint")
         .checked = ui.forceAutoHint;
 
-    // Add a checkbox for the global SubpixelGlyphPositions setting. This affects character spacing.
-    CreateCheckbox("UI::SetSubpixelGlyphPositions", "HandleSubpixelGlyphPositions")
-        .checked = ui.subpixelGlyphPositions;
-
     // Add a drop-down menu to control the font hinting level.
-    Array<String> items = {
+    Array<String> levels = {
         "FONT_HINT_LEVEL_NONE",
         "FONT_HINT_LEVEL_LIGHT",
         "FONT_HINT_LEVEL_NORMAL"
     };
-    CreateMenu("UI::SetFontHintLevel", items, "HandleFontHintLevel")
+    CreateMenu("UI::SetFontHintLevel", levels, "HandleFontHintLevel")
         .selection = ui.fontHintLevel;
 
+    // Add a drop-down menu to control the subpixel threshold.
+    Array<String> thresholds = {
+        "0",
+        "3",
+        "6",
+        "9",
+        "12",
+        "15",
+        "18",
+        "21"
+    };
+    CreateMenu("UI::SetFontSubpixelThreshold", thresholds, "HandleFontSubpixel")
+        .selection = ui.fontSubpixelThreshold / 3;
+
+    // Add a drop-down menu to control oversampling.
+    Array<String>  limits = {
+        "1",
+        "2",
+        "3",
+        "4",
+        "5",
+        "6",
+        "7",
+        "8"
+    };
+    CreateMenu("UI::SetFontOversampling", limits, "HandleFontOversampling")
+        .selection = ui.fontOversampling - 1;
+
     // Set the mouse mode to use in the sample
     SampleInitMouseMode(MM_FREE);
 }
@@ -128,11 +152,11 @@ DropDownList@ CreateMenu(String label, Array<String> items, String handler)
         list.AddItem(t);
         t.text = items[i];
         t.SetStyleAuto();
+        t.minWidth = t.rowWidths[0] + 10;
         t.AddTag(TEXT_TAG);
     }
     
     text.maxWidth = text.rowWidths[0];
-    list.maxWidth = text.rowWidths[0] * 1.5;
     
     SubscribeToEvent(list, "ItemSelected", handler);
     return list;
@@ -178,21 +202,28 @@ void HandleSRGB(StringHash eventType, VariantMap& eventData)
     }
 }
 
-void HandleSubpixelGlyphPositions(StringHash eventType, VariantMap& eventData)
+void HandleFontHintLevel(StringHash eventType, VariantMap& eventData)
 {
-    CheckBox@ box = eventData["Element"].GetPtr();
-    bool checked = box.checked;
+    DropDownList@ list = eventData["Element"].GetPtr();
+    int i = list.selection;
 
-    ui.subpixelGlyphPositions = checked;
+    ui.fontHintLevel = FontHintLevel(i);    
 }
 
+void HandleFontSubpixel(StringHash eventType, VariantMap& eventData)
+{
+    DropDownList@ list = eventData["Element"].GetPtr();
+    int i = list.selection;
+
+    ui.fontSubpixelThreshold = i * 3;    
+}
 
-void HandleFontHintLevel(StringHash eventType, VariantMap& eventData)
+void HandleFontOversampling(StringHash eventType, VariantMap& eventData)
 {
     DropDownList@ list = eventData["Element"].GetPtr();
     int i = list.selection;
 
-    ui.fontHintLevel = FontHintLevel(i);    
+    ui.fontOversampling = i + 1;
 }
 
 // Create XML patch instructions for screen joystick layout specific to this sample app