This commit is contained in:
2025-01-02 05:15:16 +03:00
parent 3c66a65b40
commit ac00eb18a9
96 changed files with 384134 additions and 230 deletions

View File

@@ -26,6 +26,7 @@
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
</ItemGroup>
<ItemGroup>

View File

@@ -11,7 +11,7 @@ layout (location = 3) in int aTextureId;
layout (location = 4) in mat4 aModelMatrix;
layout (location = 0) out vec2 oUV;
layout (location = 1) out int oTextureId;
layout (location = 1) flat out int oTextureId;
layout (location = 2) out vec3 oNormal;
void main()
@@ -35,7 +35,7 @@ layout (location = 0) out vec4 FragColor;
void main()
{
vec3 lightColor = vec3(iUV, 0);
vec3 lightColor = vec3(1);
FragColor = vec4(lightColor, 1.0);
if (iTextureId >= 0)
FragColor *= texture(uTexture[iTextureId], iUV);

View File

@@ -12,7 +12,7 @@ layout (location = 4) in mat4 aModel;
layout (location = 0) out vec4 oColor;
layout (location = 1) out vec2 oUV;
layout (location = 2) out int oTextureId;
layout (location = 2) flat out int oTextureId;
void main()
{

View File

@@ -0,0 +1,61 @@
// #type vertex
#version 460 core
uniform mat4 u_Projection;
uniform mat4 u_View;
layout (location = 0) in vec4 a_Position;
layout (location = 1) in vec2 a_UV;
layout (location = 2) in vec4 a_Color;
layout (location = 3) in int a_AtlasId;
layout (location = 4) in vec2 a_UnitRange;
layout (location = 5) in mat4 a_Model;
layout (location = 0) out vec4 o_Color;
layout (location = 1) out vec2 o_UV;
layout (location = 2) flat out int o_AtlasId;
layout (location = 3) flat out vec2 o_UnitRange;
void main()
{
o_Color = a_Color;
o_UV = a_UV;
o_AtlasId = a_AtlasId;
o_UnitRange = a_UnitRange;
gl_Position = u_Projection * u_View * a_Model * a_Position;
}
// #type fragment
#version 460 core
uniform sampler2D u_Atlas[16];
layout (location = 0) in vec4 i_Color;
layout (location = 1) in vec2 i_UV;
layout (location = 2) flat in int i_AtlasId;
layout (location = 3) flat in vec2 i_UnitRange;
layout (location = 0) out vec4 FragColor;
float median(float r, float g, float b) {
return max(min(r, g), min(max(r, g), b));
}
float screenPxRange() {
vec2 screenTexSize = vec2(1.0) / fwidth(i_UV);
return max(0.5 * dot(i_UnitRange, screenTexSize), 1.0);
}
void main()
{
vec3 msd = texture(u_Atlas[i_AtlasId], i_UV).rgb;
float sd = median(msd.r, msd.g, msd.b);
float screenPxDistance = screenPxRange() * (sd - 0.5);
float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
// this is a temporary solution for TODO: fix depth test self discard on overlap
if (opacity == 0.0) {
discard;
}
FragColor = mix(vec4(0), i_Color, opacity);
}

View File

@@ -0,0 +1,109 @@
using Engine.Asset.Font.Metadata;
using Engine.Graphics.Texture;
using OpenTK.Mathematics;
namespace Engine.Asset.Font;
public class Font
{
public StaticTexture AtlasTexture => _atlasTexture;
public Metadata.Metadata Metadata => _metadata;
public Vector2 UnitRange => _unitRange;
private readonly StaticTexture _atlasTexture;
private readonly Metadata.Metadata _metadata;
private readonly Dictionary<int, Glyph> _glyphs = new();
private readonly Dictionary<int, GlyphData> _glyphData = new();
private readonly Dictionary<(int, int), Kerning> _kernings = new();
private readonly Vector2 _unitRange;
public Font(StaticTexture parAtlasTexture, Metadata.Metadata parMetadata)
{
_atlasTexture = parAtlasTexture;
_metadata = parMetadata;
_unitRange = new Vector2(_metadata.Atlas.DistanceRange / _metadata.Atlas.Width,
_metadata.Atlas.DistanceRange / _metadata.Atlas.Height);
LoadGlyphs();
LoadKernings();
}
public Glyph? GetGlyph(int parUnicode)
{
return _glyphs.GetValueOrDefault(parUnicode);
}
public GlyphData? GetGlyphData(int parUnicode)
{
return _glyphData.GetValueOrDefault(parUnicode);
}
public Kerning? GetKerning(int parUnicode1, int parUnicode2)
{
return _kernings.GetValueOrDefault((parUnicode1, parUnicode2));
}
public FontIterator Iterator(string parText)
{
return new FontIterator(this, parText);
}
public Vector2 Measure(string parText)
{
var fontIterator = Iterator(parText);
_ = fontIterator.ToList();
return new Vector2(fontIterator.MaxWidth, fontIterator.MaxHeight);
}
private void LoadGlyphs()
{
foreach (var glyph in _metadata.Glyphs)
{
_glyphs.Add(glyph.Unicode, glyph);
if (glyph.PlaneBounds == null || glyph.AtlasBounds == null)
{
continue;
}
_glyphData.Add(glyph.Unicode, new GlyphData(_metadata, glyph));
}
}
private void LoadKernings()
{
foreach (var kerning in _metadata.Kerning)
{
_kernings.Add((kerning.Unicode1, kerning.Unicode2), kerning);
}
}
public record GlyphData
{
public Vector2[] Positions { get; }
public Vector2[] UVs { get; }
public GlyphData(in Metadata.Metadata parMetadata, in Glyph parGlyph)
{
var size = new Vector2(parMetadata.Atlas.Width, parMetadata.Atlas.Height);
Positions =
[
new Vector2(parGlyph.PlaneBounds!.Left, parGlyph.PlaneBounds.Bottom),
new Vector2(parGlyph.PlaneBounds.Left, parGlyph.PlaneBounds.Top),
new Vector2(parGlyph.PlaneBounds.Right, parGlyph.PlaneBounds.Bottom),
new Vector2(parGlyph.PlaneBounds.Right, parGlyph.PlaneBounds.Top),
];
UVs =
[
new Vector2(parGlyph.AtlasBounds!.Left, parGlyph.AtlasBounds.Bottom) / size,
new Vector2(parGlyph.AtlasBounds.Left, parGlyph.AtlasBounds.Top) / size,
new Vector2(parGlyph.AtlasBounds.Right, parGlyph.AtlasBounds.Bottom) / size,
new Vector2(parGlyph.AtlasBounds.Right, parGlyph.AtlasBounds.Top) / size,
];
}
}
}

View File

@@ -0,0 +1,119 @@
using System.Collections;
using OpenTK.Mathematics;
namespace Engine.Asset.Font;
public class FontIterator : IEnumerable<FontIterator.NextGlyphData>
{
public float MaxWidth => _maxWidth;
public float MaxHeight => _maxHeight;
private readonly Font _font;
private readonly string _text;
private int _currentIndex;
private int _previousCodepoint = -1;
private int _lineCharCount;
private Vector2 _cursor = Vector2.Zero;
private Vector2 _kerning = Vector2.Zero;
private float _maxWidth;
private float _maxHeight;
public FontIterator(Font parFont, string parText)
{
_font = parFont;
_text = parText;
}
private static bool IsLineBreak(int parCodepoint)
{
return parCodepoint == '\n';
}
private static bool IsTab(int parCodepoint)
{
return parCodepoint == '\t';
}
private void Tab()
{
var spaceGlyph = _font.GetGlyph(' ');
if (spaceGlyph == null)
return;
var missingSpaces = 4 - _lineCharCount % 4;
_cursor.X += missingSpaces * spaceGlyph.Advance;
}
private void LineBreak()
{
_kerning = Vector2.Zero;
_cursor.X = 0;
_cursor.Y += _font.Metadata.Metrics.LineHeight;
_lineCharCount = 0;
_previousCodepoint = -1;
}
public IEnumerator<NextGlyphData> GetEnumerator()
{
while (_currentIndex < _text.Length)
{
var codepoint = char.ConvertToUtf32(_text, _currentIndex);
if (IsLineBreak(codepoint))
{
LineBreak();
_currentIndex += 1;
continue;
}
if (IsTab(codepoint))
{
Tab();
_currentIndex += 1;
continue;
}
var glyph = _font.GetGlyph(codepoint);
if (glyph == null)
{
glyph = _font.GetGlyph('?');
if (glyph == null)
{
_currentIndex += 1;
continue;
}
}
if (_previousCodepoint != -1)
{
var kerning = _font.GetKerning(_previousCodepoint, glyph.Unicode);
if (kerning != null)
{
_kerning.X += kerning.Advance;
}
}
var glyphData = _font.GetGlyphData(glyph.Unicode);
_maxWidth = Math.Max(_maxWidth, _cursor.X + glyph.Advance);
_maxHeight = Math.Max(_maxHeight, _cursor.Y + _font.Metadata.Metrics.Ascender);
if (glyphData != null)
yield return new NextGlyphData(glyphData.Positions, glyphData.UVs, (_cursor + _kerning) * new Vector2(1, -1));
_cursor.X += glyph.Advance;
_lineCharCount++;
_previousCodepoint = glyph.Unicode;
_currentIndex += 1;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public record NextGlyphData(Vector2[] Positions, Vector2[] UVs, Vector2 Offset);
}

View File

@@ -0,0 +1,28 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Atlas))]
[Serializable]
public record Atlas
{
[JsonPropertyName("distanceRange")]
[JsonInclude]
public float DistanceRange { get; private set; }
[JsonPropertyName("size")]
[JsonInclude]
public float Size { get; private set; }
[JsonPropertyName("width")]
[JsonInclude]
public int Width { get; private set; }
[JsonPropertyName("height")]
[JsonInclude]
public int Height { get; private set; }
[JsonPropertyName("yOrigin")]
[JsonInclude]
public string YOrigin { get; private set; }
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Bounds))]
[Serializable]
public record Bounds
{
[JsonPropertyName("left")]
[JsonInclude]
public float Left { get; private set; }
[JsonPropertyName("bottom")]
[JsonInclude]
public float Bottom { get; private set; }
[JsonPropertyName("right")]
[JsonInclude]
public float Right { get; private set; }
[JsonPropertyName("top")]
[JsonInclude]
public float Top { get; private set; }
}

View File

@@ -0,0 +1,25 @@
using System.Text.Json.Serialization;
using OpenTK.Mathematics;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Glyph))]
[Serializable]
public record Glyph
{
[JsonPropertyName("unicode")]
[JsonInclude]
public int Unicode { get; private set; }
[JsonPropertyName("advance")]
[JsonInclude]
public float Advance { get; private set; }
[JsonPropertyName("planeBounds")]
[JsonInclude]
public Bounds? PlaneBounds { get; private set; }
[JsonPropertyName("atlasBounds")]
[JsonInclude]
public Bounds? AtlasBounds { get; private set; }
}

View File

@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Kerning))]
[Serializable]
public record Kerning
{
[JsonPropertyName("unicode1")]
[JsonInclude]
public int Unicode1 { get; private set; }
[JsonPropertyName("unicode2")]
[JsonInclude]
public int Unicode2 { get; private set; }
[JsonPropertyName("advance")]
[JsonInclude]
public float Advance { get; private set; }
}

View File

@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Metadata))]
[Serializable]
public record Metadata
{
[JsonPropertyName("atlas")]
[JsonInclude]
public Atlas Atlas { get; private set; }
[JsonPropertyName("metrics")]
[JsonInclude]
public Metrics Metrics { get; private set; }
[JsonPropertyName("glyphs")]
[JsonInclude]
public Glyph[] Glyphs { get; private set; }
[JsonPropertyName("kerning")]
[JsonInclude]
public Kerning[] Kerning { get; private set; }
}

View File

@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
namespace Engine.Asset.Font.Metadata;
[JsonSerializable(typeof(Metrics))]
[Serializable]
public record Metrics
{
[JsonPropertyName("emSize")]
[JsonInclude]
public float EmSize { get; private set; }
[JsonPropertyName("lineHeight")]
[JsonInclude]
public float LineHeight { get; private set; }
[JsonPropertyName("ascender")]
[JsonInclude]
public float Ascender { get; private set; }
[JsonPropertyName("descender")]
[JsonInclude]
public float Descender { get; private set; }
[JsonPropertyName("underlineY")]
[JsonInclude]
public float UnderlineY { get; private set; }
[JsonPropertyName("underlineThickness")]
public float UnderlineThickness { get; private set; }
}

View File

@@ -3,19 +3,19 @@ using Engine.Graphics.Texture;
namespace Engine.Asset;
public class Image<T>(T[,] parPixels)
where T : struct, IPixel
public class Image<T> where T : struct, IPixel
{
public int Width { get; }
public int Height { get; }
public T[,] Pixels { get; } = parPixels;
public T[,] Pixels { get; }
public T this[int parY, int parX] => Pixels[parY, parX];
public Image(int parWidth, int parHeight) : this(new T[parHeight, parWidth])
public Image(int parWidth, int parHeight)
{
Width = parWidth;
Height = parHeight;
Pixels = new T[parHeight, parWidth];
}
public DynamicTexture ToDynamicTexture()

View File

@@ -1,8 +1,16 @@
using System.Diagnostics;
using System.Text;
using Engine.Asset;
using Engine.Asset.Font;
using Engine.Asset.Mesh;
using Engine.Graphics;
using Engine.Graphics.Pixel;
using Engine.Graphics.Shader;
using Engine.Graphics.Texture;
using Engine.Input;
using Engine.Resource;
using Engine.Resource.Loader;
using Engine.Scene;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
@@ -50,9 +58,15 @@ public sealed class Engine
private IInputHandler? _inputHandler;
private IPresenter? _presenter;
internal ResourceManager EngineResourceManager => _engineResourceManager;
public IResourceManager AssetResourceManager => _assetResourceManager;
private readonly ResourceManager _engineResourceManager;
private readonly ResourceManager _assetResourceManager;
private Thread? _updateThread;
public Engine(int parWidth, int parHeight, bool parHeadless, string parTitle, ILogger parLogger)
public Engine(int parWidth, int parHeight, bool parHeadless, string parTitle, string parAssetFolder,
ILogger parLogger)
{
if (Instance != null)
{
@@ -61,20 +75,59 @@ public sealed class Engine
Instance = this;
Log.Logger = parLogger;
_logger = Log.ForContext<Engine>();
var settings = new NativeWindowSettings
{
ClientSize = parHeadless ? new Vector2i(1, 1) : new Vector2i(parWidth, parHeight),
Title = parTitle,
StartVisible = !parHeadless,
APIVersion = new Version(4, 6),
Profile = ContextProfile.Core
Profile = ContextProfile.Core,
DepthBits = 0,
StencilBits = 0
};
Renderer = new Renderer(parWidth, parHeight, settings);
Window = new Window(this, Renderer.NativeWindow, parHeadless);
_engineResourceManager = CreateEngineResourceManager();
_assetResourceManager = CreateAssetResourceManager(parAssetFolder);
Log.Logger = parLogger;
_logger = Log.ForContext<Engine>();
_logger.Information("Created asset resource manager in {AssetFolder}", parAssetFolder);
Renderer = new Renderer(this, parWidth, parHeight, settings);
Window = new Window(this, Renderer.NativeWindow, parHeadless);
}
private static ResourceManager CreateEngineResourceManager()
{
var memoryStreamProvider = new MemoryResourceStreamProvider();
memoryStreamProvider.AddResource("shader/mesh", Encoding.UTF8.GetBytes(ShaderResource.Mesh));
memoryStreamProvider.AddResource("shader/quad", Encoding.UTF8.GetBytes(ShaderResource.Quad));
memoryStreamProvider.AddResource("shader/text", Encoding.UTF8.GetBytes(ShaderResource.Text));
var resourceManager = new ResourceManager(memoryStreamProvider);
RegisterDefaultLoaders(resourceManager);
return resourceManager;
}
private static ResourceManager CreateAssetResourceManager(string parAssetFolder)
{
var filesystemStreamProvider = new FilesystemResourceStreamProvider(parAssetFolder);
var resourceManager = new ResourceManager(filesystemStreamProvider);
RegisterDefaultLoaders(resourceManager);
return resourceManager;
}
private static void RegisterDefaultLoaders(ResourceManager parResourceManager)
{
parResourceManager.RegisterLoader<Program>(new ProgramLoader());
parResourceManager.RegisterLoader<Mesh>(new MeshLoader());
parResourceManager.RegisterLoader<Image<Rgba8>>(new ImageLoader());
parResourceManager.RegisterLoader<Texture>(new TextureLoader());
parResourceManager.RegisterLoader<Font>(new FontLoader());
}
private readonly object _sceneLock = new();
@@ -89,6 +142,11 @@ public sealed class Engine
_updateThread.Join();
}
public void Close()
{
Presenter?.Exit();
}
private void RunRender()
{
while (!Presenter?.IsExiting ?? false)
@@ -114,18 +172,11 @@ public sealed class Engine
SceneManager.Render();
}
Renderer.QuadRenderer.Render(projection, view);
Renderer.QuadRenderer.Reset();
Renderer.GlobalMeshRenderer.Render(projection, view);
Renderer.GlobalMeshRenderer.Reset();
Renderer.EndFrame(projection, view);
Presenter!.Present(Renderer.RenderTexture);
Presenter!.Render();
#if DEBUG
Debug.RenderDocEndFrame();
#endif
@@ -144,7 +195,14 @@ public sealed class Engine
lock (_sceneLock)
{
SceneManager.Update(deltaTime);
try
{
SceneManager.Update(deltaTime);
}
catch (Exception ex)
{
_logger.Error(ex, "Exception in scene update");
}
}
deltaTime = timer.Elapsed.TotalSeconds;

View File

@@ -13,6 +13,7 @@ public sealed class EngineBuilder
private bool _headless;
private int _width = 1;
private int _height = 1;
private string _assetFolder = "./";
private Func<Engine, IPresenter>? _presenterFunc;
private Func<Engine, IInputHandler>? _inputHandlerFunc;
@@ -51,6 +52,12 @@ public sealed class EngineBuilder
return this;
}
public EngineBuilder AssetFolder(string parAssetFolder)
{
_assetFolder = parAssetFolder;
return this;
}
public EngineBuilder Presenter(Func<Engine, IPresenter> parPresenterFunc)
{
_presenterFunc = parPresenterFunc;
@@ -94,7 +101,7 @@ public sealed class EngineBuilder
public Engine Build()
{
var logger = BuildLogger();
var engine = new Engine(_width, _height, _headless, _title, logger);
var engine = new Engine(_width, _height, _headless, _title, _assetFolder, logger);
var presenter = _presenterFunc?.Invoke(engine);
if (presenter != null)

View File

@@ -1,6 +1,7 @@
using Engine.Graphics.Pixel;
using Engine.Graphics.Render.Mesh;
using Engine.Graphics.Render.Quad;
using Engine.Graphics.Render.Text;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
@@ -8,18 +9,23 @@ namespace Engine.Graphics;
public class GenericRenderer : IRenderer
{
public QuadRenderer QuadRenderer => _quadRenderer ??= new QuadRenderer(1024 * 8);
public GlobalMeshRenderer GlobalMeshRenderer => _globalMeshRenderer ??= new GlobalMeshRenderer(1024);
public QuadRenderer QuadRenderer => _quadRenderer ??= new QuadRenderer(_engine, 1024 * 8);
public AnyMeshRenderer AnyMeshRenderer => _globalMeshRenderer ??= new AnyMeshRenderer(_engine, 1024);
public TextRenderer TextRenderer => _textRenderer ??= new TextRenderer(_engine, 1024 * 8);
private QuadRenderer? _quadRenderer;
private GlobalMeshRenderer? _globalMeshRenderer;
private AnyMeshRenderer? _globalMeshRenderer;
private TextRenderer? _textRenderer;
private readonly Engine _engine;
internal readonly Framebuffer.Framebuffer _framebuffer;
private bool _frameStarted;
public GenericRenderer(int parWidth, int parHeight)
public GenericRenderer(Engine parEngine, int parWidth, int parHeight)
{
_engine = parEngine;
_framebuffer = Framebuffer.Framebuffer.Builder(parWidth, parHeight)
.AddColorAttachment<Rgba8>()
.AddDepthAttachment()
@@ -38,14 +44,17 @@ public class GenericRenderer : IRenderer
_framebuffer.Bind();
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.ClearColor(0.0f, 0.0f, 0.0f, 0.0f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
QuadRenderer.Render(parProjectionMatrix, parViewMatrix);
QuadRenderer.Reset();
GlobalMeshRenderer.Render(parProjectionMatrix, parViewMatrix);
GlobalMeshRenderer.Reset();
AnyMeshRenderer.Render(parProjectionMatrix, parViewMatrix);
AnyMeshRenderer.Reset();
TextRenderer.Render(parProjectionMatrix, parViewMatrix);
TextRenderer.Reset();
_framebuffer.Unbind();

View File

@@ -13,4 +13,5 @@ public interface IPresenter : IUpdate, IRender
event Action<ResizeEventArgs> Resize;
void Present(IConstTexture parTexture);
void Exit();
}

View File

@@ -1,18 +1,20 @@
namespace Engine.Graphics.Pipeline;
public class RenderLayer
public class RenderLayer : IComparable<RenderLayer>
{
public static readonly RenderLayer DEFAULT = new("default");
public static readonly RenderLayer HUD = new("hud");
public static readonly RenderLayer OVERLAY = new("overlay");
public static readonly RenderLayer DEFAULT = new("default", 0);
public static readonly RenderLayer OVERLAY = new("overlay", 1);
public static readonly RenderLayer HUD = new("hud", 2);
public static readonly IReadOnlyList<RenderLayer> ALL = new List<RenderLayer> { DEFAULT, HUD, OVERLAY }.AsReadOnly();
public static readonly IReadOnlyList<RenderLayer> ALL = new List<RenderLayer> { DEFAULT, OVERLAY, HUD }.AsReadOnly();
public string Name { get; }
private readonly int _order;
private RenderLayer(string parName)
private RenderLayer(string parName, int parOrder)
{
Name = parName;
_order = parOrder;
}
public override string ToString()
@@ -20,6 +22,11 @@ public class RenderLayer
return Name;
}
public int CompareTo(RenderLayer? parOther)
{
return _order.CompareTo(parOther?._order);
}
public override int GetHashCode()
{
return Name.GetHashCode();

View File

@@ -50,7 +50,8 @@ public abstract class InstancedRenderer<C, I>
return;
}
_instanceVertexBuffer.UploadData(_instanceVertices, _queuedInstanceCount);
if (DataChanged())
_instanceVertexBuffer.UploadData(_instanceVertices, _queuedInstanceCount);
_vertexArray.Bind();
_program.Bind();
@@ -71,4 +72,9 @@ public abstract class InstancedRenderer<C, I>
protected virtual void SetAdditionalUniforms(Program parProgram)
{
}
protected virtual bool DataChanged()
{
return true;
}
}

View File

@@ -3,23 +3,23 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Mesh;
public class GlobalMeshRenderer(int parMaxInstanceCount)
public class AnyMeshRenderer(Engine parEngine, int parMaxInstanceCount)
{
private readonly Dictionary<Asset.Mesh.Mesh, MeshRenderer> _meshRenderers = new();
private readonly HashSet<Asset.Mesh.Mesh> _frameMeshes = [];
private readonly Program _program = ProgramLoader.LoadFromSource(ShaderResource.Mesh);
private readonly Program _program = parEngine.EngineResourceManager.Load<Program>("shader/mesh");
public void Commit(Asset.Mesh.Mesh parMesh, Matrix4 parModelMatrix)
public void Commit(Asset.Mesh.Mesh parMesh, Matrix4 parModelMatrix, Texture.Texture? parAlbedo = null)
{
if (_meshRenderers.TryGetValue(parMesh, out var meshRenderer))
{
meshRenderer.Commit(parModelMatrix);
meshRenderer.Commit(parModelMatrix, parAlbedo);
}
else
{
var newMeshRenderer = new MeshRenderer(parMesh, parMaxInstanceCount, _program);
newMeshRenderer.Commit(parModelMatrix);
newMeshRenderer.Commit(parModelMatrix, parAlbedo);
_meshRenderers.Add(parMesh, newMeshRenderer);
}
@@ -42,11 +42,11 @@ public class GlobalMeshRenderer(int parMaxInstanceCount)
meshRenderer.Reset();
}
var meshes = _meshRenderers.Keys;
var unusedMeshes = meshes.Where(parMesh => !_frameMeshes.Contains(parMesh));
foreach (var unusedMesh in unusedMeshes)
var meshes = _meshRenderers.Keys.ToList();
foreach (var mesh in meshes)
{
_meshRenderers.Remove(unusedMesh);
if (!_frameMeshes.Contains(mesh))
_meshRenderers.Remove(mesh);
}
_frameMeshes.Clear();

View File

@@ -8,4 +8,9 @@ public struct MeshInstanceVertex : IVertex
{
[Vertex(VertexAttribType.Int)] public int _textureId;
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
public override int GetHashCode()
{
return HashCode.Combine(_textureId, _modelMatrix);
}
}

View File

@@ -17,6 +17,9 @@ public class MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program
private readonly TextureUnitMap _textureUnitMap = new(16);
private readonly int[] _textureUnitIndices = new int[16];
private int _frameHash;
private int _previousHash;
public void Commit(Matrix4 parModelMatrix, Texture.Texture? parTexture = null)
{
if (_queuedInstanceCount >= _instanceCount)
@@ -32,6 +35,9 @@ public class MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program
_instanceVertices[_queuedInstanceCount]._textureId = textureId;
_instanceVertices[_queuedInstanceCount]._modelMatrix = parModelMatrix;
_frameHash = HashCode.Combine(_frameHash, _instanceVertices[_queuedInstanceCount]);
_queuedInstanceCount++;
}
@@ -45,4 +51,17 @@ public class MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program
parProgram.SetUniform("uTexture", _textureUnitIndices);
}
protected override bool DataChanged()
{
return _frameHash != _previousHash;
}
public override void Reset()
{
base.Reset();
_textureUnitMap.Reset();
_previousHash = _frameHash;
_frameHash = 0;
}
}

View File

@@ -9,4 +9,9 @@ public struct QuadInstanceVertex : IVertex
[Vertex(VertexAttribType.Float, 4)] public Vector4 _color;
[Vertex(VertexAttribType.Int)] public int _textureId;
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
public override int GetHashCode()
{
return HashCode.Combine(_color, _textureId, _modelMatrix);
}
}

View File

@@ -8,16 +8,20 @@ namespace Engine.Graphics.Render.Quad;
public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVertex>
{
private readonly TextureUnitMap _textureUnitMap = new(16);
private readonly int[] _textureUnitIndices = new int[16];
public QuadRenderer(int parInstanceCount)
: base(PrimitiveType.Triangles, parInstanceCount, [0, 1, 2, 2, 3, 0], [
new QuadCommonVertex { _position = new Vector3(-0.5f, -0.5f, 0), _uv = new Vector2(0, 1) },
new QuadCommonVertex { _position = new Vector3(0.5f, -0.5f, 0), _uv = new Vector2(1, 1) },
new QuadCommonVertex { _position = new Vector3(0.5f, 0.5f, 0), _uv = new Vector2(1, 0) },
new QuadCommonVertex { _position = new Vector3(-0.5f, 0.5f, 0), _uv = new Vector2(0, 0) }
private int _frameHash;
private int _previousHash;
public QuadRenderer(Engine parEngine, int parInstanceCount)
: base(PrimitiveType.Triangles, parInstanceCount, [0, 2, 1, 2, 3, 1], [
new QuadCommonVertex { _position = new Vector3(-0.5f, -0.5f, 0), _uv = new Vector2(0, 0) },
new QuadCommonVertex { _position = new Vector3(-0.5f, 0.5f, 0), _uv = new Vector2(0, 1) },
new QuadCommonVertex { _position = new Vector3(0.5f, -0.5f, 0), _uv = new Vector2(1, 0) },
new QuadCommonVertex { _position = new Vector3(0.5f, 0.5f, 0), _uv = new Vector2(1, 1) },
],
ProgramLoader.LoadFromSource(ShaderResource.Quad))
parEngine.EngineResourceManager.Load<Program>("shader/quad"))
{
}
@@ -37,6 +41,9 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
_instanceVertices[_queuedInstanceCount]._modelMatrix = parModelMatrix;
_instanceVertices[_queuedInstanceCount]._color = parColor;
_instanceVertices[_queuedInstanceCount]._textureId = textureId;
_frameHash = HashCode.Combine(_frameHash, _instanceVertices[_queuedInstanceCount]);
_queuedInstanceCount++;
}
@@ -51,9 +58,16 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
parProgram.SetUniform("uTexture", _textureUnitIndices);
}
protected override bool DataChanged()
{
return _frameHash != _previousHash;
}
public override void Reset()
{
base.Reset();
_textureUnitMap.Reset();
_previousHash = _frameHash;
_frameHash = 0;
}
}

View File

@@ -0,0 +1,13 @@
using Engine.Graphics.Buffer.Vertex;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
public struct GlyphCommonVertex : IVertex
{
[Vertex(VertexAttribType.Float, 4)] public Vector4 _color;
[Vertex(VertexAttribType.Int)] public int _atlasId;
[Vertex(VertexAttribType.Float, 2)] public Vector2 _unitRange;
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
}

View File

@@ -0,0 +1,15 @@
using Engine.Graphics.Buffer.Vertex;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
public struct GlyphVertex : IVertex
{
[Vertex(VertexAttribType.Float, 2)] public Vector2 _position;
[Vertex(VertexAttribType.Float, 2)] public Vector2 _uv;
[Vertex(VertexAttribType.Float, 4)] public Vector4 _color;
[Vertex(VertexAttribType.Int)] public int _atlasId;
[Vertex(VertexAttribType.Float, 2)] public Vector2 _unitRange;
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
}

View File

@@ -0,0 +1,125 @@
using Engine.Asset.Font;
using Engine.Graphics.Buffer;
using Engine.Graphics.Shader;
using Engine.Graphics.Texture;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
public class TextRenderer
{
private readonly IndexBuffer _indexBuffer;
private readonly VertexArray _vertexArray;
private readonly VertexBuffer<GlyphVertex> _glyphVertexBuffer;
// private readonly VertexBuffer<GlyphCommonVertex> _glyphCommonVertexBuffer;
private readonly Program _program;
private readonly int _characterCount;
private int _queuedCharacterCount;
private readonly TextureUnitMap _textureUnitMap = new(16);
private readonly int[] _textureUnitIndices = new int[16];
private readonly GlyphVertex[] _glyphVertices;
// private readonly GlyphCommonVertex[] _glyphCommonVertices;
public TextRenderer(Engine parEngine, int parCharacterCount)
{
_characterCount = parCharacterCount;
_glyphVertices = new GlyphVertex[parCharacterCount * 4];
// _glyphCommonVertices = new GlyphCommonVertex[parCharacterCount];
_program = parEngine.EngineResourceManager.Load<Program>("shader/text");
_indexBuffer = new IndexBuffer(CreateIndices(_characterCount));
_glyphVertexBuffer = new VertexBuffer<GlyphVertex>(_characterCount * 4,
BufferStorageFlags.DynamicStorageBit);
// _glyphCommonVertexBuffer = new VertexBuffer<GlyphCommonVertex>(_characterCount,
// BufferStorageFlags.DynamicStorageBit);
_vertexArray = new VertexArray();
_vertexArray.BindIndexBuffer(_indexBuffer);
_vertexArray.BindVertexBuffer(_glyphVertexBuffer, 0, 0);
// _vertexArray.BindVertexBuffer(_glyphCommonVertexBuffer, 1, 1);
}
public void Commit(Font parFont, string parText, Vector4 parColor, in Matrix4 parModelMatrix)
{
if (_queuedCharacterCount >= _characterCount)
{
throw new InvalidOperationException("Character count exceeded");
}
var textureId = _textureUnitMap.GetUnit(parFont.AtlasTexture);
var fontIterator = parFont.Iterator(parText);
foreach (var glyphData in fontIterator)
{
for (var i = 0; i < 4; i++)
{
_glyphVertices[_queuedCharacterCount * 4 + i]._position = glyphData.Positions[i] + glyphData.Offset;
_glyphVertices[_queuedCharacterCount * 4 + i]._uv = glyphData.UVs[i];
_glyphVertices[_queuedCharacterCount * 4 + i]._color = parColor;
_glyphVertices[_queuedCharacterCount * 4 + i]._atlasId = textureId;
_glyphVertices[_queuedCharacterCount * 4 + i]._unitRange = parFont.UnitRange;
_glyphVertices[_queuedCharacterCount * 4 + i]._modelMatrix = parModelMatrix;
}
_queuedCharacterCount++;
}
}
public void Render(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix)
{
if (_queuedCharacterCount <= 0)
{
return;
}
_glyphVertexBuffer.UploadData(_glyphVertices, _queuedCharacterCount * 4);
// _glyphCommonVertexBuffer.UploadData(_glyphCommonVertices, _queuedCharacterCount);
_vertexArray.Bind();
_program.Bind();
_program.SetUniform("u_Projection", in parProjectionMatrix);
_program.SetUniform("u_View", in parViewMatrix);
foreach (var (texture, unit) in _textureUnitMap.Textures)
{
texture.BindUnit(unit);
_textureUnitIndices[unit] = unit;
}
_program.SetUniform("u_Atlas", _textureUnitIndices);
GL.DrawElements(PrimitiveType.Triangles, _queuedCharacterCount * 6, DrawElementsType.UnsignedInt, 0);
}
public void Reset()
{
_textureUnitMap.Reset();
_queuedCharacterCount = 0;
}
private static uint[] CreateIndices(int parCharacterCount)
{
var indices = new uint[parCharacterCount * 6];
for (var i = 0; i < parCharacterCount; i++)
{
var index = i * 6;
var offset = i * 4;
indices[index + 0] = (uint)offset + 0;
indices[index + 1] = (uint)offset + 2;
indices[index + 2] = (uint)offset + 1;
indices[index + 3] = (uint)offset + 2;
indices[index + 4] = (uint)offset + 3;
indices[index + 5] = (uint)offset + 1;
}
return indices;
}
}

View File

@@ -1,10 +1,11 @@
using Engine.Graphics.Pipeline;
using System.Collections.Generic;
using Engine.Graphics.Pipeline;
using Engine.Graphics.Pixel;
using Engine.Graphics.Render.Mesh;
using Engine.Graphics.Render.Quad;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using OpenTK.Windowing.Desktop;
using Serilog;
namespace Engine.Graphics;
@@ -13,12 +14,22 @@ public class Renderer
internal Framebuffer.Framebuffer RenderFramebuffer => _framebuffer;
internal Texture.Texture RenderTexture => _framebuffer.TextureInternal!;
public QuadRenderer QuadRenderer { get; }
public GlobalMeshRenderer GlobalMeshRenderer { get; }
private QuadRenderer QuadRenderer { get; }
public int ViewportWidth => _framebuffer.Width;
public int ViewportHeight => _framebuffer.Height;
private readonly Dictionary<RenderLayer, GenericRenderer> _renderers = new();
private readonly SortedDictionary<RenderLayer, GenericRenderer> _renderers = new();
public GenericRenderer this[RenderLayer parRenderLayer]
{
get
{
if (_renderers.TryGetValue(parRenderLayer, out var renderer))
return renderer;
throw new InvalidOperationException($"Renderer for layer {parRenderLayer} not found");
}
}
internal NativeWindow NativeWindow { get; }
@@ -27,9 +38,10 @@ public class Renderer
private readonly Queue<Action> _scheduleActions = new();
public Renderer(int parWidth, int parHeight, NativeWindowSettings parSettings)
public Renderer(Engine parEngine, int parWidth, int parHeight, NativeWindowSettings parSettings)
{
Thread.CurrentThread.Name = "RendererThread";
_renderThread = Thread.CurrentThread;
_renderThread.Name = "RendererThread";
#if DEBUG
Debug.InitializeRenderDoc();
@@ -39,15 +51,16 @@ public class Renderer
InitializeOpenGl(parWidth, parHeight);
_renderThread = Thread.CurrentThread;
_framebuffer = Framebuffer.Framebuffer.Builder(parWidth, parHeight)
.AddColorAttachment<Rgb8>()
.AddDepthAttachment()
.AddColorAttachment<Rgba8>()
.Build();
QuadRenderer = new QuadRenderer(1024 * 8);
GlobalMeshRenderer = new GlobalMeshRenderer(1024);
QuadRenderer = new QuadRenderer(parEngine, 1);
foreach (var layer in RenderLayer.ALL)
{
_renderers.Add(layer, new GenericRenderer(parEngine, parWidth, parHeight));
}
}
private void InitializeOpenGl(int parWidth, int parHeight)
@@ -57,6 +70,7 @@ public class Renderer
#endif
GL.Enable(EnableCap.DepthTest);
// GL.Enable(EnableCap.CullFace);
// GL.Enable(EnableCap.FramebufferSrgb);
@@ -81,39 +95,63 @@ public class Renderer
_scheduleActions.Enqueue(parAction);
}
internal Task<T> Schedule<T>(Func<T> parAction)
{
var completionSource = new TaskCompletionSource<T>();
Schedule(() =>
{
try
{
completionSource.SetResult(parAction());
}
catch (Exception ex)
{
completionSource.SetException(ex);
}
});
return completionSource.Task;
}
internal void StartFrame()
{
EnsureRenderThread();
RunScheduledActions();
_framebuffer.Bind();
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit);
// foreach (var renderer in _renderers.Values)
// {
// renderer.StartFrame();
// }
foreach (var renderer in _renderers.Values)
{
renderer.StartFrame();
}
}
internal void EndFrame(in Matrix4 parViewMatrix, in Matrix4 parProjectionMatrix)
{
EnsureRenderThread();
// foreach (var renderer in _renderers.Values)
// {
// renderer.EndFrame(in parViewMatrix, in parProjectionMatrix);
// }
//
// foreach (var renderer in _renderers.Values)
// {
// QuadRenderer.Commit(Matrix4.CreateScale(2f), Vector4.One, renderer._framebuffer.TextureInternal);
// }
//
// QuadRenderer.Render(in parProjectionMatrix, in parViewMatrix);
// QuadRenderer.Reset();
foreach (var renderer in _renderers.Values)
{
renderer.EndFrame(in parViewMatrix, in parProjectionMatrix);
}
_framebuffer.Bind();
// GL.Disable(EnableCap.DepthTest);
// GL.Disable(EnableCap.CullFace);
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit);
foreach (var renderer in _renderers.Values)
{
QuadRenderer.Commit(Matrix4.CreateScale(2f, -2f, 1f), Vector4.One, renderer._framebuffer.TextureInternal);
QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity);
QuadRenderer.Reset();
}
// GL.Enable(EnableCap.DepthTest);
// GL.Enable(EnableCap.CullFace);
_framebuffer.Unbind();
}
@@ -122,6 +160,11 @@ public class Renderer
{
_framebuffer.Resize(parWidth, parHeight);
GL.Viewport(0, 0, parWidth, parHeight);
foreach (var renderer in _renderers.Values)
{
renderer.Resize(parWidth, parHeight);
}
}
private void RunScheduledActions()

View File

@@ -125,6 +125,8 @@ public class Program : OpenGlObject
if (linkStatus == 0)
{
var log = GL.GetProgramInfoLog(programId);
GL.DeleteProgram(programId);
throw new ShaderLinkException(log);
}
@@ -133,11 +135,15 @@ public class Program : OpenGlObject
if (validateStatus == 0)
{
var log = GL.GetProgramInfoLog(programId);
GL.DeleteProgram(programId);
throw new ShaderValidationException(log);
}
}
finally
{
GL.DetachShader(programId, parVertexShader);
GL.DetachShader(programId, parFragmentShader);
GL.DeleteShader(parVertexShader);
GL.DeleteShader(parFragmentShader);
}

View File

@@ -123,7 +123,6 @@ public abstract class Texture : OpenGlObject, ITexture
public void BindUnit(int parUnit = 0)
{
GL.ActiveTexture(TextureUnit.Texture0 + parUnit);
GL.BindTextureUnit(parUnit, Handle);
}

View File

@@ -1,9 +1,12 @@
using Engine.Scene;
using OpenTK.Mathematics;
namespace Engine.Input;
public interface IInputHandler : IUpdate
{
Vector2 MousePosition { get; }
bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode);
bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode);

View File

@@ -1,16 +1,25 @@
using OpenTK.Windowing.GraphicsLibraryFramework;
using OpenTK.Mathematics;
using OpenTK.Windowing.GraphicsLibraryFramework;
namespace Engine.Input;
public class WindowInputHandler(Window parWindow) : IInputHandler
{
public Vector2 MousePosition => parWindow.NativeWindow.MouseState.Position;
private KeyboardState _previousKeyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
private KeyboardState _keyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
private MouseState _previousMouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
private MouseState _mouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
public void Update(double parDeltaTime)
{
_previousKeyboardState = _keyboardState;
_keyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
_previousMouseState = _mouseState;
_mouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
}
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode)
@@ -26,12 +35,13 @@ public class WindowInputHandler(Window parWindow) : IInputHandler
public bool IsMouseButtonPressed(MouseButtonCode parButtonCode)
{
return parWindow.NativeWindow.MouseState.IsButtonDown(MapMouseButtonCode(parButtonCode));
return _mouseState.IsButtonDown(MapMouseButtonCode(parButtonCode));
}
public bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode)
{
return parWindow.NativeWindow.MouseState.IsButtonPressed(MapMouseButtonCode(parButtonCode));
return _mouseState.IsButtonDown(MapMouseButtonCode(parButtonCode)) &&
!_previousMouseState.IsButtonDown(MapMouseButtonCode(parButtonCode));
}
private static MouseButton MapMouseButtonCode(MouseButtonCode parButton)

View File

@@ -0,0 +1,9 @@
namespace Engine.Resource;
public class FilesystemResourceStreamProvider(string parBasePath) : IResourceStreamProvider
{
public Stream GetStream(string parPath)
{
return File.OpenRead(Path.Combine(parBasePath, parPath));
}
}

View File

@@ -0,0 +1,6 @@
namespace Engine.Resource;
public interface IResourceLoader
{
object Load(string parPath, IResourceStreamProvider parStreamProvider);
}

View File

@@ -0,0 +1,6 @@
namespace Engine.Resource;
public interface IResourceManager
{
T Load<T>(string parPath) where T : class;
}

View File

@@ -0,0 +1,6 @@
namespace Engine.Resource;
public interface IResourceStreamProvider
{
Stream GetStream(string parPath);
}

View File

@@ -0,0 +1,26 @@
using System.Text.Json;
using Engine.Asset.Font;
using Engine.Asset.Font.Metadata;
namespace Engine.Resource.Loader;
public class FontLoader : IResourceLoader
{
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var metadataPath = Path.Combine(parPath, "metadata.json");
using var metadataStream = parStreamProvider.GetStream(metadataPath);
var metadata = JsonSerializer.Deserialize<Metadata>(metadataStream);
if (metadata == null)
{
throw new InvalidOperationException($"Failed to load metadata from {metadataPath}");
}
var atlasPath = Path.Combine(parPath, "atlas.png");
using var atlasStream = parStreamProvider.GetStream(atlasPath);
var atlasTexture = ImageLoader.Load(atlasStream).ToStaticTexture();
return new Font(atlasTexture, metadata);
}
}

View File

@@ -0,0 +1,35 @@
using System.Runtime.InteropServices;
using Engine.Graphics.Pixel;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace Engine.Resource.Loader;
public class ImageLoader : IResourceLoader
{
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
using var stream = parStreamProvider.GetStream(parPath);
return Load(stream);
}
internal static Asset.Image<Rgba8> Load(Stream parStream)
{
var sharpImage = SixLabors.ImageSharp.Image.Load<Rgba32>(parStream);
if (sharpImage == null)
{
throw new InvalidOperationException($"Failed to load image from stream");
}
sharpImage.Mutate(parImageContext => parImageContext.Flip(FlipMode.Vertical));
var image = new Asset.Image<Rgba8>(sharpImage.Width, sharpImage.Height);
var span = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetArrayDataReference(image.Pixels),
image.Pixels.Length * Marshal.SizeOf<Rgba8>());
sharpImage.CopyPixelDataTo(span);
return image;
}
}

View File

@@ -0,0 +1,19 @@
using Engine.Asset.Mesh.Loader;
namespace Engine.Resource.Loader;
public class MeshLoader : IResourceLoader
{
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var extension = Path.GetExtension(parPath);
using var reader = new StreamReader(parStreamProvider.GetStream(parPath));
return extension switch
{
".obj" => ObjMeshLoader.Load(reader),
".stl" => StlMeshLoader.Load(reader),
_ => throw new InvalidOperationException($"Unsupported mesh format: {extension}")
};
}
}

View File

@@ -1,21 +1,24 @@
using System.Text;
using System.Text.RegularExpressions;
using Engine.Graphics.Shader;
namespace Engine.Graphics.Shader;
namespace Engine.Resource.Loader;
public static partial class ProgramLoader
public partial class ProgramLoader : IResourceLoader
{
[GeneratedRegex(@"^//\s+#type\s+(?<type>[a-z]+)$", RegexOptions.Compiled)]
private static partial Regex TypeRegex();
public static Program LoadFromSource(string parSource)
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var textReader = new StreamReader(parStreamProvider.GetStream(parPath));
var vertexSource = new StringBuilder();
var fragmentSource = new StringBuilder();
var inFragment = false;
var inVertex = false;
foreach (var line in parSource.Split('\n').Select(parLine => parLine.TrimEnd()))
while (textReader.ReadLine() is { } line)
{
var match = TypeRegex().Match(line);
if (match.Success)

View File

@@ -0,0 +1,12 @@
namespace Engine.Resource.Loader;
public class TextureLoader : IResourceLoader
{
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
using var stream = parStreamProvider.GetStream(parPath);
var image = ImageLoader.Load(stream);
return image.ToStaticTexture();
}
}

View File

@@ -0,0 +1,16 @@
namespace Engine.Resource;
public class MemoryResourceStreamProvider : IResourceStreamProvider
{
private readonly Dictionary<string, byte[]> _resources = new();
public Stream GetStream(string parPath)
{
return new MemoryStream(_resources[parPath]);
}
internal void AddResource(string parPath, byte[] parData)
{
_resources.Add(parPath, parData);
}
}

View File

@@ -0,0 +1,36 @@
namespace Engine.Resource;
public class ResourceHandle<T>
{
public T? Value
{
get
{
if (_isLoaded)
{
return _value!;
}
if (!_task.IsCompleted)
{
return _defaultValue;
}
_value = _task.Result;
_isLoaded = true;
return _value!;
}
}
private readonly T? _defaultValue;
private readonly Task<T> _task;
private T? _value;
private bool _isLoaded;
public ResourceHandle(Task<T> parTask, T? parDefaultValue)
{
_defaultValue = parDefaultValue;
_task = parTask;
}
}

View File

@@ -0,0 +1,71 @@
namespace Engine.Resource;
public class ResourceManager : IResourceManager
{
internal IResourceStreamProvider StreamProvider => _streamProvider;
private readonly IResourceStreamProvider _streamProvider;
private readonly Dictionary<Type, IResourceLoader> _loaders = new();
private readonly Dictionary<Type, ResourceStorage> _storages = new();
public ResourceManager(IResourceStreamProvider parStreamProvider)
{
_streamProvider = parStreamProvider;
}
internal void RegisterLoader<T>(IResourceLoader parLoader) where T : class
{
_loaders.Add(typeof(T), parLoader);
}
public T Load<T>(string parPath) where T : class
{
if (!_storages.TryGetValue(typeof(T), out var storage))
{
storage = new ResourceStorage();
_storages.Add(typeof(T), storage);
}
if (storage.Get<T>(parPath) is { } cachedResource)
{
return cachedResource;
}
var loader = _loaders.GetValueOrDefault(typeof(T));
if (loader == null)
{
throw new InvalidOperationException($"No loader found for type {typeof(T)}");
}
var resource = loader.Load(parPath, _streamProvider);
storage.Add(parPath, resource);
return (T)resource;
}
internal void Reset()
{
_storages.Clear();
}
private class ResourceStorage
{
private readonly Dictionary<string, object> _resources = new();
public void Add<T>(string parPath, T parResource) where T : class
{
_resources.Add(parPath, parResource);
}
public T? Get<T>(string parPath) where T : class
{
return _resources.TryGetValue(parPath, out var resource) ? (T)resource : null;
}
public void Remove(string parPath)
{
_resources.Remove(parPath);
}
}
}

View File

@@ -58,34 +58,7 @@ namespace Engine {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to #shader vertex
///#version 330 core
///
///uniform mat4 uViewMatrix;
///uniform mat4 uProjectionMatrix;
///
///layout(location = 0) in vec3 aPos;
///layout(location = 1) in vec3 aNormal;
///layout(location = 2) in vec2 aTexCoords;
///layout(location = 3) in mat4 aModelMatrix;
///
///out vec2 outTexCoords;
///
///void main()
///{
/// gl_Position = uProjectionMatrix * uViewMatrix * aModelMatrix * vec4(aPos, 1.0);
/// outTexCoords = aTexCoords;
///}
///
///#shader fragment
///#version 330 core
///
///uniform sampler2D uTexture;
///
///layout(location [rest of string was truncated]&quot;;.
/// </summary>
internal static string Mesh {
get {
return ResourceManager.GetString("Mesh", resourceCulture);
@@ -97,5 +70,11 @@ namespace Engine {
return ResourceManager.GetString("Quad", resourceCulture);
}
}
internal static string Text {
get {
return ResourceManager.GetString("Text", resourceCulture);
}
}
}
}

View File

@@ -31,4 +31,8 @@
<data name="Quad" type="System.Resources.ResXFileRef" xml:space="preserve">
<value>../../assets/shader/quad.shader;System.String, mscorlib, Version=4.0.0.0, Culture=neutral;utf-8</value>
</data>
<data name="Text" type="System.Resources.ResXFileRef" xml:space="preserve">
<value>../../assets/shader/text.shader;System.String, mscorlib, Version=4.0.0.0, Culture=neutral;utf-8</value>
</data>
</root>

View File

@@ -13,28 +13,62 @@ public class OrthographicCamera(
{
public Axis FixedAxis { get; set; } = parAxis;
public float Size { get; set; } = parSize;
public bool UseScreenSize { get; set; } = false;
public override Matrix4 View => GameObject.Transform.TransformMatrix.Inverted();
public override Matrix4 Projection => FixedAxis == Axis.X
? Matrix4.CreateOrthographic(Size, -Size / AspectRatio, NearPlane, FarPlane)
: Matrix4.CreateOrthographic(Size * AspectRatio, -Size, NearPlane, FarPlane);
public override Matrix4 Projection => GetProjectionMatrix();
private Matrix4 GetProjectionMatrix()
{
var size = GameObject.Transform.Size.Xy;
return Matrix4.CreateOrthographic(size.X, size.Y, -NearPlane, -FarPlane);
}
public override Vector3 ScreenToWorld(Vector2 parScreenPosition)
{
var offset = FixedAxis == Axis.X
? new Vector2(Size, Size / AspectRatio)
: new Vector2(Size * AspectRatio, Size);
offset /= 2;
var normalized = parScreenPosition / ScreenSize - new Vector2(0.5f);
normalized.X *= 2;
normalized.Y *= -2;
return new Vector4(parScreenPosition.X - offset.X, parScreenPosition.Y - offset.Y, 0, 1)
return new Vector4(normalized.X, normalized.Y, 0, 1)
.MulProject(Projection.Inverted())
.MulProject(GameObject.Transform.TransformMatrix)
.Xyz;
}
public override Vector2 WorldToScreen(Vector3 parWorldPosition)
{
throw new NotImplementedException();
var normalized = new Vector4(parWorldPosition, 1)
.MulProject(GameObject.Transform.TransformMatrix.Inverted())
.MulProject(Projection)
.Xy;
normalized.X /= 2;
normalized.Y /= -2;
return (normalized + new Vector2(0.5f)) * ScreenSize;
}
public override void Update(double parDeltaTime)
{
if (UseScreenSize)
{
GameObject.Transform.Size.Xy = ScreenSize.ToVector2();
}
else
{
if (FixedAxis == Axis.X)
{
GameObject.Transform.Size.X = Size;
GameObject.Transform.Size.Y = Size / AspectRatio;
}
else
{
GameObject.Transform.Size.X = Size * AspectRatio;
GameObject.Transform.Size.Y = Size;
}
}
}
public enum Axis

View File

@@ -1,11 +1,10 @@
using Engine.Util;
using OpenTK.Mathematics;
using Math = System.Math;
namespace Engine.Scene.Component.BuiltIn;
public class PerspectiveCamera(
float parFieldOfView = 90.0f,
float parFieldOfView = 60.0f,
float parNearPlane = 0.01f,
float parFarPlane = 1000f
)
@@ -18,9 +17,9 @@ public class PerspectiveCamera(
get
{
var transformMatrix = GameObject.Transform.TransformMatrix;
var forward = new Vector4(0, 0, 1, 1).MulProject(transformMatrix);
var eye = new Vector4(0, 0, 0, 1).MulProject(transformMatrix);
var up = (new Vector4(0, -1, 0, 1).MulProject(transformMatrix) - eye).Normalized();
var forward = new Vector4(0, 1, 0, 1).MulProject(in transformMatrix);
var eye = new Vector4(0, 0, 0, 1).MulProject(in transformMatrix);
var up = (new Vector4(0, 0, 1, 1).MulProject(in transformMatrix) - eye).Normalized();
return Matrix4.LookAt(eye.Xyz, forward.Xyz, up.Xyz);
}
@@ -32,11 +31,26 @@ public class PerspectiveCamera(
public override Vector3 ScreenToWorld(Vector2 parScreenPosition)
{
throw new NotImplementedException();
var normalized = parScreenPosition / ScreenSize - new Vector2(0.5f);
normalized.X *= 2;
normalized.Y *= -2;
return new Vector4(normalized.X, normalized.Y, 0, 1)
.MulProject(Projection.Inverted())
.MulProject(GameObject.Transform.TransformMatrix)
.Xyz;
}
public override Vector2 WorldToScreen(Vector3 parWorldPosition)
{
throw new NotImplementedException();
var normalized = new Vector4(parWorldPosition, 1)
.MulProject(GameObject.Transform.TransformMatrix.Inverted())
.MulProject(Projection)
.Xy;
normalized.X /= 2;
normalized.Y /= -2;
return (normalized + new Vector2(0.5f)) * ScreenSize;
}
}

View File

@@ -1,4 +1,5 @@
using Engine.Graphics.Texture;
using Engine.Graphics.Pipeline;
using Engine.Graphics.Texture;
using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn.Renderer;
@@ -7,11 +8,13 @@ public class Box2DRenderer : Component
{
public ref Vector4 Color => ref _color;
public Texture? Texture { get; set; } = null;
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
private Vector4 _color = Vector4.One;
public override void Render()
{
Engine.Instance.Renderer.QuadRenderer.Commit(GameObject.Transform.TransformMatrix, Color, Texture);
Engine.Instance.Renderer[RenderLayer].QuadRenderer
.Commit(GameObject.Transform.FullTransformMatrix, Color, Texture);
}
}

View File

@@ -1,13 +1,18 @@
using Engine.Asset.Mesh;
using Engine.Graphics.Pipeline;
using Engine.Graphics.Texture;
namespace Engine.Scene.Component.BuiltIn.Renderer;
public class MeshRenderer : Component
{
public Mesh Mesh { get; set; } = null!;
public Texture? Albedo { get; set; } = null;
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
public override void Render()
{
Engine.Instance.Renderer.GlobalMeshRenderer.Commit(Mesh, GameObject.Transform.TransformMatrix);
Engine.Instance.Renderer[RenderLayer].AnyMeshRenderer
.Commit(Mesh, GameObject.Transform.FullTransformMatrix, Albedo);
}
}

View File

@@ -0,0 +1,25 @@
using Engine.Asset.Font;
using Engine.Graphics.Pipeline;
using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn.Renderer;
public class TextRenderer : Component
{
public Font Font { get; set; } = null!;
public string? Text { get; set; }
public ref Vector4 Color => ref _color;
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
private Vector4 _color = Vector4.One;
public override void Render()
{
if (Text == null)
return;
Engine.Instance.Renderer[RenderLayer].TextRenderer
.Commit(Font, Text, Color, GameObject.Transform.FullTransformMatrix);
}
}

View File

@@ -7,12 +7,12 @@ public class Transform : Component
private Vector3 _translation = Vector3.Zero;
private Quaternion _rotation = Quaternion.Identity;
private Vector3 _scale = Vector3.One;
private Vector3 _localScale = Vector3.One;
private Vector3 _size = Vector3.One;
public ref Vector3 Translation => ref _translation;
public ref Quaternion Rotation => ref _rotation;
public ref Vector3 Scale => ref _scale;
public ref Vector3 LocalScale => ref _localScale;
public ref Vector3 Size => ref _size;
public Matrix4 LocalTransformMatrix => Matrix4.CreateScale(Scale) *
Matrix4.CreateFromQuaternion(Rotation) *
@@ -20,6 +20,8 @@ public class Transform : Component
public Matrix4 TransformMatrix => LocalTransformMatrix * ParentTransformMatrix;
public Matrix4 FullTransformMatrix => Matrix4.CreateScale(Size) * TransformMatrix;
private Matrix4 ParentTransformMatrix
{
get
@@ -32,7 +34,7 @@ public class Transform : Component
public Transform Clone()
{
var clone =
new Transform { Translation = Translation, Rotation = Rotation, Scale = Scale, LocalScale = LocalScale };
new Transform { Translation = Translation, Rotation = Rotation, Scale = Scale, Size = Size };
return clone;
}

View File

@@ -28,7 +28,7 @@ public class Scene : IUpdate, IRender
public T? FindFirstComponent<T>() where T : Component.Component
{
return Hierarchy.Objects.Select(parGameObject => parGameObject.GetComponent<T>()).FirstOrDefault();
return Hierarchy.Objects.Select(parGameObject => parGameObject.GetComponent<T>()).FirstOrDefault(parComponent => parComponent != null);
}
public void Update(double parDeltaTime)
@@ -87,10 +87,15 @@ public class Scene : IUpdate, IRender
});
}
public void AddChild(GameObject parParent, GameObject parGameObject)
public void SetChild(GameObject parParent, GameObject parChild)
{
Add(parGameObject);
Hierarchy.AddChild(parParent, parGameObject);
Hierarchy.AddChild(parParent, parChild);
}
public void AddChild(GameObject parParent, GameObject parChild)
{
Add(parChild);
SetChild(parParent, parChild);
}
public void Remove(GameObject parGameObject)
@@ -115,14 +120,14 @@ public class Scene : IUpdate, IRender
{
Hierarchy.ProcessChanges();
while (_sceneActions.TryDequeue(out var action))
{
action();
}
foreach (var gameObject in Hierarchy.Objects)
{
gameObject.ProcessChanges();
}
while (_sceneActions.TryDequeue(out var action))
{
action();
}
}
}

View File

@@ -22,7 +22,8 @@ public class SceneManager : IUpdate, IRender
_currentScene.Enter();
}
_currentScene?.Update(parDeltaTime);
if (parDeltaTime != 0)
_currentScene?.Update(parDeltaTime);
}
public void Render()

View File

@@ -66,10 +66,15 @@ public class Window : IPresenter
GL.BlitNamedFramebuffer(_engine.Renderer.RenderFramebuffer.Handle, 0,
0, parTexture.Height, parTexture.Width, 0,
0, 0, Width, Height,
ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit,
ClearBufferMask.ColorBufferBit,
BlitFramebufferFilter.Nearest
);
}
public void Exit()
{
_window.Close();
}
}
public static class NativeWindowExtensions