From a881f1a0862ef6b522d778f00602c6ced3a3424f Mon Sep 17 00:00:00 2001 From: Harrison Date: Thu, 2 Jul 2020 13:13:04 -0500 Subject: [PATCH] Basic font rendering working. --- src/SlatedGameToolkit.Framework/GameEngine.cs | 1 + .../Graphics/OpenGL/GLContext.cs | 27 +- .../Graphics/Render/IMesh.cs | 1 + .../Graphics/Render/MeshBatch.cs | 14 +- .../Graphics/Render/RectangleMesh.cs | 45 ++- .../Graphics/Text/BitmapFont.cs | 272 ++++++++++++++++++ .../Graphics/Text/DynamicFont.cs | 12 - .../Graphics/Text/DynamicGlyph.cs | 14 - .../Graphics/Textures/ITexture.cs | 12 +- .../Graphics/Textures/Texture.cs | 13 +- .../Graphics/Window/WindowContextsManager.cs | 5 + .../Resources/default.frag | 8 +- .../Resources/default.vert | 1 - .../Utilities/RectangleUtils.cs | 16 ++ .../Playground/earwig_factory_rg.ttf | Bin 0 -> 63900 bytes .../SlatedGameToolkit.Tools.csproj | 1 + .../Utilities/Playground/MainState.cs | 15 +- .../Collections/Caching/LRUCacheTests.cs | 20 ++ 18 files changed, 421 insertions(+), 56 deletions(-) create mode 100644 src/SlatedGameToolkit.Framework/Graphics/Text/BitmapFont.cs delete mode 100644 src/SlatedGameToolkit.Framework/Graphics/Text/DynamicFont.cs delete mode 100644 src/SlatedGameToolkit.Framework/Graphics/Text/DynamicGlyph.cs create mode 100644 src/SlatedGameToolkit.Framework/Utilities/RectangleUtils.cs create mode 100644 src/SlatedGameToolkit.Tools/Resources/Playground/earwig_factory_rg.ttf diff --git a/src/SlatedGameToolkit.Framework/GameEngine.cs b/src/SlatedGameToolkit.Framework/GameEngine.cs index 8307488..8422539 100644 --- a/src/SlatedGameToolkit.Framework/GameEngine.cs +++ b/src/SlatedGameToolkit.Framework/GameEngine.cs @@ -177,6 +177,7 @@ namespace SlatedGameToolkit.Framework { } } catch (Exception e) { Logger.Fatal(e.ToString()); + throw e; } finally { stopped = true; manager.Dispose(); diff --git a/src/SlatedGameToolkit.Framework/Graphics/OpenGL/GLContext.cs b/src/SlatedGameToolkit.Framework/Graphics/OpenGL/GLContext.cs index 588b8f8..643197c 100644 --- a/src/SlatedGameToolkit.Framework/Graphics/OpenGL/GLContext.cs +++ b/src/SlatedGameToolkit.Framework/Graphics/OpenGL/GLContext.cs @@ -13,6 +13,8 @@ namespace SlatedGameToolkit.Framework.Graphics.OpenGL [SuppressMessage("ReSharper", "InconsistentNaming")] public class GLContext : IDisposable { + #region OpenGLFunctions + public IntPtr Handle { get; private set; } private bool disposed; @@ -224,6 +226,7 @@ namespace SlatedGameToolkit.Framework.Graphics.OpenGL public void Enable(EnableCap cap) { glEnable.Invoke(cap); + DetectGLError(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] @@ -251,6 +254,7 @@ namespace SlatedGameToolkit.Framework.Graphics.OpenGL public void BlendFunc(BlendingFactor sfactor, BlendingFactor dfactor) { glBlendFunc.Invoke(sfactor, dfactor); + DetectGLError(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] @@ -305,6 +309,7 @@ namespace SlatedGameToolkit.Framework.Graphics.OpenGL public void PixelStorei(PixelStoreParameter pname, int param) { glPixelStorei.Invoke(pname, param); + DetectGLError(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] @@ -362,12 +367,13 @@ namespace SlatedGameToolkit.Framework.Graphics.OpenGL } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate void PFNGLGETINTEGERVPROC(GetPName pname, out int data); + private delegate void PFNGLGETINTEGERVPROC(GetPName pname, int[] data); private PFNGLGETINTEGERVPROC glGetIntegerv; - public void GetIntegerv(GetPName pname, out int data) + public void GetIntegerv(GetPName pname, int[] data) { - glGetIntegerv.Invoke(pname, out data); + glGetIntegerv.Invoke(pname, data); + DetectGLError(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] @@ -530,6 +536,7 @@ namespace SlatedGameToolkit.Framework.Graphics.OpenGL public void TexSubImage2D(TextureTarget target, int level, int xoffset, int yoffset, int width, int height, PixelFormat format, PixelType type, IntPtr pixels) { glTexSubImage2D.Invoke(target, level, xoffset, yoffset, width, height, format, type, pixels); + DetectGLError(); } [UnmanagedFunctionPointer(CallingConvention.Cdecl)] @@ -3153,6 +3160,7 @@ namespace SlatedGameToolkit.Framework.Graphics.OpenGL { glSampleMaski.Invoke(maskNumber, mask); } + #endregion public GLContext(IntPtr windowHandle) { @@ -3162,6 +3170,7 @@ namespace SlatedGameToolkit.Framework.Graphics.OpenGL throw new FrameworkSDLException(); } + #region OpenGLDelegateAssignment glCullFace = Marshal.GetDelegateForFunctionPointer(loader.Invoke("glCullFace")); glFrontFace = Marshal.GetDelegateForFunctionPointer(loader.Invoke("glFrontFace")); glHint = Marshal.GetDelegateForFunctionPointer(loader.Invoke("glHint")); @@ -3506,19 +3515,29 @@ namespace SlatedGameToolkit.Framework.Graphics.OpenGL glTexImage3DMultisample = Marshal.GetDelegateForFunctionPointer(loader.Invoke("glTexImage3DMultisample")); glGetMultisamplefv = Marshal.GetDelegateForFunctionPointer(loader.Invoke("glGetMultisamplefv")); glSampleMaski = Marshal.GetDelegateForFunctionPointer(loader.Invoke("glSampleMaski")); + #endregion } /// /// Checks for any issues in this OpenGL Context and throws an exception if it detects one. /// May throw OpenGLException. /// - private void DetectGLError() { + public void DetectGLError() { OpenGL.ErrorCode code = GetError(); if (code != OpenGL.ErrorCode.NoError) { throw new OpenGLErrorException(code); } } + public void GetViewport(out int x, out int y, out int width, out int height) { + int[] viewport = new int[4]; + GetIntegerv(GetPName.Viewport, viewport); + x = viewport[0]; + y = viewport[1]; + width = viewport[2]; + height = viewport[3]; + } + protected virtual void Dispose(bool disposing) { if (!disposed) diff --git a/src/SlatedGameToolkit.Framework/Graphics/Render/IMesh.cs b/src/SlatedGameToolkit.Framework/Graphics/Render/IMesh.cs index 2e2c51b..3ffed11 100644 --- a/src/SlatedGameToolkit.Framework/Graphics/Render/IMesh.cs +++ b/src/SlatedGameToolkit.Framework/Graphics/Render/IMesh.cs @@ -32,5 +32,6 @@ namespace SlatedGameToolkit.Framework.Graphics.Render /// /// A color for this mesh. Color Color { get; } + } } \ No newline at end of file diff --git a/src/SlatedGameToolkit.Framework/Graphics/Render/MeshBatch.cs b/src/SlatedGameToolkit.Framework/Graphics/Render/MeshBatch.cs index 0ebe499..c20a8eb 100644 --- a/src/SlatedGameToolkit.Framework/Graphics/Render/MeshBatch.cs +++ b/src/SlatedGameToolkit.Framework/Graphics/Render/MeshBatch.cs @@ -15,7 +15,7 @@ namespace SlatedGameToolkit.Framework.Graphics.Render private bool disposed; private float batchDelta; public GLContext GLContext {get; private set; } - private int projALoc, viewALoc, modelALoc, texturedALoc; + private int projALoc, viewALoc, modelALoc, texturedALoc, singleChanneledALoc; private Camera camera; private RenderProgram renderProgram; private ITexture texture; @@ -54,6 +54,7 @@ namespace SlatedGameToolkit.Framework.Graphics.Render viewALoc = GLContext.GetUniformLocation(renderProgram.Handle, "view"); projALoc = GLContext.GetUniformLocation(renderProgram.Handle, "projection"); texturedALoc = GLContext.GetUniformLocation(renderProgram.Handle, "textured"); + singleChanneledALoc = GLContext.GetUniformLocation(renderProgram.Handle, "singleChanneled"); vertexBuffers.defineVertexAttributes(definitions: definitions); GLContext.UniformMatrix4fv(projALoc, 1, false, camera.ProjectionMatrix.ToColumnMajorArray()); @@ -80,15 +81,18 @@ namespace SlatedGameToolkit.Framework.Graphics.Render IMoveable moveable = mesh as IMoveable; if (moveable != null) { if (GameEngine.UpdatesPerSecond <= 0) { - camera.Position = camera.MoveTo; + moveable.Position = moveable.MoveTo; } else { - camera.Position = camera.Position + (camera.MoveTo - camera.Position) * batchDelta; + moveable.Position = moveable.Position + (moveable.MoveTo - moveable.Position) * batchDelta; } } if (mesh.Texture?.Handle != this.texture?.Handle) { Flush(); this.texture = mesh.Texture; GLContext.Uniform1i(texturedALoc, texture == null ? 0 : 1); + if (texture != null) { + GLContext.Uniform1i(singleChanneledALoc, texture.SingleChanneled ? 1 : 0); + } } ValueTuple[] vertices = mesh.Vertices; uint[] indices = mesh.Elements; @@ -134,7 +138,9 @@ namespace SlatedGameToolkit.Framework.Graphics.Render } protected virtual void Flush() { - texture?.Use(); + if (texture != null) { + GLContext.BindTexture(TextureTarget.Texture2D, texture.Handle); + } renderProgram.Use(); if (GameEngine.UpdatesPerSecond <= 0) { camera.Position = camera.MoveTo; diff --git a/src/SlatedGameToolkit.Framework/Graphics/Render/RectangleMesh.cs b/src/SlatedGameToolkit.Framework/Graphics/Render/RectangleMesh.cs index 3e9d767..f71fdd0 100644 --- a/src/SlatedGameToolkit.Framework/Graphics/Render/RectangleMesh.cs +++ b/src/SlatedGameToolkit.Framework/Graphics/Render/RectangleMesh.cs @@ -11,8 +11,7 @@ namespace SlatedGameToolkit.Framework.Graphics.Render private bool changed; private Vector3 rotation; private Vector2 origin, dimensions; - public readonly Vector2[] textureCoords = new Vector2[4]; - public ValueTuple[] vertices = new ValueTuple[4]; + private ValueTuple[] vertices = new ValueTuple[4]; private uint[] indices = new uint[] {0, 1, 3, 1, 2, 3}; public ValueTuple[] Vertices @@ -78,6 +77,19 @@ namespace SlatedGameToolkit.Framework.Graphics.Render } } + public RectangleF Bounds { + get { + return new RectangleF(X, Y, Width, Height); + } + + set { + X = value.X; + Y = value.Y; + Width = value.Width; + Height = value.Height; + } + } + public uint[] Elements { get {return indices; } } public Vector3 Rotation @@ -99,24 +111,33 @@ namespace SlatedGameToolkit.Framework.Graphics.Render public Color Color { get; set; } - public RectangleMesh(ITexture texture, Color color) + public RectangleMesh(RectangleF textureRegion, ITexture texture, Color color) { this.Texture = texture; this.Color = color; - this.textureCoords[0].X = 0; - this.textureCoords[0].Y = 1; + this.vertices[0].Item2.X = textureRegion.X; + this.vertices[0].Item2.Y = textureRegion.Y; - this.textureCoords[1].X = 1; - this.textureCoords[1].Y = 1; + this.vertices[1].Item2.X = textureRegion.X + textureRegion.Width; + this.vertices[1].Item2.Y = textureRegion.Y; - this.textureCoords[2].X = 1; - this.textureCoords[2].Y = 0; + this.vertices[2].Item2.X = textureRegion.X + textureRegion.Width; + this.vertices[2].Item2.Y = textureRegion.Y + textureRegion.Height; - this.textureCoords[3].X = 0; - this.textureCoords[3].Y = 0; + this.vertices[3].Item2.X = textureRegion.X; + this.vertices[3].Item2.Y = textureRegion.Y + textureRegion.Height; } + public RectangleMesh(RectangleF meshBounds, RectangleF textureRegion, ITexture texture, Color color) : this(textureRegion, texture, color) { + this.Bounds = meshBounds; + } + + public RectangleMesh(ITexture texture, Color color) : this(new Rectangle(0, 0, 1, 1), texture, color) { + } + + + private void CalculateVertices() { if (!changed) return; @@ -128,7 +149,7 @@ namespace SlatedGameToolkit.Framework.Graphics.Render for (int i = 0; i < vertices.Length; i++) { - vertices[i] = (Vector3.Transform(baseVerts[i], matRot), textureCoords[i]); + vertices[i].Item1 = Vector3.Transform(baseVerts[i], matRot); } changed = false; } diff --git a/src/SlatedGameToolkit.Framework/Graphics/Text/BitmapFont.cs b/src/SlatedGameToolkit.Framework/Graphics/Text/BitmapFont.cs new file mode 100644 index 0000000..0d474a7 --- /dev/null +++ b/src/SlatedGameToolkit.Framework/Graphics/Text/BitmapFont.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Numerics; +using System.Runtime.InteropServices; +using SlatedGameToolkit.Framework.Graphics.OpenGL; +using SlatedGameToolkit.Framework.Graphics.Render; +using SlatedGameToolkit.Framework.Graphics.Textures; +using SlatedGameToolkit.Framework.Graphics.Window; +using SlatedGameToolkit.Framework.Utilities; +using SlatedGameToolkit.Framework.Utilities.Collections; +using StbTrueTypeSharp; + +namespace SlatedGameToolkit.Framework.Graphics.Text +{ + public class BitmapFont : IDisposable + { + private StbTrueType.stbtt_fontinfo info; + private byte[] fontData; + private bool disposed; + private float scale; + private int bitmapLength; + private int pixelHeight; + public int PixelHeight { + get { + return pixelHeight; + } + + set { + pixelHeight = value; + scale = StbTrueType.stbtt_ScaleForPixelHeight(info, value); + } + } + + private float kerning; + public float Kerning { + get { + return kerning; + } + + set { + kerning = value; + } + } + private Dictionary<(float, char), int> bitmapLocations; + private LRUCache glyphIndexCache; + private FontBitmap[] textureBuffers; + private int drawingTo = 0; + + GLContext glContext; + + /// + /// Creates a font with the given data. + /// + /// The byte array containing all ttf data. + /// The OpenGL context to use to associate with. + /// The size of the caches. + /// The length of one side of a bitmap, therefore, the total would be this squared. + /// The number of backing textures to use. + /// An initial set of characters to load. + public unsafe BitmapFont(byte[] fontData, GLContext glContext = null, int cacheSize = 1024, int bitmapLength = 512, int numberOfTextures = 2, params char[] initialCharacters) { + this.glContext = glContext ?? WindowContextsManager.CurrentGL; + if (bitmapLength < 0) throw new ArgumentOutOfRangeException("bitmapLength"); + if (fontData == null || fontData.Length <= 0) throw new ArgumentException("fontData must be an array of bytes representing a TTF file."); + glyphIndexCache = new LRUCache(cacheSize); + bitmapLocations = new Dictionary<(float, char), int>(); + this.fontData = fontData; + + textureBuffers = new FontBitmap[numberOfTextures]; + this.bitmapLength = bitmapLength; + for (int i = 0; i < textureBuffers.Length; i++) + { + textureBuffers[i] = new FontBitmap(this.glContext, bitmapLength); + } + + info = new StbTrueType.stbtt_fontinfo(); + + fixed (byte* data = &fontData[0]) { + StbTrueType.stbtt_InitFont(info, data, 0); + } + + PixelHeight = 64; + } + + public unsafe BitmapFont(string path, GLContext glContext = null, int cacheSize = 1024, int bitmapLength = 512, int numberOfTextures = 2, params char[] initialCharacters) : + this(File.ReadAllBytes(path), glContext, cacheSize, bitmapLength, numberOfTextures, initialCharacters) + { + } + + /// + /// Loads the requested character. + /// + /// The character to load. + /// True if already loaded or successfully loaded, false if could not load. + public unsafe bool LoadGlyph(char character) { + if (!(bitmapLocations.ContainsKey((scale, character)) && textureBuffers[bitmapLocations[(scale, character)]].Contains(scale, character))) { + bitmapLocations.Remove((scale, character)); + int glyphIndex = glyphIndexCache.ComputeIfNonExistent(character, (c) => StbTrueType.stbtt_FindGlyphIndex(info, c)); + int x0 = 0, y0 = 0, x1 = 0, y1 = 0; + StbTrueType.stbtt_GetGlyphBitmapBox(info, glyphIndex, scale, scale, &x0, &y0, &x1, &y1); + if (!textureBuffers[drawingTo].CanAdd(x1 - x0, y1 - y0)) { + return false; + } + + textureBuffers[drawingTo].Upload(character, x1 - x0, y1 - y0, scale, info, glyphIndex); + bitmapLocations.Add((scale, character), drawingTo); + } + return true; + } + + public unsafe void Draw(MeshBatch batch, double delta, float x, float y, string characters, Color color) { + float horizontal = x; + foreach (char c in characters) + { + int used = 0; + while (!LoadGlyph(c)) { + used++; + drawingTo++; + if (drawingTo > textureBuffers.Length) { + drawingTo = 0; + } + if (used >= textureBuffers.Length) { + batch.End(); + batch.Begin(Matrix4x4.Identity, delta); + used = 0; + } + } + ITexture texture = textureBuffers[bitmapLocations[(scale, c)]]; + RectangleF glyphBounds = textureBuffers[bitmapLocations[(scale, c)]].GetGlyphTextureBounds(scale, c); + int width, height, vX, vY; + glContext.GetViewport(out vX, out vY, out width, out height); + float glyphWidth = glyphBounds.Width / width; + float glyphHeight = glyphBounds.Height / height; + RectangleMesh mesh = new RectangleMesh(new RectangleF(horizontal, y, glyphWidth, glyphHeight), glyphBounds.MultiplyBy(1f/bitmapLength), texture, color); + horizontal += (glyphBounds.Width - kerning) / width; + batch.Draw(mesh); + } + } + + public ITexture GetActiveTextureBacking() { + return textureBuffers[drawingTo]; + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + } + foreach (FontBitmap bitmap in textureBuffers) + { + bitmap.Dispose(); + } + disposed = true; + } + } + + ~BitmapFont() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + private class FontBitmap : ITexture + { + private GLContext glContext; + private bool disposed; + private int bitmapLength; + private int lastRow; + private Point bitmapPosition; + private Dictionary<(float, char), RectangleF> glyphBitmaps = new Dictionary<(float, char), RectangleF>(); + public uint Handle {get; private set;} + + public uint Width {get; private set;} + + public uint Height {get; private set;} + + public bool SingleChanneled => true; + + public unsafe FontBitmap(GLContext glContext, int length) { + uint[] handles = new uint[1]; + this.glContext = glContext; + glContext.GenTextures(handles.Length, handles); + Handle = handles[0]; + glContext.BindTexture(TextureTarget.Texture2D, Handle); + glContext.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Red, (uint)length, (uint)length, 0, PixelFormat.Red, PixelType.UnsignedByte, IntPtr.Zero); + glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); + glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); + glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); + glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + this.bitmapLength = length; + } + + public bool Contains(float scale, char character) { + return glyphBitmaps.ContainsKey((scale, character)); + } + + public RectangleF GetGlyphTextureBounds(float scale, char character) { + return glyphBitmaps[(scale, character)]; + } + + public unsafe void Upload(char character, int width, int height, float scale, StbTrueType.stbtt_fontinfo info, int glyphIndex) { + if (bitmapPosition.X + width < bitmapLength) { // Within the first row + if (bitmapPosition.Y - lastRow < height) { + bitmapPosition.Y = lastRow + height + 1; + if (bitmapPosition.Y >= bitmapLength) { + bitmapPosition = Point.Empty; + glyphBitmaps.Clear(); + } + } // the prev row + the current bitmaps height will be the maximum height. + bitmapPosition.X += width + 1; // move the x position. + glContext.BindTexture(TextureTarget.Texture2D, Handle); + int w = 0, h = 0, xoff = 0, yoff = 0; + byte* dataPtr = StbTrueType.stbtt_GetGlyphBitmap(info, scale, scale, glyphIndex, &w, &h, &xoff, &yoff); + IntPtr data = new IntPtr(dataPtr); + Rectangle drawLocation = new Rectangle(bitmapPosition.X - width, bitmapPosition.Y - height, width, height); + int prevAlignment = 0; + int[] prevAlignArr = new int[1]; + glContext.GetIntegerv(GetPName.UnpackAlignment, prevAlignArr); + prevAlignment = prevAlignArr[0]; + if (prevAlignment != 1) { + glContext.PixelStorei(PixelStoreParameter.UnpackAlignment, 1); + } + glContext.TexSubImage2D(TextureTarget.Texture2D, 0, drawLocation.X, drawLocation.Y, drawLocation.Width, drawLocation.Height, PixelFormat.Red, PixelType.UnsignedByte, data); + if (prevAlignment != 1) { + glContext.PixelStorei(PixelStoreParameter.UnpackAlignment, prevAlignment); + } + StbTrueType.stbtt_FreeBitmap(dataPtr, null); + glyphBitmaps.Add((scale, character), new Rectangle(drawLocation.X, bitmapLength - drawLocation.Y - drawLocation.Height, drawLocation.Width, drawLocation.Height)); + + } else { // Next row + lastRow = bitmapPosition.Y; // sets the previous row var to the previous row. + bitmapPosition.X = 0; + Upload(character, width, height, scale, info, glyphIndex); + } + } + + public bool CanAdd(int width, int height) { + return (bitmapPosition.X + width < bitmapLength) && (bitmapPosition.Y + height < bitmapLength); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + } + glContext.DeleteTextures(1, new uint[] {Handle}); + disposed = true; + } + } + + ~FontBitmap() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + } +} \ No newline at end of file diff --git a/src/SlatedGameToolkit.Framework/Graphics/Text/DynamicFont.cs b/src/SlatedGameToolkit.Framework/Graphics/Text/DynamicFont.cs deleted file mode 100644 index 32cd4dd..0000000 --- a/src/SlatedGameToolkit.Framework/Graphics/Text/DynamicFont.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; - -namespace SlatedGameToolkit.Framework.Graphics.Text -{ - public class DynamicFont { - private readonly Dictionary glyphLocations = new Dictionary(); - - public DynamicFont() { - - } - } -} \ No newline at end of file diff --git a/src/SlatedGameToolkit.Framework/Graphics/Text/DynamicGlyph.cs b/src/SlatedGameToolkit.Framework/Graphics/Text/DynamicGlyph.cs deleted file mode 100644 index a203699..0000000 --- a/src/SlatedGameToolkit.Framework/Graphics/Text/DynamicGlyph.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Numerics; - -namespace SlatedGameToolkit.Framework.Graphics.Text -{ - public struct DynamicGlyph { - public readonly char character; - public readonly (Vector2, Vector2)[] vertices; - - public DynamicGlyph(char character, (Vector2, Vector2)[] vertices) { - this.character = character; - this.vertices = vertices; - } - } -} \ No newline at end of file diff --git a/src/SlatedGameToolkit.Framework/Graphics/Textures/ITexture.cs b/src/SlatedGameToolkit.Framework/Graphics/Textures/ITexture.cs index 4f3cd85..9d3b57d 100644 --- a/src/SlatedGameToolkit.Framework/Graphics/Textures/ITexture.cs +++ b/src/SlatedGameToolkit.Framework/Graphics/Textures/ITexture.cs @@ -1,8 +1,16 @@ +using System; + namespace SlatedGameToolkit.Framework.Graphics.Textures { - public interface ITexture + public interface ITexture : IDisposable { + /// + /// Whether or not this model uses a texture that is single chanelled. + /// + /// true for single channeled. + bool SingleChanneled { get; } uint Handle {get;} - void Use(); + uint Width {get;} + uint Height {get;} } } \ No newline at end of file diff --git a/src/SlatedGameToolkit.Framework/Graphics/Textures/Texture.cs b/src/SlatedGameToolkit.Framework/Graphics/Textures/Texture.cs index 4557aa1..b1fc453 100644 --- a/src/SlatedGameToolkit.Framework/Graphics/Textures/Texture.cs +++ b/src/SlatedGameToolkit.Framework/Graphics/Textures/Texture.cs @@ -7,13 +7,19 @@ using SlatedGameToolkit.Framework.AssetSystem; namespace SlatedGameToolkit.Framework.Graphics.Textures { public class Texture : IAssetUseable, ITexture { - public readonly int width, height; + public readonly uint width, height; private bool disposed; private GLContext glContext; private uint handle; public uint Handle {get { return handle; }} + public uint Width => width; + + public uint Height => height; + + public bool SingleChanneled => false; + /// /// Creates an OpenGL Texture2D in the given GL Context. /// @@ -21,8 +27,8 @@ namespace SlatedGameToolkit.Framework.Graphics.Textures /// The openGL context to associate this with. If null, will use the currently active context. Defaults to null. public unsafe Texture(TextureData textureData, GLContext context = null) { this.glContext = context ?? WindowContextsManager.CurrentGL; - this.width = textureData.width; - this.height = textureData.height; + this.width = (uint)textureData.width; + this.height = (uint)textureData.height; uint[] handles = new uint[1]; glContext.GenTextures(1, handles); this.handle = handles[0]; @@ -32,6 +38,7 @@ namespace SlatedGameToolkit.Framework.Graphics.Textures glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear); glContext.TexParameteri(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); fixed(void* p = &textureData.data[0]) { + glContext.PixelStorei(PixelStoreParameter.UnpackAlignment, 4); glContext.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, (uint)textureData.width, (uint)textureData.height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, new IntPtr(p)); } glContext.GenerateMipmap(OpenGL.TextureTarget.Texture2D); diff --git a/src/SlatedGameToolkit.Framework/Graphics/Window/WindowContextsManager.cs b/src/SlatedGameToolkit.Framework/Graphics/Window/WindowContextsManager.cs index 0a3095b..aa24cea 100644 --- a/src/SlatedGameToolkit.Framework/Graphics/Window/WindowContextsManager.cs +++ b/src/SlatedGameToolkit.Framework/Graphics/Window/WindowContextsManager.cs @@ -16,7 +16,12 @@ namespace SlatedGameToolkit.Framework.Graphics.Window get { return current; } + + set { + value.MakeCurrent(); + } } + private static Dictionary existingWindows = new Dictionary(); /// diff --git a/src/SlatedGameToolkit.Framework/Resources/default.frag b/src/SlatedGameToolkit.Framework/Resources/default.frag index da9e2f1..15c20d9 100644 --- a/src/SlatedGameToolkit.Framework/Resources/default.frag +++ b/src/SlatedGameToolkit.Framework/Resources/default.frag @@ -4,13 +4,19 @@ out vec4 outputColor; in vec2 texCoord; in vec4 color; +uniform bool singleChanneled; uniform bool textured; uniform sampler2D texture0; void main() { if (textured) { - outputColor = texture(texture0, texCoord) * color; + if (singleChanneled) { + float singleVal = texture(texture0, vec2(texCoord.x, 1 - texCoord.y)).r; + outputColor = vec4(singleVal); + } else { + outputColor = texture(texture0, vec2(texCoord.x, 1 - texCoord.y)) * color; + } } else { outputColor = color; } diff --git a/src/SlatedGameToolkit.Framework/Resources/default.vert b/src/SlatedGameToolkit.Framework/Resources/default.vert index a02eaf5..78902c0 100644 --- a/src/SlatedGameToolkit.Framework/Resources/default.vert +++ b/src/SlatedGameToolkit.Framework/Resources/default.vert @@ -14,6 +14,5 @@ void main() { texCoord = aTexCoord; color = aColor; - gl_Position = projection * view * models * vec4(aPosition, 1.0); } \ No newline at end of file diff --git a/src/SlatedGameToolkit.Framework/Utilities/RectangleUtils.cs b/src/SlatedGameToolkit.Framework/Utilities/RectangleUtils.cs new file mode 100644 index 0000000..7ecc883 --- /dev/null +++ b/src/SlatedGameToolkit.Framework/Utilities/RectangleUtils.cs @@ -0,0 +1,16 @@ +using System.Drawing; + +namespace SlatedGameToolkit.Framework.Utilities +{ + public static class RectangleUtils + { + public static RectangleF MultiplyBy(this RectangleF rectangle, float multiplier) { + RectangleF rect = rectangle; + rect.X *= multiplier; + rect.Y *= multiplier; + rect.Width *= multiplier; + rect.Height *= multiplier; + return rect; + } + } +} \ No newline at end of file diff --git a/src/SlatedGameToolkit.Tools/Resources/Playground/earwig_factory_rg.ttf b/src/SlatedGameToolkit.Tools/Resources/Playground/earwig_factory_rg.ttf new file mode 100644 index 0000000000000000000000000000000000000000..769d55a078c0cd645a7b9172a41dcd712d12ca4e GIT binary patch literal 63900 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 literal 0 HcmV?d00001 diff --git a/src/SlatedGameToolkit.Tools/SlatedGameToolkit.Tools.csproj b/src/SlatedGameToolkit.Tools/SlatedGameToolkit.Tools.csproj index 2b669f4..736253b 100644 --- a/src/SlatedGameToolkit.Tools/SlatedGameToolkit.Tools.csproj +++ b/src/SlatedGameToolkit.Tools/SlatedGameToolkit.Tools.csproj @@ -10,6 +10,7 @@ netcoreapp3.1 1.0.0.0 A tool to help with developing a game using SlatedGameToolkit. + true diff --git a/src/SlatedGameToolkit.Tools/Utilities/Playground/MainState.cs b/src/SlatedGameToolkit.Tools/Utilities/Playground/MainState.cs index f7b61c7..ae7e089 100644 --- a/src/SlatedGameToolkit.Tools/Utilities/Playground/MainState.cs +++ b/src/SlatedGameToolkit.Tools/Utilities/Playground/MainState.cs @@ -1,14 +1,18 @@ using System; using System.Drawing; +using System.IO; using System.Numerics; +using System.Runtime.InteropServices; using SDL2; using SlatedGameToolkit.Commons.Loaders; using SlatedGameToolkit.Framework.Graphics.Render; +using SlatedGameToolkit.Framework.Graphics.Text; using SlatedGameToolkit.Framework.Graphics.Textures; using SlatedGameToolkit.Framework.Graphics.Window; using SlatedGameToolkit.Framework.Input.Devices; using SlatedGameToolkit.Framework.StateSystem; using SlatedGameToolkit.Framework.StateSystem.States; +using StbTrueTypeSharp; namespace SlatedGameToolkit.Tools.Utilities.Playground { @@ -17,6 +21,7 @@ namespace SlatedGameToolkit.Tools.Utilities.Playground private WindowContext window; private Camera2D camera; private MeshBatch renderer; + private BitmapFont font; private Texture logoTexture, fillerTexture; private RectangleMesh logo, textureTester, untextured; @@ -36,6 +41,7 @@ namespace SlatedGameToolkit.Tools.Utilities.Playground { logoTexture.Dispose(); fillerTexture.Dispose(); + font.Dispose(); renderer.Dispose(); window.Dispose(); } @@ -45,13 +51,13 @@ namespace SlatedGameToolkit.Tools.Utilities.Playground return "main state"; } - public void Initialize(StateManager manager) + public unsafe void Initialize(StateManager manager) { window = new WindowContext("SlatedGameToolkit Playground"); camera = new Camera2D(2, 2); renderer = new MeshBatch(camera); - logoTexture = TextureLoader.Load2DTexture("Resources/Playground/yhdnbgnc.png", null); + logoTexture = TextureLoader.Load2DTexture("Resources/Playground/yhdnbgnc.png"); logo = new RectangleMesh(logoTexture, Color.White); logo.Width = 0.5f; logo.Height = 0.5f; @@ -59,7 +65,7 @@ namespace SlatedGameToolkit.Tools.Utilities.Playground logo.Y = -0.25f; - fillerTexture = TextureLoader.Load2DTexture("Resources/Playground/filler.png", null); + fillerTexture = TextureLoader.Load2DTexture("Resources/Playground/filler.png"); textureTester = new RectangleMesh(fillerTexture, Color.White); textureTester.Width = 0.15f; textureTester.Height = 0.15f; @@ -70,6 +76,8 @@ namespace SlatedGameToolkit.Tools.Utilities.Playground untextured.Height = 0.1f; untextured.X = 0.25f; untextured.Y = - 0.15f; + + font = new BitmapFont("Resources/Playground/earwig_factory_rg.ttf"); } public void Render(double delta) @@ -78,6 +86,7 @@ namespace SlatedGameToolkit.Tools.Utilities.Playground renderer.Draw(logo); renderer.Draw(textureTester); renderer.Draw(untextured); + font.Draw(renderer, delta, 0.25f, -0.25f, "123", Color.White); renderer.End(); } diff --git a/tests/SlatedGameToolkit.Framework.Tests/Utilities/Collections/Caching/LRUCacheTests.cs b/tests/SlatedGameToolkit.Framework.Tests/Utilities/Collections/Caching/LRUCacheTests.cs index d76de4d..276b289 100644 --- a/tests/SlatedGameToolkit.Framework.Tests/Utilities/Collections/Caching/LRUCacheTests.cs +++ b/tests/SlatedGameToolkit.Framework.Tests/Utilities/Collections/Caching/LRUCacheTests.cs @@ -50,5 +50,25 @@ namespace SlatedGameToolkit.Framework.Tests.Utilities.Collections.Caching cache[5] = 5; Assert.IsTrue(cache.ContainsKey(2)); } + + [Test] + public void ComputeIfNonexistentTest() { + LRUCache cache = new LRUCache(4); + for (int i = 0; i < cache.MaxLength; i++) { + cache.ComputeIfNonExistent(i, (a) => a); + } + } + + [Test] + public void ComputeIfNonexistentTestExists() { + LRUCache cache = new LRUCache(4); + for (int i = 0; i < cache.MaxLength; i++) { + cache.ComputeIfNonExistent(i, (a) => a); + } + + for (int i = 0; i < cache.MaxLength; i++) { + Assert.AreEqual(i, cache.ComputeIfNonExistent(i, (a) => -a)); + } + } } } \ No newline at end of file