Add initial version of text rendering.

This commit is contained in:
H. Utku Maden 2025-01-22 22:42:43 +03:00
parent afa6ed4f4c
commit b1aadae0f2
9 changed files with 459 additions and 0 deletions

@ -51,6 +51,7 @@ namespace Dashboard.Drawing.OpenGL
ResourcePool.IncrementReference();
AddExecutor(new BaseCommandExecutor());
AddExecutor(new TextCommandExecutor());
}
~ContextExecutor()

@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BlurgText" Version="0.1.0-nightly-19" />
<PackageReference Include="OpenTK.Graphics" Version="5.0.0-pre.13" />
</ItemGroup>
@ -18,6 +19,8 @@
<ItemGroup>
<EmbeddedResource Include="Executors\simple.frag" />
<EmbeddedResource Include="Executors\simple.vert" />
<EmbeddedResource Include="Executors\text.vert" />
<EmbeddedResource Include="Executors\text.frag" />
</ItemGroup>
</Project>

@ -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<string> Extensions { get; } = new[] { "DB_Text" };
public IContextExecutor Executor { get; private set; }
private BlurgEngine Engine => Executor.ResourcePool.GetResourceManager<BlurgEngine>();
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<TextCommandArgs>();
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<Call, Vertex>
{
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);
}
}
}
}

@ -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;
}

@ -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;
}

@ -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<int> _textures = new List<int>();
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);
}
}

@ -0,0 +1,13 @@
namespace Dashboard.Drawing.OpenGL.Text
{
public class BlurgFontExtension : IDrawExtension
{
public string Name { get; } = "BLURG_Font";
public IReadOnlyList<IDrawExtension> Requires { get; } = new [] { FontExtension.Instance };
public IReadOnlyList<IDrawCommand> Commands { get; } = new IDrawCommand[] { };
private BlurgFontExtension() {}
public static readonly BlurgFontExtension Instance = new BlurgFontExtension();
}
}

@ -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);
}
}
}

@ -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<Vector3> points = new List<Vector3>();
IFont font = executor.ResourcePool.GetResourceManager<BlurgEngine>()
.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);