diff --git a/Dashboard.Drawing.OpenGL/ContextExecutor.cs b/Dashboard.Drawing.OpenGL/ContextExecutor.cs index 510a8aa..b744d70 100644 --- a/Dashboard.Drawing.OpenGL/ContextExecutor.cs +++ b/Dashboard.Drawing.OpenGL/ContextExecutor.cs @@ -51,6 +51,7 @@ namespace Dashboard.Drawing.OpenGL ResourcePool.IncrementReference(); AddExecutor(new BaseCommandExecutor()); + AddExecutor(new TextCommandExecutor()); } ~ContextExecutor() diff --git a/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj b/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj index 4c9837e..05c6aa9 100644 --- a/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj +++ b/Dashboard.Drawing.OpenGL/Dashboard.Drawing.OpenGL.csproj @@ -8,6 +8,7 @@ + @@ -18,6 +19,8 @@ + + diff --git a/Dashboard.Drawing.OpenGL/Executors/TextCommandExecutor.cs b/Dashboard.Drawing.OpenGL/Executors/TextCommandExecutor.cs new file mode 100644 index 0000000..8b0da71 --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Executors/TextCommandExecutor.cs @@ -0,0 +1,243 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using BlurgText; +using Dashboard.Drawing.OpenGL.Text; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; + +namespace Dashboard.Drawing.OpenGL.Executors +{ + public class TextCommandExecutor : ICommandExecutor, IInitializer + { + public IEnumerable Extensions { get; } = new[] { "DB_Text" }; + public IContextExecutor Executor { get; private set; } + private BlurgEngine Engine => Executor.ResourcePool.GetResourceManager(); + public bool IsInitialized { get; private set; } + + private DrawCallRecorder _recorder; + private int _program = 0; + private int _transformsLocation = -1; + private int _atlasLocation = -1; + private int _borderWidthLocation = -1; + private int _borderColorLocation = -1; + private int _fillColorLocation = -1; + + public TextCommandExecutor() + { + Executor = null!; + _recorder = new DrawCallRecorder(this); + } + + public void Initialize() + { + if (IsInitialized) + return; + IsInitialized = true; + + Assembly self = typeof(TextCommandExecutor).Assembly; + + using Stream vsource = self.GetManifestResourceStream("Dashboard.Drawing.OpenGL.Executors.text.vert")!; + using Stream fsource = self.GetManifestResourceStream("Dashboard.Drawing.OpenGL.Executors.text.frag")!; + int vs = ShaderUtil.CompileShader(ShaderType.VertexShader, vsource); + int fs = ShaderUtil.CompileShader(ShaderType.FragmentShader, fsource); + _program = ShaderUtil.LinkProgram(vs, fs, new [] + { + "a_v3Position", + "a_v2TexCoords", + }); + GL.DeleteShader(vs); + GL.DeleteShader(fs); + + _transformsLocation = GL.GetUniformLocation(_program, "m4Transforms"); + _atlasLocation = GL.GetUniformLocation(_program, "txAtlas"); + _borderWidthLocation = GL.GetUniformLocation(_program, "fBorderWidth"); + _borderColorLocation = GL.GetUniformLocation(_program, "v4BorderColor"); + _fillColorLocation = GL.GetUniformLocation(_program, "v4FillColor"); + + _recorder.Initialize(); + } + + public void SetContextExecutor(IContextExecutor executor) + { + Executor = executor; + } + + public void BeginFrame() + { + } + + public void BeginDraw() + { + _recorder.Clear(); + } + + public void EndDraw() + { + GL.UseProgram(_program); + GL.Enable(EnableCap.Blend); + GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + _recorder.Execute(); + GL.Disable(EnableCap.Blend); + } + + public void EndFrame() + { + } + + public void ProcessCommand(ICommandFrame frame) + { + switch (frame.Command.Name) + { + case "Text": + DrawText(frame); + break; + } + } + + private void DrawText(ICommandFrame frame) + { + TextCommandArgs args = frame.GetParameter(); + DbBlurgFont font = (DbBlurgFont)args.Font; + + BlurgColor color; + switch (args.TextBrush) + { + case SolidBrush solid: + color = new BlurgColor() + { + R = solid.Color.R, + G = solid.Color.G, + B = solid.Color.B, + A = solid.Color.A, + }; + break; + default: + color = new BlurgColor() { R = 255, G = 0, B = 255, A = 255 }; + break; + } + + BlurgResult? result = Engine.Blurg.BuildString(font.Font, font.Size, color, args.Text); + + if (result == null) + return; + + Vector3 position = new Vector3(args.Position.X, args.Position.Y, args.Position.Z); + ExecuteBlurgResult(result, position); + + result.Dispose(); + } + + private void ExecuteBlurgResult(BlurgResult result, Vector3 position) + { + Matrix4 transforms = Executor.TransformStack.Top; + + for (int i = 0; i < result.Count; i++) + { + BlurgRect rect = result[i]; + + int texture = (int)rect.UserData; + Vector4 color = new Vector4(rect.Color.R / 255f, rect.Color.G / 255f, rect.Color.B / 255f, + rect.Color.A / 255f); + + if (i == 0) + { + _recorder.Begin(PrimitiveType.Triangles, new Call() + { + Texture = texture, + FillColor = color, + Transforms = transforms, + }); + } + else if ( + _recorder.CurrentCall.Texture != texture || + _recorder.CurrentCall.FillColor != color) + { + _recorder.End(); + Call call = new Call() + { + Texture = texture, + FillColor = color, + Transforms = transforms, + }; + _recorder.Begin(PrimitiveType.Triangles, call); + } + + Vector3 p00 = new Vector3(rect.X, rect.Y, 0) + position; + Vector3 p10 = p00 + new Vector3(rect.Width, 0, 0); + Vector3 p11 = p00 + new Vector3(rect.Width, rect.Height, 0); + Vector3 p01 = p00 + new Vector3(0, rect.Height, 0); + + Vector2 uv00 = new Vector2(rect.U0, rect.V0); + Vector2 uv10 = new Vector2(rect.U1, rect.V0); + Vector2 uv11 = new Vector2(rect.U1, rect.V1); + Vector2 uv01 = new Vector2(rect.U0, rect.V1); + + _recorder.Vertex(p00, uv00); + _recorder.Vertex(p10, uv10); + _recorder.Vertex(p11, uv11); + + _recorder.Vertex(p00, uv00); + _recorder.Vertex(p11, uv11); + _recorder.Vertex(p01, uv01); + } + _recorder.End(); + } + + private struct Call + { + public Matrix4 Transforms = Matrix4.Identity; + public int Texture = 0; + public float BorderWidth = 0f; + public Vector4 FillColor = Vector4.One; + public Vector4 BorderColor = new Vector4(0,0,0,1); + + public Call() + { + } + } + + [StructLayout(LayoutKind.Explicit, Size = 8 * sizeof(float))] + private struct Vertex + { + [FieldOffset(0)] + public Vector3 Position; + [FieldOffset(4 * sizeof(float))] + public Vector2 TexCoords; + } + + private class DrawCallRecorder : DrawCallRecorder + { + private TextCommandExecutor Executor { get; } + + public DrawCallRecorder(TextCommandExecutor executor) + { + Executor = executor; + } + + public void Vertex(Vector3 position, Vector2 texCoords) + { + Vertex(new Vertex(){Position = position, TexCoords = texCoords}); + } + + protected override void PrepareCall(in Call call) + { + Matrix4 transforms = call.Transforms; + GL.UniformMatrix4f(Executor._transformsLocation, 1, true, ref transforms); + GL.Uniform1f(Executor._borderWidthLocation, call.BorderWidth); + GL.Uniform4f(Executor._borderColorLocation, 1, in call.BorderColor); + GL.Uniform4f(Executor._fillColorLocation, 1, in call.FillColor); + GL.Uniform1i(Executor._atlasLocation, 0); + GL.ActiveTexture(TextureUnit.Texture0); + GL.BindTexture(TextureTarget.Texture2d, call.Texture); + } + + protected override void SetVertexFormat() + { + GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, VertexSize, 0); + GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, VertexSize, 4*sizeof(float)); + GL.EnableVertexAttribArray(0); + GL.EnableVertexAttribArray(1); + } + } + } +} diff --git a/Dashboard.Drawing.OpenGL/Executors/text.frag b/Dashboard.Drawing.OpenGL/Executors/text.frag new file mode 100644 index 0000000..9914b22 --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Executors/text.frag @@ -0,0 +1,21 @@ +#version 140 + +in vec3 v_v3Position; +in vec2 v_v2TexCoords; + +out vec4 f_Color; + +uniform sampler2D txAtlas; +uniform float fBorderWidth; +uniform vec4 v4BorderColor; +uniform vec4 v4FillColor; + +void main() { + // For now just honor the fill color + + vec4 color = texture(txAtlas, v_v2TexCoords) * v4FillColor; + + if (color.a <= 0.1) + discard; + f_Color = color; +} diff --git a/Dashboard.Drawing.OpenGL/Executors/text.vert b/Dashboard.Drawing.OpenGL/Executors/text.vert new file mode 100644 index 0000000..27ae926 --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Executors/text.vert @@ -0,0 +1,18 @@ +#version 140 + +in vec3 a_v3Position; +in vec2 a_v2TexCoords; + +out vec3 v_v3Position; +out vec2 v_v2TexCoords; + +uniform mat4 m4Transforms; + +void main(void) +{ + vec4 position = vec4(a_v3Position, 1) * m4Transforms; + gl_Position = position; + v_v3Position = position.xyz/position.w; + + v_v2TexCoords = a_v2TexCoords; +} diff --git a/Dashboard.Drawing.OpenGL/Text/BlurgEngine.cs b/Dashboard.Drawing.OpenGL/Text/BlurgEngine.cs new file mode 100644 index 0000000..783261c --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Text/BlurgEngine.cs @@ -0,0 +1,126 @@ +using BlurgText; +using OpenTK.Graphics.OpenGL; +using OPENGL = OpenTK.Graphics.OpenGL; + +namespace Dashboard.Drawing.OpenGL.Text +{ + public class BlurgEngine : IResourceManager, IGLDisposable + { + public string Name { get; } = "BlurgEngine"; + public Blurg Blurg { get; } + public bool SystemFontsEnabled { get; } + + private readonly List _textures = new List(); + + public BlurgEngine() + { + Blurg = new Blurg(AllocateTexture, UpdateTexture); + + SystemFontsEnabled = Blurg.EnableSystemFonts(); + } + + ~BlurgEngine() + { + Dispose(false, true); + } + + public DbBlurgFont AddFont(string path) + { + BlurgFont? font = Blurg.AddFontFile(path) ?? throw new Exception("Failed to load the font file."); + return new DbBlurgFont(font, 12f); + } + + public DbBlurgFont AddFont(Stream stream) + { + string path; + Stream dest; + for (int i = 0;; i++) + { + path = Path.GetTempFileName(); + try + { + dest = File.Open(path, FileMode.CreateNew, FileAccess.Write, FileShare.None); + } + catch (IOException ex) + { + if (i < 3) + continue; + else + throw new Exception("Could not open a temporary file for writing the font.", ex); + } + + break; + } + + stream.CopyTo(dest); + dest.Dispose(); + + DbBlurgFont? font = AddFont(path); + File.Delete(path); + return font; + } + + public DbBlurgFont? QueryFont(string family, FontWeight weight, FontSlant slant, FontStretch stretch) + { + // Ignore the stretch argument. + bool italic = slant != FontSlant.Normal; + BlurgFont? font = Blurg.QueryFont(family, new BlurgText.FontWeight((int)weight), italic); + + if (font != null) + return new DbBlurgFont(font, 12f); + return null; + } + + private void UpdateTexture(IntPtr texture, IntPtr buffer, int x, int y, int width, int height) + { + GL.BindTexture(TextureTarget.Texture2d, (int)texture); + GL.TexSubImage2D(TextureTarget.Texture2d, 0, x, y, width, height, OPENGL.PixelFormat.Rgba, PixelType.UnsignedByte, buffer); + // GL.TexSubImage2D(TextureTarget.Texture2d, 0, x, y, width, height, OPENGL.PixelFormat.Red, PixelType.Byte, buffer); + } + + private IntPtr AllocateTexture(int width, int height) + { + int texture = GL.GenTexture(); + + GL.BindTexture(TextureTarget.Texture2d, texture); + GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.Rgba, width, height, 0, OPENGL.PixelFormat.Rgba, PixelType.UnsignedByte, IntPtr.Zero); + // GL.TexImage2D(TextureTarget.Texture2d, 0, InternalFormat.R8, width, height, 0, OPENGL.PixelFormat.Red, PixelType.Byte, IntPtr.Zero); + + GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); + GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + // GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleR, (int)TextureSwizzle.One); + // GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleG, (int)TextureSwizzle.One); + // GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleB, (int)TextureSwizzle.One); + // GL.TexParameteri(TextureTarget.Texture2d, TextureParameterName.TextureSwizzleA, (int)TextureSwizzle.Red); + + _textures.Add(texture); + + return texture; + } + + private bool _isDisposed = false; + + private void Dispose(bool disposing, bool safeExit) + { + if (_isDisposed) + return; + _isDisposed = true; + + if (disposing) + { + Blurg.Dispose(); + GC.SuppressFinalize(this); + } + + if (safeExit) + { + foreach (int texture in _textures) + ContextCollector.Global.DeleteTexture(texture); + } + } + + public void Dispose() => Dispose(true, true); + + public void Dispose(bool safeExit) => Dispose(true, safeExit); + } +} diff --git a/Dashboard.Drawing.OpenGL/Text/BlurgFontExtension.cs b/Dashboard.Drawing.OpenGL/Text/BlurgFontExtension.cs new file mode 100644 index 0000000..8ca8e6d --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Text/BlurgFontExtension.cs @@ -0,0 +1,13 @@ +namespace Dashboard.Drawing.OpenGL.Text +{ + public class BlurgFontExtension : IDrawExtension + { + public string Name { get; } = "BLURG_Font"; + public IReadOnlyList Requires { get; } = new [] { FontExtension.Instance }; + public IReadOnlyList Commands { get; } = new IDrawCommand[] { }; + + private BlurgFontExtension() {} + + public static readonly BlurgFontExtension Instance = new BlurgFontExtension(); + } +} diff --git a/Dashboard.Drawing.OpenGL/Text/DbBlurgFont.cs b/Dashboard.Drawing.OpenGL/Text/DbBlurgFont.cs new file mode 100644 index 0000000..c0a5696 --- /dev/null +++ b/Dashboard.Drawing.OpenGL/Text/DbBlurgFont.cs @@ -0,0 +1,26 @@ +using BlurgText; + +namespace Dashboard.Drawing.OpenGL.Text +{ + public class DbBlurgFont : IFont + { + public IDrawExtension Kind { get; } = BlurgFontExtension.Instance; + public BlurgFont Font { get; } + public float Size { get; } + public string Family => Font.FamilyName; + public FontWeight Weight => (FontWeight)Font.Weight.Value; + public FontSlant Slant => Font.Italic ? FontSlant.Italic : FontSlant.Normal; + public FontStretch Stretch => FontStretch.Normal; + + public DbBlurgFont(BlurgFont font, float size) + { + Font = font; + Size = size; + } + + public DbBlurgFont WithSize(float size) + { + return new DbBlurgFont(Font, size); + } + } +} diff --git a/tests/Dashboard.TestApplication/Program.cs b/tests/Dashboard.TestApplication/Program.cs index f8bbcd3..2567345 100644 --- a/tests/Dashboard.TestApplication/Program.cs +++ b/tests/Dashboard.TestApplication/Program.cs @@ -1,6 +1,8 @@ using Dashboard.Drawing; using System.Drawing; +using Dashboard; using Dashboard.Drawing.OpenGL; +using Dashboard.Drawing.OpenGL.Text; using OpenTK.Graphics; using OpenTK.Platform; using OpenTK.Graphics.OpenGL; @@ -96,6 +98,12 @@ TK.Window.SetMode(wnd, WindowMode.Normal); List points = new List(); +IFont font = executor.ResourcePool.GetResourceManager() + .QueryFont("Nimbus Mono", FontWeight._500, FontSlant.Normal, FontStretch.Normal)! + .WithSize(12f); + +queue.Text(new sys.Vector3(96, 96, 0), bg, "Hello World!", font); +queue.Text(new sys.Vector3(128, 12, 0), bg, "japenis too! uwa ~~~ アホ", font); queue.Line(new sys.Vector2(64, 256), new sys.Vector2(64+256, 256), 0, 64f, fg); queue.Rect(new sys.Vector2(16, 16), new sys.Vector2(96, 96), 0, fg, bg, 8f);