From 826ac68873f17e3f7ddc86e690d2f853ca34d893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20L=C3=BCbe=C3=9F?= Date: Wed, 5 Jan 2022 14:03:30 +0100 Subject: [PATCH] Basic Harfbuzz support and use MSDGen with Skia --- GlitchyEngine/src/Renderer/Text/Font.bf | 197 +++++++++++++++++- .../src/Renderer/Text/FontRenderer.bf | 84 +++++++- GlitchyEngine/vendor/freetype | 2 +- GlitchyEngine/vendor/msdfgen | 2 +- Sandbox/content/Shaders/msdfShader.hlsl | 38 +++- Sandbox/src/ExampleLayer2D.bf | 5 +- 6 files changed, 316 insertions(+), 12 deletions(-) diff --git a/GlitchyEngine/src/Renderer/Text/Font.bf b/GlitchyEngine/src/Renderer/Text/Font.bf index a79c5f6..91e14a2 100644 --- a/GlitchyEngine/src/Renderer/Text/Font.bf +++ b/GlitchyEngine/src/Renderer/Text/Font.bf @@ -4,6 +4,7 @@ using GlitchyEngine.Math; using GlitchyEngine.Core; using System.Collections; using msdfgen; +using static FreeType.HarfBuzz; using internal GlitchyEngine.Renderer.Text; @@ -22,6 +23,7 @@ namespace GlitchyEngine.Renderer.Text public int32 Width, Height; public double TranslationX, TranslationY; + //public double Scale; /// How many pixels we have to move the pen after drawing this glyph. public float Advance; @@ -50,7 +52,8 @@ namespace GlitchyEngine.Renderer.Text internal Texture2D _atlas ~ _?.ReleaseRef(); private Int3 _atlasSize; - private Dictionary _glyphs = new .() ~ DeleteDictionaryAndValues!(_); + private Dictionary _glyphs = new .() ~ delete _;//DeleteDictionaryAndValues!(_); + private Dictionary _glyphsById = new .() ~ DeleteDictionaryAndValues!(_); GlyphDescriptor _missingGlyph; private SamplerState _sampler ~ _.ReleaseRef(); @@ -60,6 +63,10 @@ namespace GlitchyEngine.Renderer.Text internal double _range = 4.0; + private hb_blob_t* blob ~ hb_blob_destroy(_); + private hb_face_t* face ~ hb_face_destroy(_); + private hb_font_t* font ~ hb_font_destroy(_); + /** * Gets or sets the fallback Font for this Font. * The Fallback font will be used to render Glyphs that are not defined in the current font. @@ -113,8 +120,6 @@ namespace GlitchyEngine.Renderer.Text // Set default sampler Sampler = null; - _glyphs.Add('\0', new GlyphDescriptor(){Font = this}); - _fontSize = fontSize; _faceIndex = faceIndex; _hasColor = hasColor; @@ -130,7 +135,23 @@ namespace GlitchyEngine.Renderer.Text _range = 4.0 / _geometryScaler; + GlyphDescriptor nullDesc = new GlyphDescriptor(){Font = this}; + nullDesc.GlyphIndex = FreeType.Get_Char_Index(_face, '\0'); + _glyphs.Add('\0', nullDesc); + _glyphsById.Add(nullDesc.GlyphIndex, nullDesc); + LoadGlyphs(firstChar, charCount); + + // HarfBuzz + { + blob = hb_blob_create_from_file(fontPath.CStr()); /* or hb_blob_create_from_file_or_fail() */ + face = hb_face_create(blob, 0); + font = hb_font_create(face); + + hb_font_set_scale(font, (.)fontSize * 64, (.)fontSize * 64); + } + + //TestMSDF(); } public void LoadGlyphs(char32 firstChar, uint32 charCount) @@ -140,6 +161,37 @@ namespace GlitchyEngine.Renderer.Text UpdateAtlas(); } + public void LoadGlyphs(uint32 firstGlyph, uint32 charCount) + { + ExtendRange(firstGlyph, firstGlyph + charCount); + + UpdateAtlas(); + } + + internal GlyphDescriptor GetGlyph(uint32 glyphId, bool allowDynamicLoading = true) + { + GlyphDescriptor desc; + + if(_glyphsById.TryGetValue(glyphId, out desc)) + { + // if the char is not defined in the current font try to load it from the fallback + if(desc == null && _fallback != null) + { + desc = _fallback.GetGlyph(glyphId, allowDynamicLoading); + } + } + else if(allowDynamicLoading) + { + // TODO: make reloading optional + uint32 start = (glyphId >> 4) << 4; + LoadGlyphs(start, 64); + + desc = GetGlyph(glyphId, false); + } + + return desc ?? _missingGlyph; + } + internal GlyphDescriptor GetGlyph(char32 char, bool allowDynamicLoading = true) { GlyphDescriptor desc; @@ -176,6 +228,8 @@ namespace GlitchyEngine.Renderer.Text //desc.Face = _face; desc.Font = this; desc.GlyphIndex = FreeType.Get_Char_Index(_face, char); + + _glyphsById.TryAdd(desc.GlyphIndex, desc); // if the char is not contained by the font we set the desc to null if(desc.GlyphIndex == 0) @@ -190,6 +244,33 @@ namespace GlitchyEngine.Renderer.Text } } } + + void ExtendRange(uint32 firstGlyph, uint32 rangeEnd) + { + // TODO: refactor + + for(uint32 glyphId = firstGlyph; glyphId < rangeEnd; glyphId++) + { + GlyphDescriptor desc = new GlyphDescriptor(); + if(_glyphsById.TryAdd(glyphId, desc)) + { + //desc.Face = _face; + desc.Font = this; + desc.GlyphIndex = glyphId;//FreeType.Get_Char_Index(_face, char); + + // if the char is not contained by the font we set the desc to null + if(desc.GlyphIndex == 0) + { + delete desc; + _glyphsById[glyphId] = null; + } + } + else + { + delete desc; + } + } + } void UpdateAtlas() { @@ -227,7 +308,7 @@ namespace GlitchyEngine.Renderer.Text int32 atlasHeight = _atlasSize.Y; int32 atlasArray = _atlasSize.Z; - for(var (char, desc) in _glyphs) + for(var (char, desc) in _glyphsById) { if(desc == null || desc.IsCalculated) { @@ -324,7 +405,7 @@ namespace GlitchyEngine.Renderer.Text } } - for(let (char, desc) in _glyphs) + for(let (char, desc) in _glyphsById) { if(desc == null) continue; @@ -356,10 +437,13 @@ namespace GlitchyEngine.Renderer.Text desc.Advance = (float)(advance * _geometryScaler); + //shape.OrientContours(); + msdfgen.ResolveShapeGeometry(shape); + shape.Normalize(); var bounds = shape.GetBounds(); - + // prepare projection double width; @@ -407,6 +491,8 @@ namespace GlitchyEngine.Renderer.Text desc.TranslationX = translationX; desc.TranslationY = translationY; + //desc.Scale = scale; + desc.AdjustToBaseLine = (float)(-translationY * _geometryScaler); desc.AdjustToPen = (float)(-translationX); @@ -414,6 +500,101 @@ namespace GlitchyEngine.Renderer.Text return true; } + /* + void TestMSDF() + { + GlyphDescriptor desc = scope .(); + desc.GlyphIndex = FreeType.Get_Char_Index(_face, 'A'); + + var v = _range; + + _range = 4.0f; + _geometryScaler = 1.0f; + + Calculate(ref desc); + + // prepare shape + + double advance = 0; + + Shape shape; + msdfgen.LoadGlyph(out shape, ref _face, 36, out advance); + + bool b = msdfgen.ResolveShapeGeometry(shape); + + if(!b) + { + + } + + shape.Normalize(); + + var bounds = shape.GetBounds(); + + //shape.ReverseIfNeeded(bounds); + + msdfgen.EdgeColoringSimple(shape, 3.0); + + // prepare projection + msdfgen.Projection projection = .(); + projection.ScaleX = _geometryScaler; + projection.ScaleY = _geometryScaler; + + projection.TranslationX = desc.TranslationX; + projection.TranslationY = desc.TranslationY; + + int bufferX = desc.Width; + int bufferY = desc.Height; + + using(Bitmap bitmap = .((.)bufferX, (.)bufferY)) + { + MSDFGeneratorConfig config = .(); + + msdfgen.GenerateMSDF(*(Bitmap*)&bitmap, shape, projection, _range, config); + + // Check if we have to invert + /* + Line 1039 + // Get sign of signed distance outside bounds + Point2 p(bounds.l-(bounds.r-bounds.l)-1, bounds.b-(bounds.t-bounds.b)-1); + double distance = SimpleTrueShapeDistanceFinder::oneShotDistance(shape, p); + orientation = distance <= 0 ? KEEP : REVERSE; + + if (invert) + { + invertColor<3>(bitmap); + } + */ + + uint8[] pixels = new:ScopedAlloc! uint8[desc.Width * desc.Height * 4]; + + //int8 ToInt8(float f) => (.)Math.Clamp(127f * f, int8.MinValue, int8.MaxValue); + uint8 ToInt8(float f) => (.)Math.Clamp(255 * f, uint8.MinValue, uint8.MaxValue); + + for(int y = 0; y < desc.Height; y++) + for(int x = 0; x < desc.Width; x++) + { + ColorRGB pixel = bitmap.Pixels[(y) * bufferX + x]; + + int index = ((desc.Height - y - 1) * desc.Width + x) * 4; + + pixels[index + 0] = ToInt8(pixel.Red); + pixels[index + 1] = ToInt8(pixel.Green); + pixels[index + 2] = ToInt8(pixel.Blue); + + pixels[index + 3] = uint8.MaxValue; + } + + LodePng.LodePng.Encode32File("testA.png", pixels.CArray(), (.)desc.Width, (.)desc.Height); + + //_atlas.SetData((Color*)pixels.Ptr, (.)desc.MapCoord.X, (.)desc.MapCoord.Y, + // (.)desc.Width, (.)desc.Height, (.)desc.MapCoord.Z); + } + + _range = v; + } + */ + void GenerateMSDF(GlyphDescriptor desc) { // prepare shape @@ -423,11 +604,13 @@ namespace GlitchyEngine.Renderer.Text Shape shape; msdfgen.LoadGlyph(out shape, ref _face, desc.GlyphIndex, out advance); + msdfgen.ResolveShapeGeometry(shape); + shape.Normalize(); var bounds = shape.GetBounds(); - shape.ReverseIfNeeded(bounds); + //shape.ReverseIfNeeded(bounds); msdfgen.EdgeColoringSimple(shape, 3.0); diff --git a/GlitchyEngine/src/Renderer/Text/FontRenderer.bf b/GlitchyEngine/src/Renderer/Text/FontRenderer.bf index 23e8a97..991950b 100644 --- a/GlitchyEngine/src/Renderer/Text/FontRenderer.bf +++ b/GlitchyEngine/src/Renderer/Text/FontRenderer.bf @@ -3,6 +3,7 @@ using FreeType; using System.Diagnostics; using GlitchyEngine.Math; using System.Collections; +using static FreeType.HarfBuzz; using internal GlitchyEngine.Renderer.Text; using internal GlitchyEngine.Renderer; @@ -109,6 +110,87 @@ namespace GlitchyEngine.Renderer.Text //float f = 1.0f; //int32 depthInt = *(int32*)&f; + // Shape!!!!!!!!!!!!!!!!!!! + + // 1. Create a buffer and put your text in it: + hb_buffer_t *buf; + buf = hb_buffer_create(); + hb_buffer_add_utf8(buf, text.CStr(), -1, 0, -1); + + // 2. Set the script, language and direction of the buffer. + hb_buffer_set_direction(buf, .HB_DIRECTION_LTR); + hb_buffer_set_script(buf, .HB_SCRIPT_LATIN); + hb_buffer_set_language(buf, hb_language_from_string("en".CStr(), -1)); + + // 3. Get the face + hb_font_t* hb_font = font.[Friend]font; + + // 4. Shape: + hb_shape(hb_font, buf, null, 0); + + // 5. Get the glyph and position information. + uint32 glyph_count = ?; + hb_glyph_info_t* glyph_info = hb_buffer_get_glyph_infos(buf, &glyph_count); + hb_glyph_position_t* glyph_pos = hb_buffer_get_glyph_positions(buf, &glyph_count); + + for (uint32 i < glyph_count) + { + hb_codepoint_t glyphid = glyph_info[i].codepoint; + hb_position_t x_offset = glyph_pos[i].x_offset; + hb_position_t y_offset = glyph_pos[i].y_offset; + hb_position_t x_advance = glyph_pos[i].x_advance; + hb_position_t y_advance = glyph_pos[i].y_advance; + + // Get glyph from Font + var glyphDesc = font.GetGlyph(glyphid); + + // This never happens + Debug.Assert(glyphDesc != null); + + // The glyph might belong to a fallback font + var glyphFont = glyphDesc.Font; + float glyphFontScale = (float)fontSize / glyphFont._fontSize; + + Texture2D atlas = glyphFont._atlas; + + // Add reference to atlas in case it is recreated during rendering + if(!atlasses.Contains(atlas)) + { + atlasses.Add(atlas..AddRef()); + } + + Vector2 atlasSize = .(atlas.Width, atlas.Height); + + float adjustToPenX = glyphDesc.AdjustToPen; + adjustToPenX *= glyphFontScale; + + float adjustToBaseline = glyphDesc.AdjustToBaseLine; + adjustToBaseline *= glyphFontScale; + + // Rectangle on the screen + Vector4 viewportRect = .( + penPosition + adjustToPenX, + baseline + adjustToBaseline, + glyphDesc.Width * glyphFontScale, + glyphDesc.Height * glyphFontScale); + + // Rectangle on the font atlas + Vector4 texRect = .(glyphDesc.MapCoord.X, glyphDesc.MapCoord.Y, glyphDesc.Width, glyphDesc.Height); + + Color glyphColor = glyphDesc.IsBitmap ? bitmapColor : fontColor; + + texRect /= Vector4(atlasSize, atlasSize); + + Renderer2D.DrawQuad(Vector3(viewportRect.X + viewportRect.Z / 2, viewportRect.Y + viewportRect.W / 2, 1), .(viewportRect.Z, viewportRect.W), 0, .Red); + + Renderer2D.DrawQuad(Vector2(viewportRect.X + viewportRect.Z / 2, viewportRect.Y + viewportRect.W / 2), .(viewportRect.Z, viewportRect.W), 0, atlas, glyphColor, texRect); + + Renderer2D.DrawQuad(Vector3(penPosition, baseline, -1), .(1), 0, .Green); + + penPosition += (x_advance / 64) * glyphFontScale; + baseline += (y_advance / 64) * glyphFontScale; + } + /* // enumerate through the unicode codepoints for(char32 char in text.DecodedChars) { @@ -182,7 +264,7 @@ namespace GlitchyEngine.Renderer.Text // Increase depth for the next glyph //depthInt--; } - + */ Renderer2D.Flush(); // TODO: not good! diff --git a/GlitchyEngine/vendor/freetype b/GlitchyEngine/vendor/freetype index c539bd7..f41e3fd 160000 --- a/GlitchyEngine/vendor/freetype +++ b/GlitchyEngine/vendor/freetype @@ -1 +1 @@ -Subproject commit c539bd7e13310f34a1da66be93ae4cd12b8e5e4d +Subproject commit f41e3fd511f7306dc95045a4a40aac903a45eac6 diff --git a/GlitchyEngine/vendor/msdfgen b/GlitchyEngine/vendor/msdfgen index ac77962..9f19e63 160000 --- a/GlitchyEngine/vendor/msdfgen +++ b/GlitchyEngine/vendor/msdfgen @@ -1 +1 @@ -Subproject commit ac7796290521b71c0e621b592067b67fa5641901 +Subproject commit 9f19e63a51b6108bf6c22fad058d1b059ce72c81 diff --git a/Sandbox/content/Shaders/msdfShader.hlsl b/Sandbox/content/Shaders/msdfShader.hlsl index 25b0c8e..015213e 100644 --- a/Sandbox/content/Shaders/msdfShader.hlsl +++ b/Sandbox/content/Shaders/msdfShader.hlsl @@ -35,11 +35,34 @@ PS_Input VS(VS_Input input) return output; } +/* +in vec2 texCoord; +out vec4 color; +uniform sampler2D msdf; +uniform vec4 bgColor; +uniform vec4 fgColor; + float median(float r, float g, float b) { return max(min(r, g), min(max(r, g), b)); } -float ScreenPxRange(float2 texcoord) { +void main() { + vec3 msd = texture(msdf, texCoord).rgb; + float sd = median(msd.r, msd.g, msd.b); + float screenPxDistance = screenPxRange()*(sd - 0.5); + float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); + color = mix(bgColor, fgColor, opacity); +} +*/ + + +float median(float r, float g, float b) +{ + return max(min(r, g), min(max(r, g), b)); +} + +float ScreenPxRange(float2 texcoord) +{ float2 screenTexSize = 1.0f / fwidth(texcoord); return max(0.5f * dot(UnitRange, screenTexSize), 1.0f); } @@ -54,4 +77,17 @@ float4 PS(PS_Input input) : SV_Target0 return float4(input.Color.rgb, opacity * input.Color.a); } +/* +// 2D +float4 PS(PS_Input input) : SV_Target0 +{ + float3 msd = Texture.Sample(Sampler, input.TexCoord).rgb; + float sd = median(msd.r, msd.g, msd.b); + float screenPxDistance = screenPixelRange*(sd - 0.5); + float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); + + return float4(input.Color.rgb, opacity * input.Color.a); +} +*/ + #effect[VS=VS, PS=PS] \ No newline at end of file diff --git a/Sandbox/src/ExampleLayer2D.bf b/Sandbox/src/ExampleLayer2D.bf index fb16445..f00af87 100644 --- a/Sandbox/src/ExampleLayer2D.bf +++ b/Sandbox/src/ExampleLayer2D.bf @@ -69,9 +69,11 @@ namespace Sandbox var japanese = new Font("C:\\Windows\\Fonts\\YuGothM.ttc", 64, true, '\0', 1); var emojis = new Font("C:\\Windows\\Fonts\\seguiemj.ttf", 64, true, '😂' - 10, 1); var mathstuff = new Font("C:\\Windows\\Fonts\\cambria.ttc", 64, true, 'α', 1); + var cascadiaCode = new Font("C:\\Windows\\Fonts\\CascadiaCode.ttf", 32, true, 'A', 1); fonty.Fallback = japanese..ReleaseRefNoDelete(); japanese.Fallback = emojis..ReleaseRefNoDelete(); emojis.Fallback = mathstuff..ReleaseRefNoDelete(); + mathstuff.Fallback = cascadiaCode..ReleaseRefNoDelete(); _textureViewer = new TextureViewer(); @@ -112,7 +114,7 @@ namespace Sandbox Renderer2D.DrawCircle(Vector3(-2, -2, -2f), Vector2(1), .GreenYellow); - FontRenderer.DrawText(fonty, "Hallo! gjy", 0, 0, 64, .White, .White); + FontRenderer.DrawText(fonty.Fallback.Fallback.Fallback.Fallback, "Hallö! gjy ÄAwww www", 0, 0, 64, .White, .White); // Hallo! gjy Renderer2D.EndScene(); } @@ -143,6 +145,7 @@ namespace Sandbox ImGui.End(); + //_textureViewer.ViewTexture(fonty.Fallback.Fallback.Fallback.Fallback.[Friend]_atlas); _textureViewer.ViewTexture(fonty.[Friend]_atlas); _context.SetRenderTarget(null);