Improved font rendering quality and always enabled linear texture interpolation for both minification and magnification

This commit is contained in:
Leon Styhre 2023-09-27 20:36:54 +02:00
parent bf16bed03f
commit ee2573345f
11 changed files with 93 additions and 101 deletions

View file

@ -715,7 +715,8 @@ void GuiScraperSearch::render(const glm::mat4& parentTrans)
// Slight adjustment upwards so the busy grid is not rendered precisely at the text edge.
trans = glm::translate(
trans, glm::vec3 {0.0f, -(mRenderer->getScreenResolutionModifier() * 10.0f), 0.0f});
trans,
glm::vec3 {0.0f, std::round(-(mRenderer->getScreenResolutionModifier() * 10.0f)), 0.0f});
if (mBlockAccept) {
mRenderer->setMatrix(trans);

View file

@ -157,7 +157,7 @@ void GuiScraperSingle::onSizeChanged()
else
mGrid.setColWidthPerc(1, 0.04f);
mGrid.setSize(glm::round(mSize));
mGrid.setSize(mSize);
mBackground.fitTo(mSize);
// Add some extra margins to the game name.

View file

@ -79,14 +79,13 @@ void HelpStyle::applyTheme(const std::shared_ptr<ThemeData>& theme, const std::s
iconColorDimmed = iconColor;
if (elem->has("fontPath") || elem->has("fontSize")) {
font = Font::getFromTheme(elem, ThemeFlags::ALL, font, 0.0f, false);
font = Font::getFromTheme(elem, ThemeFlags::ALL, font);
if (!elem->has("fontSizeDimmed"))
fontDimmed = Font::getFromTheme(elem, ThemeFlags::ALL, font, 0.0f, false);
fontDimmed = Font::getFromTheme(elem, ThemeFlags::ALL, font);
}
if (elem->has("fontSizeDimmed")) {
fontDimmed = Font::getFromTheme(elem, ThemeFlags::ALL, font, 0.0f, false, 1.0f, true);
}
if (elem->has("fontSizeDimmed"))
fontDimmed = Font::getFromTheme(elem, ThemeFlags::ALL, font, 0.0f, 1.0f, true);
if (elem->has("entrySpacing"))
entrySpacing = glm::clamp(elem->get<float>("entrySpacing"), 0.0f, 0.04f);

View file

@ -237,5 +237,5 @@ void DateTimeComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (properties & LINE_SPACING && elem->has("lineSpacing"))
setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f));
setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, false));
setFont(Font::getFromTheme(elem, properties, mFont, maxHeight));
}

View file

@ -180,7 +180,7 @@ void DateTimeEditComponent::render(const glm::mat4& parentTrans)
if (mAlignRight)
off.x += referenceSize - mTextCache->metrics.size.x;
trans = glm::translate(trans, off);
trans = glm::translate(trans, glm::round(off));
mRenderer->setMatrix(trans);

View file

@ -252,21 +252,22 @@ void TextComponent::render(const glm::mat4& parentTrans)
mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, mBgColor, mBgColor, false,
mOpacity * mThemeOpacity, mDimming);
if (mTextCache) {
const glm::vec2& textSize {mTextCache->metrics.size};
const float textHeight {mTextCache->metrics.size.y};
float yOff {0.0f};
float yOffDebugOverlay {0.0f};
if (mSize.y > textSize.y) {
if (mSize.y > textHeight) {
switch (mVerticalAlignment) {
case ALIGN_TOP: {
yOff = 0.0f;
break;
}
case ALIGN_BOTTOM: {
yOff = mSize.y - textSize.y;
yOff = mSize.y - textHeight;
break;
}
case ALIGN_CENTER: {
yOff = (mSize.y - textSize.y) / 2.0f;
yOff = (mSize.y - textHeight) / 2.0f;
break;
}
default: {
@ -275,8 +276,9 @@ void TextComponent::render(const glm::mat4& parentTrans)
}
}
else {
// If height is smaller than the font height, then always center vertically.
yOff = (mSize.y - textSize.y) / 2.0f;
// If height is smaller than the font height, then centering is done in
// Font::buildTextCache()
yOffDebugOverlay = (mSize.y - textHeight) / 2.0f;
}
// Draw the overall textbox area. If we're inside a vertical scrollable container then
@ -286,7 +288,7 @@ void TextComponent::render(const glm::mat4& parentTrans)
mRenderer->drawRect(0.0f, 0.0f, mSize.x, mSize.y, 0x0000FF33, 0x0000FF33);
}
trans = glm::translate(trans, glm::vec3 {0.0f, yOff, 0.0f});
trans = glm::translate(trans, glm::vec3 {0.0f, std::round(yOff), 0.0f});
mRenderer->setMatrix(trans);
if (Settings::getInstance()->getBool("DebugText")) {
@ -295,14 +297,14 @@ void TextComponent::render(const glm::mat4& parentTrans)
if (mScrollOffset1 <= mTextCache->metrics.size.x) {
const float width {mTextCache->metrics.size.x - mScrollOffset1};
mRenderer->drawRect(
mScrollOffset1 + relativeScaleOffset, 0.0f,
mScrollOffset1 + relativeScaleOffset, yOffDebugOverlay,
width > mSize.x * mRelativeScale ? mSize.x * mRelativeScale : width,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
}
}
else if (mHorizontalScrolling && secondPass) {
if ((mSize.x * mRelativeScale) - -mScrollOffset2 > 0.0f) {
mRenderer->drawRect(relativeScaleOffset, 0.0f,
mRenderer->drawRect(relativeScaleOffset, yOffDebugOverlay,
(mSize.x * mRelativeScale) - -mScrollOffset2,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
}
@ -310,19 +312,19 @@ void TextComponent::render(const glm::mat4& parentTrans)
else {
switch (mHorizontalAlignment) {
case ALIGN_LEFT: {
mRenderer->drawRect(0.0f, 0.0f, mTextCache->metrics.size.x,
mRenderer->drawRect(0.0f, yOffDebugOverlay, mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
break;
}
case ALIGN_CENTER: {
mRenderer->drawRect((mSize.x - mTextCache->metrics.size.x) / 2.0f, 0.0f,
mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
mRenderer->drawRect((mSize.x - mTextCache->metrics.size.x) / 2.0f,
yOffDebugOverlay, mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x000033, 0x00000033);
break;
}
case ALIGN_RIGHT: {
mRenderer->drawRect(mSize.x - mTextCache->metrics.size.x, 0.0f,
mTextCache->metrics.size.x,
mRenderer->drawRect(mSize.x - mTextCache->metrics.size.x,
yOffDebugOverlay, mTextCache->metrics.size.x,
mTextCache->metrics.size.y, 0x00000033, 0x00000033);
break;
}
@ -467,10 +469,13 @@ void TextComponent::onTextChanged()
lineHeight = mFont->loadGlyphs(text + "\n") * mLineSpacing;
const bool isMultiline {mAutoCalcExtent.y == 1 || mSize.y * mRelativeScale > lineHeight};
float offsetY {0.0f};
if (mHorizontalScrolling) {
if (lineHeight > mSize.y)
offsetY = (mSize.y - lineHeight) / 2.0f;
mTextCache = std::shared_ptr<TextCache>(
font->buildTextCache(text, 0.0f, 0.0f, mColor, mLineSpacing));
font->buildTextCache(text, 0.0f, offsetY, mColor, mLineSpacing));
}
else if (isMultiline && !isScrollable) {
const std::string wrappedText {
@ -482,9 +487,12 @@ void TextComponent::onTextChanged()
mHorizontalAlignment, mLineSpacing, mNoTopMargin));
}
else {
mTextCache = std::shared_ptr<TextCache>(font->buildTextCache(
font->wrapText(text, mSize.x, 0.0f, mLineSpacing, isMultiline), glm::vec2 {0.0f, 0.0f},
mColor, mSize.x, mHorizontalAlignment, mLineSpacing, mNoTopMargin));
if (!isMultiline && lineHeight > mSize.y)
offsetY = (mSize.y - lineHeight) / 2.0f;
mTextCache = std::shared_ptr<TextCache>(
font->buildTextCache(font->wrapText(text, mSize.x, 0.0f, mLineSpacing, isMultiline),
glm::vec2 {0.0f, offsetY}, mColor, mSize.x, mHorizontalAlignment,
mLineSpacing, mNoTopMargin));
}
if (mAutoCalcExtent.y)
@ -754,7 +762,7 @@ void TextComponent::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (properties & LINE_SPACING && elem->has("lineSpacing"))
setLineSpacing(glm::clamp(elem->get<float>("lineSpacing"), 0.5f, 3.0f));
setFont(Font::getFromTheme(elem, properties, mFont, maxHeight, false));
setFont(Font::getFromTheme(elem, properties, mFont, maxHeight));
// We need to do this after setting the font as the scroll speed is calculated from its size.
if (mHorizontalScrolling)

View file

@ -1664,8 +1664,8 @@ void CarouselComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
List::mTierList = IList<CarouselEntry, T>::LIST_SCROLL_STYLE_MEDIUM;
// Ccale the font size with the itemScale property value.
mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, false,
(mItemScale >= 1.0f ? mItemScale : 1.0f));
mFont =
Font::getFromTheme(elem, properties, mFont, 0.0f, (mItemScale >= 1.0f ? mItemScale : 1.0f));
if (elem->has("textRelativeScale"))
mTextRelativeScale = glm::clamp(elem->get<float>("textRelativeScale"), 0.2f, 1.0f);

View file

@ -1344,7 +1344,7 @@ void GridComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
if (elem->has("unfocusedItemDimming"))
mUnfocusedItemDimming = glm::clamp(elem->get<float>("unfocusedItemDimming"), 0.0f, 1.0f);
mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, (mItemScale > 1.0f));
mFont = Font::getFromTheme(elem, properties, mFont);
if (elem->has("textRelativeScale"))
mTextRelativeScale = glm::clamp(elem->get<float>("textRelativeScale"), 0.2f, 1.0f);

View file

@ -403,7 +403,7 @@ template <typename T> void TextListComponent<T>::render(const glm::mat4& parentT
// Render text.
glm::mat4 drawTrans {trans};
drawTrans = glm::translate(drawTrans, offset);
drawTrans = glm::translate(drawTrans, glm::round(offset));
mRenderer->setMatrix(drawTrans);
if (i == mCursor && backgroundColor != 0x00000000) {
@ -503,7 +503,7 @@ void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme,
glm::clamp(elem->get<float>("textHorizontalScrollGap"), 0.1f, 5.0f);
}
mFont = Font::getFromTheme(elem, properties, mFont, 0.0f, false);
mFont = Font::getFromTheme(elem, properties, mFont);
if (properties & ALIGNMENT) {
if (elem->has("horizontalAlignment")) {

View file

@ -15,11 +15,10 @@
#include "utils/PlatformUtil.h"
#include "utils/StringUtil.h"
Font::Font(float size, const std::string& path, const bool linearMagnify)
Font::Font(float size, const std::string& path)
: mRenderer {Renderer::getInstance()}
, mPath(path)
, mFontSize {size}
, mLinearMagnify {linearMagnify}
, mLetterHeight {0.0f}
, mMaxGlyphHeight {static_cast<int>(std::round(size))}
{
@ -46,8 +45,7 @@ Font::~Font()
{
unload(ResourceManager::getInstance());
auto fontEntry =
sFontMap.find(std::tuple<float, std::string, bool>(mFontSize, mPath, mLinearMagnify));
auto fontEntry = sFontMap.find(std::tuple<float, std::string>(mFontSize, mPath));
if (fontEntry != sFontMap.cend())
sFontMap.erase(fontEntry);
@ -58,11 +56,11 @@ Font::~Font()
}
}
std::shared_ptr<Font> Font::get(float size, const std::string& path, const bool linearMagnify)
std::shared_ptr<Font> Font::get(float size, const std::string& path)
{
const std::string canonicalPath {Utils::FileSystem::getCanonicalPath(path)};
const std::tuple<float, std::string, bool> def {
size, canonicalPath.empty() ? getDefaultPath() : canonicalPath, linearMagnify};
const std::tuple<float, std::string> def {size, canonicalPath.empty() ? getDefaultPath() :
canonicalPath};
auto foundFont = sFontMap.find(def);
if (foundFont != sFontMap.cend()) {
@ -70,7 +68,7 @@ std::shared_ptr<Font> Font::get(float size, const std::string& path, const bool
return foundFont->second.lock();
}
std::shared_ptr<Font> font {new Font(std::get<0>(def), std::get<1>(def), std::get<2>(def))};
std::shared_ptr<Font> font {new Font(std::get<0>(def), std::get<1>(def))};
sFontMap[def] = std::weak_ptr<Font>(font);
ResourceManager::getInstance().addReloadable(font);
return font;
@ -139,8 +137,8 @@ TextCache* Font::buildTextCache(const std::string& text,
float lineSpacing,
bool noTopMargin)
{
float x {offset[0] + (xLen != 0 ? getNewlineStartOffset(text, 0, xLen, alignment) : 0)};
float yTop {0.0f};
float x {offset.x + (xLen != 0 ? getNewlineStartOffset(text, 0, xLen, alignment) : 0)};
int yTop {0};
float yBot {0.0f};
if (noTopMargin) {
@ -148,14 +146,11 @@ TextCache* Font::buildTextCache(const std::string& text,
yBot = getHeight(1.5);
}
else {
// TODO: This is lacking some precision which is especially visible at higher resolutions
// like 4K where the text is not always placed entirely correctly vertically. Try to find
// a way to improve on this.
yTop = getGlyph('S')->bearing.y;
yBot = getHeight(lineSpacing);
}
float y {offset[1] + (yBot + yTop) / 2.0f};
float y {offset.y + ((yBot + yTop) / 2.0f)};
// Vertices by texture.
std::map<FontTexture*, std::vector<Renderer::Vertex>> vertMap;
@ -312,7 +307,7 @@ std::string Font::wrapText(const std::string& text,
Glyph* glyph {getGlyph(charID)};
if (glyph != nullptr) {
charWidth = glyph->advance.x;
charWidth = static_cast<float>(glyph->advance.x);
byteCount = cursor - i;
}
else {
@ -435,7 +430,6 @@ std::shared_ptr<Font> Font::getFromTheme(const ThemeData::ThemeElement* elem,
unsigned int properties,
const std::shared_ptr<Font>& orig,
const float maxHeight,
const bool linearMagnify,
const float sizeMultiplier,
const bool fontSizeDimmed)
{
@ -475,7 +469,7 @@ std::shared_ptr<Font> Font::getFromTheme(const ThemeData::ThemeElement* elem,
path = getDefaultPath();
}
return get(size, path, linearMagnify);
return get(size, path);
}
size_t Font::getMemUsage() const
@ -538,14 +532,13 @@ std::vector<std::string> Font::getFallbackFontPaths()
return fontPaths;
}
Font::FontTexture::FontTexture(const int mFontSize, const bool linearMagnifyArg)
Font::FontTexture::FontTexture(const int mFontSize)
{
textureId = 0;
rowHeight = 0;
writePos = glm::ivec2 {0, 0};
linearMagnify = linearMagnifyArg;
writePos = glm::ivec2 {1, 1};
// Set the texture to a reasonable size, if we run out of space for adding glyphs then
// Set the texture atlas to a reasonable size, if we run out of space for adding glyphs then
// more textures will be created dynamically.
textureSize = glm::ivec2 {mFontSize * 6, mFontSize * 6};
}
@ -556,24 +549,24 @@ Font::FontTexture::~FontTexture()
deinitTexture();
}
bool Font::FontTexture::findEmpty(const glm::ivec2& size, glm::ivec2& cursor_out)
bool Font::FontTexture::findEmpty(const glm::ivec2& size, glm::ivec2& cursorOut)
{
if (size.x >= textureSize.x || size.y >= textureSize.y)
if (size.x > textureSize.x || size.y > textureSize.y)
return false;
if (writePos.x + size.x >= textureSize.x &&
if (writePos.x + size.x + 1 > textureSize.x &&
writePos.y + rowHeight + size.y + 1 < textureSize.y) {
// Row is full, but the glyph should fit on the next row so move the cursor there.
// Leave 1 pixel of space between glyphs so that pixels from adjacent glyphs will
// not get sampled during scaling which would lead to edge artifacts.
writePos = glm::ivec2 {0, writePos.y + rowHeight + 1};
// Leave 1 pixel of space between glyphs so that pixels from adjacent glyphs will not
// get sampled during scaling and interpolation, which would lead to edge artifacts.
writePos = glm::ivec2 {1, writePos.y + rowHeight + 1};
rowHeight = 0;
}
if (writePos.x + size.x >= textureSize.x || writePos.y + size.y >= textureSize.y)
if (writePos.x + size.x + 1 > textureSize.x || writePos.y + size.y + 1 > textureSize.y)
return false; // No it still won't fit.
cursor_out = writePos;
cursorOut = writePos;
// Leave 1 pixel of space between glyphs.
writePos.x += size.x + 1;
@ -590,9 +583,9 @@ void Font::FontTexture::initTexture()
// glyphs will not be visible. That would otherwise lead to edge artifacts as these pixels
// would get sampled during scaling.
std::vector<uint8_t> texture(textureSize.x * textureSize.y * 4, 0);
textureId = Renderer::getInstance()->createTexture(0, Renderer::TextureType::RED, true,
linearMagnify, false, false, textureSize.x,
textureSize.y, &texture[0]);
textureId =
Renderer::getInstance()->createTexture(0, Renderer::TextureType::RED, true, true, false,
false, textureSize.x, textureSize.y, &texture[0]);
}
void Font::FontTexture::deinitTexture()
@ -669,28 +662,26 @@ void Font::unloadTextures()
}
void Font::getTextureForNewGlyph(const glm::ivec2& glyphSize,
FontTexture*& tex_out,
glm::ivec2& cursor_out)
FontTexture*& texOut,
glm::ivec2& cursorOut)
{
if (mTextures.size()) {
// Check if the most recent texture has space available for the glyph.
tex_out = mTextures.back().get();
texOut = mTextures.back().get();
// Will this one work?
if (tex_out->findEmpty(glyphSize, cursor_out))
if (texOut->findEmpty(glyphSize, cursorOut))
return; // Yes.
}
mTextures.emplace_back(
std::make_unique<FontTexture>(static_cast<int>(std::round(mFontSize)), mLinearMagnify));
tex_out = mTextures.back().get();
tex_out->initTexture();
mTextures.emplace_back(std::make_unique<FontTexture>(static_cast<int>(std::round(mFontSize))));
texOut = mTextures.back().get();
texOut->initTexture();
bool ok {tex_out->findEmpty(glyphSize, cursor_out)};
if (!ok) {
if (!texOut->findEmpty(glyphSize, cursorOut)) {
LOG(LogError) << "Glyph too big to fit on a new texture (glyph size > "
<< tex_out->textureSize.x << ", " << tex_out->textureSize.y << ")";
tex_out = nullptr;
<< texOut->textureSize.x << ", " << texOut->textureSize.y << ")";
texOut = nullptr;
}
}
@ -770,15 +761,13 @@ Font::Glyph* Font::getGlyph(const unsigned int id)
Glyph& glyph {mGlyphMap[id]};
glyph.texture = tex;
glyph.texPos = glm::vec2 {cursor.x / static_cast<float>(tex->textureSize.x),
cursor.y / static_cast<float>(tex->textureSize.y)};
glyph.texSize = glm::vec2 {glyphSize.x / static_cast<float>(tex->textureSize.x),
glyphSize.y / static_cast<float>(tex->textureSize.y)};
glyph.advance = glm::vec2 {static_cast<float>(glyphSlot->metrics.horiAdvance) / 64.0f,
static_cast<float>(glyphSlot->metrics.vertAdvance) / 64.0f};
glyph.bearing = glm::vec2 {static_cast<float>(glyphSlot->metrics.horiBearingX) / 64.0f,
static_cast<float>(glyphSlot->metrics.horiBearingY) / 64.0f};
glyph.rows = glyphSlot->bitmap.rows;
glyph.texPos = {cursor.x / static_cast<float>(tex->textureSize.x),
cursor.y / static_cast<float>(tex->textureSize.y)};
glyph.texSize = {glyphSize.x / static_cast<float>(tex->textureSize.x),
glyphSize.y / static_cast<float>(tex->textureSize.y)};
glyph.advance = {glyphSlot->metrics.horiAdvance >> 6, glyphSlot->metrics.vertAdvance >> 6};
glyph.bearing = {glyphSlot->metrics.horiBearingX >> 6, glyphSlot->metrics.horiBearingY >> 6};
glyph.rows = glyphSize.y;
// Upload glyph bitmap to texture.
mRenderer->updateTexture(tex->textureId, 0, Renderer::TextureType::RED, cursor.x, cursor.y,

View file

@ -38,9 +38,7 @@ class Font : public IReloadable
{
public:
virtual ~Font();
static std::shared_ptr<Font> get(float size,
const std::string& path = getDefaultPath(),
const bool linearMagnify = false);
static std::shared_ptr<Font> get(float size, const std::string& path = getDefaultPath());
static float getMiniFont()
{
static float sMiniFont {0.030f *
@ -133,7 +131,6 @@ public:
unsigned int properties,
const std::shared_ptr<Font>& orig,
const float maxHeight = 0.0f,
const bool linearMagnify = false,
const float sizeMultiplier = 1.0f,
const bool fontSizeDimmed = false);
@ -143,7 +140,7 @@ public:
static size_t getTotalMemUsage();
private:
Font(float size, const std::string& path, const bool linearMagnify);
Font(float size, const std::string& path);
static void initLibrary();
struct FontTexture {
@ -151,11 +148,10 @@ private:
glm::ivec2 textureSize;
glm::ivec2 writePos;
int rowHeight;
bool linearMagnify;
FontTexture(const int mFontSize, const bool linearMagnifyArg);
FontTexture(const int mFontSize);
~FontTexture();
bool findEmpty(const glm::ivec2& size, glm::ivec2& cursor_out);
bool findEmpty(const glm::ivec2& size, glm::ivec2& cursorOut);
// You must call initTexture() after creating a FontTexture to get a textureId.
// Initializes the OpenGL texture according to this FontTexture's settings,
@ -178,8 +174,8 @@ private:
FontTexture* texture;
glm::vec2 texPos;
glm::vec2 texSize; // In texels.
glm::vec2 advance;
glm::vec2 bearing;
glm::ivec2 advance;
glm::ivec2 bearing;
int rows;
};
@ -188,8 +184,8 @@ private:
void unloadTextures();
void getTextureForNewGlyph(const glm::ivec2& glyphSize,
FontTexture*& tex_out,
glm::ivec2& cursor_out);
FontTexture*& texOut,
glm::ivec2& cursorOut);
std::vector<std::string> getFallbackFontPaths();
FT_Face getFaceForChar(unsigned int id);
@ -203,7 +199,7 @@ private:
void clearFaceCache() { mFaceCache.clear(); }
static inline FT_Library sLibrary {nullptr};
static inline std::map<std::tuple<float, std::string, bool>, std::weak_ptr<Font>> sFontMap;
static inline std::map<std::tuple<float, std::string>, std::weak_ptr<Font>> sFontMap;
Renderer* mRenderer;
std::vector<std::unique_ptr<FontTexture>> mTextures;
@ -212,7 +208,6 @@ private:
const std::string mPath;
float mFontSize;
const bool mLinearMagnify;
float mLetterHeight;
int mMaxGlyphHeight;
};