diff --git a/Source/Editor/Windows/Assets/FontWindow.cs b/Source/Editor/Windows/Assets/FontWindow.cs index 3f4a7a681..1a69f7724 100644 --- a/Source/Editor/Windows/Assets/FontWindow.cs +++ b/Source/Editor/Windows/Assets/FontWindow.cs @@ -21,6 +21,10 @@ namespace FlaxEditor.Windows.Assets /// 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; } } diff --git a/Source/Engine/Content/Upgraders/FontAssetUpgrader.h b/Source/Engine/Content/Upgraders/FontAssetUpgrader.h index 80c2e6c39..4ca6e6d73 100644 --- a/Source/Engine/Content/Upgraders/FontAssetUpgrader.h +++ b/Source/Engine/Content/Upgraders/FontAssetUpgrader.h @@ -5,6 +5,7 @@ #if USE_EDITOR #include "BinaryAssetUpgrader.h" +#include "Engine/Render2D/FontAsset.h" /// /// 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 diff --git a/Source/Engine/ContentImporters/ImportFont.cpp b/Source/Engine/ContentImporters/ImportFont.cpp index c7dc01fe6..964f3907e 100644 --- a/Source/Engine/ContentImporters/ImportFont.cpp +++ b/Source/Engine/ContentImporters/ImportFont.cpp @@ -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 diff --git a/Source/Engine/Render2D/FontAsset.cs b/Source/Engine/Render2D/FontAsset.cs index 72520db5d..624daab5b 100644 --- a/Source/Engine/Render2D/FontAsset.cs +++ b/Source/Engine/Render2D/FontAsset.cs @@ -1,5 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; + namespace FlaxEngine { partial struct FontOptions @@ -11,7 +13,7 @@ namespace FlaxEngine /// true if this object has the same value as ; otherwise, false public bool Equals(FontOptions other) { - return Hinting == other.Hinting && Flags == other.Flags; + return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode; } /// @@ -23,10 +25,7 @@ namespace FlaxEngine /// public override int GetHashCode() { - unchecked - { - return ((int)Hinting * 397) ^ (int)Flags; - } + return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode); } /// @@ -37,7 +36,7 @@ namespace FlaxEngine /// true if has the same value as ; otherwise, false. 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; } /// @@ -48,7 +47,7 @@ namespace FlaxEngine /// true if has a different value than ; otherwise,false. 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; } } } diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index b1ea87e0a..c9966c68b 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -66,6 +66,22 @@ API_ENUM(Attributes="Flags") enum class FontFlags : byte Italic = 4, }; +/// +/// The font rasterization mode. +/// +API_ENUM() enum class FontRasterMode : byte +{ + /// + /// Use the default FreeType rasterizer to render font atlases. + /// + Bitmap, + + /// + /// Use the MSDF generator to render font atlases. Need to be rendered with a compatible material. + /// + MSDF, +}; + DECLARE_ENUM_OPERATORS(FontFlags); /// @@ -84,6 +100,11 @@ API_STRUCT() struct FontOptions /// The flags. /// API_FIELD() FontFlags Flags; + + /// + /// The rasterization mode. + /// + API_FIELD() FontRasterMode RasterMode; }; /// @@ -91,7 +112,7 @@ API_STRUCT() struct FontOptions /// API_CLASS(NoSpawn) class FLAXENGINE_API FontAsset : public BinaryAsset { - DECLARE_BINARY_ASSET_HEADER(FontAsset, 3); + DECLARE_BINARY_ASSET_HEADER(FontAsset, 4); friend Font; private: diff --git a/Source/Engine/Render2D/FontManager.cpp b/Source/Engine/Render2D/FontManager.cpp index 0375814ae..2c75a6f23 100644 --- a/Source/Engine/Render2D/FontManager.cpp +++ b/Source/Engine/Render2D/FontManager.cpp @@ -13,6 +13,7 @@ #include #include #include +#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(glyph->advance.x); - entry.OffsetY = glyph->bitmap_top; - entry.OffsetX = glyph->bitmap_left; - entry.IsValid = true; - entry.BearingY = Convert26Dot6ToRoundedPixel(glyph->metrics.horiBearingY); - entry.Height = Convert26Dot6ToRoundedPixel(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(glyph->advance.x); + entry.OffsetY = glyph->bitmap_top; + entry.OffsetX = glyph->bitmap_left; + entry.IsValid = true; + entry.BearingY = Convert26Dot6ToRoundedPixel(glyph->metrics.horiBearingY); + entry.Height = Convert26Dot6ToRoundedPixel(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(glyph->advance.x); + entry.OffsetY = msdf_top; + entry.OffsetX = msdf_left; + entry.IsValid = true; + entry.BearingY = Convert26Dot6ToRoundedPixel(glyph->metrics.horiBearingY); + entry.Height = Convert26Dot6ToRoundedPixel(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(); - atlas->Setup(PixelFormat::R8_UNorm, FontTextureAtlas::PaddingStyle::PadWithZero); + atlas->Setup(requiredFormat, FontTextureAtlas::PaddingStyle::PadWithZero); Atlases.Add(atlas); atlasIndex++; diff --git a/Source/Engine/Render2D/FontTextureAtlas.h b/Source/Engine/Render2D/FontTextureAtlas.h index fe92ed67c..3a6ca98ae 100644 --- a/Source/Engine/Render2D/FontTextureAtlas.h +++ b/Source/Engine/Render2D/FontTextureAtlas.h @@ -105,6 +105,14 @@ public: return _height; } + /// + /// Gets the atlas pixel format. + /// + FORCE_INLINE PixelFormat GetPixelFormat() const + { + return _format; + } + /// /// Gets the atlas size. /// @@ -186,8 +194,8 @@ public: /// /// Returns glyph's bitmap data of the slot. /// - /// The slot in atlas. - /// The width of the slot. + /// The slot in atlas. + /// The width of the slot. /// The height of the slot. /// The stride of the slot. /// The pointer to the bitmap data of the given slot. diff --git a/Source/Engine/Render2D/MSDFGenerator.h b/Source/Engine/Render2D/MSDFGenerator.h new file mode 100644 index 000000000..4db941211 --- /dev/null +++ b/Source/Engine/Render2D/MSDFGenerator.h @@ -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 +#include + +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(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(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(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(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& 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(Math::CeilToInt(bounds.r - bounds.l + pxRange)); + int32 height = static_cast(Math::CeilToInt(bounds.t - bounds.b + pxRange)); + msdfgen::Bitmap 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& 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(Math::CeilToInt(bounds.t + pxRange / 2.0)); + left = static_cast(Math::FloorToInt(bounds.l + pxRange / 2.0)); + } +};