diff --git a/.gitmodules b/.gitmodules index f326578..a9bee96 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "vendor/lodepng-beef"] path = vendor/lodepng-beef url = https://github.com/aharabada/lodepng-beef.git +[submodule "GlitchyEngine\\vendor\\freetype"] + path = GlitchyEngine\\vendor\\freetype + url = https://github.com/aharabada/FreeType-beef.git diff --git a/BeefSpace.toml b/BeefSpace.toml index c5c761e..da02317 100644 --- a/BeefSpace.toml +++ b/BeefSpace.toml @@ -1,5 +1,5 @@ FileVersion = 1 -Projects = {Sandbox = {Path = "Sandbox"}, GlitchyEngine = {Path = "GlitchyEngine"}, GlitchLog = {Path = "GlitchLog"}, DirectX = {Path = "GlitchyEngine/vendor/directx/DirectX"}, ImGui = {Path = "GlitchyEngine/vendor/imgui-beef/ImGui"}, ImGuiImplWin32 = {Path = "GlitchyEngine/vendor/imgui-beef/ImGuiImplWin32"}, ImGuiImplDX11 = {Path = "GlitchyEngine/vendor/imgui-beef/ImGuiImplDX11"}, DirectXTK = {Path = "GlitchyEngine/vendor/DirectXTK/DirectXTK-beef"}, LodePng = {Path = "vendor/lodepng-beef/lodepng-beef"}} +Projects = {Sandbox = {Path = "Sandbox"}, GlitchyEngine = {Path = "GlitchyEngine"}, GlitchLog = {Path = "GlitchLog"}, DirectX = {Path = "GlitchyEngine/vendor/directx/DirectX"}, ImGui = {Path = "GlitchyEngine/vendor/imgui-beef/ImGui"}, ImGuiImplWin32 = {Path = "GlitchyEngine/vendor/imgui-beef/ImGuiImplWin32"}, ImGuiImplDX11 = {Path = "GlitchyEngine/vendor/imgui-beef/ImGuiImplDX11"}, DirectXTK = {Path = "GlitchyEngine/vendor/DirectXTK/DirectXTK-beef"}, LodePng = {Path = "vendor/lodepng-beef/lodepng-beef"}, FreeType = {Path = "GlitchyEngine/vendor/freetype"}} [Workspace] StartupProject = "Sandbox" diff --git a/GlitchyEngine/BeefProj.toml b/GlitchyEngine/BeefProj.toml index debb06d..c79e15f 100644 --- a/GlitchyEngine/BeefProj.toml +++ b/GlitchyEngine/BeefProj.toml @@ -1,5 +1,5 @@ FileVersion = 1 -Dependencies = {GlitchLog = "*", corlib = "*", DirectX = "*", ImGui = "*", ImGuiImplWin32 = "*", ImGuiImplDX11 = "*", DirectXTK = "*"} +Dependencies = {GlitchLog = "*", corlib = "*", DirectX = "*", ImGui = "*", ImGuiImplWin32 = "*", ImGuiImplDX11 = "*", DirectXTK = "*", FreeType = "*"} [Project] Name = "GlitchyEngine" diff --git a/GlitchyEngine/src/Renderer/Text/Font.bf b/GlitchyEngine/src/Renderer/Text/Font.bf new file mode 100644 index 0000000..471d779 --- /dev/null +++ b/GlitchyEngine/src/Renderer/Text/Font.bf @@ -0,0 +1,334 @@ +using System; +using FreeType; +using GlitchyEngine.Math; +using System.Collections; + +using internal GlitchyEngine.Renderer.Text; + +namespace GlitchyEngine.Renderer.Text +{ + public class Font + { + public class GlyphDescriptor + { + //public FT_Face Face; + public Font Font; + + public FT_UInt GlyphIndex; + + public Int32_3 MapCoord; + public int32 SizeX, SizeY; + + public FT_Glyph_Metrics Metrics; + + public bool IsBitmap; + + public bool IsCalculated = false; + public bool IsRendered = false; + } + + private GraphicsContext _context ~ _.ReleaseRef(); + internal FT_Face _face ~ FreeType.Done_Face(_face); + + private Font _fallback; + + private uint32 _fontSize; + private int32 _faceIndex; + private bool _hasColor; + + private Int32_3 _penPos; + private int32 _lastRowHeight; + private Texture2D _atlas ~ _?.ReleaseRef(); + private Int32_3 _atlasSize; + + private Dictionary _glyphs = new .() ~ DeleteDictionaryAndValues!(_); + GlyphDescriptor _missingGlyph; + + private SamplerState _sampler ~ _.ReleaseRef(); + + /** + * Gets or sets the Sampler that will be used to sample the spritefont. + * Setting to null will result in the use of LinearClamp + */ + public SamplerState Sampler + { + get => _sampler; + set + { + // when value and _sampler are null we don't shortcut because we set _sampler to LinearClamp + if(_sampler == value && value != null) + return; + + _sampler?.ReleaseRef(); + + _sampler = (value ?? SamplerStateManager.LinearClamp)..AddRef(); + + _atlas?.SamplerState = _sampler; + } + } + + public this(GraphicsContext context, String fontPath, uint32 fontSize, bool hasColor = true, char32 firstChar = '\0', uint32 charCount = 128, int32 faceIndex = 0) + { + _context = context..AddRef(); + + // Set default sampler + Sampler = null; + + FontRenderer.InitLibrary(); + + _glyphs.Add('\0', new GlyphDescriptor(){Font = this}); + + _fontSize = fontSize; + _faceIndex = faceIndex; + _hasColor = hasColor; + + var res = FreeType.New_Face(FontRenderer.Library, fontPath, faceIndex, &_face); + Log.EngineLogger.Assert(res.Success, scope $"New_Face failed({(int)res}): {res}"); + + res = FreeType.Set_Pixel_Sizes(_face, 0, _fontSize); + Log.EngineLogger.Assert(res.Success, scope $"Set_Pixel_Sizes failed({(int)res}): {res}"); + + LoadGlyphs(firstChar, charCount); + } + + public void LoadGlyphs(char32 firstChar, uint32 charCount) + { + ExtendRange(firstChar, firstChar + charCount); + + UpdateAtlas(); + } + + internal GlyphDescriptor GetGlyph(char32 char, bool allowDynamicLoading = true) + { + GlyphDescriptor desc; + + if(_glyphs.TryGetValue(char, 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(char, allowDynamicLoading); + } + } + else if(allowDynamicLoading) + { + // TODO: make reloading optional + char32 start = (char >> 4) << 4; + LoadGlyphs(start, 64); + + desc = GetGlyph(char, false); + } + + return desc ?? _missingGlyph; + } + + void ExtendRange(char32 firstChar, char32 rangeEnd) + { + // TODO: refactor + + for(char32 char = firstChar; char < rangeEnd; char++) + { + GlyphDescriptor desc = new GlyphDescriptor(); + if(_glyphs.TryAdd(char, desc)) + { + //desc.Face = _face; + desc.Font = this; + desc.GlyphIndex = 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; + _glyphs[char] = null; + } + } + else + { + delete desc; + } + } + } + + void UpdateAtlas() + { + DrawAtlas(); + } + + Int32_3 PrepareAtlas() + { + const uint32 maxRes = 16384; // D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION + const uint32 maxArray = 2048; // D3D11_REQ_TEXTURE2D_ARRAY_AXIS_DIMENSION + + ref Int32_3 pen = ref _penPos; + ref int32 penLeft = ref _penPos.X; + ref int32 penTop = ref _penPos.Y; + ref int32 penArray = ref _penPos.Z; + ref int32 rowHeight = ref _lastRowHeight; + + int32 atlasWidth = _atlasSize.X; + int32 atlasHeight = _atlasSize.Y; + int32 atlasArray = _atlasSize.Z; + + for(let (char, desc) in _glyphs) + { + if(desc == null || desc.IsCalculated) + { + continue; + } + + let loadResult = FreeType.Load_Glyph(_face, desc.GlyphIndex, .Color | .TargetNormal); + if(loadResult.Error) + { + Log.EngineLogger.Error($"Failed to load glyph for '{char}'(Char Code:{(uint32)char} | Error {(int)loadResult}): {loadResult}"); + } + + desc.Metrics = _face.glyph.metrics; + + int32 glyphWidth = (desc.Metrics.width / 64) + 1; + int32 glyphHeight = (desc.Metrics.height / 64) + 1; + + // when size is 0 (1 because we added 1) the glyph has no image + if(glyphWidth != 1 || glyphHeight != 1) + { + int32 glyphRight = penLeft + glyphWidth; + + if(glyphRight > maxRes) + { + penLeft = 0; + penTop += rowHeight; + rowHeight = 0; + } + + int32 glyphBottom = penTop + glyphHeight; + + if(glyphBottom > maxRes) + { + penLeft = 0; + penTop = 0; + rowHeight = 0; + + penArray++; + } + + if(penArray >= maxArray) + { + Log.EngineLogger.Error("Spritefont exceeds maximum texture size."); + } + } + + desc.MapCoord = pen; + desc.SizeX = glyphWidth - 1; + desc.SizeY = glyphHeight - 1; + + if(desc.GlyphIndex == 0 && _missingGlyph == null) + { + _missingGlyph = desc; + } + + penLeft += glyphWidth; + + if (penLeft > atlasWidth) + atlasWidth = penLeft; + + if (glyphHeight > rowHeight) + rowHeight = glyphHeight; + + if (penTop + glyphHeight > atlasHeight) + atlasHeight = penTop + glyphHeight; + + if (penArray == atlasArray) + atlasArray++; + + desc.IsCalculated = true; + } + + return .(atlasWidth, atlasHeight, atlasArray); + } + + void DrawAtlas() + { + Int32_3 oldAtlasSize = _atlasSize; + _atlasSize = PrepareAtlas(); + + if(_atlasSize != oldAtlasSize) + { + var oldAtlas = _atlas; + + Texture2DDesc desc; + desc.Format = .B8G8R8A8_UNorm; + desc.MipLevels = 1; + desc.Width = (.)_atlasSize.X; + desc.Height = (.)_atlasSize.Y; + desc.ArraySize = (.)_atlasSize.Z; + desc.CpuAccess = .None; + desc.Usage = .Default; + + _atlas = new Texture2D(_context, desc); + _atlas.SamplerState = _sampler; + + if(oldAtlas != null) + { + oldAtlas.CopyTo(_atlas); + oldAtlas.ReleaseRef(); + } + } + + for(let (char, desc) in _glyphs) + { + if(desc == null) + continue; + + var glyphIndex = desc.GlyphIndex; + + if(desc.IsRendered || glyphIndex == 0 || desc.SizeX == 0 || desc.SizeY == 0) + { + continue; + } + + var loadResult = FreeType.Load_Glyph(_face, glyphIndex, .Color | .Render | .TargetNormal); + if(loadResult.Error) + { + Log.EngineLogger.Error($"Failed to render glyph for '{char}'(Char Code:{(uint32)char} | Error {(int)loadResult}): {loadResult}"); + } + + FT_GlyphSlot glyphSlot = _face.glyph; + FT_Bitmap bitmap = glyphSlot.bitmap; + + //ResourceBox glyphBox = .((.)desc.MapCoord.X, (.)desc.MapCoord.X, 0, + // (.)desc.MapCoord.X + (.)desc.SizeX, (.)desc.MapCoord.Y + (.)desc.SizeY, 1); + + if(_face.glyph.format == .Bitmap) + { + if(bitmap.pixel_mode == .Gray) + { + Color[] pixels = new Color[bitmap.width * bitmap.rows]; + defer delete pixels; + + for(int i < pixels.Count) + { + uint8 gray = bitmap.buffer[i]; + + pixels[i] = .(gray, gray, gray, gray); + } + + _atlas.SetData(pixels.Ptr, (.)desc.MapCoord.X, (.)desc.MapCoord.Y, + (.)desc.SizeX, (.)desc.SizeY, (.)desc.MapCoord.Z); + } + else if(bitmap.pixel_mode == .Bgra) + { + _atlas.SetData((.)bitmap.buffer, (.)desc.MapCoord.X, (.)desc.MapCoord.Y, + (.)desc.SizeX, (.)desc.SizeY, (.)desc.MapCoord.Z); + + desc.IsBitmap = true; + } + } + else + { + Runtime.NotImplemented(); + } + + desc.IsRendered = true; + } + } + } +} diff --git a/GlitchyEngine/src/Renderer/Text/FontRenderer.bf b/GlitchyEngine/src/Renderer/Text/FontRenderer.bf new file mode 100644 index 0000000..48717b8 --- /dev/null +++ b/GlitchyEngine/src/Renderer/Text/FontRenderer.bf @@ -0,0 +1,124 @@ +using System; +using FreeType; +using System.Diagnostics; +using GlitchyEngine.Math; +using System.Collections; + +using internal GlitchyEngine.Renderer.Text; + +namespace GlitchyEngine.Renderer.Text +{ + public class FontRenderer + { + internal static FT_Library Library ~ FreeType.Done_FreeType(_); + + internal static void InitLibrary() + { + if(Library == null) + { + var res = FreeType.Init_FreeType(&Library); + Log.EngineLogger.Assert(res.Success, scope $"Init_FreeType failed({(int)res}): {res}"); + } + } + + private GraphicsContext _context ~ _.ReleaseRef(); + + public this(GraphicsContext context) + { + FontRenderer.InitLibrary(); + + _context = context..AddRef(); + } + + public void DrawText(Renderer2D renderer, Font font, String text, float x, float y, Color fontColor = .White, Color bitmapColor = .White) + { + if(text.IsWhiteSpace) + return; + + // Todo: + float scale = 1.0f; + + // Space between two baselines + float linespace = ((font._face.size.metrics.ascender - font._face.size.metrics.descender) >> 6);// + lineGap; + + // The line we are writing on + float baseline = y + linespace; + // Where we are writing the next glyph on the line + float penPosition = x; + + int32 lastLine = 0; + int32 line = 0; + + List atlasses = scope .(); + + //GlyphDescriptor missingGlyph = font.CharMap.GetValue('\0'); + String.UTF8Enumerator textEnumerator = String.UTF8Enumerator(text, 0, text.Length); + for(char32 char in textEnumerator) + { + if(char == '\n') + { + line++; + continue; + } + + // number of lines that the cursor moved up or down + int32 lineDiff = line - lastLine; + if(lineDiff != 0) + { + baseline += linespace * lineDiff; + + // TODO: make carriage return optional? + penPosition = x; + + lastLine = line; + } + + // Get glyph from Font + var glyphDesc = font.GetGlyph(char); + + // This never happens + if(glyphDesc == null) + { + Debug.Break(); + continue; + } + + // Rectangle on the screen + Vector4 viewportRect = .(penPosition + (glyphDesc.Metrics.horiBearingX / 64) * scale, baseline - (glyphDesc.Metrics.horiBearingY / 64) * scale, glyphDesc.SizeX * scale, glyphDesc.SizeY * scale); + // Rectangle on the font atlas + Vector4 texRect = .(glyphDesc.MapCoord.X, glyphDesc.MapCoord.Y, glyphDesc.SizeX, glyphDesc.SizeY); + + Color glyphColor = glyphDesc.IsBitmap ? bitmapColor : fontColor; + + /* + if(QueueQuad!(viewportRect, texRect, glyphDesc.MapZ, glyphColor)) + { + DrawQueue(context); + } + */ + + Texture2D atlas = glyphDesc.Font.[Friend]_atlas; + + if(!atlasses.Contains(atlas)) + { + atlasses.Add(atlas..AddRef()); + } + + Vector2 atlasSize = .(atlas.Width, atlas.Height); + + texRect /= Vector4(atlasSize, atlasSize); + + renderer.Draw(atlas, viewportRect.X, viewportRect.Y, viewportRect.Z, viewportRect.W, glyphColor, 0.0f, texRect); + + penPosition += (glyphDesc.Metrics.horiAdvance / 64) * scale; + } + + renderer.End(); + + for(int i < atlasses.Count) + { + atlasses[i].ReleaseRef(); + } + } + } +} diff --git a/GlitchyEngine/vendor/directx b/GlitchyEngine/vendor/directx index 7fc4733..0399d40 160000 --- a/GlitchyEngine/vendor/directx +++ b/GlitchyEngine/vendor/directx @@ -1 +1 @@ -Subproject commit 7fc473343221fbd90624542ca31974203e0b9ea6 +Subproject commit 0399d40bf807ea6c0dddf2d5274cfbf37cc7b37e diff --git a/Sandbox/content/Fonts/GutenbergTextura.ttf b/Sandbox/content/Fonts/GutenbergTextura.ttf new file mode 100644 index 0000000..88c4e1e Binary files /dev/null and b/Sandbox/content/Fonts/GutenbergTextura.ttf differ diff --git a/Sandbox/src/SandboxApp2D.bf b/Sandbox/src/SandboxApp2D.bf index c7363e8..3aed0cd 100644 --- a/Sandbox/src/SandboxApp2D.bf +++ b/Sandbox/src/SandboxApp2D.bf @@ -8,6 +8,7 @@ using ImGui; using GlitchyEngine.Renderer; using GlitchyEngine.Math; using GlitchyEngine.World; +using GlitchyEngine.Renderer.Text; namespace Sandbox { @@ -57,6 +58,8 @@ namespace Sandbox Renderer2D Renderer2D ~ delete _; + Texture2D _testTexture ~ _?.ReleaseRef(); + [AllowAppend] public this() : base("Example") { @@ -97,8 +100,36 @@ namespace Sandbox Renderer2D = new Renderer2D(_context, _effectLibrary); //Init2D(); + + Texture2DDesc desc = .(); + desc.Format = .R8G8B8A8_UNorm; + desc.Width = 2; + desc.Height = 2; + desc.ArraySize = 1; + desc.CpuAccess = .None; + desc.MipLevels = 1; + desc.Usage = .Default; + + _testTexture = new Texture2D(_context, desc); + + Color[4] colors = .( + .Red, .Green, + .Blue, .White); + + _testTexture.SetData(&colors, 0, 1, 1, 1); + //_testTexture.SetData(&colors, 0, 0, 2, 2); + + fonty = new Font(_context, "C:\\Windows\\Fonts\\arial.ttf", 64, true, 'A', 16);//C:\\Windows\\Fonts\\seguiemj.ttf + emojies = new Font(_context, "C:\\Windows\\Fonts\\seguiemj.ttf", 64, true, '😂' - 10, 1); + fonty.[Friend]_fallback = emojies; + + fontRenderer = new FontRenderer(_context); } + Font fonty ~ delete _; + Font emojies ~ delete _; + FontRenderer fontRenderer ~ delete _; + VertexLayout layout ~ delete _; GeometryBinding quadBinding ~ _?.ReleaseRef(); @@ -225,7 +256,8 @@ namespace Sandbox _context.SetViewport(_context.SwapChain.BackbufferViewport); - Renderer2D.Begin(.SortByTexture, .(80, 80)); + /* + Renderer2D.Begin(.FrontToBack, .(80, 80)); //_opaqueBlendState.Bind(); _alphaBlendState.Bind(); @@ -237,7 +269,7 @@ namespace Sandbox //_texture.Bind(0); //_ge_logo.Bind(1); - + for(int x < 40) for(int y < 40) { @@ -253,7 +285,7 @@ namespace Sandbox } } - Renderer2D.Draw(_texture, 0, 0, 20, 10, .White, 20); + Renderer2D.Draw(_ge_logo, 0, 0, 180, 40, .White, 20); /* Renderer2D.Draw(_texture, 10, 50, 100, 100, .White); @@ -268,6 +300,13 @@ namespace Sandbox //Renderer2D.Draw(_ge_logo, 80, 50, 100, 100, .White); + Renderer2D.End(); + */ + _alphaBlendState.Bind(); + Renderer2D.Begin(.FrontToBack, .(_context.SwapChain.Width, _context.SwapChain.Height)); + + fontRenderer.DrawText(Renderer2D, fonty, "Hallo Welt, wie geht es dir? 😂😂😂\nMir geht es gut, danke der Nachfrage, wie geht es dir?\nMir geht es hervorragend: Meine Engine kann endlich Text rendern\n und es scheint ganz in ordnung zu sein.", 0, 0); + Renderer2D.End(); } @@ -285,17 +324,44 @@ namespace Sandbox private bool OnImGuiRender(ImGuiRenderEvent e) { + return true; + ImGui.Begin("Test"); ImGui.ColorEdit3("Square Color", ref _squareColor0); _squareColor1 = ColorRGBA.White - _squareColor0; + //ImGui.Begin + + //ImGui.Image(fonty.[Friend]_atlas.[Friend]nativeView, .(100, 200)); + //ImGui.Scrollbar(.X); + //ImGui.Image(fonty.[Friend]_atlas.[Friend]nativeView, .(fonty.[Friend]_atlas.Width, fonty.[Friend]_atlas.Height), .(0.0f, 0.0f), .(1.0f, 1.0f), .(1.0f, 1.0f, 1.0f, 1.0f), .(1.0f, 0, 0, 1)); + ImGui.End(); + TextureViewer(); + return false; } + float zoom = 1.0f; + + private void TextureViewer() + { + ImGui.Begin("Texture Viewer"); + + ImGui.SliderFloat("Zoom", &zoom, 0.01f, 100.0f); + + ImGui.BeginChild("imageChild", default, true, .HorizontalScrollbar); + + ImGui.Image(fonty.[Friend]_atlas.[Friend]nativeView, .(fonty.[Friend]_atlas.Width * zoom, fonty.[Friend]_atlas.Height * zoom), .(0.0f, 0.0f), .(1.0f, 1.0f), .(1.0f, 1.0f, 1.0f, 1.0f), .(1.0f, 0, 0, 1)); + + ImGui.EndChild(); + + ImGui.End(); + } + private bool OnWindowResize(WindowResizeEvent e) { _depthTarget.ReleaseRef();