using System; using FreeType; using System.Diagnostics; using GlitchyEngine.Math; using System.Collections; using static FreeType.HarfBuzz; using internal GlitchyEngine.Renderer.Text; using internal GlitchyEngine.Renderer; namespace GlitchyEngine.Renderer.Text { public static class FontRenderer { internal static FT_Library s_Library; public static Effect _msdfEffect; internal static bool s_isInitialized; internal static void Init() { Debug.Profiler.ProfileFunction!(); if(s_isInitialized) return; InitFreetype(); _msdfEffect = new Effect("content\\Shaders\\msdfShader.hlsl"); s_isInitialized = true; } internal static void Deinit() { Debug.Profiler.ProfileFunction!(); _msdfEffect.ReleaseRef(); DeinitFreetype(); s_isInitialized = false; } private static void InitFreetype() { Debug.Profiler.ProfileFunction!(); if(s_Library == null) { var res = FreeType.Init_FreeType(&s_Library); Log.EngineLogger.Assert(res.Success, scope $"Failed to initialize freetype ({(int)res}): {res}"); } } private static void DeinitFreetype() { Debug.Profiler.ProfileFunction!(); FreeType.Done_FreeType(s_Library); } public class PreparedText : RefCounted { //public List Lines ~ ClearAndDeleteItems!(_); public Font Font ~ _?.ReleaseRef(); public List Glyphs ~ delete:append _; public float AdvanceX; public float AdvanceY; [AllowAppend] public this(Font font) { //List lines = append .(); //Lines = lines; List glyphs = append .(); Glyphs = glyphs; if(font != null) Font = font..AddRef(); } public static readonly Self Empty = new Self(null) ~ _.ReleaseRef(); } /* public class PreparedLine { public int Line; public List Glyphs; [AllowAppend] public this() { List glyphs = append .(); Glyphs = glyphs; } } */ public struct PreparedGlyph { public Font Font; public uint32 GlyphId; public Vector2 Position; public float Scale; //public float AdvanceX; //public float AdvanceY; public this(Font font, uint32 glyphId, Vector2 position, float scale)//, float advanceX, float advanceY) { Font = font; GlyphId = glyphId; Position = position; Scale = scale; //AdvanceX = advanceX; //AdvanceY = advanceY; } } public enum TextDirection { case LeftToRight; case RightToLeft; case TopToBottom; case BottomToTop; } public static PreparedText PrepareText(Font font, String text, float fontSize, Color fontColor = .White, Color bitmapColor = .White, float lineSpaceScale = 1.0f, TextDirection direction = .LeftToRight) { Debug.Profiler.ProfileRendererFunction!(); PreparedText preparedText = new PreparedText(font); float scale = fontSize / (float)font._fontSize; // Space between two baselines float linespace = (float)font._linespace * lineSpaceScale * scale; // The line we are writing on float baseline = 0; // Position of the next character on the line float penPosition = 0; // TODO: use stringview? StringView textBuffer = .(text, 0, 0);//new:ScopedAlloc! String(); hb_buffer_t* buf = hb_buffer_create(); //PreparedLine currentLine = new PreparedLine(); // Stack to keep track of the text direction. List textDirectionStack = scope .(); textDirectionStack.Add(direction); int movedLines = 0; Font currentFont = font; int currentIndex = 0; void FlushShapeBuffer() { Debug.Profiler.ProfileRendererFunction!(); float fontScale = (float)fontSize / currentFont._fontSize; hb_buffer_clear_contents(buf); hb_buffer_add_utf8(buf, text.Ptr, (.)text.Length, (.)(textBuffer.Ptr - text.Ptr), (.)textBuffer.Length); textBuffer.Ptr += textBuffer.Length; textBuffer.Length = 0; // Set the script, language and direction of the buffer. // TODO: change direction, script, etc. TextDirection direction = textDirectionStack.Back; switch (direction) { case .LeftToRight: hb_buffer_set_direction(buf, .HB_DIRECTION_LTR); case .RightToLeft: hb_buffer_set_direction(buf, .HB_DIRECTION_RTL); case .TopToBottom: hb_buffer_set_direction(buf, .HB_DIRECTION_TTB); case .BottomToTop: hb_buffer_set_direction(buf, .HB_DIRECTION_BTT); } hb_buffer_set_script(buf, .HB_SCRIPT_LATIN); hb_buffer_set_language(buf, hb_language_from_string("en".CStr(), -1)); hb_shape(font._harfBuzzFont, 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; PreparedGlyph glyph = .(font, glyphid, .(penPosition, baseline), fontScale);//, x_advance / 64, y_advance / 64); preparedText.Glyphs.Add(glyph); penPosition += (x_advance / 64) * fontScale; baseline += (y_advance / 64) * fontScale; } preparedText.AdvanceX = Math.Max(preparedText.AdvanceX, penPosition); } void ResetPenPos() { preparedText.AdvanceX = Math.Max(preparedText.AdvanceX, penPosition); penPosition = 0; } for (char32 char in text.DecodedChars) { defer { currentIndex = @char.NextIndex; } switch(char) { case '\n': // Only flush after the first new line if (movedLines == 0) FlushShapeBuffer(); // carriage return movedLines++; continue; case '\u{240}': FlushShapeBuffer(); // carriage return ResetPenPos(); continue; case '\u{2066}': // LEFT-TO-RIGHT ISOLATE FlushShapeBuffer(); textDirectionStack.Add(.RightToLeft); continue; case '\u{2067}': // RIGHT-TO-LEFT ISOLATE FlushShapeBuffer(); textDirectionStack.Add(.RightToLeft); continue; // TODO: FIRST STRONG ISOLATE (U+2068) case '\u{2069}': // POP DIRECTIONAL ISOLATE FlushShapeBuffer(); if (textDirectionStack.Count > 1) textDirectionStack.PopBack(); continue; } // Get the font that can draw the char. Use the given font if no fallback is found (will draw the "missing glyph"). Font charFont = Font.GetDrawingFont(char, font) ?? font; if (charFont != font) { FlushShapeBuffer(); currentFont = charFont; } // move baseline if necessary if(movedLines != 0) { // move baseline baseline -= linespace * movedLines; // TODO: make carriage return optional? // return pen to start of line ResetPenPos(); movedLines = 0; } //textBuffer.Append(char); if(textBuffer.Length == 0) { textBuffer.Ptr = text.Ptr + currentIndex; } textBuffer.Length += @char.NextIndex - currentIndex; } FlushShapeBuffer(); preparedText.AdvanceY = baseline; return preparedText; } public static void DrawText(PreparedText text, float x, float y, Color fontColor = .White) { DrawText(text, Matrix.Translation(x, y, 0), fontColor); } public static void DrawText(PreparedText text, Matrix transform, Color fontColor = .White) { Debug.Profiler.ProfileRendererFunction!(); text.AddRef(); defer text.ReleaseRef(); if (text.Glyphs.Count == 0) return; Renderer2D.Flush(); // TODO: this is very not good! var lastEffect = Renderer2D.[Friend]s_currentEffect; Renderer2D.[Friend]s_currentEffect = _msdfEffect..AddRef(); // TODO: oh no.... // Copy viewProjection from current effect Matrix viewProjection = lastEffect.Variables["ViewProjection"].[Friend]GetData(); _msdfEffect.Variables["ViewProjection"].SetData(viewProjection); // TODO: this doesn't really work with fallback fonts Vector2 unitRange = Vector2((float)text.Font._range) / Vector2(text.Font._atlas.Width, text.Font._atlas.Height); _msdfEffect.Variables["UnitRange"].SetData(unitRange); List atlasses = scope .(); for (PreparedGlyph glyph in text.Glyphs) { // Get glyph from Font var glyphDesc = glyph.Font.GetGlyph(glyph.GlyphId); // This never happens Debug.Assert(glyphDesc != null); // The glyph might belong to a fallback font var glyphFont = glyphDesc.Font; //float glyphFontScale = (float)glyph.Scale / 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; adjustToPenX *= glyph.Scale; float adjustToBaseline = glyphDesc.AdjustToBaseLine; //adjustToBaseline *= glyphFontScale; adjustToBaseline *= glyph.Scale; // Rectangle on the screen Vector4 viewportRect = .( // TODO: merge adjustToPenX and adjustToBaseline into Position glyph.Position.X + adjustToPenX, glyph.Position.Y + adjustToBaseline, glyphDesc.Width * glyph.Scale, glyphDesc.Height * glyph.Scale); // Rectangle on the font atlas Vector4 texRect = .(glyphDesc.MapCoord.X, glyphDesc.MapCoord.Y, glyphDesc.Width, glyphDesc.Height); Color glyphColor = fontColor;//Color.White;// TODO: glyphDesc.IsBitmap ? bitmapColor : fontColor; texRect /= Vector4(atlasSize, atlasSize); // Show quads // Renderer2D.DrawQuad(Vector3(viewportRect.X + viewportRect.Z / 2, viewportRect.Y + viewportRect.W / 2, 1), .(viewportRect.Z, viewportRect.W), 0, .Red); Vector3 position = .(viewportRect.X + viewportRect.Z / 2, viewportRect.Y + viewportRect.W / 2, 0); Matrix glyphTransform = transform * Matrix.Translation(position) * Matrix.Scaling(viewportRect.Z, viewportRect.W, 1.0f); Renderer2D.DrawQuad(glyphTransform, atlas, glyphColor, texRect); //Renderer2D.DrawQuad(Vector2(viewportRect.X + viewportRect.Z / 2, viewportRect.Y + viewportRect.W / 2), .(viewportRect.Z, viewportRect.W), 0, atlas, glyphColor, texRect); // Show pen positions // Renderer2D.DrawQuad(Vector3(Vector2(x, y) + glyph.Position, -1), .(1), 0, .Green); } Renderer2D.Flush(); // TODO: not good! // Change back effect _msdfEffect.ReleaseRef(); Renderer2D.[Friend]s_currentEffect = lastEffect; // release all atlas textures for(int i < atlasses.Count) { atlasses[i].ReleaseRef(); } } /** @brief Draws a given text using a specified font stack and renderer. * @param renderer The 2D renderer that will be used to draw the text. * @param font the Fontstack that will be used to draw the text. * @param x The horizontal position of the text (i.e. distance between left side of viewport and the left side of the left-most character, well not really but close enough). * @param y The vertical position of the text (i.e. distance between top side of viewport and the top side of the top-most character, well not really but close enough). * @param fontSize The font size in pixels. Note: due to technical reasons the actual size might deviate from the specified size. * @param fontColor The (default) color used for glyphs that don't have color (e.g. letters or emojies if the font doesn't use bitmaps for them). * @param bitmapColor The (default) color used for glyphs that are bitmaps (e.g. emojies, if the font provies them as bitmap). * @param lineGapOffset Can be used to manually increase or decrease the gap between lines. */ public static void DrawText(Font font, String text, float x, float y, float fontSize, Color fontColor = .White, Color bitmapColor = .White, float lineGapOffset = 0) { Debug.Profiler.ProfileRendererFunction!(); if(text.IsWhiteSpace) return; Renderer2D.Flush(); // TODO: this is very not good! var lastEffect = Renderer2D.[Friend]s_currentEffect; Renderer2D.[Friend]s_currentEffect = _msdfEffect..AddRef(); // TODO: oh no.... // Copy viewProjection from current effect Matrix viewProjection = lastEffect.Variables["ViewProjection"].[Friend]GetData(); _msdfEffect.Variables["ViewProjection"].SetData(viewProjection); float scale = (float)fontSize / (float)font._fontSize; _msdfEffect.Variables["screenPixelRange"].SetData(scale * 4.0f); // TODO: this doesn't really work with fallback fonts Vector2 unitRange = Vector2((float)font._range) / Vector2(font._atlas.Width, font._atlas.Height); _msdfEffect.Variables["UnitRange"].SetData(unitRange); // Space between two baselines float linespace = ((float)font._linespace + lineGapOffset) * scale; // The line we are writing on float baseline = y;// + linespace; //renderer.Draw(null, x, baseline, 10000, 1, .Blue); // Position of the next character on the line float penPosition = x; // how many lines we moved up or down (e.g. after a \n) int movedLines = 0; List atlasses = scope .(); // We render every char with a slightly greater depth, so that the chars don't cull each other. // In order to do that we increment an integer for each glyph and use it as the depth for the next one. // Whenn passing the depth to the renderer we treat the integer as the binary representation of a float. // Therefore every increment will increment the mantissa of the float and is therefore equivalent to the // smallest possible increase a float can represent. // Note: This might do funky stuff when objects are very close to each other. But realistically whe have to do // about 8 million (2^23) increments to reach a depth of 1 so I think it's safe enough. //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._harfBuzzFont; // 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; /* if(glyphid == (.)'\n') { movedLines++; continue; } // move baseline if necessary if(movedLines != 0) { // move baseline baseline -= linespace * movedLines; // TODO: make carriage return optional? // return pen to start of line penPosition = x; movedLines = 0; } */ // 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); // Show quads // 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); // Show pen positions // 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) { if(char == '\n') { movedLines++; continue; } // move baseline if necessary if(movedLines != 0) { // move baseline baseline -= linespace * movedLines; // TODO: make carriage return optional? // return pen to start of line penPosition = x; movedLines = 0; } //renderer.Draw(null, penPosition, baseline - fontSize, 1, fontSize, .Lime); // Get glyph from Font var glyphDesc = font.GetGlyph(char); // 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(Vector2(viewportRect.X + viewportRect.Z / 2, viewportRect.Y + viewportRect.W / 2), .(viewportRect.Z, viewportRect.W), 0, atlas, glyphColor, texRect); //renderer.Draw(atlas, viewportRect.X, viewportRect.Y, viewportRect.Z, viewportRect.W, glyphColor, *(float*)(&depthInt), texRect); penPosition += glyphDesc.Advance * glyphFontScale; // Increase depth for the next glyph //depthInt--; } Renderer2D.Flush(); // TODO: not good! // Change back effect _msdfEffect.ReleaseRef(); Renderer2D.[Friend]s_currentEffect = lastEffect; // release all atlas textures for(int i < atlasses.Count) { atlasses[i].ReleaseRef(); } } } }