start implementing MSDF

pull/52/head
cosmonaut 2023-12-05 19:03:52 -08:00
parent 2e890fd696
commit ad5c88aa52
4 changed files with 374 additions and 0 deletions

View File

@ -0,0 +1,43 @@
using System.Runtime.CompilerServices;
namespace MoonWorks.Graphics.Font
{
/* UTF-8 Decoder */
/* Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
* See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.
*/
public static class Decoder
{
static byte[] utf8d = new byte[] {
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
};
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe uint Decode(uint* state, uint* codep, uint dbyte) {
uint type = utf8d[dbyte];
*codep = (uint) ((*state != 0) ?
(dbyte & 0x3fu) | (*codep << 6) :
(0xff >> (int) type) & (dbyte));
*state = utf8d[256 + *state*16 + type];
return *state;
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.IO;
using System.Text.Json;
namespace MoonWorks.Graphics.Font.MSDF;
public class Font : IDisposable
{
public Texture Texture { get; }
internal AtlasData AtlasData;
static JsonSerializerOptions SerializerOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
IncludeFields = true
};
static AtlasDataContext Context = new AtlasDataContext(SerializerOptions);
private bool IsDisposed;
public static Font Load(
GraphicsDevice graphicsDevice,
CommandBuffer commandBuffer,
string jsonPath
) {
var atlasData = (AtlasData) JsonSerializer.Deserialize(File.ReadAllText(jsonPath), typeof(AtlasData), Context);
var imagePath = Path.ChangeExtension(jsonPath, ".png");
var texture = Texture.FromImageFile(graphicsDevice, commandBuffer, imagePath);
return new Font(texture, atlasData);
}
private Font(Texture texture, AtlasData atlasData)
{
Texture = texture;
AtlasData = atlasData;
}
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
Texture.Dispose();
}
IsDisposed = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}

View File

@ -0,0 +1,85 @@
using System.Text.Json.Serialization;
using MoonWorks.Math.Float;
namespace MoonWorks.Graphics.Font.MSDF;
[JsonSerializable(typeof(AtlasData))]
internal partial class AtlasDataContext : JsonSerializerContext
{
}
// Reads from an atlas generated by msdf-atlas-gen
public struct AtlasData
{
public Atlas Atlas;
public Metrics Metrics;
public Glyph[] Glyphs;
}
public struct Atlas
{
public FieldType Type;
public int DistanceRange;
public int Size;
public int Width;
public int Height;
public Origin YOrigin;
}
public struct Metrics
{
public int EmSize;
public float LineHeight;
public float Ascender;
public float Descender;
public float UnderlineY;
public float UnderlineThickness;
}
public struct Glyph
{
public uint Unicode;
public float Advance;
public Bounds PlaneBounds;
public Bounds AtlasBounds;
}
public struct Bounds
{
public float Left;
public float Bottom;
public float Right;
public float Top;
}
public enum FieldType
{
Hardmask,
Softmask,
SDF,
PSDF,
MSDF,
MTSDF
}
public enum Origin
{
Top,
Bottom
}
public struct FontVertex : IVertexType
{
public Vector3 Position;
public Vector2 TexCoord;
public Color Color;
private static readonly VertexElementFormat[] vertexElementFormats = new VertexElementFormat[]
{
VertexElementFormat.Vector3,
VertexElementFormat.Vector2,
VertexElementFormat.Color
};
public static VertexElementFormat[] Formats => vertexElementFormats;
}

View File

@ -0,0 +1,188 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MoonWorks.Graphics.Font.MSDF;
public unsafe class TextBatch : IDisposable
{
public const int MAX_CHARS = 4096;
public const int MAX_VERTICES = MAX_CHARS * 4;
public const int MAX_INDICES = MAX_CHARS * 6;
public GraphicsDevice GraphicsDevice;
public Font Font;
public Buffer VertexBuffer;
public Buffer IndexBuffer;
public byte* VertexData;
public byte* IndexData;
private int CurrentVertexDataLengthInBytes;
private int CurrentIndexDataLengthInBytes;
private byte* StringBytes;
private int StringBytesCount = 16;
private bool IsDisposed;
public TextBatch(GraphicsDevice graphicsDevice)
{
GraphicsDevice = graphicsDevice;
}
public void Start()
{
VertexBuffer = Buffer.Create<FontVertex>(GraphicsDevice, BufferUsageFlags.Vertex, MAX_VERTICES);
IndexBuffer = Buffer.Create<uint>(GraphicsDevice, BufferUsageFlags.Index, MAX_INDICES);
VertexData = (byte*) NativeMemory.Alloc((nuint) (MAX_VERTICES * Unsafe.SizeOf<FontVertex>()));
IndexData = (byte*) NativeMemory.Alloc((nuint) (MAX_INDICES * Unsafe.SizeOf<uint>()));
StringBytes = (byte*) NativeMemory.Alloc(128);
}
public void AddText(
string text,
float x,
float y,
float depth,
Color color,
HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment verticalAlignment = VerticalAlignment.Baseline
) {
uint decodeState;
uint codepoint;
var byteCount = System.Text.Encoding.UTF8.GetByteCount(text);
if (StringBytesCount < byteCount)
{
StringBytes = (byte*) NativeMemory.Realloc(StringBytes, (nuint) byteCount);
StringBytesCount = byteCount;
}
fixed (char* chars = text)
{
var bytesWritten = System.Text.Encoding.UTF8.GetBytes(chars, text.Length, StringBytes, byteCount);
y += GetVerticalAlignOffset(verticalAlignment);
if (horizontalAlignment == HorizontalAlignment.Right)
{
// TODO: check text bounds to adjust alignment
}
for (var i = 0; i < bytesWritten; i += 1)
{
if (Decoder.Decode(&decodeState, &codepoint, StringBytes[i]) > 0)
{
if (decodeState == 1)
{
// Something went wrong!
return;
}
continue;
}
// TODO: we need to convert AtlasData so that codepoints are looked up by key
if (IsWhitespace(codepoint))
{
x += GetHorizontalAdvance(codepoint);
continue;
}
}
// TODO: draw the rest of the owl
}
}
// Call this after you have made all the Draw calls you want.
public void UploadBufferData(CommandBuffer commandBuffer)
{
if (CurrentVertexDataLengthInBytes > 0 && CurrentIndexDataLengthInBytes > 0)
{
commandBuffer.SetBufferData(VertexBuffer, (nint) VertexData, 0, (uint) CurrentVertexDataLengthInBytes);
commandBuffer.SetBufferData(IndexBuffer, (nint) IndexData, 0, (uint) CurrentIndexDataLengthInBytes);
}
}
private float GetHorizontalAdvance(uint codepoint)
{
Font.AtlasData.Glyphs
}
private float GetVerticalAlignOffset(VerticalAlignment verticalAlignment)
{
switch (verticalAlignment)
{
case VerticalAlignment.Baseline:
return 0;
case VerticalAlignment.Top:
return Font.AtlasData.Metrics.Ascender;
case VerticalAlignment.Middle:
return (Font.AtlasData.Metrics.Ascender + Font.AtlasData.Metrics.Descender) / 2f;
case VerticalAlignment.Bottom:
return Font.AtlasData.Metrics.Descender;
default:
return 0;
}
}
private static bool IsWhitespace(uint codepoint)
{
switch (codepoint)
{
case 0x0020:
case 0x00A0:
case 0x1680:
case 0x202F:
case 0x205F:
case 0x3000:
return true;
default:
if (codepoint > 0x2000 && codepoint <= 0x200A)
{
return true;
}
return false;
}
}
protected virtual void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
VertexBuffer.Dispose();
IndexBuffer.Dispose();
}
NativeMemory.Free(VertexData);
NativeMemory.Free(IndexData);
NativeMemory.Free(StringBytes);
IsDisposed = true;
}
}
~TextBatch()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: false);
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}