initial support for MSDF font

This commit is contained in:
fibref
2026-02-14 19:42:31 +08:00
parent 0eac545271
commit 75a1b14beb
8 changed files with 285 additions and 65 deletions

View File

@@ -21,6 +21,10 @@ namespace FlaxEditor.Windows.Assets
/// </summary>
private sealed class PropertiesProxy
{
[DefaultValue(FontRasterMode.Bitmap)]
[EditorOrder(5), EditorDisplay("Properties"), Tooltip("The rasterization mode used when generating font atlases.")]
public FontRasterMode RasterMode;
[DefaultValue(FontHinting.Default)]
[EditorOrder(10), EditorDisplay("Properties"), Tooltip("The font hinting used when rendering characters.")]
public FontHinting Hinting;
@@ -41,7 +45,8 @@ namespace FlaxEditor.Windows.Assets
{
options = new FontOptions
{
Hinting = Hinting
Hinting = Hinting,
RasterMode = RasterMode
};
if (AntiAliasing)
options.Flags |= FontFlags.AntiAliasing;
@@ -57,6 +62,7 @@ namespace FlaxEditor.Windows.Assets
AntiAliasing = (options.Flags & FontFlags.AntiAliasing) == FontFlags.AntiAliasing;
Bold = (options.Flags & FontFlags.Bold) == FontFlags.Bold;
Italic = (options.Flags & FontFlags.Italic) == FontFlags.Italic;
RasterMode = options.RasterMode;
}
}

View File

@@ -5,6 +5,7 @@
#if USE_EDITOR
#include "BinaryAssetUpgrader.h"
#include "Engine/Render2D/FontAsset.h"
/// <summary>
/// Font Asset Upgrader
@@ -17,10 +18,33 @@ public:
{
const Upgrader upgraders[] =
{
{},
{ 3, 4, &Upgrade_3_To_4 },
};
setup(upgraders, ARRAY_COUNT(upgraders));
}
private:
struct FontOptionsOld
{
FontHinting Hinting;
FontFlags Flags;
};
static bool Upgrade_3_To_4(AssetMigrationContext& context)
{
ASSERT(context.Input.SerializedVersion == 3 && context.Output.SerializedVersion == 4);
FontOptionsOld optionsOld;
Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptionsOld));
FontOptions options;
options.Hinting = optionsOld.Hinting;
options.Flags = optionsOld.Flags;
options.RasterMode = FontRasterMode::Bitmap;
context.Output.CustomData.Copy(&options);
return CopyChunk(context, 0);
}
};
#endif

View File

@@ -12,12 +12,13 @@
CreateAssetResult ImportFont::Import(CreateAssetContext& context)
{
// Base
IMPORT_SETUP(FontAsset, 3);
IMPORT_SETUP(FontAsset, 4);
// Setup header
FontOptions options;
options.Hinting = FontHinting::Default;
options.Flags = FontFlags::AntiAliasing;
options.RasterMode = FontRasterMode::Bitmap;
context.Data.CustomData.Copy(&options);
// Open the file

View File

@@ -1,5 +1,7 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System;
namespace FlaxEngine
{
partial struct FontOptions
@@ -11,7 +13,7 @@ namespace FlaxEngine
/// <returns><c>true</c> if this object has the same value as <paramref name="other" />; otherwise, <c>false</c> </returns>
public bool Equals(FontOptions other)
{
return Hinting == other.Hinting && Flags == other.Flags;
return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode;
}
/// <inheritdoc />
@@ -23,10 +25,7 @@ namespace FlaxEngine
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
{
return ((int)Hinting * 397) ^ (int)Flags;
}
return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode);
}
/// <summary>
@@ -37,7 +36,7 @@ namespace FlaxEngine
/// <returns><c>true</c> if <paramref name="left" /> has the same value as <paramref name="right" />; otherwise, <c>false</c>.</returns>
public static bool operator ==(FontOptions left, FontOptions right)
{
return left.Hinting == right.Hinting && left.Flags == right.Flags;
return left.Hinting == right.Hinting && left.Flags == right.Flags && left.RasterMode == right.RasterMode;
}
/// <summary>
@@ -48,7 +47,7 @@ namespace FlaxEngine
/// <returns><c>true</c> if <paramref name="left" /> has a different value than <paramref name="right" />; otherwise,<c>false</c>.</returns>
public static bool operator !=(FontOptions left, FontOptions right)
{
return left.Hinting != right.Hinting || left.Flags != right.Flags;
return left.Hinting != right.Hinting || left.Flags != right.Flags || left.RasterMode != right.RasterMode;
}
}
}

View File

@@ -66,6 +66,22 @@ API_ENUM(Attributes="Flags") enum class FontFlags : byte
Italic = 4,
};
/// <summary>
/// The font rasterization mode.
/// </summary>
API_ENUM() enum class FontRasterMode : byte
{
/// <summary>
/// Use the default FreeType rasterizer to render font atlases.
/// </summary>
Bitmap,
/// <summary>
/// Use the MSDF generator to render font atlases. Need to be rendered with a compatible material.
/// </summary>
MSDF,
};
DECLARE_ENUM_OPERATORS(FontFlags);
/// <summary>
@@ -84,6 +100,11 @@ API_STRUCT() struct FontOptions
/// The flags.
/// </summary>
API_FIELD() FontFlags Flags;
/// <summary>
/// The rasterization mode.
/// </summary>
API_FIELD() FontRasterMode RasterMode;
};
/// <summary>
@@ -91,7 +112,7 @@ API_STRUCT() struct FontOptions
/// </summary>
API_CLASS(NoSpawn) class FLAXENGINE_API FontAsset : public BinaryAsset
{
DECLARE_BINARY_ASSET_HEADER(FontAsset, 3);
DECLARE_BINARY_ASSET_HEADER(FontAsset, 4);
friend Font;
private:

View File

@@ -13,6 +13,7 @@
#include <ThirdParty/freetype/ftsynth.h>
#include <ThirdParty/freetype/ftbitmap.h>
#include <ThirdParty/freetype/internal/ftdrv.h>
#include "Engine/Render2D/MSDFGenerator.h"
namespace FontManagerImpl
{
@@ -125,7 +126,11 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry)
// Set load flags
uint32 glyphFlags = FT_LOAD_NO_BITMAP;
const bool useAA = EnumHasAnyFlags(options.Flags, FontFlags::AntiAliasing);
if (useAA)
if (options.RasterMode == FontRasterMode::MSDF)
{
glyphFlags |= FT_LOAD_NO_AUTOHINT | FT_LOAD_NO_HINTING;
}
else if (useAA)
{
switch (options.Hinting)
{
@@ -185,75 +190,102 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry)
FT_GlyphSlot_Oblique(face->glyph);
}
// Render glyph to the bitmap
FT_GlyphSlot glyph = face->glyph;
FT_Render_Glyph(glyph, useAA ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO);
FT_Bitmap* bitmap = &glyph->bitmap;
FT_Bitmap tmpBitmap;
if (bitmap->pixel_mode != FT_PIXEL_MODE_GRAY)
int32 glyphWidth = 0;
int32 glyphHeight = 0;
if (options.RasterMode == FontRasterMode::Bitmap)
{
// Convert the bitmap to 8bpp grayscale
FT_Bitmap_New(&tmpBitmap);
FT_Bitmap_Convert(Library, bitmap, &tmpBitmap, 4);
bitmap = &tmpBitmap;
}
ASSERT(bitmap && bitmap->pixel_mode == FT_PIXEL_MODE_GRAY);
// Render glyph to the bitmap
FT_GlyphSlot glyph = face->glyph;
FT_Render_Glyph(glyph, useAA ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO);
// Fill the character data
entry.AdvanceX = Convert26Dot6ToRoundedPixel<int16>(glyph->advance.x);
entry.OffsetY = glyph->bitmap_top;
entry.OffsetX = glyph->bitmap_left;
entry.IsValid = true;
entry.BearingY = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.horiBearingY);
entry.Height = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.height);
FT_Bitmap* bitmap = &glyph->bitmap;
FT_Bitmap tmpBitmap;
if (bitmap->pixel_mode != FT_PIXEL_MODE_GRAY) {
// Convert the bitmap to 8bpp grayscale
FT_Bitmap_New(&tmpBitmap);
FT_Bitmap_Convert(Library, bitmap, &tmpBitmap, 4);
bitmap = &tmpBitmap;
}
ASSERT(bitmap && bitmap->pixel_mode == FT_PIXEL_MODE_GRAY);
// Allocate memory
const int32 glyphWidth = bitmap->width;
const int32 glyphHeight = bitmap->rows;
GlyphImageData.Clear();
GlyphImageData.Resize(glyphWidth * glyphHeight);
// Fill the character data
entry.AdvanceX = Convert26Dot6ToRoundedPixel<int16>(glyph->advance.x);
entry.OffsetY = glyph->bitmap_top;
entry.OffsetX = glyph->bitmap_left;
entry.IsValid = true;
entry.BearingY = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.horiBearingY);
entry.Height = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.height);
// End for empty glyphs
if (GlyphImageData.IsEmpty())
{
entry.TextureIndex = MAX_uint8;
if (bitmap == &tmpBitmap)
// Allocate memory
glyphWidth = bitmap->width;
glyphHeight = bitmap->rows;
GlyphImageData.Clear();
GlyphImageData.Resize(glyphWidth * glyphHeight);
// End for empty glyphs
if (GlyphImageData.IsEmpty())
{
entry.TextureIndex = MAX_uint8;
if (bitmap == &tmpBitmap)
{
FT_Bitmap_Done(Library, bitmap);
bitmap = nullptr;
}
return false;
}
// Copy glyph data after rasterization (row by row)
for (int32 row = 0; row < glyphHeight; row++)
{
Platform::MemoryCopy(&GlyphImageData[row * glyphWidth], &bitmap->buffer[row * bitmap->pitch], glyphWidth);
}
// Normalize gray scale images not using 256 colors
if (bitmap->num_grays != 256) {
const int32 scale = 255 / (bitmap->num_grays - 1);
for (byte& pixel : GlyphImageData) {
pixel *= scale;
}
}
// Free temporary bitmap if used
if (bitmap == &tmpBitmap) {
FT_Bitmap_Done(Library, bitmap);
bitmap = nullptr;
}
return false;
}
// Copy glyph data after rasterization (row by row)
for (int32 row = 0; row < glyphHeight; row++)
else
{
Platform::MemoryCopy(&GlyphImageData[row * glyphWidth], &bitmap->buffer[row * bitmap->pitch], glyphWidth);
}
// Generate bitmap for MSDF
FT_GlyphSlot glyph = face->glyph;
int16 msdf_top = 0;
int16 msdf_left = 0;
MSDFGenerator::GenerateMSDF(glyph, GlyphImageData, glyphWidth, glyphHeight, msdf_top, msdf_left);
// Normalize gray scale images not using 256 colors
if (bitmap->num_grays != 256)
{
const int32 scale = 255 / (bitmap->num_grays - 1);
for (byte& pixel : GlyphImageData)
{
pixel *= scale;
// End for empty glyphs
if (GlyphImageData.IsEmpty()) {
entry.TextureIndex = MAX_uint8;
return false;
}
}
// Free temporary bitmap if used
if (bitmap == &tmpBitmap)
{
FT_Bitmap_Done(Library, bitmap);
bitmap = nullptr;
// Fill the character data
entry.AdvanceX = Convert26Dot6ToRoundedPixel<int16>(glyph->advance.x);
entry.OffsetY = msdf_top;
entry.OffsetX = msdf_left;
entry.IsValid = true;
entry.BearingY = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.horiBearingY);
entry.Height = Convert26Dot6ToRoundedPixel<int16>(glyph->metrics.height);
}
// Find atlas for the character texture
PixelFormat requiredFormat = options.RasterMode == FontRasterMode::MSDF ? PixelFormat::R8G8B8A8_UNorm : PixelFormat::R8_UNorm;
int32 atlasIndex = 0;
const FontTextureAtlasSlot* slot = nullptr;
for (; atlasIndex < Atlases.Count() && slot == nullptr; atlasIndex++)
for (; atlasIndex < Atlases.Count() && slot == nullptr; atlasIndex++) {
if (Atlases[atlasIndex]->GetPixelFormat() != requiredFormat)
continue;
slot = Atlases[atlasIndex]->AddEntry(glyphWidth, glyphHeight, GlyphImageData);
}
atlasIndex--;
// Check if there is no atlas for this character
@@ -261,7 +293,7 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry)
{
// Create new atlas
auto atlas = Content::CreateVirtualAsset<FontTextureAtlas>();
atlas->Setup(PixelFormat::R8_UNorm, FontTextureAtlas::PaddingStyle::PadWithZero);
atlas->Setup(requiredFormat, FontTextureAtlas::PaddingStyle::PadWithZero);
Atlases.Add(atlas);
atlasIndex++;

View File

@@ -105,6 +105,14 @@ public:
return _height;
}
/// <summary>
/// Gets the atlas pixel format.
/// </summary>
FORCE_INLINE PixelFormat GetPixelFormat() const
{
return _format;
}
/// <summary>
/// Gets the atlas size.
/// </summary>
@@ -186,8 +194,8 @@ public:
/// <summary>
/// Returns glyph's bitmap data of the slot.
/// </summary>
/// <param name="slot">The slot in atlas.</param>
/// <param name="width">The width of the slot.</param>
/// <param name="slot">The slot in atlas.</param>
/// <param name="width">The width of the slot.</param>
/// <param name="height">The height of the slot.</param>
/// <param name="stride">The stride of the slot.</param>
/// <returns>The pointer to the bitmap data of the given slot.</returns>

View File

@@ -0,0 +1,129 @@
// Copyright (c) Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Math/Math.h"
#include <ThirdParty/freetype/ftoutln.h>
#include <ThirdParty/msdfgen/msdfgen.h>
class MSDFGenerator
{
using Point2 = msdfgen::Point2;
using Shape = msdfgen::Shape;
using Contour = msdfgen::Contour;
using EdgeHolder = msdfgen::EdgeHolder;
static Point2 ftPoint2(const FT_Vector& vector, double scale)
{
return Point2(scale * vector.x, scale * vector.y);
}
struct FtContext
{
double scale;
Point2 position;
Shape* shape;
Contour* contour;
};
static int ftMoveTo(const FT_Vector* to, void* user)
{
FtContext* context = reinterpret_cast<FtContext*>(user);
if (!(context->contour && context->contour->edges.empty()))
context->contour = &context->shape->addContour();
context->position = ftPoint2(*to, context->scale);
return 0;
}
static int ftLineTo(const FT_Vector* to, void* user)
{
FtContext* context = reinterpret_cast<FtContext*>(user);
Point2 endpoint = ftPoint2(*to, context->scale);
if (endpoint != context->position)
{
context->contour->addEdge(EdgeHolder(context->position, endpoint));
context->position = endpoint;
}
return 0;
}
static int ftConicTo(const FT_Vector* control, const FT_Vector* to, void* user)
{
FtContext* context = reinterpret_cast<FtContext*>(user);
Point2 endpoint = ftPoint2(*to, context->scale);
if (endpoint != context->position)
{
context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control, context->scale), endpoint));
context->position = endpoint;
}
return 0;
}
static int ftCubicTo(const FT_Vector* control1, const FT_Vector* control2, const FT_Vector* to, void* user)
{
FtContext* context = reinterpret_cast<FtContext*>(user);
Point2 endpoint = ftPoint2(*to, context->scale);
if (endpoint != context->position || msdfgen::crossProduct(ftPoint2(*control1, context->scale) - endpoint, ftPoint2(*control2, context->scale) - endpoint))
{
context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control1, context->scale), ftPoint2(*control2, context->scale), endpoint));
context->position = endpoint;
}
return 0;
}
public:
static void GenerateMSDF(FT_GlyphSlot glyph, Array<byte>& output, int32& outputWidth, int32& outputHeight, int16& top, int16& left)
{
Shape shape;
shape.contours.clear();
shape.inverseYAxis = true;
FtContext context = { };
context.scale = 1.0 / 64.0;
context.shape = &shape;
FT_Outline_Funcs ftFunctions;
ftFunctions.move_to = &ftMoveTo;
ftFunctions.line_to = &ftLineTo;
ftFunctions.conic_to = &ftConicTo;
ftFunctions.cubic_to = &ftCubicTo;
ftFunctions.shift = 0;
ftFunctions.delta = 0;
FT_Outline_Decompose(&glyph->outline, &ftFunctions, &context);
shape.normalize();
edgeColoringSimple(shape, 3.0);
// Todo: make this configurable
// Also hard-coded in material: MSDFFontMaterial
const double pxRange = 4.0;
msdfgen::Shape::Bounds bounds = shape.getBounds();
int32 width = static_cast<int32>(Math::CeilToInt(bounds.r - bounds.l + pxRange));
int32 height = static_cast<int32>(Math::CeilToInt(bounds.t - bounds.b + pxRange));
msdfgen::Bitmap<float, 4> msdf(width, height);
msdfgen::SDFTransformation t(
msdfgen::Projection(1.0, msdfgen::Vector2(-bounds.l + pxRange / 2.0, -bounds.b + pxRange / 2.0)), msdfgen::Range(pxRange)
);
generateMTSDF(msdf, shape, t);
output.Resize(width * height * 4);
const msdfgen::BitmapConstRef<float, 4>& bitmap = msdf;
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
output[(y * width + x) * 4] = msdfgen::pixelFloatToByte(bitmap(x, y)[0]);
output[(y * width + x) * 4 + 1] = msdfgen::pixelFloatToByte(bitmap(x, y)[1]);
output[(y * width + x) * 4 + 2] = msdfgen::pixelFloatToByte(bitmap(x, y)[2]);
output[(y * width + x) * 4 + 3] = msdfgen::pixelFloatToByte(bitmap(x, y)[3]);
}
}
outputWidth = width;
outputHeight = height;
top = static_cast<int16>(Math::CeilToInt(bounds.t + pxRange / 2.0));
left = static_cast<int16>(Math::FloorToInt(bounds.l + pxRange / 2.0));
}
};