add docs for engine

This commit is contained in:
2025-01-07 06:25:09 +03:00
parent 0df34ef0ec
commit 926ce601d9
92 changed files with 3051 additions and 170 deletions

View File

@@ -4,16 +4,46 @@ using OpenTK.Mathematics;
namespace Engine.Asset.Font;
/// <summary>
/// Represents a font loaded from a texture atlas and metadata.
/// </summary>
public class Font
{
/// <summary>
/// The static texture atlas used for rendering the font.
/// </summary>
public StaticTexture AtlasTexture { get; }
/// <summary>
/// The metadata associated with the font.
/// </summary>
public Metadata.Metadata Metadata { get; }
/// <summary>
/// The unit range of the font, calculated from the distance range and the atlas size.
/// </summary>
public Vector2 UnitRange { get; }
/// <summary>
/// A dictionary mapping Unicode code points to glyphs.
/// </summary>
private readonly Dictionary<int, Glyph> _glyphs = new();
/// <summary>
/// A dictionary mapping Unicode code points to glyph data.
/// </summary>
private readonly Dictionary<int, GlyphData> _glyphData = new();
/// <summary>
/// A dictionary mapping pairs of Unicode code points to kerning data.
/// </summary>
private readonly Dictionary<(int, int), Kerning> _kernings = new();
/// <summary>
/// Initializes a new instance of the <see cref="Font"/> class with the specified texture atlas and metadata.
/// </summary>
/// <param name="parAtlasTexture">The static texture atlas for the font.</param>
/// <param name="parMetadata">The metadata for the font.</param>
public Font(StaticTexture parAtlasTexture, Metadata.Metadata parMetadata)
{
AtlasTexture = parAtlasTexture;
@@ -25,26 +55,52 @@ public class Font
LoadKernings();
}
/// <summary>
/// Retrieves the glyph associated with the specified Unicode value.
/// </summary>
/// <param name="parUnicode">The Unicode value of the glyph.</param>
/// <returns>The glyph for the specified Unicode value, or <c>null</c> if not found.</returns>
public Glyph? GetGlyph(int parUnicode)
{
return _glyphs.GetValueOrDefault(parUnicode);
}
/// <summary>
/// Retrieves the glyph data associated with the specified Unicode value.
/// </summary>
/// <param name="parUnicode">The Unicode value of the glyph.</param>
/// <returns>The glyph data for the specified Unicode value, or <c>null</c> if not found.</returns>
public GlyphData? GetGlyphData(int parUnicode)
{
return _glyphData.GetValueOrDefault(parUnicode);
}
/// <summary>
/// Retrieves the kerning information between two specified Unicode values.
/// </summary>
/// <param name="parUnicode1">The Unicode value of the first character.</param>
/// <param name="parUnicode2">The Unicode value of the second character.</param>
/// <returns>The kerning information, or <c>null</c> if not found.</returns>
public Kerning? GetKerning(int parUnicode1, int parUnicode2)
{
return _kernings.GetValueOrDefault((parUnicode1, parUnicode2));
}
/// <summary>
/// Creates an iterator for traversing through the specified text using the font.
/// </summary>
/// <param name="parText">The text to iterate over.</param>
/// <returns>An iterator for the text.</returns>
public FontIterator Iterator(string parText)
{
return new FontIterator(this, parText);
}
/// <summary>
/// Measures the size of the specified text when rendered using the font.
/// </summary>
/// <param name="parText">The text to measure.</param>
/// <returns>The size of the text in <see cref="Vector2"/>.</returns>
public Vector2 Measure(string parText)
{
var fontIterator = Iterator(parText);
@@ -53,6 +109,9 @@ public class Font
return new Vector2(fontIterator.MaxWidth, fontIterator.MaxHeight);
}
/// <summary>
/// Loads the glyphs from the metadata into the font with their associated data.
/// </summary>
private void LoadGlyphs()
{
foreach (var glyph in Metadata.Glyphs)
@@ -68,6 +127,9 @@ public class Font
}
}
/// <summary>
/// Loads the kerning information from the metadata into the font.
/// </summary>
private void LoadKernings()
{
foreach (var kerning in Metadata.Kerning)
@@ -76,11 +138,26 @@ public class Font
}
}
/// <summary>
/// Represents the data associated with a single glyph in the font.
/// </summary>
public record GlyphData
{
/// <summary>
/// The positions of the glyph's vertices.
/// </summary>
public Vector2[] Positions { get; }
/// <summary>
/// The texture UV coordinates for the glyph's vertices.
/// </summary>
public Vector2[] UVs { get; }
/// <summary>
/// Initializes a new instance of the <see cref="GlyphData"/> record with the given metadata and glyph.
/// </summary>
/// <param name="parMetadata">The font metadata.</param>
/// <param name="parGlyph">The glyph for which to create data.</param>
public GlyphData(in Metadata.Metadata parMetadata, in Glyph parGlyph)
{
var size = new Vector2(parMetadata.Atlas.Width, parMetadata.Atlas.Height);

View File

@@ -3,26 +3,71 @@ using OpenTK.Mathematics;
namespace Engine.Asset.Font;
/// <summary>
/// An iterator that traverses through the characters of a given text using a font, calculating the glyph data, positions, and kerning.
/// </summary>
public class FontIterator : IEnumerable<FontIterator.NextGlyphData>
{
/// <summary>
/// The maximum width encountered while measuring the text.
/// </summary>
public float MaxWidth { get; private set; }
/// <summary>
/// The maximum height encountered while measuring the text.
/// </summary>
public float MaxHeight { get; private set; }
/// <summary>
/// The font to use for text iteration.
/// </summary>
private readonly Font _font;
/// <summary>
/// The text to iterate over.
/// </summary>
private readonly string _text;
/// <summary>
/// The current index in the text.
/// </summary>
private int _currentIndex;
/// <summary>
/// The previous Unicode code point.
/// </summary>
private int _previousCodepoint = -1;
/// <summary>
/// The number of characters on the current line.
/// </summary>
private int _lineCharCount;
/// <summary>
/// The current cursor position.
/// </summary>
private Vector2 _cursor = Vector2.Zero;
/// <summary>
/// The current kerning offset.
/// </summary>
private Vector2 _kerning = Vector2.Zero;
/// <summary>
/// Initializes a new instance of the <see cref="FontIterator"/> class with the specified font and text.
/// </summary>
/// <param name="parFont">The font to use for rendering.</param>
/// <param name="parText">The text to iterate over.</param>
public FontIterator(Font parFont, string parText)
{
_font = parFont;
_text = parText;
}
/// <summary>
/// Returns an enumerator that iterates through the next glyph data for the text.
/// </summary>
/// <returns>An enumerator for the next glyph data in the text.</returns>
public IEnumerator<NextGlyphData> GetEnumerator()
{
while (_currentIndex < _text.Length)
@@ -80,21 +125,38 @@ public class FontIterator : IEnumerable<FontIterator.NextGlyphData>
}
}
/// <summary>
/// Returns a non-generic enumerator for the <see cref="FontIterator"/>.
/// </summary>
/// <returns>A non-generic enumerator for the font iterator.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
/// <summary>
/// Determines if the provided codepoint represents a line break character.
/// </summary>
/// <param name="parCodepoint">The codepoint to check.</param>
/// <returns><c>true</c> if the codepoint represents a line break, otherwise <c>false</c>.</returns>
private static bool IsLineBreak(int parCodepoint)
{
return parCodepoint == '\n';
}
/// <summary>
/// Determines if the provided codepoint represents a tab character.
/// </summary>
/// <param name="parCodepoint">The codepoint to check.</param>
/// <returns><c>true</c> if the codepoint represents a tab character, otherwise <c>false</c>.</returns>
private static bool IsTab(int parCodepoint)
{
return parCodepoint == '\t';
}
/// <summary>
/// Handles the tab character by advancing the cursor position to the next tab stop.
/// </summary>
private void Tab()
{
var spaceGlyph = _font.GetGlyph(' ');
@@ -107,6 +169,9 @@ public class FontIterator : IEnumerable<FontIterator.NextGlyphData>
_cursor.X += missingSpaces * spaceGlyph.Advance;
}
/// <summary>
/// Handles a line break by resetting the cursor position and updating the line height.
/// </summary>
private void LineBreak()
{
_kerning = Vector2.Zero;
@@ -116,5 +181,8 @@ public class FontIterator : IEnumerable<FontIterator.NextGlyphData>
_previousCodepoint = -1;
}
/// <summary>
/// Contains the data for the next glyph in the iteration, including its positions, texture UVs, and offset.
/// </summary>
public record NextGlyphData(Vector2[] Positions, Vector2[] UVs, Vector2 Offset);
}

View File

@@ -2,26 +2,44 @@
namespace Engine.Asset.Font.Metadata;
/// <summary>
/// Represents the texture atlas used for font rendering.
/// </summary>
[JsonSerializable(typeof(Atlas))]
[Serializable]
public record Atlas
{
/// <summary>
/// The distance range for the atlas.
/// </summary>
[JsonPropertyName("distanceRange")]
[JsonInclude]
public float DistanceRange { get; private set; }
/// <summary>
/// The size of the atlas.
/// </summary>
[JsonPropertyName("size")]
[JsonInclude]
public float Size { get; private set; }
/// <summary>
/// The width of the atlas.
/// </summary>
[JsonPropertyName("width")]
[JsonInclude]
public int Width { get; private set; }
/// <summary>
/// The height of the atlas.
/// </summary>
[JsonPropertyName("height")]
[JsonInclude]
public int Height { get; private set; }
/// <summary>
/// The Y-origin of the atlas.
/// </summary>
[JsonPropertyName("yOrigin")]
[JsonInclude]
public string YOrigin { get; private set; }

View File

@@ -2,22 +2,37 @@
namespace Engine.Asset.Font.Metadata;
/// <summary>
/// Defines the bounds of a font glyph in the coordinate space.
/// </summary>
[JsonSerializable(typeof(Bounds))]
[Serializable]
public record Bounds
{
/// <summary>
/// The left boundary of the bounds.
/// </summary>
[JsonPropertyName("left")]
[JsonInclude]
public float Left { get; private set; }
/// <summary>
/// The bottom boundary of the bounds.
/// </summary>
[JsonPropertyName("bottom")]
[JsonInclude]
public float Bottom { get; private set; }
/// <summary>
/// The right boundary of the bounds.
/// </summary>
[JsonPropertyName("right")]
[JsonInclude]
public float Right { get; private set; }
/// <summary>
/// The top boundary of the bounds.
/// </summary>
[JsonPropertyName("top")]
[JsonInclude]
public float Top { get; private set; }

View File

@@ -2,22 +2,37 @@
namespace Engine.Asset.Font.Metadata;
/// <summary>
/// Represents a single glyph in the font with associated metadata.
/// </summary>
[JsonSerializable(typeof(Glyph))]
[Serializable]
public record Glyph
{
/// <summary>
/// The Unicode code point of the glyph.
/// </summary>
[JsonPropertyName("unicode")]
[JsonInclude]
public int Unicode { get; private set; }
/// <summary>
/// The advance width of the glyph.
/// </summary>
[JsonPropertyName("advance")]
[JsonInclude]
public float Advance { get; private set; }
/// <summary>
/// The bounds of the glyph in the plane.
/// </summary>
[JsonPropertyName("planeBounds")]
[JsonInclude]
public Bounds? PlaneBounds { get; private set; }
/// <summary>
/// The bounds of the glyph in the atlas.
/// </summary>
[JsonPropertyName("atlasBounds")]
[JsonInclude]
public Bounds? AtlasBounds { get; private set; }

View File

@@ -2,18 +2,30 @@
namespace Engine.Asset.Font.Metadata;
/// <summary>
/// Represents kerning information between two characters in the font.
/// </summary>
[JsonSerializable(typeof(Kerning))]
[Serializable]
public record Kerning
{
/// <summary>
/// The Unicode value of the first character in the kerning pair.
/// </summary>
[JsonPropertyName("unicode1")]
[JsonInclude]
public int Unicode1 { get; private set; }
/// <summary>
/// The Unicode value of the second character in the kerning pair.
/// </summary>
[JsonPropertyName("unicode2")]
[JsonInclude]
public int Unicode2 { get; private set; }
/// <summary>
/// The amount of kerning (adjustment) between the two characters.
/// </summary>
[JsonPropertyName("advance")]
[JsonInclude]
public float Advance { get; private set; }

View File

@@ -2,22 +2,37 @@
namespace Engine.Asset.Font.Metadata;
/// <summary>
/// Represents metadata for a font, including atlas, metrics, glyphs, and kerning.
/// </summary>
[JsonSerializable(typeof(Metadata))]
[Serializable]
public record Metadata
{
/// <summary>
/// The atlas information for the font.
/// </summary>
[JsonPropertyName("atlas")]
[JsonInclude]
public Atlas Atlas { get; private set; }
/// <summary>
/// The metrics of the font.
/// </summary>
[JsonPropertyName("metrics")]
[JsonInclude]
public Metrics Metrics { get; private set; }
/// <summary>
/// The array of glyphs in the font.
/// </summary>
[JsonPropertyName("glyphs")]
[JsonInclude]
public Glyph[] Glyphs { get; private set; }
/// <summary>
/// The array of kerning pairs for the font.
/// </summary>
[JsonPropertyName("kerning")]
[JsonInclude]
public Kerning[] Kerning { get; private set; }

View File

@@ -2,30 +2,51 @@
namespace Engine.Asset.Font.Metadata;
/// <summary>
/// Represents the metrics for a font, including size and line height.
/// </summary>
[JsonSerializable(typeof(Metrics))]
[Serializable]
public record Metrics
{
/// <summary>
/// The em size of the font.
/// </summary>
[JsonPropertyName("emSize")]
[JsonInclude]
public float EmSize { get; private set; }
/// <summary>
/// The line height of the font.
/// </summary>
[JsonPropertyName("lineHeight")]
[JsonInclude]
public float LineHeight { get; private set; }
/// <summary>
/// The ascender height of the font.
/// </summary>
[JsonPropertyName("ascender")]
[JsonInclude]
public float Ascender { get; private set; }
/// <summary>
/// The descender height of the font.
/// </summary>
[JsonPropertyName("descender")]
[JsonInclude]
public float Descender { get; private set; }
/// <summary>
/// The Y-coordinate of the underline.
/// </summary>
[JsonPropertyName("underlineY")]
[JsonInclude]
public float UnderlineY { get; private set; }
/// <summary>
/// The thickness of the underline in the font.
/// </summary>
[JsonPropertyName("underlineThickness")]
public float UnderlineThickness { get; private set; }
}

View File

@@ -3,12 +3,34 @@ using Engine.Graphics.Texture;
namespace Engine.Asset;
/// <summary>
/// Represents an image containing pixels of a specified type.
/// The image stores pixel data in a 2D array and provides methods for converting it into textures.
/// </summary>
public class Image<T> where T : struct, IPixel
{
/// <summary>
/// The width of the image, in pixels.
/// </summary>
public int Width { get; }
/// <summary>
/// The height of the image, in pixels.
/// </summary>
public int Height { get; }
/// <summary>
/// The 2D array of pixels representing the image's content.
/// Each element in the array is of type <typeparamref name="T"/>.
/// </summary>
public T[,] Pixels { get; }
/// <summary>
/// Initializes a new instance of the <see cref="Image{T}"/> class with specified width and height.
/// Allocates a 2D array to hold pixel data.
/// </summary>
/// <param name="parWidth">The width of the image in pixels.</param>
/// <param name="parHeight">The height of the image in pixels.</param>
public Image(int parWidth, int parHeight)
{
Width = parWidth;
@@ -16,8 +38,18 @@ public class Image<T> where T : struct, IPixel
Pixels = new T[parHeight, parWidth];
}
/// <summary>
/// Indexer to access individual pixels in the image by their Y (row) and X (column) coordinates.
/// </summary>
/// <param name="parY">The Y coordinate (row) of the pixel.</param>
/// <param name="parX">The X coordinate (column) of the pixel.</param>
/// <returns>The pixel at the specified coordinates.</returns>
public T this[int parY, int parX] => Pixels[parY, parX];
/// <summary>
/// Converts the image into a dynamic texture that can be uploaded to the GPU.
/// </summary>
/// <returns>A <see cref="DynamicTexture"/> representing the image.</returns>
public DynamicTexture ToDynamicTexture()
{
var texture = DynamicTexture.Create<T>(Width, Height);
@@ -25,6 +57,11 @@ public class Image<T> where T : struct, IPixel
return texture;
}
/// <summary>
/// Converts the image into a static texture that can be uploaded to the GPU.
/// Static textures are optimized for performance but cannot be updated after creation.
/// </summary>
/// <returns>A <see cref="StaticTexture"/> representing the image.</returns>
public StaticTexture ToStaticTexture()
{
var texture = StaticTexture.Create<T>(Width, Height);

View File

@@ -1,9 +1,23 @@
namespace Engine.Asset.Mesh.Loader;
/// <summary>
/// Defines the interface for a mesh loader that can load meshes from a given stream.
/// </summary>
public interface IMeshLoader
{
/// <summary>
/// Loads a mesh from the provided text reader with optional parameters.
/// </summary>
/// <param name="parReader">The text reader to read the mesh data from.</param>
/// <param name="parParameters">Optional parameters to control the mesh loading behavior.</param>
/// <returns>The loaded mesh.</returns>
public Mesh LoadMesh(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default);
/// <summary>
/// Optimizes the provided mesh by removing duplicate vertices and reusing indices.
/// </summary>
/// <param name="parMesh">The mesh to optimize.</param>
/// <returns>A new optimized mesh.</returns>
internal static Mesh Optimize(Mesh parMesh)
{
var optimizedMesh = new Mesh();

View File

@@ -1,12 +1,33 @@
namespace Engine.Asset.Mesh.Loader;
/// <summary>
/// Specifies options for mesh loading behavior, such as loading normals, UVs, and optimization.
/// </summary>
[Flags]
public enum MeshLoaderParameters
{
/// <summary>
/// No options selected.
/// </summary>
None = 0,
/// <summary>
/// Load normals for the mesh.
/// </summary>
LoadNormals = 1 << 0,
/// <summary>
/// Load UVs for the mesh.
/// </summary>
LoadUVs = 1 << 1,
/// <summary>
/// Optimize the mesh by removing duplicate vertices and reusing indices.
/// </summary>
Optimize = 1 << 2,
/// <summary>
/// Default set of options: load normals, load UVs, and optimize the mesh.
/// </summary>
Default = LoadNormals | LoadUVs | Optimize
}

View File

@@ -3,19 +3,40 @@ using OpenTK.Mathematics;
namespace Engine.Asset.Mesh.Loader;
/// <summary>
/// A mesh loader for loading meshes from OBJ file format.
/// </summary>
public class ObjMeshLoader : IMeshLoader
{
/// <summary>
/// The singleton instance of the <see cref="ObjMeshLoader"/>.
/// </summary>
private static readonly ObjMeshLoader INSTANCE = new();
/// <summary>
/// Initializes a new instance of the <see cref="ObjMeshLoader"/> class.
/// </summary>
private ObjMeshLoader()
{
}
/// <summary>
/// Loads a mesh from the provided text reader in the OBJ format with optional parameters.
/// </summary>
/// <param name="parReader">The text reader to read the OBJ data from.</param>
/// <param name="parParameters">Optional parameters to control the mesh loading behavior.</param>
/// <returns>The loaded mesh.</returns>
public static Mesh Load(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default)
{
return INSTANCE.LoadMesh(parReader, parParameters);
}
/// <summary>
/// Loads a mesh from the provided text reader in the OBJ format with optional parameters.
/// </summary>
/// <param name="parReader">The text reader to read the OBJ data from.</param>
/// <param name="parParameters">Optional parameters to control the mesh loading behavior.</param>
/// <returns>The loaded mesh.</returns>
public Mesh LoadMesh(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default)
{
var mesh = new Mesh();

View File

@@ -3,19 +3,40 @@ using OpenTK.Mathematics;
namespace Engine.Asset.Mesh.Loader;
/// <summary>
/// A mesh loader for loading meshes from STL file format.
/// </summary>
public class StlMeshLoader : IMeshLoader
{
/// <summary>
/// The singleton instance of the <see cref="StlMeshLoader"/>.
/// </summary>
private static readonly StlMeshLoader INSTANCE = new();
/// <summary>
/// Initializes a new instance of the <see cref="StlMeshLoader"/> class.
/// </summary>
private StlMeshLoader()
{
}
/// <summary>
/// Loads a mesh from the provided text reader in the STL format with optional parameters.
/// </summary>
/// <param name="parReader">The text reader to read the STL data from.</param>
/// <param name="parParameters">Optional parameters to control the mesh loading behavior.</param>
/// <returns>The loaded mesh.</returns>
public static Mesh Load(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default)
{
return INSTANCE.LoadMesh(parReader, parParameters);
}
/// <summary>
/// Loads a mesh from the provided text reader in the STL format with optional parameters.
/// </summary>
/// <param name="parReader">The text reader to read the STL data from.</param>
/// <param name="parParameters">Optional parameters to control the mesh loading behavior.</param>
/// <returns>The loaded mesh.</returns>
public Mesh LoadMesh(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default)
{
var mesh = new Mesh();

View File

@@ -4,26 +4,68 @@ using OpenTK.Mathematics;
namespace Engine.Asset.Mesh;
/// <summary>
/// Represents a 3D mesh consisting of vertices and indices used for rendering.
/// </summary>
public class Mesh
{
/// <summary>
/// A read-only list of indices representing the mesh's geometry.
/// </summary>
public IReadOnlyList<uint> Indices => _indices;
/// <summary>
/// A read-only list of vertices representing the mesh's geometry.
/// </summary>
public IReadOnlyList<Vertex> Vertices => _vertices;
/// <summary>
/// Internal list of indices representing the mesh's geometry.
/// </summary>
internal IList<uint> IndicesInternal => _indices;
/// <summary>
/// Internal list of vertices representing the mesh's geometry.
/// </summary>
internal IList<Vertex> VerticesInternal => _vertices;
/// <summary>
/// List of indices representing the mesh's geometry.
/// </summary>
private readonly List<uint> _indices = [];
/// <summary>
/// List of vertices representing the mesh's geometry.
/// </summary>
private readonly List<Vertex> _vertices = [];
/// <summary>
/// Computes a hash code for the mesh based on its vertices and indices.
/// </summary>
/// <returns>The hash code for the mesh.</returns>
public override int GetHashCode()
{
return HashCode.Combine(Indices, Vertices);
}
/// <summary>
/// Represents a vertex in a mesh, including position, normal, and UV coordinates.
/// </summary>
public record struct Vertex : IVertex
{
/// <summary>
/// The position of the vertex in 3D space.
/// </summary>
[Vertex(VertexAttribType.Float, 3)] public Vector3 _position;
/// <summary>
/// The normal of the vertex for lighting calculations.
/// </summary>
[Vertex(VertexAttribType.Float, 3)] public Vector3 _normal;
/// <summary>
/// The texture coordinates (UVs) of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 2)] public Vector2 _uv;
}
}

View File

@@ -20,17 +20,49 @@ using Debug = Engine.Graphics.Debug;
namespace Engine;
/// <summary>
/// Represents the main engine class responsible for managing resources, rendering, input, and scenes.
/// </summary>
public sealed class Engine
{
/// <summary>
/// The input handler of the engine.
/// </summary>
public IInputHandler? InputHandler { get; internal set; }
/// <summary>
/// The scene manager for managing and updating scenes.
/// </summary>
public SceneManager SceneManager { get; } = new();
/// <summary>
/// The resource manager responsible for asset management.
/// </summary>
public IResourceManager AssetResourceManager { get; }
/// <summary>
/// The path to the data folder used by the engine.
/// </summary>
public string DataFolder { get; }
/// <summary>
/// The singleton instance of the engine.
/// </summary>
internal static Engine Instance { get; private set; } = null!;
/// <summary>
/// The resource manager for engine-specific resources.
/// </summary>
internal ResourceManager EngineResourceManager { get; }
/// <summary>
/// The renderer responsible for managing rendering operations.
/// </summary>
internal Renderer Renderer { get; }
/// <summary>
/// The presenter responsible for displaying rendered content.
/// </summary>
internal IPresenter? Presenter
{
get => _presenter;
@@ -38,26 +70,54 @@ public sealed class Engine
{
if (_presenter != null)
{
_presenter.Resize -= PresenterResize;
_presenter.OnResize -= PresenterResize;
}
_presenter = value;
if (_presenter != null)
{
_presenter.Resize += PresenterResize;
_presenter.OnResize += PresenterResize;
}
}
}
/// <summary>
/// The application window that holds rendering context.
/// </summary>
internal Window Window { get; }
/// <summary>
/// The logger instance used by the engine.
/// </summary>
private readonly ILogger _logger;
/// <summary>
/// The lock used to synchronize scene access.
/// </summary>
private readonly object _sceneLock = new();
/// <summary>
/// The presenter used to display rendered content.
/// </summary>
private IPresenter? _presenter;
/// <summary>
/// The thread used to run the update loop.
/// </summary>
private Thread? _updateThread;
/// <summary>
/// Initializes a new instance of the <see cref="Engine"/> class.
/// </summary>
/// <param name="parWidth">The width of the rendering window.</param>
/// <param name="parHeight">The height of the rendering window.</param>
/// <param name="parHeadless">Indicates whether the engine should run in headless mode.</param>
/// <param name="parTitle">The title of the application window.</param>
/// <param name="parAssetFolder">The path to the asset folder.</param>
/// <param name="parDataFolder">The path to the data folder.</param>
/// <param name="parLogger">The logger instance to use for logging.</param>
/// <exception cref="InvalidOperationException">Thrown if an engine instance is already running.</exception>
public Engine(int parWidth, int parHeight, bool parHeadless, string parTitle, string parAssetFolder,
string parDataFolder,
ILogger parLogger)
@@ -96,6 +156,31 @@ public sealed class Engine
Window = new Window(this, Renderer.NativeWindow, parHeadless);
}
/// <summary>
/// Starts the engine's update and render threads.
/// </summary>
public void Run()
{
_updateThread = new Thread(RunUpdate) { Name = "UpdateThread" };
_updateThread.Start();
RunRender();
_updateThread.Join();
}
/// <summary>
/// Closes the engine and stops all running threads.
/// </summary>
public void Close()
{
Presenter?.Exit();
}
/// <summary>
/// Creates and initializes the engine resource manager.
/// </summary>
/// <returns>The initialized resource manager.</returns>
private static ResourceManager CreateEngineResourceManager()
{
var memoryStreamProvider = new MemoryResourceStreamProvider();
@@ -109,6 +194,11 @@ public sealed class Engine
return resourceManager;
}
/// <summary>
/// Creates and initializes the asset resource manager.
/// </summary>
/// <param name="parAssetFolder">The path to the asset folder.</param>
/// <returns>The initialized resource manager.</returns>
private static ResourceManager CreateAssetResourceManager(string parAssetFolder)
{
var filesystemStreamProvider = new FilesystemResourceStreamProvider(parAssetFolder);
@@ -119,6 +209,10 @@ public sealed class Engine
return resourceManager;
}
/// <summary>
/// Registers the default loaders for the resource manager.
/// </summary>
/// <param name="parResourceManager">The resource manager to register loaders for.</param>
private static void RegisterDefaultLoaders(ResourceManager parResourceManager)
{
parResourceManager.RegisterLoader<Program>(new ProgramLoader());
@@ -128,21 +222,9 @@ public sealed class Engine
parResourceManager.RegisterLoader<Font>(new FontLoader());
}
public void Run()
{
_updateThread = new Thread(RunUpdate) { Name = "UpdateThread" };
_updateThread.Start();
RunRender();
_updateThread.Join();
}
public void Close()
{
Presenter?.Exit();
}
/// <summary>
/// Runs the render loop for the engine.
/// </summary>
private void RunRender()
{
while (!Presenter?.IsExiting ?? false)
@@ -184,6 +266,9 @@ public sealed class Engine
}
}
/// <summary>
/// Runs the update loop for the engine.
/// </summary>
private void RunUpdate()
{
var timer = Stopwatch.StartNew();
@@ -211,6 +296,10 @@ public sealed class Engine
}
}
/// <summary>
/// Handles the presenter resize event to adjust the renderer's viewport.
/// </summary>
/// <param name="parEventArgs">The resize event arguments.</param>
private void PresenterResize(ResizeEventArgs parEventArgs)
{
if (parEventArgs.Width == 0 || parEventArgs.Height == 0)

View File

@@ -7,36 +7,99 @@ using Serilog.Sinks.SystemConsole.Themes;
namespace Engine;
/// <summary>
/// Provides a builder for creating and configuring an instance of the <see cref="Engine"/> class.
/// </summary>
public sealed class EngineBuilder
{
/// <summary>
/// The title of the application window.
/// </summary>
private string _title = "";
/// <summary>
/// Indicates whether the engine should run in headless mode.
/// </summary>
private bool _headless;
/// <summary>
/// The width of the rendering window.
/// </summary>
private int _width = 1;
/// <summary>
/// The height of the rendering window.
/// </summary>
private int _height = 1;
/// <summary>
/// The path to the asset folder.
/// </summary>
private string _assetFolder = "./asset";
/// <summary>
/// The path to the data folder.
/// </summary>
private string _dataFolder = "./data";
/// <summary>
/// The input handler factory.
/// </summary>
private Func<Engine, IInputHandler>? _inputHandlerFunc;
/// <summary>
/// The presenter factory.
/// </summary>
private Func<Engine, IPresenter>? _presenterFunc;
// Logging
/// <summary>
/// Indicates whether to log to the console.
/// </summary>
private bool _logToConsole;
/// <summary>
/// Indicates whether to log to a file.
/// </summary>
private bool _logToFile;
/// <summary>
/// The path to the log file.
/// </summary>
private string? _logFilePath;
/// <summary>
/// The log level.
/// </summary>
private LogEventLevel _logLevel = LogEventLevel.Information;
/// <summary>
/// Sets the title of the engine window.
/// </summary>
/// <param name="parTitle">The title to use for the engine window.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder Title(string parTitle)
{
_title = parTitle;
return this;
}
/// <summary>
/// Configures the engine to run in headless mode.
/// </summary>
/// <param name="parHeadless">Indicates whether to enable headless mode. Defaults to <c>true</c>.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder Headless(bool parHeadless = true)
{
_headless = parHeadless;
return this;
}
/// <summary>
/// Sets the width of the engine window.
/// </summary>
/// <param name="parWidth">The width in pixels. Must be greater than zero.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder Width(int parWidth)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parWidth);
@@ -45,6 +108,11 @@ public sealed class EngineBuilder
return this;
}
/// <summary>
/// Sets the height of the engine window.
/// </summary>
/// <param name="parHeight">The height in pixels. Must be greater than zero.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder Height(int parHeight)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(parHeight);
@@ -53,36 +121,68 @@ public sealed class EngineBuilder
return this;
}
/// <summary>
/// Sets the folder path for assets used by the engine.
/// </summary>
/// <param name="parAssetFolder">The folder path containing asset files.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder AssetFolder(string parAssetFolder)
{
_assetFolder = parAssetFolder;
return this;
}
/// <summary>
/// Sets the folder path for data files used by the engine.
/// </summary>
/// <param name="parDataFolder">The folder path containing data files.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder DataFolder(string parDataFolder)
{
_dataFolder = parDataFolder;
return this;
}
/// <summary>
/// Specifies the input handler to be used by the engine.
/// </summary>
/// <param name="parInputHandlerFunc">A function that creates an input handler for the engine.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder InputHandler(Func<Engine, IInputHandler> parInputHandlerFunc)
{
_inputHandlerFunc = parInputHandlerFunc;
return this;
}
/// <summary>
/// Specifies the presenter to be used by the engine.
/// </summary>
/// <param name="parPresenterFunc">A function that creates a presenter for the engine.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder Presenter(Func<Engine, IPresenter> parPresenterFunc)
{
_presenterFunc = parPresenterFunc;
return this;
}
/// <summary>
/// Configures logging to output to the console.
/// </summary>
/// <param name="parLogToConsole">Indicates whether to enable console logging. Defaults to <c>true</c>.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder LogToConsole(bool parLogToConsole = true)
{
_logToConsole = parLogToConsole;
return this;
}
/// <summary>
/// Configures logging to output to a file.
/// </summary>
/// <param name="parLogToFile">Indicates whether to enable file logging. Defaults to <c>true</c>.</param>
/// <param name="parLogFilePath">The path of the log file. Cannot be <c>null</c> if file logging is enabled.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="parLogFilePath"/> is <c>null</c> when <paramref name="parLogToFile"/> is <c>true</c>.</exception>
public EngineBuilder LogToFile(bool parLogToFile = true, string? parLogFilePath = null)
{
if (parLogToFile && parLogFilePath == null)
@@ -95,12 +195,21 @@ public sealed class EngineBuilder
return this;
}
/// <summary>
/// Sets the minimum log level for logging.
/// </summary>
/// <param name="parLogLevel">The minimum level of log events to capture.</param>
/// <returns>The current instance of <see cref="EngineBuilder"/> for chaining.</returns>
public EngineBuilder LogLevel(LogEventLevel parLogLevel)
{
_logLevel = parLogLevel;
return this;
}
/// <summary>
/// Builds and returns a new instance of the <see cref="Engine"/> class based on the configured settings.
/// </summary>
/// <returns>A fully configured instance of <see cref="Engine"/>.</returns>
public Engine Build()
{
var logger = BuildLogger();
@@ -121,6 +230,10 @@ public sealed class EngineBuilder
return engine;
}
/// <summary>
/// Configures and builds a logger based on the current logging settings.
/// </summary>
/// <returns>A configured instance of <see cref="Logger"/>.</returns>
private Logger BuildLogger()
{
const string template =

View File

@@ -3,10 +3,21 @@ using Serilog;
namespace Engine.Graphics.Buffer;
/// <summary>
/// Represents an OpenGL index buffer, used for storing indices for rendering.
/// </summary>
public class IndexBuffer : OpenGlObject
{
/// <summary>
/// The number of indices in the buffer.
/// </summary>
internal int Count { get; }
/// <summary>
/// Initializes a new index buffer with a specified number of elements.
/// </summary>
/// <param name="parCount">The number of indices the buffer will store.</param>
/// <param name="parFlags">Optional storage flags for the buffer.</param>
public IndexBuffer(int parCount, BufferStorageFlags parFlags = BufferStorageFlags.None)
{
Count = parCount;
@@ -20,6 +31,11 @@ public class IndexBuffer : OpenGlObject
}
/// <summary>
/// Initializes a new index buffer with the provided data.
/// </summary>
/// <param name="parData">The data to populate the buffer with.</param>
/// <param name="parFlags">Optional storage flags for the buffer.</param>
public IndexBuffer(uint[] parData, BufferStorageFlags parFlags = BufferStorageFlags.None)
{
Count = parData.Length;
@@ -32,11 +48,21 @@ public class IndexBuffer : OpenGlObject
Log.Debug("Index buffer {Handle} created with {Count} elements", Handle, Count);
}
/// <summary>
/// Uploads data to the buffer starting at offset 0.
/// </summary>
/// <param name="parData">The data to upload.</param>
public void UploadData(uint[] parData)
{
UploadData(0, parData);
}
/// <summary>
/// Uploads data to the buffer at the specified offset.
/// </summary>
/// <param name="parOffset">The offset (in elements) where data should be uploaded.</param>
/// <param name="parData">The data to upload.</param>
/// <exception cref="ArgumentException">Thrown if the offset is invalid or the data exceeds buffer size.</exception>
public void UploadData(int parOffset, uint[] parData)
{
if (parOffset < 0)
@@ -52,16 +78,19 @@ public class IndexBuffer : OpenGlObject
GL.NamedBufferSubData(Handle, parOffset, parData.Length * sizeof(uint), parData);
}
/// <inheritdoc />
internal override void Bind()
{
GL.BindBuffer(BufferTarget.ElementArrayBuffer, Handle);
}
/// <inheritdoc />
internal override void Unbind()
{
GL.BindBuffer(BufferTarget.ElementArrayBuffer, 0);
}
/// <inheritdoc />
protected override void Destroy()
{
GL.DeleteBuffer(Handle);

View File

@@ -4,18 +4,38 @@ using OpenTK.Graphics.OpenGL;
namespace Engine.Graphics.Buffer.Vertex;
/// <summary>
/// Interface for a vertex structure used in graphics buffers.
/// Provides methods for reflecting on vertex fields and checking their validity.
/// </summary>
public interface IVertex
{
/// <summary>
/// Retrieves the fields of a type implementing <see cref="IVertex"/>.
/// </summary>
/// <typeparam name="T">The type to retrieve the fields for.</typeparam>
/// <returns>An ordered enumerable of <see cref="FieldInfo"/> for the fields in the type.</returns>
public static IOrderedEnumerable<FieldInfo> GetFields<T>()
{
return GetFields(typeof(T));
}
/// <summary>
/// Retrieves the fields of a specified type implementing <see cref="IVertex"/>.
/// </summary>
/// <param name="parType">The type to retrieve the fields for.</param>
/// <returns>An ordered enumerable of <see cref="FieldInfo"/> for the fields in the type.</returns>
public static IOrderedEnumerable<FieldInfo> GetFields(Type parType)
{
return parType.GetFields(BindingFlags.Public | BindingFlags.Instance).OrderBy(parF => parF.MetadataToken);
}
/// <summary>
/// Validates if a type is a valid <see cref="IVertex"/>.
/// Checks if all fields are value types, have <see cref="VertexAttribute"/> attributes, and match their size.
/// </summary>
/// <param name="parType">The type to validate.</param>
/// <returns>True if the type is valid, otherwise false.</returns>
public static bool IsValid(Type parType)
{
if (!parType.IsValueType || !parType.IsAssignableTo(typeof(IVertex)))
@@ -50,6 +70,11 @@ public interface IVertex
return totalSize == Marshal.SizeOf(parType);
}
/// <summary>
/// Gets the size of a vertex attribute based on its OpenGL type.
/// </summary>
/// <param name="parType">The OpenGL type of the attribute.</param>
/// <returns>The size of the attribute in bytes.</returns>
public static int AttributeSize(VertexAttribType parType)
{
return parType switch

View File

@@ -2,14 +2,40 @@
namespace Engine.Graphics.Buffer.Vertex;
/// <summary>
/// Attribute used to describe a vertex field.
/// Provides details about the attribute's type, component count, normalization, and repetition.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class VertexAttribute : Attribute
{
/// <summary>
/// OpenGL type of the vertex attribute.
/// </summary>
public VertexAttribType Type { get; }
/// <summary>
/// Number of components in the vertex attribute.
/// </summary>
public int ComponentCount { get; }
/// <summary>
/// Whether the vertex attribute is normalized.
/// </summary>
public bool Normalized { get; }
/// <summary>
/// Number of times the vertex attribute is repeated.
/// </summary>
public int RepeatCount { get; }
/// <summary>
/// Initializes a new instance of the <see cref="VertexAttribute"/> class.
/// </summary>
/// <param name="parType">The OpenGL type of the vertex attribute.</param>
/// <param name="parComponentCount">The number of components in the vertex attribute.</param>
/// <param name="parRepeatCount">The repeat count for the vertex attribute.</param>
/// <param name="parNormalized">Whether the attribute is normalized.</param>
public VertexAttribute(VertexAttribType parType, int parComponentCount = 1, int parRepeatCount = 1,
bool parNormalized = false
)

View File

@@ -6,10 +6,19 @@ using Serilog;
namespace Engine.Graphics.Buffer;
/// <summary>
/// Represents a vertex array object in OpenGL, managing vertex buffer bindings and configurations.
/// </summary>
public class VertexArray : OpenGlObject
{
/// <summary>
/// Stores the enabled attribute flags for this vertex array.
/// </summary>
private int _enabledAttributes;
/// <summary>
/// Initializes a new instance of the <see cref="VertexArray"/> class, creating an OpenGL vertex array object.
/// </summary>
public VertexArray()
{
GL.CreateVertexArrays(1, out int handle);
@@ -18,6 +27,10 @@ public class VertexArray : OpenGlObject
Log.Debug("Vertex array {Handle} created", Handle);
}
/// <summary>
/// Binds an index buffer to this vertex array.
/// </summary>
/// <param name="parBuffer">The index buffer to bind.</param>
public void BindIndexBuffer(IndexBuffer parBuffer)
{
GL.VertexArrayElementBuffer(Handle, parBuffer.Handle);
@@ -25,6 +38,13 @@ public class VertexArray : OpenGlObject
Log.Debug("Index buffer {Buffer} bound to vertex array {Handle}", parBuffer.Handle, Handle);
}
/// <summary>
/// Binds a vertex buffer to this vertex array and configures its vertex attributes.
/// </summary>
/// <typeparam name="T">The type of the vertex buffer, which must implement the <see cref="IVertex"/> interface.</typeparam>
/// <param name="parBuffer">The vertex buffer to bind.</param>
/// <param name="parBindingIndex">The binding index for the vertex buffer. Defaults to 0.</param>
/// <param name="parDivisor">The divisor for instancing, which controls how often the vertex data is used. Defaults to 0.</param>
public void BindVertexBuffer<T>(VertexBuffer<T> parBuffer, int parBindingIndex = 0, int parDivisor = 0)
where T : struct, IVertex
{
@@ -52,6 +72,13 @@ public class VertexArray : OpenGlObject
parBuffer.Handle, Handle, parBindingIndex, parDivisor);
}
/// <summary>
/// Configures a vertex attribute for a vertex array.
/// </summary>
/// <param name="parAttribute">The vertex attribute to configure.</param>
/// <param name="parBaseLocation">The base location for the attribute.</param>
/// <param name="parBaseOffset">The base offset for the attribute.</param>
/// <param name="parBindingIndex">The binding index for the attribute.</param>
private void SetupAttribute(VertexAttribute parAttribute, int parBaseLocation, int parBaseOffset, int parBindingIndex)
{
var size = parAttribute.ComponentCount * IVertex.AttributeSize(parAttribute.Type);
@@ -69,16 +96,19 @@ public class VertexArray : OpenGlObject
}
}
/// <inheritdoc />
internal override void Bind()
{
GL.BindVertexArray(Handle);
}
/// <inheritdoc />
internal override void Unbind()
{
GL.BindVertexArray(0);
}
/// <inheritdoc />
protected override void Destroy()
{
GL.DeleteVertexArray(Handle);

View File

@@ -5,13 +5,29 @@ using Serilog;
namespace Engine.Graphics.Buffer;
/// <summary>
/// Represents an OpenGL vertex buffer for storing vertex data.
/// </summary>
/// <typeparam name="T">The vertex structure type implementing <see cref="IVertex"/>.</typeparam>
public class VertexBuffer<T> : OpenGlObject
where T : struct, IVertex
{
/// <summary>
/// The number of vertices in the buffer.
/// </summary>
internal int Count { get; }
/// <summary>
/// The size of a single vertex in bytes.
/// </summary>
private readonly int _stride = Marshal.SizeOf<T>();
/// <summary>
/// Initializes a new vertex buffer with a specified number of elements.
/// </summary>
/// <param name="parCount">The number of vertices the buffer will store.</param>
/// <param name="parFlags">Optional storage flags for the buffer.</param>
/// <exception cref="ArgumentException">Thrown if the type is not a valid vertex type or the count is invalid.</exception>
public VertexBuffer(int parCount, BufferStorageFlags parFlags = BufferStorageFlags.None)
{
if (!IVertex.IsValid(typeof(T)))
@@ -34,6 +50,12 @@ public class VertexBuffer<T> : OpenGlObject
Log.Debug("Vertex buffer {Handle} created with {Count} elements of type {Type}", Handle, Count, typeof(T).Name);
}
/// <summary>
/// Initializes a new vertex buffer with the provided data.
/// </summary>
/// <param name="parData">The data to populate the buffer with.</param>
/// <param name="parFlags">Optional storage flags for the buffer.</param>
/// <exception cref="ArgumentException">Thrown if the type is not a valid vertex type or the data is invalid.</exception>
public VertexBuffer(T[] parData, BufferStorageFlags parFlags = BufferStorageFlags.None)
{
if (!IVertex.IsValid(typeof(T)))
@@ -56,16 +78,32 @@ public class VertexBuffer<T> : OpenGlObject
Log.Debug("Vertex buffer {Handle} created with {Count} elements of type {Type}", Handle, Count, typeof(T).Name);
}
/// <summary>
/// Uploads data to the buffer starting at offset 0.
/// </summary>
/// <param name="parData">The data to upload.</param>
public void UploadData(T[] parData)
{
UploadData(0, parData, parData.Length);
}
/// <summary>
/// Uploads data to the buffer starting at offset 0 with a specified count.
/// </summary>
/// <param name="parData">The data to upload.</param>
/// <param name="parCount">The number of elements to upload.</param>
public void UploadData(T[] parData, int parCount)
{
UploadData(0, parData, parCount);
}
/// <summary>
/// Uploads data to the buffer at the specified offset.
/// </summary>
/// <param name="parOffset">The offset (in elements) where data should be uploaded.</param>
/// <param name="parData">The data to upload.</param>
/// <param name="parCount">The number of elements to upload.</param>
/// <exception cref="ArgumentException">Thrown if parameters are invalid or data exceeds buffer size.</exception>
public void UploadData(int parOffset, T[] parData, int parCount)
{
if (parOffset < 0)
@@ -91,16 +129,19 @@ public class VertexBuffer<T> : OpenGlObject
GL.NamedBufferSubData(Handle, parOffset * _stride, parCount * _stride, parData);
}
/// <inheritdoc />
internal override void Bind()
{
GL.BindBuffer(BufferTarget.ArrayBuffer, Handle);
}
/// <inheritdoc />
internal override void Unbind()
{
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
}
/// <inheritdoc />
protected override void Destroy()
{
GL.DeleteBuffer(Handle);

View File

@@ -2,9 +2,23 @@
namespace Engine.Graphics.Camera;
/// <summary>
/// Defines the interface for camera functionality, including projection and view matrices.
/// </summary>
public interface ICamera
{
/// <summary>
/// The matrix representing the camera's view transformation.
/// </summary>
Matrix4 View { get; }
/// <summary>
/// The matrix representing the camera's projection transformation.
/// </summary>
Matrix4 Projection { get; }
/// <summary>
/// The dimensions of the screen in pixels.
/// </summary>
Vector2i ScreenSize { get; internal set; }
}

View File

@@ -2,9 +2,17 @@
namespace Engine.Graphics.Camera;
/// <summary>
/// Represents a camera that uses a screenspace projection.
/// </summary>
public class ScreenspaceCamera : ICamera
{
/// <inheritdoc/>
public Matrix4 View => Matrix4.Identity;
/// <inheritdoc/>
public Matrix4 Projection => Matrix4.Identity;
/// <inheritdoc/>
public Vector2i ScreenSize { get; set; }
}

View File

@@ -6,31 +6,59 @@ using Serilog.Events;
namespace Engine.Graphics;
/// <summary>
/// Provides debugging utilities for OpenGL and integrates with RenderDoc for frame capturing.
/// </summary>
internal static class Debug
{
/// <summary>
/// The RenderDoc API instance for capturing and debugging GPU frames.
/// </summary>
private static RenderDoc _renderDocApi;
/// <summary>
/// Configures OpenGL debugging and enables debug output.
/// </summary>
public static void Setup()
{
GL.Enable(EnableCap.DebugOutput);
GL.DebugMessageCallback(DebugCallback, IntPtr.Zero);
}
/// <summary>
/// Initializes RenderDoc support for capturing GPU frames.
/// </summary>
public static void InitializeRenderDoc()
{
RenderDoc.Load(out _renderDocApi);
}
/// <summary>
/// Starts a new RenderDoc frame capture.
/// </summary>
public static void RenderDocStartFrame()
{
// _renderDocApi.API.StartFrameCapture(IntPtr.Zero, IntPtr.Zero);
}
/// <summary>
/// Ends the current RenderDoc frame capture.
/// </summary>
public static void RenderDocEndFrame()
{
// _renderDocApi.API.EndFrameCapture(IntPtr.Zero, IntPtr.Zero);
}
/// <summary>
/// Callback for handling OpenGL debug messages.
/// </summary>
/// <param name="parSource">The source of the debug message.</param>
/// <param name="parType">The type of the debug message.</param>
/// <param name="parId">The identifier of the debug message.</param>
/// <param name="parSeverity">The severity of the debug message.</param>
/// <param name="parLength">The length of the debug message string.</param>
/// <param name="parMessage">A pointer to the debug message string.</param>
/// <param name="parUserParam">A pointer to user-defined data.</param>
private static void DebugCallback(DebugSource parSource, DebugType parType, int parId, DebugSeverity parSeverity,
int parLength,
IntPtr parMessage, IntPtr parUserParam)
@@ -73,6 +101,11 @@ internal static class Debug
);
}
/// <summary>
/// Maps OpenGL debug severity levels to Serilog logging levels.
/// </summary>
/// <param name="parSeverity">The severity of the OpenGL debug message.</param>
/// <returns>The corresponding Serilog log level.</returns>
private static LogEventLevel GetLogLevel(DebugSeverity parSeverity)
{
return parSeverity switch

View File

@@ -5,8 +5,15 @@ using Serilog;
namespace Engine.Graphics.Framebuffer;
/// <summary>
/// Represents an OpenGL framebuffer object, which manages a collection of attachments,
/// such as color and depth buffers, and provides functionality for binding, resizing, and checking status.
/// </summary>
public class Framebuffer : OpenGlObject
{
/// <summary>
/// The width of the framebuffer.
/// </summary>
public int Width
{
get => _width;
@@ -18,6 +25,9 @@ public class Framebuffer : OpenGlObject
}
}
/// <summary>
/// The height of the framebuffer.
/// </summary>
public int Height
{
get => _height;
@@ -29,13 +39,32 @@ public class Framebuffer : OpenGlObject
}
}
/// <summary>
/// Retrieves the internal texture attached to the framebuffer at the color attachment point.
/// </summary>
internal Texture.Texture? TextureInternal => GetAttachment<DynamicTexture>(FramebufferAttachment.ColorAttachment0);
/// <summary>
/// A collection of framebuffer attachments, mapped by their attachment point.
/// </summary>
private readonly IDictionary<FramebufferAttachment, IFramebufferAttachment> _attachments;
/// <summary>
/// The width of the framebuffer.
/// </summary>
private int _width;
/// <summary>
/// The height of the framebuffer.
/// </summary>
private int _height;
/// <summary>
/// Initializes a new instance of the <see cref="Framebuffer"/> class with the specified width, height, and attachments.
/// </summary>
/// <param name="parWidth">The width of the framebuffer.</param>
/// <param name="parHeight">The height of the framebuffer.</param>
/// <param name="parAttachments">A dictionary of attachments to associate with the framebuffer.</param>
internal Framebuffer(int parWidth, int parHeight,
IDictionary<FramebufferAttachment, IFramebufferAttachment> parAttachments)
{
@@ -58,11 +87,23 @@ public class Framebuffer : OpenGlObject
}
}
/// <summary>
/// Creates a builder for creating a new framebuffer with the specified width and height.
/// </summary>
/// <param name="parWidth">The width of the framebuffer.</param>
/// <param name="parHeight">The height of the framebuffer.</param>
/// <returns>A <see cref="FramebufferBuilder"/> that can be used to customize the framebuffer.</returns>
public static FramebufferBuilder Builder(int parWidth, int parHeight)
{
return new FramebufferBuilder(parWidth, parHeight);
}
/// <summary>
/// Retrieves the attachment of the specified type from the framebuffer.
/// </summary>
/// <typeparam name="T">The type of the attachment to retrieve.</typeparam>
/// <param name="parAttachment">The attachment point to retrieve the attachment from.</param>
/// <returns>The attachment if found, otherwise <c>null</c>.</returns>
internal T? GetAttachment<T>(FramebufferAttachment parAttachment) where T : IFramebufferAttachment
{
if (!_attachments.TryGetValue(parAttachment, out var attachmentValue))
@@ -78,6 +119,11 @@ public class Framebuffer : OpenGlObject
return default;
}
/// <summary>
/// Resizes the framebuffer and all its attachments to the specified width and height.
/// </summary>
/// <param name="parWidth">The new width of the framebuffer.</param>
/// <param name="parHeight">The new height of the framebuffer.</param>
public void Resize(int parWidth, int parHeight)
{
if (Width == parWidth && Height == parHeight)
@@ -96,16 +142,19 @@ public class Framebuffer : OpenGlObject
Log.Debug("Framebuffer {Handle} resized to {Width}x{Height}", Handle, parWidth, parHeight);
}
/// <inheritdoc/>
internal override void Bind()
{
GL.BindFramebuffer(FramebufferTarget.Framebuffer, Handle);
}
/// <inheritdoc/>
internal override void Unbind()
{
GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
}
/// <inheritdoc/>
protected override void Destroy()
{
GL.DeleteFramebuffer(Handle);

View File

@@ -4,13 +4,52 @@ using OpenTK.Graphics.OpenGL;
namespace Engine.Graphics.Framebuffer;
public class FramebufferBuilder(int parWidth, int parHeight)
/// <summary>
/// A builder class used to construct framebuffers with custom attachments, such as color and depth buffers.
/// </summary>
public class FramebufferBuilder
{
/// <summary>
/// The width of the framebuffer.
/// </summary>
private readonly int _width;
/// <summary>
/// The height of the framebuffer.
/// </summary>
private readonly int _height;
/// <summary>
/// A list of attachments to be added to the framebuffer.
/// </summary>
private readonly List<AttachmentSpecification> _attachments = [];
/// <summary>
/// A set of used attachment points.
/// </summary>
private readonly HashSet<FramebufferAttachment> _usedAttachments = [];
/// <summary>
/// The current color attachment point.
/// </summary>
private FramebufferAttachment _currentColorAttachment = FramebufferAttachment.ColorAttachment0;
/// <summary>
/// Initializes a new instance of the <see cref="FramebufferBuilder"/> class with the specified width and height.
/// </summary>
/// <param name="parWidth">The width of the framebuffer.</param>
/// <param name="parHeight">The height of the framebuffer.</param>
public FramebufferBuilder(int parWidth, int parHeight)
{
_width = parWidth;
_height = parHeight;
}
/// <summary>
/// Adds a color attachment to the framebuffer.
/// </summary>
/// <typeparam name="T">The pixel format to use for the color attachment.</typeparam>
/// <returns>The builder instance for method chaining.</returns>
public FramebufferBuilder AddColorAttachment<T>()
where T : struct, IPixel
{
@@ -30,6 +69,10 @@ public class FramebufferBuilder(int parWidth, int parHeight)
});
}
/// <summary>
/// Adds a depth attachment to the framebuffer.
/// </summary>
/// <returns>The builder instance for method chaining.</returns>
public FramebufferBuilder AddDepthAttachment()
{
return AddAttachment(new RenderbufferAttachmentSpecification
@@ -39,6 +82,26 @@ public class FramebufferBuilder(int parWidth, int parHeight)
});
}
/// <summary>
/// Builds the framebuffer based on the current configuration.
/// </summary>
/// <returns>A new <see cref="Framebuffer"/> instance.</returns>
public Framebuffer Build()
{
var attachments = _attachments
.ToDictionary(
parAttachment => parAttachment.FramebufferAttachment,
parAttachment => parAttachment.Create(_width, _height)
);
return new Framebuffer(_width, _height, attachments);
}
/// <summary>
/// Adds a custom attachment to the framebuffer.
/// </summary>
/// <param name="parAttachment">The attachment specification to add.</param>
/// <returns>The builder instance for method chaining.</returns>
private FramebufferBuilder AddAttachment(AttachmentSpecification parAttachment)
{
if (_usedAttachments.Contains(parAttachment.FramebufferAttachment))
@@ -53,43 +116,67 @@ public class FramebufferBuilder(int parWidth, int parHeight)
return this;
}
public Framebuffer Build()
{
var attachments = _attachments
.ToDictionary(
parAttachment => parAttachment.FramebufferAttachment,
parAttachment => parAttachment.Create(parWidth, parHeight)
);
return new Framebuffer(parWidth, parHeight, attachments);
}
/// <summary>
/// Represents a specification for a texture attachment in a framebuffer.
/// </summary>
private class TextureAttachmentSpecification : AttachmentSpecification
{
/// <summary>
/// The pixel format for the texture attachment.
/// </summary>
public PixelFormat PixelFormat { get; init; }
/// <summary>
/// The pixel type for the texture attachment.
/// </summary>
public PixelType PixelType { get; init; }
/// <summary>
/// The internal format for the texture attachment.
/// </summary>
public PixelInternalFormat PixelInternalFormat { get; init; }
/// <inheritdoc/>
public override DynamicTexture Create(int parWidth, int parHeight)
{
return new DynamicTexture(parWidth, parHeight, PixelFormat, PixelType, PixelInternalFormat);
}
}
/// <summary>
/// Represents a specification for a renderbuffer attachment in a framebuffer.
/// </summary>
private class RenderbufferAttachmentSpecification : AttachmentSpecification
{
/// <summary>
/// The storage format for the renderbuffer attachment.
/// </summary>
public RenderbufferStorage RenderbufferStorage { get; init; }
/// <inheritdoc/>
public override Renderbuffer Create(int parWidth, int parHeight)
{
return new Renderbuffer(parWidth, parHeight, RenderbufferStorage);
}
}
/// <summary>
/// Represents a base class for framebuffer attachment specifications.
/// Provides common functionality for creating attachments like textures or renderbuffers.
/// </summary>
private abstract class AttachmentSpecification
{
/// <summary>
/// The attachment point for the framebuffer.
/// </summary>
public FramebufferAttachment FramebufferAttachment { get; init; }
/// <summary>
/// Creates a new attachment for the framebuffer based on the specific type of attachment.
/// </summary>
/// <param name="parWidth">The width of the attachment.</param>
/// <param name="parHeight">The height of the attachment.</param>
/// <returns>An instance of <see cref="IFramebufferAttachment"/> that represents the created attachment.</returns>
public abstract IFramebufferAttachment Create(int parWidth, int parHeight);
}
}

View File

@@ -2,11 +2,27 @@
namespace Engine.Graphics.Framebuffer;
/// <summary>
/// Interface representing a framebuffer attachment, providing methods to attach, resize, and manage the attachment's OpenGL handle.
/// </summary>
internal interface IFramebufferAttachment
{
/// <summary>
/// The handle that uniquely identifies the OpenGL object.
/// </summary>
int Handle { get; }
/// <summary>
/// Resizes the framebuffer attachment to the specified width and height.
/// </summary>
/// <param name="parWidth">The new width of the framebuffer attachment.</param>
/// <param name="parHeight">The new height of the framebuffer attachment.</param>
void Resize(int parWidth, int parHeight);
/// <summary>
/// Attaches the framebuffer attachment to a specified framebuffer and attachment point.
/// </summary>
/// <param name="parFramebuffer">The framebuffer to attach to.</param>
/// <param name="parAttachment">The attachment point of the framebuffer to attach to.</param>
void Attach(Framebuffer parFramebuffer, FramebufferAttachment parAttachment);
}

View File

@@ -2,13 +2,32 @@
namespace Engine.Graphics.Framebuffer;
/// <summary>
/// Represents a renderbuffer object in OpenGL, which can be used as an attachment to a framebuffer.
/// </summary>
public class Renderbuffer : OpenGlObject, IFramebufferAttachment
{
/// <summary>
/// The width of the renderbuffer.
/// </summary>
public int Width { get; private set; }
/// <summary>
/// The height of the renderbuffer.
/// </summary>
public int Height { get; private set; }
/// <summary>
/// The format of the renderbuffer (e.g., depth, stencil, or color).
/// </summary>
private readonly RenderbufferStorage _format;
/// <summary>
/// Initializes a new instance of the <see cref="Renderbuffer"/> class with specified dimensions and format.
/// </summary>
/// <param name="parWidth">The width of the renderbuffer.</param>
/// <param name="parHeight">The height of the renderbuffer.</param>
/// <param name="parFormat">The format of the renderbuffer (storage format).</param>
public Renderbuffer(int parWidth, int parHeight, RenderbufferStorage parFormat)
{
Width = parWidth;
@@ -21,6 +40,7 @@ public class Renderbuffer : OpenGlObject, IFramebufferAttachment
GL.NamedRenderbufferStorage(Handle, _format, Width, Height);
}
/// <inheritdoc/>
public void Resize(int parWidth, int parHeight)
{
if (Width == parWidth && Height == parHeight)
@@ -34,21 +54,25 @@ public class Renderbuffer : OpenGlObject, IFramebufferAttachment
GL.NamedRenderbufferStorage(Handle, _format, Width, Height);
}
/// <inheritdoc/>
public void Attach(Framebuffer parFramebuffer, FramebufferAttachment parAttachment)
{
GL.NamedFramebufferRenderbuffer(parFramebuffer.Handle, parAttachment, RenderbufferTarget.Renderbuffer, Handle);
}
/// <inheritdoc/>
internal override void Bind()
{
GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, Handle);
}
/// <inheritdoc/>
internal override void Unbind()
{
GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0);
}
/// <inheritdoc/>
protected override void Destroy()
{
GL.DeleteRenderbuffer(Handle);

View File

@@ -7,21 +7,62 @@ using OpenTK.Mathematics;
namespace Engine.Graphics;
/// <summary>
/// A generic renderer that supports rendering quads, meshes, and text.
/// </summary>
public class GenericRenderer : IRenderer
{
/// <summary>
/// Provides functionality to render quads.
/// </summary>
public QuadRenderer QuadRenderer => _quadRenderer ??= new QuadRenderer(_engine, 1024 * 8);
/// <summary>
/// Provides functionality to render any type of mesh.
/// </summary>
public AnyMeshRenderer AnyMeshRenderer => _anyMeshRenderer ??= new AnyMeshRenderer(_engine, 1024);
/// <summary>
/// Provides functionality to render text.
/// </summary>
public TextRenderer TextRenderer => _textRenderer ??= new TextRenderer(_engine, 1024 * 8);
/// <summary>
/// The framebuffer used for rendering.
/// </summary>
internal readonly Framebuffer.Framebuffer _framebuffer;
/// <summary>
/// The engine instance associated with this renderer.
/// </summary>
private readonly Engine _engine;
/// <summary>
/// The quad renderer instance, lazily initialized.
/// </summary>
private QuadRenderer? _quadRenderer;
/// <summary>
/// The mesh renderer instance, lazily initialized.
/// </summary>
private AnyMeshRenderer? _anyMeshRenderer;
/// <summary>
/// The text renderer instance, lazily initialized.
/// </summary>
private TextRenderer? _textRenderer;
/// <summary>
/// Indicates whether the frame has been started.
/// </summary>
private bool _frameStarted;
/// <summary>
/// Initializes a new instance of the <see cref="GenericRenderer"/> class.
/// </summary>
/// <param name="parEngine">The engine instance to use for rendering.</param>
/// <param name="parWidth">The initial width of the framebuffer.</param>
/// <param name="parHeight">The initial height of the framebuffer.</param>
public GenericRenderer(Engine parEngine, int parWidth, int parHeight)
{
_engine = parEngine;
@@ -32,11 +73,13 @@ public class GenericRenderer : IRenderer
.Build();
}
/// <inheritdoc/>
public void StartFrame()
{
_frameStarted = true;
}
/// <inheritdoc/>
public void EndFrame(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix)
{
if (!_frameStarted)
@@ -72,6 +115,7 @@ public class GenericRenderer : IRenderer
_frameStarted = false;
}
/// <inheritdoc/>
public void Resize(int parWidth, int parHeight)
{
_framebuffer.Resize(parWidth, parHeight);

View File

@@ -4,14 +4,39 @@ using OpenTK.Windowing.Common;
namespace Engine.Graphics;
/// <summary>
/// Defines an interface for a presenter that handles updates, rendering, and presentation.
/// </summary>
public interface IPresenter : IUpdate, IRender
{
public event Action<ResizeEventArgs> Resize;
/// <summary>
/// Occurs when the presenter is resized.
/// </summary>
public event Action<ResizeEventArgs> OnResize;
/// <summary>
/// Gets the current width of the presenter.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the current height of the presenter.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets a value indicating whether the presenter is exiting.
/// </summary>
public bool IsExiting { get; }
/// <summary>
/// Presents a texture to the rendering surface.
/// </summary>
/// <param name="parTexture">The texture to present.</param>
public void Present(IConstTexture parTexture);
/// <summary>
/// Signals the presenter to exit.
/// </summary>
public void Exit();
}

View File

@@ -2,10 +2,27 @@
namespace Engine.Graphics;
/// <summary>
/// Interface defining the essential functionality for a renderer.
/// </summary>
internal interface IRenderer
{
/// <summary>
/// Prepares the renderer for a new frame.
/// </summary>
public void StartFrame();
/// <summary>
/// Finalizes the rendering pipeline for the current frame.
/// </summary>
/// <param name="parProjectionMatrix">The projection matrix to use for rendering.</param>
/// <param name="parViewMatrix">The view matrix to use for rendering.</param>
public void EndFrame(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix);
/// <summary>
/// Resizes the renderer to accommodate changes in viewport dimensions.
/// </summary>
/// <param name="parWidth">The new width of the viewport.</param>
/// <param name="parHeight">The new height of the viewport.</param>
public void Resize(int parWidth, int parHeight);
}

View File

@@ -2,15 +2,34 @@
namespace Engine.Graphics;
/// <summary>
/// Represents an abstract OpenGL object with a handle for managing GPU resources.
/// </summary>
public abstract class OpenGlObject
{
/// <summary>
/// The handle that uniquely identifies the OpenGL object.
/// </summary>
public int Handle { get; protected set; } = -1;
/// <summary>
/// Binds the OpenGL object for rendering or other operations.
/// </summary>
internal abstract void Bind();
/// <summary>
/// Unbinds the OpenGL object, restoring the previous state.
/// </summary>
internal abstract void Unbind();
/// <summary>
/// Releases the GPU resources associated with the OpenGL object.
/// </summary>
protected abstract void Destroy();
/// <summary>
/// Finalizer to ensure the OpenGL object is destroyed and resources are freed.
/// </summary>
~OpenGlObject()
{
if (Handle == -1)

View File

@@ -1,32 +1,64 @@
namespace Engine.Graphics.Pipeline;
/// <summary>
/// Represents a render layer in the rendering pipeline, providing a way to order and compare render layers.
/// </summary>
public class RenderLayer : IComparable<RenderLayer>
{
/// <summary>
/// The default render layer, with a priority of 0.
/// </summary>
public static readonly RenderLayer DEFAULT = new("default", 0);
/// <summary>
/// The overlay render layer, with a priority of 1.
/// </summary>
public static readonly RenderLayer OVERLAY = new("overlay", 1);
/// <summary>
/// The HUD render layer, with a priority of 2.
/// </summary>
public static readonly RenderLayer HUD = new("hud", 2);
/// <summary>
/// A read-only list of all render layers.
/// </summary>
public static readonly IReadOnlyList<RenderLayer> ALL = new List<RenderLayer> { DEFAULT, OVERLAY, HUD }.AsReadOnly();
/// <summary>
/// The name of the render layer.
/// </summary>
public string Name { get; }
/// <summary>
/// The priority of the render layer.
/// </summary>
private readonly int _order;
/// <summary>
/// Initializes a new instance of the <see cref="RenderLayer"/> class with the specified name and priority.
/// </summary>
/// <param name="parName">The name of the render layer.</param>
/// <param name="parOrder">The priority of the render layer.</param>
private RenderLayer(string parName, int parOrder)
{
Name = parName;
_order = parOrder;
}
/// <inheritdoc/>
public int CompareTo(RenderLayer? parOther)
{
return parOther == null ? 1 : _order.CompareTo(parOther._order);
}
/// <inheritdoc/>
public override string ToString()
{
return Name;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return Name.GetHashCode();

View File

@@ -2,11 +2,29 @@ using OpenTK.Graphics.OpenGL;
namespace Engine.Graphics.Pixel;
/// <summary>
/// Defines the basic structure for a pixel type, which includes information
/// about the pixel's format, type, internal format, and sized internal format.
/// </summary>
public interface IPixel
{
PixelFormat Format { get; }
PixelType Type { get; }
/// <summary>
/// The format of the pixel (e.g., Red, RGB, RGBA).
/// </summary>
public PixelFormat Format { get; }
PixelInternalFormat InternalFormat { get; }
SizedInternalFormat SizedInternalFormat { get; }
/// <summary>
/// The type of the pixel data (e.g., UnsignedByte).
/// </summary>
public PixelType Type { get; }
/// <summary>
/// The internal format for the pixel (e.g., R8, RGB8, etc.).
/// </summary>
public PixelInternalFormat InternalFormat { get; }
/// <summary>
/// The sized internal format for the pixel (e.g., R8, RGB8, etc.).
/// </summary>
public SizedInternalFormat SizedInternalFormat { get; }
}

View File

@@ -3,14 +3,27 @@ using OpenTK.Graphics.OpenGL;
namespace Engine.Graphics.Pixel;
/// <summary>
/// Represents a single-channel red pixel with an 8-bit format.
/// Implements the <see cref="IPixel"/> interface for defining pixel properties.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct R8 : IPixel
{
/// <inheritdoc/>
public PixelFormat Format => PixelFormat.Red;
/// <inheritdoc/>
public PixelType Type => PixelType.UnsignedByte;
/// <inheritdoc/>
public PixelInternalFormat InternalFormat => PixelInternalFormat.R8;
/// <inheritdoc/>
public SizedInternalFormat SizedInternalFormat => SizedInternalFormat.R8;
/// <summary>
/// The red channel of the pixel.
/// </summary>
public byte R;
}

View File

@@ -6,15 +6,33 @@ namespace Engine.Graphics.Pixel;
[StructLayout(LayoutKind.Sequential)]
public struct Rgb8 : IPixel
{
/// <inheritdoc/>
public PixelFormat Format => PixelFormat.Rgb;
/// <inheritdoc/>
public PixelType Type => PixelType.UnsignedByte;
/// <inheritdoc/>
public PixelInternalFormat InternalFormat => PixelInternalFormat.Rgb8;
/// <inheritdoc/>
public SizedInternalFormat SizedInternalFormat => SizedInternalFormat.Rgb8;
/// <summary>
/// The red channel of the pixel.
/// </summary>
public byte R;
/// <summary>
/// The green channel of the pixel.
/// </summary>
public byte G;
/// <summary>
/// The blue channel of the pixel.
/// </summary>
public byte B;
/// <inheritdoc/>
public override string ToString() => $"{R}, {G}, {B}";
}

View File

@@ -3,19 +3,45 @@ using OpenTK.Graphics.OpenGL;
namespace Engine.Graphics.Pixel;
/// <summary>
/// Represents a four-channel RGBA pixel with an 8-bit format for each channel.
/// Implements the <see cref="IPixel"/> interface for defining pixel properties.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct Rgba8 : IPixel
{
/// <inheritdoc/>
public PixelFormat Format => PixelFormat.Rgba;
/// <inheritdoc/>
public PixelType Type => PixelType.UnsignedByte;
/// <inheritdoc/>
public PixelInternalFormat InternalFormat => PixelInternalFormat.Rgba8;
/// <inheritdoc/>
public SizedInternalFormat SizedInternalFormat => SizedInternalFormat.Rgba8;
/// <summary>
/// The red channel of the pixel.
/// </summary>
public byte R;
/// <summary>
/// The green channel of the pixel.
/// </summary>
public byte G;
/// <summary>
/// The blue channel of the pixel.
/// </summary>
public byte B;
/// <summary>
/// The alpha channel of the pixel.
/// </summary>
public byte A;
/// <inheritdoc/>
public override string ToString() => $"{R}, {G}, {B}, {A}";
}

View File

@@ -6,22 +6,70 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render;
/// <summary>
/// Base class for rendering vertices with support for instancing.
/// This class is generic, allowing customization for the common vertex type <typeparamref name="C"/>
/// and the instance-specific vertex type <typeparamref name="I"/>.
/// </summary>
/// <typeparam name="C">The type of the common vertex, must implement <see cref="IVertex"/>.</typeparam>
/// <typeparam name="I">The type of the instance vertex, must implement <see cref="IVertex"/>.</typeparam>
public abstract class InstancedRenderer<C, I>
where C : struct, IVertex
where I : struct, IVertex
{
/// <summary>
/// The number of instances that can be rendered.
/// </summary>
protected readonly int _instanceCount;
/// <summary>
/// The array of instance vertices, each vertex is of type <typeparamref name="I"/>.
/// </summary>
protected readonly I[] _instanceVertices;
/// <summary>
/// The count of instances that are queued for rendering.
/// </summary>
protected int _queuedInstanceCount;
/// <summary>
/// The type of primitive to be rendered (e.g., triangles, lines).
/// </summary>
private readonly PrimitiveType _primitiveType;
/// <summary>
/// The shader program used for rendering.
/// </summary>
private readonly Program _program;
/// <summary>
/// The index buffer used for the rendering of indexed primitives.
/// </summary>
private readonly IndexBuffer _indexBuffer;
/// <summary>
/// The buffer containing the common vertex data, of type <typeparamref name="C"/>.
/// </summary>
private readonly VertexBuffer<C> _commonVertexBuffer;
/// <summary>
/// The buffer containing the instance-specific vertex data, of type <typeparamref name="I"/>.
/// </summary>
private readonly VertexBuffer<I> _instanceVertexBuffer;
/// <summary>
/// The vertex array object that binds all the buffers for rendering.
/// </summary>
private readonly VertexArray _vertexArray;
/// <summary>
/// Initializes a new instance of the <see cref="InstancedRenderer{C, I}"/> class.
/// </summary>
/// <param name="parPrimitiveType">The type of primitive to be rendered.</param>
/// <param name="parInstanceCount">The total number of instances to be rendered.</param>
/// <param name="parIndexBuffer">An array of indices for indexed rendering.</param>
/// <param name="parInstanceBuffer">An array of instance-specific vertex data.</param>
/// <param name="parProgram">The shader program to use for rendering.</param>
protected InstancedRenderer(PrimitiveType parPrimitiveType, int parInstanceCount,
uint[] parIndexBuffer, C[] parInstanceBuffer,
Program parProgram)
@@ -43,6 +91,11 @@ public abstract class InstancedRenderer<C, I>
_vertexArray.BindVertexBuffer(_instanceVertexBuffer, 1, 1);
}
/// <summary>
/// Renders the instanced objects with the provided projection and view matrices.
/// </summary>
/// <param name="parProjectionMatrix">The projection matrix to be used in the shader.</param>
/// <param name="parViewMatrix">The view matrix to be used in the shader.</param>
public void Render(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix)
{
if (_queuedInstanceCount <= 0)
@@ -67,15 +120,26 @@ public abstract class InstancedRenderer<C, I>
_queuedInstanceCount);
}
/// <summary>
/// Resets the renderer, clearing the queued instance count.
/// </summary>
public virtual void Reset()
{
_queuedInstanceCount = 0;
}
/// <summary>
/// Allows derived classes to set additional uniforms for the shader program.
/// </summary>
/// <param name="parProgram">The shader program to set additional uniforms for.</param>
protected virtual void SetAdditionalUniforms(Program parProgram)
{
}
/// <summary>
/// Determines whether the instance data has changed and needs to be uploaded to the GPU.
/// </summary>
/// <returns><c>true</c> if the data has changed; otherwise, <c>false</c>.</returns>
protected virtual bool DataChanged()
{
return true;

View File

@@ -3,13 +3,50 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Mesh;
public class AnyMeshRenderer(Engine parEngine, int parMaxInstanceCount)
/// <summary>
/// A renderer class that manages multiple meshes and delegates rendering to individual mesh renderers.
/// Handles batching of mesh instances and ensures that only the necessary mesh renderers are created.
/// </summary>
public class AnyMeshRenderer
{
/// <summary>
/// A dictionary that maps each mesh to its corresponding <see cref="MeshRenderer"/>.
/// </summary>
private readonly Dictionary<Asset.Mesh.Mesh, MeshRenderer> _meshRenderers = new();
/// <summary>
/// A hash set that tracks the meshes that were rendered in the current frame.
/// </summary>
private readonly HashSet<Asset.Mesh.Mesh> _frameMeshes = [];
private readonly Program _program = parEngine.EngineResourceManager.Load<Program>("shader/mesh");
/// <summary>
/// The shader program used for rendering meshes.
/// </summary>
private readonly Program _program;
/// <summary>
/// The maximum number of instances that can be rendered in a single frame.
/// </summary>
private readonly int _maxInstanceCount;
/// <summary>
/// Initializes a new instance of the <see cref="AnyMeshRenderer"/> class.
/// </summary>
/// <param name="parEngine">The engine used to load resources.</param>
/// <param name="parMaxInstanceCount">The maximum number of instances to render per frame.</param>
public AnyMeshRenderer(Engine parEngine, int parMaxInstanceCount)
{
_maxInstanceCount = parMaxInstanceCount;
_program = parEngine.EngineResourceManager.Load<Program>("shader/mesh");
}
/// <summary>
/// Commits an instance of a mesh to the renderer, adding it to the render queue with the specified model matrix and optional texture.
/// If the mesh is not already being tracked, a new <see cref="MeshRenderer"/> will be created for it.
/// </summary>
/// <param name="parMesh">The mesh to render.</param>
/// <param name="parModelMatrix">The model transformation matrix to apply to the mesh.</param>
/// <param name="parAlbedo">An optional texture to apply to the mesh. If null, no texture is applied.</param>
public void Commit(Asset.Mesh.Mesh parMesh, Matrix4 parModelMatrix, Texture.Texture? parAlbedo = null)
{
if (_meshRenderers.TryGetValue(parMesh, out var meshRenderer))
@@ -18,7 +55,7 @@ public class AnyMeshRenderer(Engine parEngine, int parMaxInstanceCount)
}
else
{
var newMeshRenderer = new MeshRenderer(parMesh, parMaxInstanceCount, _program);
var newMeshRenderer = new MeshRenderer(parMesh, _maxInstanceCount, _program);
newMeshRenderer.Commit(parModelMatrix, parAlbedo);
_meshRenderers.Add(parMesh, newMeshRenderer);
@@ -27,6 +64,11 @@ public class AnyMeshRenderer(Engine parEngine, int parMaxInstanceCount)
_frameMeshes.Add(parMesh);
}
/// <summary>
/// Renders all queued mesh instances by calling the <see cref="MeshRenderer.Render"/> method for each mesh renderer.
/// </summary>
/// <param name="parProjectionMatrix">The projection matrix to use for rendering.</param>
/// <param name="parViewMatrix">The view matrix to use for rendering.</param>
public void Render(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix)
{
foreach (var meshRenderer in _meshRenderers.Values)
@@ -35,6 +77,9 @@ public class AnyMeshRenderer(Engine parEngine, int parMaxInstanceCount)
}
}
/// <summary>
/// Resets the state of the mesh renderers and removes any meshes that were not rendered in the current frame.
/// </summary>
public void Reset()
{
foreach (var meshRenderer in _meshRenderers.Values)

View File

@@ -4,11 +4,23 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Mesh;
/// <summary>
/// Represents an instance vertex structure for a mesh.
/// This vertex structure includes texture ID and model matrix.
/// </summary>
public struct MeshInstanceVertex : IVertex
{
/// <summary>
/// The texture ID of the vertex.
/// </summary>
[Vertex(VertexAttribType.Int)] public int _textureId;
/// <summary>
/// The model matrix of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(_textureId, _modelMatrix);

View File

@@ -5,21 +5,53 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Mesh;
public class MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program parProgram)
: InstancedRenderer<Asset.Mesh.Mesh.Vertex, MeshInstanceVertex>(
PrimitiveType.Triangles,
parInstanceCount,
parMesh.Indices.ToArray(),
parMesh.Vertices.ToArray(),
parProgram
)
/// <summary>
/// A renderer class for rendering meshes using instancing.
/// Handles the instancing process for meshes, including texture binding and model transformations.
/// </summary>
public class MeshRenderer : InstancedRenderer<Asset.Mesh.Mesh.Vertex, MeshInstanceVertex>
{
/// <summary>
/// Maps textures to texture units with a limit of 16 texture units.
/// </summary>
private readonly TextureUnitMap _textureUnitMap = new(16);
/// <summary>
/// Stores the texture unit indices used for binding textures.
/// </summary>
private readonly int[] _textureUnitIndices = new int[16];
/// <summary>
/// Stores the hash of the current frame, used for detecting changes in instance data.
/// </summary>
private int _frameHash;
/// <summary>
/// Stores the hash of the previous frame.
/// </summary>
private int _previousHash;
/// <summary>
/// Initializes a new instance of the <see cref="MeshRenderer"/> class.
/// </summary>
/// <param name="parMesh">The mesh to render.</param>
/// <param name="parInstanceCount">The number of instances to render.</param>
/// <param name="parProgram">The shader program to use for rendering the mesh.</param>
public MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program parProgram)
: base(
PrimitiveType.Triangles, parInstanceCount,
parMesh.Indices.ToArray(),
parMesh.Vertices.ToArray(),
parProgram
)
{
}
/// <summary>
/// Commits an instance to the renderer, adding it to the queue with the specified model matrix and optional texture.
/// </summary>
/// <param name="parModelMatrix">The model transformation matrix for this instance.</param>
/// <param name="parTexture">An optional texture to apply to the mesh. If null, no texture is applied.</param>
public void Commit(Matrix4 parModelMatrix, Texture.Texture? parTexture = null)
{
if (_queuedInstanceCount >= _instanceCount)
@@ -41,6 +73,16 @@ public class MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program
_queuedInstanceCount++;
}
/// <inheritdoc/>
public override void Reset()
{
base.Reset();
_textureUnitMap.Reset();
_previousHash = _frameHash;
_frameHash = 0;
}
/// <inheritdoc/>
protected override void SetAdditionalUniforms(Program parProgram)
{
foreach (var (texture, unit) in _textureUnitMap.Textures)
@@ -52,16 +94,9 @@ public class MeshRenderer(Asset.Mesh.Mesh parMesh, int parInstanceCount, Program
parProgram.SetUniform("uTexture", _textureUnitIndices);
}
/// <inheritdoc/>
protected override bool DataChanged()
{
return _frameHash != _previousHash;
}
public override void Reset()
{
base.Reset();
_textureUnitMap.Reset();
_previousHash = _frameHash;
_frameHash = 0;
}
}

View File

@@ -4,8 +4,19 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Quad;
/// <summary>
/// Represents a common vertex structure for a quad.
/// This vertex structure includes position and UV coordinates.
/// </summary>
public struct QuadCommonVertex : IVertex
{
/// <summary>
/// The position of the vertex in 3D space.
/// </summary>
[Vertex(VertexAttribType.Float, 3)] public Vector3 _position;
/// <summary>
/// The UV coordinates of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 2)] public Vector2 _uv;
}

View File

@@ -4,12 +4,28 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Quad;
/// <summary>
/// Represents an instance vertex structure for a quad.
/// This vertex structure includes color, texture ID, and model matrix.
/// </summary>
public struct QuadInstanceVertex : IVertex
{
/// <summary>
/// The color of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 4)] public Vector4 _color;
/// <summary>
/// The texture ID of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float)] public float _textureId;
/// <summary>
/// The model matrix of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(_color, _textureId, _modelMatrix);

View File

@@ -5,14 +5,37 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Quad;
/// <summary>
/// A renderer class for rendering quadrilaterals (quads) using instancing.
/// Supports dynamic texture binding and manages the state for rendering multiple instances of quads.
/// </summary>
public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVertex>
{
/// <summary>
/// Maps textures to texture units with a limit of 16 texture units.
/// </summary>
private readonly TextureUnitMap _textureUnitMap = new(16);
/// <summary>
/// Stores the texture unit indices used for binding textures.
/// </summary>
private readonly int[] _textureUnitIndices = new int[16];
/// <summary>
/// Stores the hash of the current frame, used for detecting changes in instance data.
/// </summary>
private int _frameHash;
/// <summary>
/// Stores the hash of the previous frame.
/// </summary>
private int _previousHash;
/// <summary>
/// Initializes a new instance of the <see cref="QuadRenderer"/> class.
/// </summary>
/// <param name="parEngine">The engine used to load resources.</param>
/// <param name="parInstanceCount">The number of instances to render.</param>
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) },
@@ -24,6 +47,12 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
{
}
/// <summary>
/// Commits an instance to the renderer, adding it to the queue with the specified model matrix, color, and optional texture.
/// </summary>
/// <param name="parModelMatrix">The model transformation matrix for this instance.</param>
/// <param name="parColor">The color to apply to this instance.</param>
/// <param name="parTexture">An optional texture to apply to the quad. If null, no texture is applied.</param>
public void Commit(in Matrix4 parModelMatrix, in Vector4 parColor, Texture.Texture? parTexture = null)
{
if (_queuedInstanceCount >= _instanceCount)
@@ -46,6 +75,16 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
_queuedInstanceCount++;
}
/// <inheritdoc/>
public override void Reset()
{
base.Reset();
_textureUnitMap.Reset();
_previousHash = _frameHash;
_frameHash = 0;
}
/// <inheritdoc/>
protected override void SetAdditionalUniforms(Program parProgram)
{
foreach (var (texture, unit) in _textureUnitMap.Textures)
@@ -57,16 +96,9 @@ public class QuadRenderer : InstancedRenderer<QuadCommonVertex, QuadInstanceVert
parProgram.SetUniform("uTexture", _textureUnitIndices);
}
/// <inheritdoc/>
protected override bool DataChanged()
{
return _frameHash != _previousHash;
}
public override void Reset()
{
base.Reset();
_textureUnitMap.Reset();
_previousHash = _frameHash;
_frameHash = 0;
}
}

View File

@@ -4,10 +4,29 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
/// <summary>
/// Represents a common vertex structure for a glyph.
/// This vertex structure includes color, atlas ID, unit range, and model matrix.
/// </summary>
public struct GlyphCommonVertex : IVertex
{
/// <summary>
/// The color of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 4)] public Vector4 _color;
/// <summary>
/// The atlas ID of the vertex.
/// </summary>
[Vertex(VertexAttribType.Int)] public int _atlasId;
/// <summary>
/// The unit range of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 2)] public Vector2 _unitRange;
/// <summary>
/// The model matrix of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
}

View File

@@ -4,12 +4,39 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
/// <summary>
/// Represents a vertex structure for a glyph.
/// This vertex structure includes position, UV coordinates, color, atlas ID, unit range, and model matrix.
/// </summary>
public struct GlyphVertex : IVertex
{
/// <summary>
/// The position of the vertex in 2D space.
/// </summary>
[Vertex(VertexAttribType.Float, 2)] public Vector2 _position;
/// <summary>
/// The UV coordinates of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 2)] public Vector2 _uv;
/// <summary>
/// The color of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 4)] public Vector4 _color;
/// <summary>
/// The atlas ID of the vertex.
/// </summary>
[Vertex(VertexAttribType.Int)] public int _atlasId;
/// <summary>
/// The unit range of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 2)] public Vector2 _unitRange;
/// <summary>
/// The model matrix of the vertex.
/// </summary>
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
}

View File

@@ -7,21 +7,62 @@ using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Text;
/// <summary>
/// A renderer class for rendering text using glyphs from a font atlas.
/// Handles dynamic font rendering with support for textures.
/// </summary>
public class TextRenderer
{
/// <summary>
/// The shader program used for rendering the text.
/// </summary>
private readonly Program _program;
/// <summary>
/// The index buffer used for rendering the text glyphs.
/// </summary>
private readonly IndexBuffer _indexBuffer;
/// <summary>
/// The buffer holding vertex data for all glyphs to be rendered.
/// </summary>
private readonly VertexBuffer<GlyphVertex> _glyphVertexBuffer;
/// <summary>
/// The vertex array object that binds the index and vertex buffers.
/// </summary>
private readonly VertexArray _vertexArray;
/// <summary>
/// The number of characters that can be rendered at once.
/// </summary>
private readonly int _characterCount;
/// <summary>
/// The array of glyph vertices representing the text to be rendered.
/// </summary>
private readonly GlyphVertex[] _glyphVertices;
/// <summary>
/// Maps textures to texture units for rendering the font atlas.
/// </summary>
private readonly TextureUnitMap _textureUnitMap = new(16);
/// <summary>
/// Stores the texture unit indices for binding textures.
/// </summary>
private readonly int[] _textureUnitIndices = new int[16];
/// <summary>
/// The number of characters that have been queued for rendering.
/// </summary>
private int _queuedCharacterCount;
/// <summary>
/// Initializes a new instance of the <see cref="TextRenderer"/> class.
/// </summary>
/// <param name="parEngine">The engine used to load resources.</param>
/// <param name="parCharacterCount">The maximum number of characters to render.</param>
public TextRenderer(Engine parEngine, int parCharacterCount)
{
_characterCount = parCharacterCount;
@@ -39,6 +80,13 @@ public class TextRenderer
_vertexArray.BindVertexBuffer(_glyphVertexBuffer);
}
/// <summary>
/// Commits a string of text to the renderer, creating the necessary glyphs and adding them to the render queue.
/// </summary>
/// <param name="parFont">The font to use for rendering the text.</param>
/// <param name="parText">The text string to render.</param>
/// <param name="parColor">The color to apply to the text.</param>
/// <param name="parModelMatrix">The model transformation matrix to apply to the text.</param>
public void Commit(Font parFont, string parText, Vector4 parColor, in Matrix4 parModelMatrix)
{
if (_queuedCharacterCount >= _characterCount)
@@ -66,6 +114,11 @@ public class TextRenderer
}
}
/// <summary>
/// Renders the queued text characters to the screen with the given projection and view matrices.
/// </summary>
/// <param name="parProjectionMatrix">The projection matrix to use for the render.</param>
/// <param name="parViewMatrix">The view matrix to use for the render.</param>
public void Render(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix)
{
if (_queuedCharacterCount <= 0)
@@ -90,12 +143,20 @@ public class TextRenderer
GL.DrawElements(PrimitiveType.Triangles, _queuedCharacterCount * 6, DrawElementsType.UnsignedInt, 0);
}
/// <summary>
/// Resets the text renderer, clearing all queued characters and texture unit mappings.
/// </summary>
public void Reset()
{
_textureUnitMap.Reset();
_queuedCharacterCount = 0;
}
/// <summary>
/// Creates the indices for rendering the glyphs.
/// </summary>
/// <param name="parCharacterCount">The number of characters to render.</param>
/// <returns>An array of indices for rendering the glyphs.</returns>
private static uint[] CreateIndices(int parCharacterCount)
{
var indices = new uint[parCharacterCount * 6];

View File

@@ -7,22 +7,63 @@ using OpenTK.Windowing.Desktop;
namespace Engine.Graphics;
/// <summary>
/// Handles the rendering pipeline, manages render layers, and provides tools for rendering graphics in the engine.
/// </summary>
public class Renderer
{
/// <summary>
/// The width of the viewport.
/// </summary>
public int ViewportWidth => RenderFramebuffer.Width;
/// <summary>
/// The height of the viewport.
/// </summary>
public int ViewportHeight => RenderFramebuffer.Height;
/// <summary>
/// The framebuffer used for rendering.
/// </summary>
internal Framebuffer.Framebuffer RenderFramebuffer { get; }
/// <summary>
/// The texture of the render framebuffer.
/// </summary>
internal Texture.Texture RenderTexture => RenderFramebuffer.TextureInternal!;
/// <summary>
/// The native OpenGL window.
/// </summary>
internal NativeWindow NativeWindow { get; }
/// <summary>
/// Stores renderers for each render layer in a sorted dictionary.
/// </summary>
private readonly SortedDictionary<RenderLayer, GenericRenderer> _renderers = new();
/// <summary>
/// The thread dedicated to rendering.
/// </summary>
private readonly Thread _renderThread;
/// <summary>
/// Renderer for quad-based primitives.
/// </summary>
private QuadRenderer QuadRenderer { get; }
/// <summary>
/// Queue of actions scheduled to run on the render thread.
/// </summary>
private readonly Queue<Action> _scheduleActions = new();
/// <summary>
/// Initializes the renderer with the specified engine, dimensions, and native window settings.
/// </summary>
/// <param name="parEngine">The engine instance.</param>
/// <param name="parWidth">The width of the rendering viewport.</param>
/// <param name="parHeight">The height of the rendering viewport.</param>
/// <param name="parSettings">Settings for the native window.</param>
public Renderer(Engine parEngine, int parWidth, int parHeight, NativeWindowSettings parSettings)
{
_renderThread = Thread.CurrentThread;
@@ -48,6 +89,12 @@ public class Renderer
}
}
/// <summary>
/// Retrieves the renderer for the specified render layer.
/// </summary>
/// <param name="parRenderLayer">The render layer to retrieve.</param>
/// <returns>The <see cref="GenericRenderer"/> for the specified render layer.</returns>
/// <exception cref="InvalidOperationException">Thrown if the render layer does not exist.</exception>
public GenericRenderer this[RenderLayer parRenderLayer]
{
get
@@ -61,24 +108,11 @@ public class Renderer
}
}
private void InitializeOpenGl(int parWidth, int parHeight)
{
#if DEBUG
Debug.Setup();
#endif
GL.Enable(EnableCap.DepthTest);
// GL.Enable(EnableCap.CullFace);
// GL.Enable(EnableCap.FramebufferSrgb);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.Viewport(0, 0, parWidth, parHeight);
}
public void EnsureRenderThread()
/// <summary>
/// Ensures that the current thread is the render thread.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if not on the render thread in debug mode.</exception>
internal void EnsureRenderThread()
{
#if DEBUG
if (Thread.CurrentThread != _renderThread)
@@ -88,11 +122,21 @@ public class Renderer
#endif
}
/// <summary>
/// Schedules an action to be executed on the render thread.
/// </summary>
/// <param name="parAction">The action to execute.</param>
internal void Schedule(Action parAction)
{
_scheduleActions.Enqueue(parAction);
}
/// <summary>
/// Schedules a function to be executed on the render thread and returns a task representing the result.
/// </summary>
/// <typeparam name="T">The return type of the function.</typeparam>
/// <param name="parAction">The function to execute.</param>
/// <returns>A task representing the function's result.</returns>
internal Task<T> Schedule<T>(Func<T> parAction)
{
var completionSource = new TaskCompletionSource<T>();
@@ -112,6 +156,9 @@ public class Renderer
return completionSource.Task;
}
/// <summary>
/// Prepares the rendering pipeline for a new frame.
/// </summary>
internal void StartFrame()
{
EnsureRenderThread();
@@ -122,6 +169,10 @@ public class Renderer
}
}
/// <summary>
/// Finalizes the rendering pipeline for the current frame.
/// </summary>
/// <param name="parMatrices">A dictionary of matrices for render layers.</param>
internal void EndFrame(Dictionary<RenderLayer, (Matrix4, Matrix4)> parMatrices)
{
EnsureRenderThread();
@@ -158,6 +209,11 @@ public class Renderer
RenderFramebuffer.Unbind();
}
/// <summary>
/// Resizes the rendering viewport and updates associated resources.
/// </summary>
/// <param name="parWidth">The new width of the viewport.</param>
/// <param name="parHeight">The new height of the viewport.</param>
internal void Resize(int parWidth, int parHeight)
{
RenderFramebuffer.Resize(parWidth, parHeight);
@@ -169,6 +225,9 @@ public class Renderer
}
}
/// <summary>
/// Executes all scheduled actions on the render thread.
/// </summary>
internal void RunScheduledActions()
{
while (_scheduleActions.TryDequeue(out var action))
@@ -176,4 +235,26 @@ public class Renderer
action();
}
}
/// <summary>
/// Initializes OpenGL settings for rendering.
/// </summary>
/// <param name="parWidth">The width of the viewport.</param>
/// <param name="parHeight">The height of the viewport.</param>
private void InitializeOpenGl(int parWidth, int parHeight)
{
#if DEBUG
Debug.Setup();
#endif
GL.Enable(EnableCap.DepthTest);
// GL.Enable(EnableCap.CullFace);
// GL.Enable(EnableCap.FramebufferSrgb);
GL.Enable(EnableCap.Blend);
GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
GL.Viewport(0, 0, parWidth, parHeight);
}
}

View File

@@ -4,10 +4,22 @@ using Serilog;
namespace Engine.Graphics.Shader;
/// <summary>
/// Represents an OpenGL shader program that can be used for rendering.
/// The class handles the compilation, linking, and management of shaders, as well as setting uniform values.
/// </summary>
public class Program : OpenGlObject
{
/// <summary>
/// A dictionary mapping uniform names to their locations in the shader program.
/// </summary>
private readonly Dictionary<string, int> _uniforms = new();
/// <summary>
/// Initializes a new instance of the <see cref="Program"/> class with the provided vertex and fragment shader sources.
/// </summary>
/// <param name="parVertexSource">The source code for the vertex shader.</param>
/// <param name="parFragmentSource">The source code for the fragment shader.</param>
public Program(string parVertexSource, string parFragmentSource)
{
var vertexShader = CompileSource(parVertexSource, ShaderType.VertexShader);
@@ -16,6 +28,13 @@ public class Program : OpenGlObject
Handle = LinkProgram(vertexShader, fragmentShader);
}
/// <summary>
/// Sets a uniform variable in the shader program with the given value.
/// </summary>
/// <typeparam name="T">The type of the uniform value.</typeparam>
/// <param name="parName">The name of the uniform variable in the shader.</param>
/// <param name="parValue">The value to set for the uniform.</param>
/// <exception cref="ArgumentException">Thrown when an unsupported uniform type is provided.</exception>
public void SetUniform<T>(string parName, in T parValue)
{
try
@@ -62,21 +81,31 @@ public class Program : OpenGlObject
}
}
/// <inheritdoc/>
internal override void Bind()
{
GL.UseProgram(Handle);
}
/// <inheritdoc/>
internal override void Unbind()
{
GL.UseProgram(0);
}
/// <inheritdoc/>
protected override void Destroy()
{
GL.DeleteProgram(Handle);
}
/// <summary>
/// Retrieves the location of a uniform variable in the shader program.
/// If the location is not cached, it will be retrieved from OpenGL.
/// </summary>
/// <param name="parName">The name of the uniform variable.</param>
/// <returns>The location of the uniform variable.</returns>
/// <exception cref="ArgumentException">Thrown when the uniform is not found in the shader program.</exception>
private int GetUniformLocation(string parName)
{
if (_uniforms.TryGetValue(parName, out var location))
@@ -94,6 +123,13 @@ public class Program : OpenGlObject
return location;
}
/// <summary>
/// Compiles a shader from the given source code and shader type.
/// </summary>
/// <param name="parSource">The source code of the shader.</param>
/// <param name="parType">The type of shader (vertex or fragment).</param>
/// <returns>The compiled shader handle.</returns>
/// <exception cref="ShaderCompilationException">Thrown when the shader fails to compile.</exception>
private static int CompileSource(string parSource, ShaderType parType)
{
var shaderId = GL.CreateShader(parType);
@@ -112,6 +148,13 @@ public class Program : OpenGlObject
return shaderId;
}
/// <summary>
/// Links a vertex and fragment shader into a single shader program.
/// </summary>
/// <param name="parVertexShader">The compiled vertex shader handle.</param>
/// <param name="parFragmentShader">The compiled fragment shader handle.</param>
/// <returns>The linked shader program handle.</returns>
/// <exception cref="ShaderLinkException">Thrown when the shader program fails to link.</exception>
private static int LinkProgram(int parVertexShader, int parFragmentShader)
{
var programId = GL.CreateProgram();
@@ -152,13 +195,52 @@ public class Program : OpenGlObject
}
}
public class ShaderCompilationException(ShaderType parType, string parMessage)
: Exception($"Failed to compile {parType} shader: {parMessage}")
/// <summary>
/// Exception thrown when a shader fails to compile.
/// </summary>
public class ShaderCompilationException : Exception
{
public ShaderType ShaderType { get; } = parType;
/// <summary>
/// The type of the shader that failed to compile.
/// </summary>
public ShaderType ShaderType { get; }
/// <summary>
/// Initializes a new instance of the <see cref="ShaderCompilationException"/> class.
/// </summary>
/// <param name="parType">The type of shader that failed to compile.</param>
/// <param name="parMessage">The error message from the shader compilation failure.</param>
public ShaderCompilationException(ShaderType parType, string parMessage) : base(
$"Failed to compile {parType} shader: {parMessage}")
{
ShaderType = parType;
}
}
public class ShaderLinkException(string parMessage) : Exception($"Failed to link shader program: {parMessage}");
/// <summary>
/// Exception thrown when a shader program fails to link.
/// </summary>
public class ShaderLinkException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ShaderLinkException"/> class.
/// </summary>
/// <param name="parMessage">The error message from the shader linking failure.</param>
public ShaderLinkException(string parMessage) : base($"Failed to link shader program: {parMessage}")
{
}
}
public class ShaderValidationException(string parMessage)
: Exception($"Failed to validate shader program: {parMessage}");
/// <summary>
/// Exception thrown when a shader program fails validation.
/// </summary>
public class ShaderValidationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ShaderValidationException"/> class.
/// </summary>
/// <param name="parMessage">The error message from the shader validation failure.</param>
public ShaderValidationException(string parMessage) : base($"Failed to validate shader program: {parMessage}")
{
}
}

View File

@@ -5,12 +5,35 @@ using Serilog;
namespace Engine.Graphics.Texture;
/// <summary>
/// Represents a dynamic texture that can be resized and attached to a framebuffer.
/// </summary>
public class DynamicTexture : Texture, IFramebufferAttachment
{
/// <summary>
/// The pixel format of the texture.
/// </summary>
private readonly PixelFormat _format;
/// <summary>
/// The pixel type of the texture.
/// </summary>
private readonly PixelType _type;
/// <summary>
/// The internal format of the texture.
/// </summary>
private readonly PixelInternalFormat _internalFormat;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicTexture"/> class.
/// The texture is created with the specified format, type, and internal format.
/// </summary>
/// <param name="parWidth">The width of the texture.</param>
/// <param name="parHeight">The height of the texture.</param>
/// <param name="parFormat">The format of the texture.</param>
/// <param name="parType">The type of the texture.</param>
/// <param name="parInternalFormat">The internal format of the texture.</param>
internal DynamicTexture(int parWidth, int parHeight, PixelFormat parFormat, PixelType parType,
PixelInternalFormat parInternalFormat)
: base(parWidth, parHeight)
@@ -24,6 +47,13 @@ public class DynamicTexture : Texture, IFramebufferAttachment
IntPtr.Zero);
}
/// <summary>
/// Creates a new <see cref="DynamicTexture"/> with the specified width, height, and pixel type.
/// </summary>
/// <typeparam name="T">The type of pixel used for the texture.</typeparam>
/// <param name="parWidth">The width of the texture.</param>
/// <param name="parHeight">The height of the texture.</param>
/// <returns>A new instance of <see cref="DynamicTexture"/>.</returns>
public static DynamicTexture Create<T>(int parWidth, int parHeight)
where T : struct, IPixel
{
@@ -31,6 +61,11 @@ public class DynamicTexture : Texture, IFramebufferAttachment
return new DynamicTexture(parWidth, parHeight, pixel.Format, pixel.Type, pixel.InternalFormat);
}
/// <summary>
/// Resizes the texture to the specified width and height.
/// </summary>
/// <param name="parWidth">The new width of the texture.</param>
/// <param name="parHeight">The new height of the texture.</param>
public void Resize(int parWidth, int parHeight)
{
if (Width == parWidth && Height == parHeight)
@@ -48,6 +83,7 @@ public class DynamicTexture : Texture, IFramebufferAttachment
Log.Debug("Texture {Handle} resized to {Width}x{Height}", Handle, Width, Height);
}
/// <inheritdoc/>
public void Attach(Framebuffer.Framebuffer parFramebuffer, FramebufferAttachment parAttachment)
{
GL.NamedFramebufferTexture(parFramebuffer.Handle, parAttachment, Handle, 0);

View File

@@ -3,22 +3,60 @@ using Engine.Graphics.Pixel;
namespace Engine.Graphics.Texture;
/// <summary>
/// Represents a read-only texture with dimensions and pixel access capabilities.
/// </summary>
public interface IConstTexture
{
/// <summary>
/// The width of the texture.
/// </summary>
public int Width { get; }
/// <summary>
/// The height of the texture.
/// </summary>
public int Height { get; }
/// <summary>
/// Reads pixels from the texture into the provided pixel array.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parX">The x-coordinate of the region to read.</param>
/// <param name="parY">The y-coordinate of the region to read.</param>
/// <param name="parWidth">The width of the region to read.</param>
/// <param name="parHeight">The height of the region to read.</param>
/// <param name="parPixels">The array to store the read pixels.</param>
public void ReadPixels<T>(int parX, int parY, int parWidth, int parHeight, T[,] parPixels)
where T : struct, IPixel;
}
/// <summary>
/// Provides extension methods for <see cref="IConstTexture"/>.
/// </summary>
public static class ConstTextureExtensions
{
/// <summary>
/// Reads all pixels from the texture.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parTexture">The texture to read from.</param>
/// <returns>A 2D array containing the pixels.</returns>
public static T[,] ReadPixels<T>(this IConstTexture parTexture) where T : struct, IPixel
{
return parTexture.ReadPixels<T>(0, 0, parTexture.Width, parTexture.Height);
}
/// <summary>
/// Reads a specified region of pixels from the texture.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parTexture">The texture to read from.</param>
/// <param name="parX">The x-coordinate of the region to read.</param>
/// <param name="parY">The y-coordinate of the region to read.</param>
/// <param name="parWidth">The width of the region to read.</param>
/// <param name="parHeight">The height of the region to read.</param>
/// <returns>A 2D array containing the pixels.</returns>
public static T[,] ReadPixels<T>(this IConstTexture parTexture, int parX, int parY, int parWidth, int parHeight)
where T : struct, IPixel
{
@@ -28,17 +66,37 @@ public static class ConstTextureExtensions
return pixels;
}
/// <summary>
/// Reads all pixels from the texture into the specified image.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parTexture">The texture to read from.</param>
/// <param name="parImage">The image to store the pixels.</param>
public static void ReadPixels<T>(this IConstTexture parTexture, Image<T> parImage) where T : struct, IPixel
{
parTexture.ReadPixels(0, 0, parImage);
}
/// <summary>
/// Reads a specified region of pixels from the texture into the specified image.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parTexture">The texture to read from.</param>
/// <param name="parX">The x-coordinate of the region to read.</param>
/// <param name="parY">The y-coordinate of the region to read.</param>
/// <param name="parImage">The image to store the pixels.</param>
public static void ReadPixels<T>(this IConstTexture parTexture, int parX, int parY, Image<T> parImage)
where T : struct, IPixel
{
parTexture.ReadPixels(parX, parY, parImage.Width, parImage.Height, parImage.Pixels);
}
/// <summary>
/// Reads all pixels from the texture into the specified 2D array.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parTexture">The texture to read from.</param>
/// <param name="parPixels">The array to store the pixels.</param>
public static void ReadPixels<T>(this IConstTexture parTexture, T[,] parPixels) where T : struct, IPixel
{
parTexture.ReadPixels(0, 0, parTexture.Width, parTexture.Height, parPixels);

View File

@@ -3,25 +3,60 @@ using Engine.Graphics.Pixel;
namespace Engine.Graphics.Texture;
/// <summary>
/// Represents a writable texture with dimensions and pixel access capabilities.
/// </summary>
public interface ITexture : IConstTexture
{
/// <summary>
/// Uploads pixels to the texture.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parX">The x-coordinate of the region to write.</param>
/// <param name="parY">The y-coordinate of the region to write.</param>
/// <param name="parWidth">The width of the region to write.</param>
/// <param name="parHeight">The height of the region to write.</param>
/// <param name="parPixels">The pixel data to upload.</param>
public void UploadPixels<T>(int parX, int parY, int parWidth, int parHeight, T[,] parPixels)
where T : struct, IPixel;
}
/// <summary>
/// Provides extension methods for <see cref="ITexture"/>.
/// </summary>
public static class TextureExtensions
{
/// <summary>
/// Uploads the entire image to the texture.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parTexture">The texture to upload to.</param>
/// <param name="parImage">The image containing the pixel data.</param>
public static void UploadPixels<T>(this ITexture parTexture, Image<T> parImage) where T : struct, IPixel
{
parTexture.UploadPixels(0, 0, parImage);
}
/// <summary>
/// Uploads a specified region of the image to the texture.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parTexture">The texture to upload to.</param>
/// <param name="parX">The x-coordinate of the region in the image.</param>
/// <param name="parY">The y-coordinate of the region in the image.</param>
/// <param name="parImage">The image containing the pixel data.</param>
public static void UploadPixels<T>(this ITexture parTexture, int parX, int parY, Image<T> parImage)
where T : struct, IPixel
{
parTexture.UploadPixels(parX, parY, parImage.Width, parImage.Height, parImage.Pixels);
}
/// <summary>
/// Uploads the entire 2D pixel array to the texture.
/// </summary>
/// <typeparam name="T">The pixel type, which must implement <see cref="IPixel"/>.</typeparam>
/// <param name="parTexture">The texture to upload to.</param>
/// <param name="parPixels">The pixel data to upload.</param>
public static void UploadPixels<T>(this ITexture parTexture, T[,] parPixels) where T : struct, IPixel
{
parTexture.UploadPixels(0, 0, parTexture.Width, parTexture.Height, parPixels);

View File

@@ -3,13 +3,30 @@ using OpenTK.Graphics.OpenGL;
namespace Engine.Graphics.Texture;
/// <summary>
/// Represents a static texture in OpenGL, created with fixed storage and format.
/// </summary>
public class StaticTexture : Texture
{
/// <summary>
/// Initializes a new instance of the <see cref="StaticTexture"/> class.
/// The texture is created with the specified width, height, and internal format.
/// </summary>
/// <param name="parWidth">The width of the texture.</param>
/// <param name="parHeight">The height of the texture.</param>
/// <param name="parFormat">The internal format of the texture.</param>
private StaticTexture(int parWidth, int parHeight, SizedInternalFormat parFormat) : base(parWidth, parHeight)
{
GL.TextureStorage2D(Handle, 1, parFormat, Width, Height);
}
/// <summary>
/// Creates a new <see cref="StaticTexture"/> with the specified width and height.
/// </summary>
/// <typeparam name="T">The type of pixel format used for the texture.</typeparam>
/// <param name="parWidth">The width of the texture.</param>
/// <param name="parHeight">The height of the texture.</param>
/// <returns>A new instance of <see cref="StaticTexture"/>.</returns>
public static StaticTexture Create<T>(int parWidth, int parHeight)
where T : struct, IPixel
{

View File

@@ -5,8 +5,13 @@ using Serilog;
namespace Engine.Graphics.Texture;
/// <summary>
/// Represents an abstract texture object used in OpenGL for 2D textures.
/// Handles the creation, binding, and destruction of textures and provides functionality for uploading and reading pixel data.
/// </summary>
public abstract class Texture : OpenGlObject, ITexture
{
/// <inheritdoc/>
public int Width
{
get => _width;
@@ -21,6 +26,7 @@ public abstract class Texture : OpenGlObject, ITexture
}
}
/// <inheritdoc/>
public int Height
{
get => _height;
@@ -35,9 +41,22 @@ public abstract class Texture : OpenGlObject, ITexture
}
}
/// <summary>
/// The width of the texture.
/// </summary>
private int _width;
/// <summary>
/// The height of the texture.
/// </summary>
private int _height;
/// <summary>
/// Initializes a new instance of the <see cref="Texture"/> class with the specified width and height.
/// Creates an OpenGL texture handle and sets default texture parameters.
/// </summary>
/// <param name="parWidth">The width of the texture.</param>
/// <param name="parHeight">The height of the texture.</param>
protected Texture(int parWidth, int parHeight)
{
Width = parWidth;
@@ -54,6 +73,7 @@ public abstract class Texture : OpenGlObject, ITexture
Log.Debug("Texture {Handle} created with {Width}x{Height}", Handle, Width, Height);
}
/// <inheritdoc/>
public void UploadPixels<T>(int parX, int parY, int parWidth, int parHeight, T[,] parPixels)
where T : struct, IPixel
{
@@ -83,6 +103,7 @@ public abstract class Texture : OpenGlObject, ITexture
GL.TextureSubImage2D(Handle, 0, parX, parY, parWidth, parHeight, format, type, parPixels);
}
/// <inheritdoc/>
public void ReadPixels<T>(int parX, int parY, int parWidth, int parHeight, T[,] parPixels)
where T : struct, IPixel
{
@@ -118,21 +139,28 @@ public abstract class Texture : OpenGlObject, ITexture
parPixels);
}
/// <summary>
/// Binds the texture to a texture unit.
/// </summary>
/// <param name="parUnit">The texture unit to which the texture should be bound.</param>
public void BindUnit(int parUnit = 0)
{
GL.BindTextureUnit(parUnit, Handle);
}
/// <inheritdoc/>
internal override void Bind()
{
GL.BindTexture(TextureTarget.Texture2D, Handle);
}
/// <inheritdoc/>
internal override void Unbind()
{
GL.BindTexture(TextureTarget.Texture2D, 0);
}
/// <inheritdoc/>
protected override void Destroy()
{
GL.DeleteTexture(Handle);

View File

@@ -1,13 +1,48 @@
namespace Engine.Graphics.Texture;
public class TextureUnitMap(int parCapacity)
/// <summary>
/// Manages the mapping of textures to texture units.
/// Allows tracking of textures and their associated texture units within a given capacity.
/// </summary>
public class TextureUnitMap
{
/// <summary>
/// The maximum number of textures that can be mapped.
/// </summary>
public int Capacity { get; }
/// <summary>
/// The current number of textures in the unit map.
/// </summary>
public int Size => _textures.Count;
public int Capacity => parCapacity;
/// <summary>
/// A read-only dictionary that holds the textures and their associated unit numbers.
/// </summary>
public IReadOnlyDictionary<Texture, int> Textures => _textures;
/// <summary>
/// A dictionary that stores the textures and their corresponding texture units.
/// </summary>
private readonly Dictionary<Texture, int> _textures = new();
/// <summary>
/// Initializes a new instance of the <see cref="TextureUnitMap"/> class with the specified capacity.
/// </summary>
/// <param name="parCapacity">The maximum number of textures that can be mapped in this unit map.</param>
public TextureUnitMap(int parCapacity)
{
Capacity = parCapacity;
}
/// <summary>
/// Retrieves the texture unit assigned to the specified texture.
/// If the texture is not already mapped, a new unit is assigned.
/// Throws an exception if the map has reached its capacity.
/// </summary>
/// <param name="parTexture">The texture for which to retrieve the assigned unit.</param>
/// <returns>The texture unit assigned to the texture.</returns>
/// <exception cref="InvalidOperationException">Thrown when the map has reached its capacity.</exception>
public int GetUnit(Texture parTexture)
{
if (_textures.TryGetValue(parTexture, out var unit))
@@ -15,7 +50,7 @@ public class TextureUnitMap(int parCapacity)
return unit;
}
if (_textures.Count >= parCapacity)
if (_textures.Count >= Capacity)
{
throw new InvalidOperationException("Texture unit map is full");
}
@@ -26,6 +61,9 @@ public class TextureUnitMap(int parCapacity)
return unit;
}
/// <summary>
/// Clears all textures from the unit map, effectively resetting it.
/// </summary>
public void Reset()
{
_textures.Clear();

View File

@@ -4,15 +4,46 @@ using OpenTK.Mathematics;
namespace Engine.Input;
/// <summary>
/// Represents an input handler interface that processes keyboard and mouse input.
/// </summary>
public interface IInputHandler : IUpdate
{
CultureInfo CurrentInputLanguage { get; }
/// <summary>
/// The current input language.
/// </summary>
public CultureInfo CurrentInputLanguage { get; }
Vector2 MousePosition { get; }
/// <summary>
/// The current mouse position in screen coordinates.
/// </summary>
public Vector2 MousePosition { get; }
bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode);
bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode);
/// <summary>
/// Checks if a specific keyboard button is currently pressed.
/// </summary>
/// <param name="parKeyboardButtonCode">The keyboard button code to check.</param>
/// <returns>True if the button is pressed; otherwise, false.</returns>
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode);
bool IsMouseButtonPressed(MouseButtonCode parButtonCode);
bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode);
/// <summary>
/// Checks if a specific keyboard button was just pressed in the current frame.
/// </summary>
/// <param name="parKeyboardButtonCode">The keyboard button code to check.</param>
/// <returns>True if the button was just pressed; otherwise, false.</returns>
public bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode);
/// <summary>
/// Checks if a specific mouse button is currently pressed.
/// </summary>
/// <param name="parButtonCode">The mouse button code to check.</param>
/// <returns>True if the button is pressed; otherwise, false.</returns>
public bool IsMouseButtonPressed(MouseButtonCode parButtonCode);
/// <summary>
/// Checks if a specific mouse button was just pressed in the current frame.
/// </summary>
/// <param name="parButtonCode">The mouse button code to check.</param>
/// <returns>True if the button was just pressed; otherwise, false.</returns>
public bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode);
}

View File

@@ -61,22 +61,42 @@ public enum KeyboardButtonCode
PageUp,
PageDown,
/// <summary>
/// Represents the total count of keyboard button codes.
/// </summary>
TotalCount = PageDown + 1
}
/// <summary>
/// Provides helper methods for working with keyboard button codes.
/// </summary>
public static class KeyboardButtonCodeHelper
{
/// <summary>
/// Gets a list of all printable keyboard button codes.
/// </summary>
/// <returns>A list of printable keyboard button codes.</returns>
public static List<KeyboardButtonCode> GetAllPrintableKeys()
{
return Enum.GetValues<KeyboardButtonCode>().Where(parX => parX.IsPrintableKey()).ToList();
}
/// <summary>
/// Determines whether a keyboard button code is printable.
/// </summary>
/// <param name="parKey">The keyboard button code to check.</param>
/// <returns>True if the key is printable; otherwise, false.</returns>
public static bool IsPrintableKey(this KeyboardButtonCode parKey)
{
return parKey is >= KeyboardButtonCode.A and <= KeyboardButtonCode.Z
or >= KeyboardButtonCode.D1 and <= KeyboardButtonCode.D0 or KeyboardButtonCode.Space;
}
/// <summary>
/// Gets the character representation of a keyboard button code based on the current input language.
/// </summary>
/// <param name="parKey">The keyboard button code to convert.</param>
/// <returns>The character representation of the key.</returns>
public static char GetChar(this KeyboardButtonCode parKey)
{
return Engine.Instance.InputHandler!.CurrentInputLanguage.Name switch
@@ -87,6 +107,11 @@ public static class KeyboardButtonCodeHelper
};
}
/// <summary>
/// Gets the character representation of a keyboard button code in English.
/// </summary>
/// <param name="parKey">The keyboard button code to convert.</param>
/// <returns>The character representation of the key.</returns>
private static char GetEnChar(this KeyboardButtonCode parKey)
{
return parKey switch
@@ -134,6 +159,11 @@ public static class KeyboardButtonCodeHelper
};
}
/// <summary>
/// Gets the character representation of a keyboard button code in Russian.
/// </summary>
/// <param name="parKey">The keyboard button code to convert.</param>
/// <returns>The character representation of the key.</returns>
private static char GetRuChar(this KeyboardButtonCode parKey)
{
return parKey switch

View File

@@ -1,10 +1,27 @@
namespace Engine.Input;
/// <summary>
/// Represents the different mouse button codes.
/// </summary>
public enum MouseButtonCode
{
/// <summary>
/// Represents the left mouse button.
/// </summary>
Left,
/// <summary>
/// Represents the right mouse button.
/// </summary>
Right,
/// <summary>
/// Represents the middle mouse button.
/// </summary>
Middle,
/// <summary>
/// Represents the total count of mouse button codes.
/// </summary>
TotalCount = Middle + 1
}

View File

@@ -4,47 +4,98 @@ using OpenTK.Windowing.GraphicsLibraryFramework;
namespace Engine.Input;
public class WindowInputHandler(Window parWindow) : IInputHandler
/// <summary>
/// Handles input for a window, processing keyboard and mouse input states.
/// </summary>
public class WindowInputHandler : IInputHandler
{
/// <inheritdoc/>
public CultureInfo CurrentInputLanguage => new(1033);
public Vector2 MousePosition => parWindow.NativeWindow.MouseState.Position;
private KeyboardState _keyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
private KeyboardState _previousKeyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
private MouseState _mouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
private MouseState _previousMouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
/// <inheritdoc/>
public Vector2 MousePosition => _window.NativeWindow.MouseState.Position;
/// <summary>
/// The window to capture input from.
/// </summary>
private readonly Window _window;
/// <summary>
/// Keyboard state in the current frame.
/// </summary>
private KeyboardState _keyboardState;
/// <summary>
/// Keyboard state in the previous frame.
/// </summary>
private KeyboardState _previousKeyboardState;
/// <summary>
/// Mouse state in the current frame.
/// </summary>
private MouseState _mouseState;
/// <summary>
/// Mouse state in the previous frame.
/// </summary>
private MouseState _previousMouseState;
/// <summary>
/// Initializes a new instance of the <see cref="WindowInputHandler"/> class.
/// </summary>
/// <param name="parWindow">The window to capture input from.</param>
public WindowInputHandler(Window parWindow)
{
_window = parWindow;
_keyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
_previousKeyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
_mouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
_previousMouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
}
/// <inheritdoc/>
public void Update(double parDeltaTime)
{
_previousKeyboardState = _keyboardState;
_keyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
_keyboardState = _window.NativeWindow.KeyboardState.GetSnapshot();
_previousMouseState = _mouseState;
_mouseState = parWindow.NativeWindow.MouseState.GetSnapshot();
_mouseState = _window.NativeWindow.MouseState.GetSnapshot();
}
/// <inheritdoc/>
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode)
{
return _keyboardState.IsKeyDown(MapKeyCode(parKeyboardButtonCode));
}
/// <inheritdoc/>
public bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode)
{
return _keyboardState.IsKeyDown(MapKeyCode(parKeyboardButtonCode)) &&
!_previousKeyboardState.IsKeyDown(MapKeyCode(parKeyboardButtonCode));
}
/// <inheritdoc/>
public bool IsMouseButtonPressed(MouseButtonCode parButtonCode)
{
return _mouseState.IsButtonDown(MapMouseButtonCode(parButtonCode));
}
/// <inheritdoc/>
public bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode)
{
return _mouseState.IsButtonDown(MapMouseButtonCode(parButtonCode)) &&
!_previousMouseState.IsButtonDown(MapMouseButtonCode(parButtonCode));
}
/// <summary>
/// Maps a custom <see cref="MouseButtonCode"/> to the GLFW <see cref="MouseButton"/>.
/// </summary>
/// <param name="parButton">The custom mouse button code.</param>
/// <returns>The corresponding GLFW mouse button.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the mouse button code is not recognized.</exception>
private static MouseButton MapMouseButtonCode(MouseButtonCode parButton)
{
return parButton switch
@@ -56,6 +107,12 @@ public class WindowInputHandler(Window parWindow) : IInputHandler
};
}
/// <summary>
/// Maps a custom <see cref="KeyboardButtonCode"/> to the GLFW <see cref="Keys"/>.
/// </summary>
/// <param name="parKeyboardButtonCode">The custom keyboard button code.</param>
/// <returns>The corresponding GLFW key.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown if the keyboard button code is not recognized.</exception>
private static Keys MapKeyCode(KeyboardButtonCode parKeyboardButtonCode)
{
return parKeyboardButtonCode switch

View File

@@ -1,9 +1,31 @@
namespace Engine.Resource;
public class FilesystemResourceStreamProvider(string parBasePath) : IResourceStreamProvider
/// <summary>
/// Provides resource streams from the file system, using a specified base path.
/// </summary>
public class FilesystemResourceStreamProvider : IResourceStreamProvider
{
/// <summary>
/// The base path for locating resources.
/// </summary>
private readonly string _basePath;
/// <summary>
/// Initializes a new instance of the <see cref="FilesystemResourceStreamProvider"/> class.
/// </summary>
/// <param name="parBasePath">The base path for locating resources.</param>
public FilesystemResourceStreamProvider(string parBasePath)
{
_basePath = parBasePath;
}
/// <summary>
/// Retrieves a stream for the resource located at the specified path, relative to the base path.
/// </summary>
/// <param name="parPath">The relative path to the resource.</param>
/// <returns>A stream providing access to the resource data.</returns>
public Stream GetStream(string parPath)
{
return File.OpenRead(Path.Combine(parBasePath, parPath));
return File.OpenRead(Path.Combine(_basePath, parPath));
}
}

View File

@@ -1,6 +1,15 @@
namespace Engine.Resource;
/// <summary>
/// Defines the interface for a resource loader that can load objects from a path using a stream provider.
/// </summary>
public interface IResourceLoader
{
object Load(string parPath, IResourceStreamProvider parStreamProvider);
/// <summary>
/// Loads a resource object from the specified path using the provided stream provider.
/// </summary>
/// <param name="parPath">The path to the resource.</param>
/// <param name="parStreamProvider">The stream provider used to access the resource data.</param>
/// <returns>The loaded resource object.</returns>
public object Load(string parPath, IResourceStreamProvider parStreamProvider);
}

View File

@@ -1,6 +1,15 @@
namespace Engine.Resource;
/// <summary>
/// Defines the interface for a resource manager that handles loading resources of specific types.
/// </summary>
public interface IResourceManager
{
T Load<T>(string parPath) where T : class;
/// <summary>
/// Loads a resource of the specified type from the given path.
/// </summary>
/// <typeparam name="T">The type of the resource to load.</typeparam>
/// <param name="parPath">The path to the resource.</param>
/// <returns>The loaded resource as an instance of type T.</returns>
public T Load<T>(string parPath) where T : class;
}

View File

@@ -1,6 +1,14 @@
namespace Engine.Resource;
/// <summary>
/// Defines the interface for a resource stream provider, responsible for retrieving streams to access resource data.
/// </summary>
public interface IResourceStreamProvider
{
Stream GetStream(string parPath);
/// <summary>
/// Retrieves a stream for the resource located at the specified path.
/// </summary>
/// <param name="parPath">The path to the resource.</param>
/// <returns>A stream providing access to the resource data.</returns>
public Stream GetStream(string parPath);
}

View File

@@ -4,8 +4,18 @@ using Engine.Asset.Font.Metadata;
namespace Engine.Resource.Loader;
/// <summary>
/// Loads font resources, including metadata and texture atlas.
/// </summary>
public class FontLoader : IResourceLoader
{
/// <summary>
/// Loads a font resource by loading its metadata and texture atlas.
/// </summary>
/// <param name="parPath">The path to the font resource directory.</param>
/// <param name="parStreamProvider">The stream provider used to read files.</param>
/// <returns>A <see cref="Font"/> instance containing the loaded metadata and texture atlas.</returns>
/// <exception cref="InvalidOperationException">Thrown if the metadata cannot be loaded.</exception>
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var metadataPath = Path.Combine(parPath, "metadata.json");

View File

@@ -6,14 +6,29 @@ using SixLabors.ImageSharp.Processing;
namespace Engine.Resource.Loader;
/// <summary>
/// Loads image resources, including processing the image data.
/// </summary>
public class ImageLoader : IResourceLoader
{
/// <summary>
/// Loads the image from the specified path using the provided stream provider.
/// </summary>
/// <param name="parPath">The path to the image resource.</param>
/// <param name="parStreamProvider">The provider for accessing resource streams.</param>
/// <returns>The loaded image resource.</returns>
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
using var stream = parStreamProvider.GetStream(parPath);
return Load(stream);
}
/// <summary>
/// Loads the image from a stream and processes it.
/// </summary>
/// <param name="parStream">The stream containing the image data.</param>
/// <returns>The processed image resource.</returns>
/// <exception cref="InvalidOperationException">Thrown if the image cannot be loaded from the stream.</exception>
internal static Asset.Image<Rgba8> Load(Stream parStream)
{
var sharpImage = Image.Load<Rgba32>(parStream);

View File

@@ -2,8 +2,18 @@
namespace Engine.Resource.Loader;
/// <summary>
/// Loads mesh resources from various formats, such as .obj and .stl.
/// </summary>
public class MeshLoader : IResourceLoader
{
/// <summary>
/// Loads a mesh resource based on the file extension and stream provider.
/// </summary>
/// <param name="parPath">The path to the mesh resource.</param>
/// <param name="parStreamProvider">The provider for accessing resource streams.</param>
/// <returns>The loaded mesh resource.</returns>
/// <exception cref="InvalidOperationException">Thrown if the mesh format is unsupported.</exception>
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var extension = Path.GetExtension(parPath);

View File

@@ -4,8 +4,17 @@ using Engine.Graphics.Shader;
namespace Engine.Resource.Loader;
/// <summary>
/// Loads shader program resources, including vertex and fragment shaders.
/// </summary>
public partial class ProgramLoader : IResourceLoader
{
/// <summary>
/// Loads a shader program from the specified path and stream provider, separating vertex and fragment shaders.
/// </summary>
/// <param name="parPath">The path to the shader program resource.</param>
/// <param name="parStreamProvider">The provider for accessing resource streams.</param>
/// <returns>The loaded shader program.</returns>
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
var textReader = new StreamReader(parStreamProvider.GetStream(parPath));

View File

@@ -1,7 +1,16 @@
namespace Engine.Resource.Loader;
/// <summary>
/// Loads texture resources, including image processing and conversion to static textures.
/// </summary>
public class TextureLoader : IResourceLoader
{
/// <summary>
/// Loads the texture from the specified path and stream provider.
/// </summary>
/// <param name="parPath">The path to the texture resource.</param>
/// <param name="parStreamProvider">The provider for accessing resource streams.</param>
/// <returns>The loaded texture resource.</returns>
public object Load(string parPath, IResourceStreamProvider parStreamProvider)
{
using var stream = parStreamProvider.GetStream(parPath);

View File

@@ -1,14 +1,30 @@
namespace Engine.Resource;
/// <summary>
/// Provides resource streams from in-memory storage.
/// </summary>
public class MemoryResourceStreamProvider : IResourceStreamProvider
{
/// <summary>
/// A dictionary mapping resource paths to their byte data.
/// </summary>
private readonly Dictionary<string, byte[]> _resources = new();
/// <summary>
/// Retrieves a stream for the resource stored in memory at the specified path.
/// </summary>
/// <param name="parPath">The path to the in-memory resource.</param>
/// <returns>A stream providing access to the resource data.</returns>
public Stream GetStream(string parPath)
{
return new MemoryStream(_resources[parPath]);
}
/// <summary>
/// Adds a resource to the in-memory storage.
/// </summary>
/// <param name="parPath">The path to associate with the resource.</param>
/// <param name="parData">The byte array representing the resource data.</param>
internal void AddResource(string parPath, byte[] parData)
{
_resources.Add(parPath, parData);

View File

@@ -1,17 +1,42 @@
namespace Engine.Resource;
/// <summary>
/// Manages loading and caching of resources.
/// </summary>
public class ResourceManager : IResourceManager
{
/// <summary>
/// The provider for resource streams.
/// </summary>
internal IResourceStreamProvider StreamProvider { get; }
/// <summary>
/// A dictionary mapping resource types to their respective loaders.
/// </summary>
private readonly Dictionary<Type, IResourceLoader> _loaders = new();
/// <summary>
/// A dictionary mapping resource types to their cached resources.
/// </summary>
private readonly Dictionary<Type, ResourceStorage> _storages = new();
/// <summary>
/// Initializes a new instance of the <see cref="ResourceManager"/> class with the specified stream provider.
/// </summary>
/// <param name="parStreamProvider">The stream provider used for resource loading.</param>
public ResourceManager(IResourceStreamProvider parStreamProvider)
{
StreamProvider = parStreamProvider;
}
/// <summary>
/// Loads a resource of the specified type from the given path, using the appropriate loader.
/// If the resource is already cached, it is returned from the cache.
/// </summary>
/// <typeparam name="T">The type of the resource to load.</typeparam>
/// <param name="parPath">The path to the resource.</param>
/// <returns>The loaded resource.</returns>
/// <exception cref="InvalidOperationException">Thrown when no loader is registered for the specified resource type.</exception>
public T Load<T>(string parPath) where T : class
{
if (!_storages.TryGetValue(typeof(T), out var storage))
@@ -38,30 +63,60 @@ public class ResourceManager : IResourceManager
return (T)resource;
}
/// <summary>
/// Registers a loader for a specific resource type.
/// </summary>
/// <typeparam name="T">The type of resource the loader is responsible for.</typeparam>
/// <param name="parLoader">The loader to register.</param>
internal void RegisterLoader<T>(IResourceLoader parLoader) where T : class
{
_loaders.Add(typeof(T), parLoader);
}
/// <summary>
/// Clears all cached resources.
/// </summary>
internal void Reset()
{
_storages.Clear();
}
/// <summary>
/// A helper class for storing resources in memory, indexed by their path.
/// </summary>
private class ResourceStorage
{
/// <summary>
/// A dictionary mapping resource paths to the resources themselves.
/// </summary>
private readonly Dictionary<string, object> _resources = new();
/// <summary>
/// Adds a resource to the storage.
/// </summary>
/// <typeparam name="T">The type of resource to store.</typeparam>
/// <param name="parPath">The path of the resource.</param>
/// <param name="parResource">The resource to store.</param>
public void Add<T>(string parPath, T parResource) where T : class
{
_resources.Add(parPath, parResource);
}
/// <summary>
/// Retrieves a resource from storage by its path.
/// </summary>
/// <typeparam name="T">The type of resource to retrieve.</typeparam>
/// <param name="parPath">The path of the resource.</param>
/// <returns>The resource, or <c>null</c> if not found.</returns>
public T? Get<T>(string parPath) where T : class
{
return _resources.TryGetValue(parPath, out var resource) ? (T)resource : null;
}
/// <summary>
/// Removes a resource from storage by its path.
/// </summary>
/// <param name="parPath">The path of the resource to remove.</param>
public void Remove(string parPath)
{
_resources.Remove(parPath);

View File

@@ -4,22 +4,18 @@ using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn;
public abstract class Camera(
float parNearPlane,
float parFarPlane
) : Component, ICamera
/// <summary>
/// Abstract base class for cameras that provide view and projection matrices.
/// </summary>
public abstract class Camera : Component, ICamera
{
public float AspectRatio { get; private set; } = 1;
public float NearPlane { get; set; } = parNearPlane;
public float FarPlane { get; set; } = parFarPlane;
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
private Vector2i _screenSize = new(1, 1);
/// <inheritdoc/>
public abstract Matrix4 View { get; }
/// <inheritdoc/>
public abstract Matrix4 Projection { get; }
/// <inheritdoc/>
public Vector2i ScreenSize
{
get => _screenSize;
@@ -30,6 +26,54 @@ public abstract class Camera(
}
}
/// <summary>
/// The aspect ratio of the camera, calculated as width divided by height.
/// </summary>
public float AspectRatio { get; private set; } = 1;
/// <summary>
/// The near clipping plane distance.
/// </summary>
public float NearPlane { get; set; }
/// <summary>
/// The far clipping plane distance.
/// </summary>
public float FarPlane { get; set; }
/// <summary>
/// The render layer for the camera.
/// </summary>
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
/// <summary>
/// The screen size of the camera.
/// </summary>
private Vector2i _screenSize = new(1, 1);
/// <summary>
/// Initializes a new instance of the <see cref="Camera"/> class.
/// </summary>
/// <param name="parNearPlane">The near clipping plane distance.</param>
/// <param name="parFarPlane">The far clipping plane distance.</param>
protected Camera(float parNearPlane, float parFarPlane)
{
NearPlane = parNearPlane;
FarPlane = parFarPlane;
}
/// <summary>
/// Converts a screen position to a world position in 3D space.
/// </summary>
/// <param name="parScreenPosition">The screen position to convert.</param>
/// <returns>The corresponding world position.</returns>
public abstract Vector3 ScreenToWorld(Vector2 parScreenPosition);
/// <summary>
/// Converts a world position to a screen position in 2D space.
/// </summary>
/// <param name="parWorldPosition">The world position to convert.</param>
/// <returns>The corresponding screen position.</returns>
public abstract Vector2 WorldToScreen(Vector3 parWorldPosition);
}

View File

@@ -3,33 +3,56 @@ using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn;
public class OrthographicCamera(
float parNearPlane = -10000f,
float parFarPlane = 10000f,
float parSize = 10f,
OrthographicCamera.Axis parAxis = OrthographicCamera.Axis.Y
)
: Camera(parNearPlane, parFarPlane)
/// <summary>
/// Represents a camera using an orthographic projection.
/// </summary>
public class OrthographicCamera : Camera
{
/// <summary>
/// The axis fixed during scaling.
/// </summary>
public enum Axis
{
X,
Y
}
/// <inheritdoc/>
public override Matrix4 Projection => GetProjectionMatrix();
/// <inheritdoc/>
public override Matrix4 View => GameObject.Transform.TransformMatrix.Inverted();
public float Size { get; set; } = parSize;
public bool UseScreenSize { get; set; } = false;
public Axis FixedAxis { get; set; } = parAxis;
/// <summary>
/// The size of the orthographic view.
/// </summary>
public float Size { get; set; }
private Matrix4 GetProjectionMatrix()
/// <summary>
/// Indicates whether to scale based on screen dimensions.
/// </summary>
public bool UseScreenSize { get; set; } = false;
/// <summary>
/// The axis to keep fixed when scaling the view.
/// </summary>
public Axis FixedAxis { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="OrthographicCamera"/> class.
/// </summary>
/// <param name="parNearPlane">The near clipping plane distance.</param>
/// <param name="parFarPlane">The far clipping plane distance.</param>
/// <param name="parSize">The size of the orthographic view.</param>
/// <param name="parAxis">The fixed axis for scaling.</param>
public OrthographicCamera(float parNearPlane = -10000f, float parFarPlane = 10000f, float parSize = 10f,
Axis parAxis = Axis.Y) : base(parNearPlane, parFarPlane)
{
var size = GameObject.Transform.Size.Xy;
return Matrix4.CreateOrthographic(size.X, size.Y, -NearPlane, -FarPlane);
Size = parSize;
FixedAxis = parAxis;
}
/// <inheritdoc/>
public override Vector3 ScreenToWorld(Vector2 parScreenPosition)
{
var normalized = (parScreenPosition / ScreenSize) - new Vector2(0.5f);
@@ -42,6 +65,7 @@ public class OrthographicCamera(
.Xyz;
}
/// <inheritdoc/>
public override Vector2 WorldToScreen(Vector3 parWorldPosition)
{
var normalized = new Vector4(parWorldPosition, 1)
@@ -55,6 +79,7 @@ public class OrthographicCamera(
return (normalized + new Vector2(0.5f)) * ScreenSize;
}
/// <inheritdoc/>
public override void Update(double parDeltaTime)
{
if (UseScreenSize)
@@ -75,4 +100,14 @@ public class OrthographicCamera(
}
}
}
/// <summary>
/// Generates the orthographic projection matrix for the camera.
/// </summary>
/// <returns>The orthographic projection matrix.</returns>
private Matrix4 GetProjectionMatrix()
{
var size = GameObject.Transform.Size.Xy;
return Matrix4.CreateOrthographic(size.X, size.Y, -NearPlane, -FarPlane);
}
}

View File

@@ -3,33 +3,42 @@ using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn;
public class PerspectiveCamera(
float parFieldOfView = 60.0f,
float parNearPlane = 0.01f,
float parFarPlane = 1000f
)
: Camera(parNearPlane, parFarPlane)
/// <summary>
/// Represents a camera using a perspective projection.
/// </summary>
public class PerspectiveCamera : Camera
{
public override Matrix4 View
{
get
{
var transformMatrix = GameObject.Transform.TransformMatrix;
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);
}
}
/// <inheritdoc/>
public override Matrix4 View => GetProjectionMatrix();
/// <inheritdoc/>
public override Matrix4 Projection =>
Matrix4.CreatePerspectiveFieldOfView(MathHelper.DegreesToRadians(FieldOfView), AspectRatio,
NearPlane, FarPlane);
/// <summary>
/// The forward direction of the camera.
/// </summary>
public Vector3 Forward => new Vector4(0, 1, 0, 1).MulProject(GameObject.Transform.TransformMatrix).Xyz;
public float FieldOfView { get; set; } = parFieldOfView;
/// <summary>
/// The vertical field of view of the camera, in degrees.
/// </summary>
public float FieldOfView { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="PerspectiveCamera"/> class.
/// </summary>
/// <param name="parFieldOfView">The vertical field of view, in degrees.</param>
/// <param name="parNearPlane">The near clipping plane distance.</param>
/// <param name="parFarPlane">The far clipping plane distance.</param>
public PerspectiveCamera(float parFieldOfView = 60.0f, float parNearPlane = 0.01f, float parFarPlane = 1000f) : base(
parNearPlane, parFarPlane)
{
FieldOfView = parFieldOfView;
}
/// <inheritdoc/>
public override Vector3 ScreenToWorld(Vector2 parScreenPosition)
{
var normalized = (parScreenPosition / ScreenSize) - new Vector2(0.5f);
@@ -42,6 +51,7 @@ public class PerspectiveCamera(
.Xyz;
}
/// <inheritdoc/>
public override Vector2 WorldToScreen(Vector3 parWorldPosition)
{
var normalized = new Vector4(parWorldPosition, 1)
@@ -54,4 +64,18 @@ public class PerspectiveCamera(
return (normalized + new Vector2(0.5f)) * ScreenSize;
}
/// <summary>
/// Generates the perspective projection matrix for the camera.
/// </summary>
/// <returns>The perspective projection matrix.</returns>
private Matrix4 GetProjectionMatrix()
{
var transformMatrix = GameObject.Transform.TransformMatrix;
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);
}
}

View File

@@ -4,14 +4,32 @@ using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn.Renderer;
/// <summary>
/// A component for rendering 2D boxes with an optional texture and color.
/// </summary>
public class Box2DRenderer : Component
{
/// <summary>
/// The color of the box.
/// </summary>
public ref Vector4 Color => ref _color;
/// <summary>
/// The texture of the box.
/// </summary>
public Texture? Texture { get; set; }
/// <summary>
/// The render layer for the box.
/// </summary>
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
/// <summary>
/// The color of the box.
/// </summary>
private Vector4 _color = Vector4.One;
/// <inheritdoc/>
public override void Render()
{
Engine.Instance.Renderer[RenderLayer].QuadRenderer

View File

@@ -4,12 +4,27 @@ using Engine.Graphics.Texture;
namespace Engine.Scene.Component.BuiltIn.Renderer;
/// <summary>
/// A component for rendering 3D meshes with optional albedo textures.
/// </summary>
public class MeshRenderer : Component
{
/// <summary>
/// The mesh to be rendered.
/// </summary>
public Mesh Mesh { get; set; } = null!;
/// <summary>
/// The albedo texture for the mesh.
/// </summary>
public Texture? Albedo { get; set; } = null;
/// <summary>
/// The render layer for the mesh.
/// </summary>
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
/// <inheritdoc/>
public override void Render()
{
Engine.Instance.Renderer[RenderLayer].AnyMeshRenderer

View File

@@ -4,15 +4,37 @@ using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn.Renderer;
/// <summary>
/// A component for rendering text with a specified font, color, and render layer.
/// </summary>
public class TextRenderer : Component
{
/// <summary>
/// The font used for rendering the text.
/// </summary>
public Font Font { get; set; } = null!;
/// <summary>
/// The color of the text.
/// </summary>
public ref Vector4 Color => ref _color;
/// <summary>
/// The text to be rendered.
/// </summary>
public string? Text { get; set; }
/// <summary>
/// The render layer for the text.
/// </summary>
public RenderLayer RenderLayer { get; set; } = RenderLayer.DEFAULT;
/// <summary>
/// The color of the text.
/// </summary>
private Vector4 _color = Vector4.One;
/// <inheritdoc/>
public override void Render()
{
if (Text == null)

View File

@@ -2,20 +2,51 @@
namespace Engine.Scene.Component.BuiltIn;
/// <summary>
/// Represents the transform component of a game object, encapsulating its position, rotation, scale, and size.
/// </summary>
public class Transform : Component
{
/// <summary>
/// The size of the object in local space.
/// </summary>
public ref Vector3 Size => ref _size;
/// <summary>
/// The scaling factor applied to the object in local space.
/// </summary>
public ref Vector3 Scale => ref _scale;
/// <summary>
/// The rotation of the object in local space, represented as a quaternion.
/// </summary>
public ref Quaternion Rotation => ref _rotation;
/// <summary>
/// The translation (position) of the object in local space.
/// </summary>
public ref Vector3 Translation => ref _translation;
/// <summary>
/// The full transformation matrix that combines size, scaling, rotation, and translation.
/// </summary>
public Matrix4 FullTransformMatrix => Matrix4.CreateScale(Size) * TransformMatrix;
/// <summary>
/// The combined transformation matrix, accounting for local and parent transformations.
/// </summary>
public Matrix4 TransformMatrix => LocalTransformMatrix * ParentTransformMatrix;
/// <summary>
/// The local transformation matrix, derived from scale, rotation, and translation.
/// </summary>
public Matrix4 LocalTransformMatrix => Matrix4.CreateScale(Scale) *
Matrix4.CreateFromQuaternion(Rotation) *
Matrix4.CreateTranslation(Translation);
/// <summary>
/// The parent transformation matrix, retrieved from the parent object in the hierarchy.
/// </summary>
private Matrix4 ParentTransformMatrix
{
get
@@ -25,16 +56,39 @@ public class Transform : Component
}
}
/// <summary>
/// The size of the object in local space.
/// </summary>
private Vector3 _size = Vector3.One;
/// <summary>
/// The scaling factor applied to the object in local space.
/// </summary>
private Vector3 _scale = Vector3.One;
/// <summary>
/// The rotation of the object in local space, represented as a quaternion.
/// </summary>
private Quaternion _rotation = Quaternion.Identity;
/// <summary>
/// The translation (position) of the object in local space.
/// </summary>
private Vector3 _translation = Vector3.Zero;
/// <summary>
/// Retrieves the full translation of the object in world space.
/// </summary>
/// <returns>A <see cref="Vector3"/> representing the object's world position.</returns>
public Vector3 GetFullTranslation()
{
return FullTransformMatrix.ExtractTranslation();
}
/// <summary>
/// Creates a clone of the current transform.
/// </summary>
/// <returns>A new <see cref="Transform"/> instance with the same properties as the original.</returns>
public Transform Clone()
{
var clone =

View File

@@ -1,46 +1,89 @@
namespace Engine.Scene.Component;
/// <summary>
/// Base class for all components, defining common lifecycle and behavior methods.
/// </summary>
public abstract class Component : IUpdate, IRender
{
/// <summary>
/// A unique identifier for the component.
/// </summary>
public Guid Id { get; } = Guid.NewGuid();
/// <summary>
/// The game object to which this component is attached.
/// </summary>
public GameObject GameObject { get; internal set; }
/// <summary>
/// Called when the component is initialized.
/// </summary>
public virtual void Awake()
{
}
/// <summary>
/// Called once at the start of the component's lifecycle.
/// </summary>
public virtual void Start()
{
}
/// <summary>
/// Called before the main update loop.
/// </summary>
/// <param name="parDeltaTime">Time elapsed since the last frame.</param>
public virtual void PreUpdate(double parDeltaTime)
{
}
/// <summary>
/// Called during the main update loop.
/// </summary>
/// <param name="parDeltaTime">Time elapsed since the last frame.</param>
public virtual void Update(double parDeltaTime)
{
}
/// <summary>
/// Called during the main render loop.
/// </summary>
public virtual void Render()
{
}
/// <summary>
/// Called when the component is destroyed.
/// </summary>
public virtual void Destroy()
{
}
/// <summary>
/// Enables the component, allowing it to participate in updates and rendering.
/// </summary>
public virtual void Enable()
{
}
/// <summary>
/// Disables the component, stopping it from participating in updates and rendering.
/// </summary>
public virtual void Disable()
{
}
}
public static class ComponentTypeExtensions
/// <summary>
/// Provides extension methods for component type analysis.
/// </summary>
internal static class ComponentTypeExtensions
{
/// <summary>
/// Retrieves the base type of a component, accounting for inheritance chains.
/// </summary>
/// <param name="parType">The type of the component.</param>
/// <returns>The base <see cref="Type"/> of the component.</returns>
internal static Type GetComponentBaseType(this Type parType)
{
var baseType = parType.BaseType;

View File

@@ -4,32 +4,83 @@ using Engine.Scene.Component.BuiltIn;
namespace Engine.Scene;
/// <summary>
/// Represents a game object in the scene, containing components and providing lifecycle management.
/// </summary>
public sealed class GameObject : IUpdate, IRender
{
/// <summary>
/// A unique identifier of the game object.
/// </summary>
public Guid Id { get; } = Guid.NewGuid();
/// <summary>
/// Indicates whether the game object is enabled based on its own state and its parent's state.
/// </summary>
public bool IsEnabled
{
get => IsSelfEnabled && IsParentEnabled;
set => _nextIsSelfEnabled = value;
}
/// <summary>
/// The transform component associated with this game object.
/// </summary>
public Transform Transform { get; }
/// <summary>
/// The scene this game object belongs to.
/// </summary>
internal Scene? Scene { get; set; }
/// <summary>
/// Indicates whether game object's parent is enabled.
/// </summary>
private bool IsParentEnabled => Scene?.Hierarchy.GetParent(this)?.IsEnabled ?? true;
/// <summary>
/// Indicates whether game object is enabled.
/// </summary>
private bool IsSelfEnabled { get; set; } = true;
private readonly HashSet<Component.Component> _addedComponents = [];
/// <summary>
/// A list of component types added to the game object.
/// </summary>
private readonly HashSet<Type> _addedComponentTypes = [];
private readonly Queue<Action> _componentActions = new();
/// <summary>
/// A list of components attached to the game object.
/// </summary>
private readonly List<Component.Component> _components = [];
/// <summary>
/// A queue of actions to be performed on the game object.
/// </summary>
private readonly Queue<Action> _componentActions = new();
/// <summary>
/// A list of components to be added to the game object.
/// </summary>
private readonly HashSet<Component.Component> _addedComponents = [];
/// <summary>
/// A list of components to be removed from the game object.
/// </summary>
private readonly HashSet<Component.Component> _removedComponents = [];
/// <summary>
/// Indicates whether the game object is going to be enabled next frame.
/// </summary>
private bool _nextIsSelfEnabled = true;
/// <summary>
/// Indicates whether the game object was enabled last frame.
/// </summary>
private bool _prevIsSelfEnabled = true;
/// <summary>
/// Initializes a new instance of the <see cref="GameObject"/> class with a default transform component.
/// </summary>
public GameObject()
{
AddComponent<Transform>();
@@ -38,6 +89,10 @@ public sealed class GameObject : IUpdate, IRender
Transform = GetComponent<Transform>()!;
}
/// <summary>
/// Initializes a new instance of the <see cref="GameObject"/> class with a specified transform.
/// </summary>
/// <param name="parTransform">The transform to associate with the game object.</param>
public GameObject(Transform parTransform)
{
AddComponent(parTransform.Clone());
@@ -46,6 +101,10 @@ public sealed class GameObject : IUpdate, IRender
Transform = GetComponent<Transform>()!;
}
/// <summary>
/// Performs pre-update operations for all components.
/// </summary>
/// <param name="parDeltaTime">The time delta since the last update.</param>
public void PreUpdate(double parDeltaTime)
{
ProcessAddedComponents();
@@ -57,6 +116,7 @@ public sealed class GameObject : IUpdate, IRender
}
}
/// <inheritdoc/>
public void Update(double parDeltaTime)
{
if (!IsEnabled)
@@ -91,6 +151,7 @@ public sealed class GameObject : IUpdate, IRender
}
}
/// <inheritdoc/>
public void Render()
{
if (!IsEnabled)
@@ -104,6 +165,9 @@ public sealed class GameObject : IUpdate, IRender
}
}
/// <summary>
/// Destroys the game object and its components.
/// </summary>
public void Destroy()
{
foreach (var component in _components)
@@ -112,6 +176,11 @@ public sealed class GameObject : IUpdate, IRender
}
}
/// <summary>
/// Retrieves a component of a specified type if it exists on the game object.
/// </summary>
/// <typeparam name="T">The type of the component to retrieve.</typeparam>
/// <returns>The component if found, otherwise null.</returns>
public T? GetComponent<T>() where T : Component.Component
{
if (!HasComponent<T>())
@@ -130,6 +199,11 @@ public sealed class GameObject : IUpdate, IRender
return null;
}
/// <summary>
/// Retrieves a component of a specified type from the game object or its children.
/// </summary>
/// <typeparam name="T">The type of the component to retrieve.</typeparam>
/// <returns>The component if found, otherwise null.</returns>
public T? GetComponentAny<T>() where T : Component.Component
{
var component = GetComponent<T>();
@@ -142,6 +216,11 @@ public sealed class GameObject : IUpdate, IRender
return component;
}
/// <summary>
/// Retrieves a component of a specified type from the game object's children.
/// </summary>
/// <typeparam name="T">The type of the component to retrieve.</typeparam>
/// <returns>The component if found, otherwise null.</returns>
public T? GetComponentInChildren<T>() where T : Component.Component
{
var children = Scene!.Hierarchy.GetChildren(this);
@@ -164,12 +243,21 @@ public sealed class GameObject : IUpdate, IRender
return null;
}
/// <summary>
/// Adds a new component of a specified type to the game object.
/// </summary>
/// <typeparam name="T">The type of the component to add.</typeparam>
public void AddComponent<T>() where T : Component.Component, new()
{
var component = new T();
AddComponent(component);
}
/// <summary>
/// Adds a new component of a specified type to the game object with optional constructor arguments.
/// </summary>
/// <typeparam name="T">The type of the component to add.</typeparam>
/// <param name="parArgs">The arguments to pass to the component's constructor.</param>
public void AddComponent<T>(params object?[] parArgs) where T : Component.Component
{
var component = (T?)Activator.CreateInstance(
@@ -189,6 +277,11 @@ public sealed class GameObject : IUpdate, IRender
AddComponent(component);
}
/// <summary>
/// Adds an existing component to the game object.
/// </summary>
/// <typeparam name="T">The type of the component to add.</typeparam>
/// <param name="parComponent">The component to add.</param>
public void AddComponent<T>(T parComponent) where T : Component.Component
{
parComponent.GameObject = this;
@@ -206,6 +299,10 @@ public sealed class GameObject : IUpdate, IRender
});
}
/// <summary>
/// Removes a component of a specified type from the game object.
/// </summary>
/// <typeparam name="T">The type of the component to remove.</typeparam>
public void RemoveComponent<T>() where T : Component.Component
{
if (typeof(T) == typeof(Transform))
@@ -232,12 +329,20 @@ public sealed class GameObject : IUpdate, IRender
});
}
/// <summary>
/// Checks whether the game object contains a component of a specified type.
/// </summary>
/// <typeparam name="T">The type of the component to check for.</typeparam>
/// <returns>True if the component exists, otherwise false.</returns>
public bool HasComponent<T>() where T : Component.Component
{
var baseType = typeof(T).GetComponentBaseType();
return _addedComponentTypes.Contains(baseType);
}
/// <summary>
/// Processes changes to the game object's components.
/// </summary>
internal void ProcessChanges()
{
IsSelfEnabled = _nextIsSelfEnabled;
@@ -248,6 +353,9 @@ public sealed class GameObject : IUpdate, IRender
}
}
/// <summary>
/// Initializes newly added components.
/// </summary>
private void ProcessAddedComponents()
{
foreach (var component in _addedComponents)
@@ -259,6 +367,9 @@ public sealed class GameObject : IUpdate, IRender
_addedComponents.Clear();
}
/// <summary>
/// Cleans up and removes components that were marked for removal.
/// </summary>
private void ProcessRemovedComponents()
{
foreach (var component in _removedComponents)
@@ -270,16 +381,19 @@ public sealed class GameObject : IUpdate, IRender
_removedComponents.Clear();
}
/// <inheritdoc/>
public override string ToString()
{
return Id.ToString();
}
/// <inheritdoc/>
public override bool Equals(object? parObj)
{
return parObj is GameObject gameObject && Id == gameObject.Id;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(Id);

View File

@@ -3,20 +3,47 @@ using Engine.Util;
namespace Engine.Scene;
/// <summary>
/// Manages a hierarchical relationship between objects, supporting parent-child relationships
/// and enabling traversal, adding/removing of objects within the hierarchy.
/// </summary>
/// <typeparam name="T">The type of the objects in the hierarchy. Must be a reference type.</typeparam>
public class Hierarchy<T>
where T : class
{
/// <summary>
/// A collection of all objects in the hierarchy (all keys in the parent lookup dictionary).
/// </summary>
internal Dictionary<T, T?>.KeyCollection Objects => _parentLookup.Keys;
/// <summary>
/// A dictionary mapping objects to their children.
/// </summary>
private readonly Dictionary<NullableObject<T>, IList<T>> _childrenLookup = new();
/// <summary>
/// A dictionary mapping objects to their parent.
/// </summary>
private readonly Dictionary<T, T?> _parentLookup = new();
/// <summary>
/// A queue of actions to be processed.
/// </summary>
private readonly ConcurrentQueue<Action> _hierarchyActions = new();
/// <summary>
/// Initializes a new instance of the <see cref="Hierarchy{T}"/> class.
/// Adds a root element with a null value to represent the root node.
/// </summary>
public Hierarchy()
{
_childrenLookup.Add(new NullableObject<T>(), new List<T>());
}
/// <summary>
/// Processes queued actions related to changes in the hierarchy (e.g., adding/removing objects).
/// This method is invoked to ensure the changes are executed in the proper order.
/// </summary>
internal void ProcessChanges()
{
while (_hierarchyActions.TryDequeue(out var action))
@@ -25,6 +52,11 @@ public class Hierarchy<T>
}
}
/// <summary>
/// Adds an object to the hierarchy. This method will create a new entry in the hierarchy
/// with the object having no parent initially, and will add it as a child of the root.
/// </summary>
/// <param name="parObj">The object to add to the hierarchy.</param>
public void Add(T parObj)
{
_hierarchyActions.Enqueue(() =>
@@ -40,6 +72,11 @@ public class Hierarchy<T>
});
}
/// <summary>
/// Removes an object from the hierarchy and all its descendants.
/// The object will be deleted, and its references will be removed from both parent and child lookup dictionaries.
/// </summary>
/// <param name="parObj">The object to remove from the hierarchy.</param>
public void Remove(T parObj)
{
foreach (var child in GetChildren(parObj))
@@ -62,11 +99,23 @@ public class Hierarchy<T>
});
}
/// <summary>
/// Adds a child object to a specific parent in the hierarchy.
/// This sets the given object's parent and adds it to the parent's list of children.
/// </summary>
/// <param name="parParent">The parent object.</param>
/// <param name="parChild">The child object to add to the parent.</param>
public void AddChild(T parParent, T parChild)
{
SetParent(parChild, parParent);
}
/// <summary>
/// Sets the parent of an object in the hierarchy.
/// This method will remove the object from its current parent and add it to the new parent.
/// </summary>
/// <param name="parChild">The child object whose parent is being changed.</param>
/// <param name="parParent">The new parent of the child object.</param>
private void SetParent(T parChild, T? parParent)
{
if (parChild.Equals(parParent))
@@ -89,11 +138,22 @@ public class Hierarchy<T>
});
}
/// <summary>
/// Determines if an object exists in the hierarchy.
/// </summary>
/// <param name="parObj">The object to check for presence in the hierarchy.</param>
/// <returns>True if the object exists in the hierarchy, otherwise false.</returns>
public bool Contains(T parObj)
{
return _parentLookup.ContainsKey(parObj) && _childrenLookup.ContainsKey(parObj);
}
/// <summary>
/// Gets the parent of a given child object in the hierarchy.
/// </summary>
/// <param name="parChild">The child object whose parent is to be retrieved.</param>
/// <returns>The parent object of the given child.</returns>
/// <exception cref="InvalidOperationException">Thrown if the child is not found in the hierarchy.</exception>
public T? GetParent(T parChild)
{
return _parentLookup.TryGetValue(parChild, out var parent)
@@ -101,11 +161,22 @@ public class Hierarchy<T>
: throw new InvalidOperationException($"Child {parChild} is not in hierarchy");
}
/// <summary>
/// Gets the children of a given parent object in the hierarchy.
/// If no parent is provided, it retrieves the children of the root node.
/// </summary>
/// <param name="parParent">The parent object whose children are to be retrieved. Null refers to the root.</param>
/// <returns>A collection of child objects of the specified parent.</returns>
public IEnumerable<T> GetChildren(T? parParent = null)
{
return _childrenLookup.TryGetValue(parParent, out var children) ? children : Enumerable.Empty<T>();
}
/// <summary>
/// Gets all descendants of a given parent object in the hierarchy, recursively.
/// </summary>
/// <param name="parParent">The parent object whose descendants are to be retrieved. Null refers to the root.</param>
/// <returns>A collection of all descendant objects of the specified parent.</returns>
public IEnumerable<T> GetAllChildren(T? parParent = null)
{
var children = GetChildren(parParent);
@@ -121,6 +192,12 @@ public class Hierarchy<T>
}
}
/// <summary>
/// Checks if a given child object is in the hierarchy of a given ancestor object.
/// </summary>
/// <param name="parAncestor">The ancestor object to check.</param>
/// <param name="parChild">The child object to check for inclusion in the hierarchy.</param>
/// <returns>True if the child is in the hierarchy of the ancestor, otherwise false.</returns>
public bool IsInHierarchy(T? parAncestor, T? parChild)
{
if (parChild == null) // if child is null (root), then it is not in hierarchy, as root can not have a parent

View File

@@ -1,6 +1,12 @@
namespace Engine.Scene;
/// <summary>
/// Defines an interface for rendering objects.
/// </summary>
public interface IRender
{
void Render();
/// <summary>
/// Renders the object.
/// </summary>
public void Render();
}

View File

@@ -1,6 +1,13 @@
namespace Engine.Scene;
/// <summary>
/// Defines an interface for updating logic over time.
/// </summary>
public interface IUpdate
{
void Update(double parDeltaTime);
/// <summary>
/// Updates the state of the object.
/// </summary>
/// <param name="parDeltaTime">The time elapsed since the last update, in seconds.</param>
public void Update(double parDeltaTime);
}

View File

@@ -4,17 +4,43 @@ using Engine.Scene.Component.BuiltIn;
namespace Engine.Scene;
/// <summary>
/// Represents a scene in the game, managing the scene hierarchy and rendering the scene.
/// </summary>
public class Scene : IUpdate, IRender
{
/// <summary>
/// Determines whether the scene is currently playing.
/// </summary>
public bool IsPlaying { get; private set; }
/// <summary>
/// The time scale for updating the scene. A value of 1.0 means normal speed.
/// </summary>
public float TimeScale { get; set; } = 1.0f;
/// <summary>
/// A hierarchy of game objects in the scene.
/// </summary>
internal Hierarchy<GameObject> Hierarchy { get; } = new();
/// <summary>
/// A read-only dictionary of cameras, categorized by render layer.
/// </summary>
internal IReadOnlyDictionary<RenderLayer, ICamera> Cameras => _cameras;
/// <summary>
/// A dictionary of cameras, categorized by render layer.
/// </summary>
private readonly Dictionary<RenderLayer, ICamera> _cameras = new();
/// <summary>
/// A private queue used to enqueue scene-related actions that should be processed later.
/// Actions are executed when the scene is processed for changes.
/// </summary>
private readonly Queue<Action> _sceneActions = [];
/// <inheritdoc/>
public void Update(double parDeltaTime)
{
if (!IsPlaying)
@@ -37,6 +63,7 @@ public class Scene : IUpdate, IRender
}
}
/// <inheritdoc/>
public void Render()
{
if (!IsPlaying)
@@ -50,6 +77,12 @@ public class Scene : IUpdate, IRender
}
}
/// <summary>
/// Finds all components of type <typeparamref name="T"/> in the scene.
/// </summary>
/// <typeparam name="T">The type of component to find.</typeparam>
/// <param name="parOnlyEnabled">If true, only enabled components are returned.</param>
/// <returns>A list of components of type <typeparamref name="T"/>.</returns>
public List<T> FindAllComponents<T>(bool parOnlyEnabled = true) where T : Component.Component
{
return Hierarchy.Objects
@@ -60,24 +93,42 @@ public class Scene : IUpdate, IRender
.ToList()!;
}
/// <summary>
/// Finds the first component of type <typeparamref name="T"/> in the scene.
/// </summary>
/// <typeparam name="T">The type of component to find.</typeparam>
/// <returns>The first component of type <typeparamref name="T"/>, or null if not found.</returns>
public T? FindFirstComponent<T>() where T : Component.Component
{
return Hierarchy.Objects.Select(parGameObject => parGameObject.GetComponent<T>())
.FirstOrDefault(parComponent => parComponent != null);
}
/// <summary>
/// Adds a game object to the scene.
/// </summary>
/// <param name="parGameObject">The game object to add.</param>
public void Add(GameObject parGameObject)
{
parGameObject.Scene = this;
Hierarchy.Add(parGameObject);
}
/// <summary>
/// Adds a child game object to the specified parent game object in the scene.
/// </summary>
/// <param name="parParent">The parent game object.</param>
/// <param name="parChild">The child game object to add.</param>
public void AddChild(GameObject parParent, GameObject parChild)
{
Add(parChild);
Hierarchy.AddChild(parParent, parChild);
}
/// <summary>
/// Removes a game object from the scene.
/// </summary>
/// <param name="parGameObject">The game object to remove.</param>
public void Remove(GameObject parGameObject)
{
var children = Hierarchy.GetAllChildren(parGameObject).ToList();
@@ -97,11 +148,20 @@ public class Scene : IUpdate, IRender
});
}
/// <summary>
/// Gets the children of a parent game object.
/// </summary>
/// <param name="parParent">The parent game object.</param>
/// <param name="parRecursive">If true, recursively retrieves all children; otherwise, retrieves only direct children.</param>
/// <returns>An enumeration of the child game objects.</returns>
public IEnumerable<GameObject> GetChildren(GameObject parParent, bool parRecursive = false)
{
return parRecursive ? Hierarchy.GetAllChildren(parParent) : Hierarchy.GetChildren(parParent);
}
/// <summary>
/// Enters the scene by adding all cameras and setting the scene state to playing.
/// </summary>
internal void Enter()
{
if (IsPlaying)
@@ -120,6 +180,9 @@ public class Scene : IUpdate, IRender
IsPlaying = true;
}
/// <summary>
/// Exits the scene and destroys all game objects in the scene
/// </summary>
internal void Exit()
{
if (!IsPlaying)
@@ -135,6 +198,9 @@ public class Scene : IUpdate, IRender
IsPlaying = false;
}
/// <summary>
/// Processes changes in the hierarchy and scene actions.
/// </summary>
private void ProcessChanges()
{
Hierarchy.ProcessChanges();

View File

@@ -1,11 +1,22 @@
namespace Engine.Scene;
/// <summary>
/// Manages the current scene in the game, handles scene transitions, and facilitates updating and rendering the current scene.
/// </summary>
public class SceneManager : IUpdate, IRender
{
/// <summary>
/// The current scene being managed by the scene manager.
/// </summary>
public Scene? CurrentScene { get; private set; }
/// <summary>
/// A function to be called to transition to the next scene.
/// This is set by the scene manager when transitioning to a new scene and reset after the transition is complete.
/// </summary>
private Func<Scene>? _nextScene;
/// <inheritdoc/>
public void Update(double parDeltaTime)
{
if (_nextScene != null)
@@ -22,11 +33,16 @@ public class SceneManager : IUpdate, IRender
}
}
/// <inheritdoc/>
public void Render()
{
CurrentScene?.Render();
}
/// <summary>
/// Queues a scene transition by setting the next scene to transition to.
/// </summary>
/// <param name="parScene">A function that provides the next scene to transition to.</param>
public void TransitionTo(Func<Scene>? parScene)
{
_nextScene = parScene;

View File

@@ -4,19 +4,45 @@ using Engine.Scene;
namespace Engine.Util;
/// <summary>
/// Provides utility functions and properties for interacting with the engine's core services,
/// including input handling, scene management, and asset management.
/// </summary>
public static class EngineUtil
{
/// <summary>
/// The engine's input handler, which processes user input.
/// </summary>
public static IInputHandler InputHandler => Engine.Instance.InputHandler!;
/// <summary>
/// The engine's scene manager, which handles the current scene and scene transitions.
/// </summary>
public static SceneManager SceneManager => Engine.Instance.SceneManager;
/// <summary>
/// The engine's asset resource manager, which handles loading and caching of assets.
/// </summary>
public static IResourceManager AssetResourceManager => Engine.Instance.AssetResourceManager;
/// <summary>
/// The engine's data folder, which contains assets and other data files.
/// </summary>
public static string DataFolder => Engine.Instance.DataFolder;
/// <summary>
/// Creates a game object and adds it to the current scene.
/// </summary>
/// <param name="parGameObject">The game object to be added to the scene.</param>
public static void CreateObject(GameObject parGameObject)
{
var scene = Engine.Instance.SceneManager.CurrentScene!;
scene.Add(parGameObject);
}
/// <summary>
/// Closes the engine, shutting down any running systems and freeing resources.
/// </summary>
public static void Close()
{
Engine.Instance.Close();

View File

@@ -2,8 +2,17 @@
namespace Engine.Util;
/// <summary>
/// Contains mathematical utility methods.
/// </summary>
public static class Math
{
/// <summary>
/// Multiplies a <see cref="Vector4"/> by a <see cref="Matrix4"/> and performs projective division.
/// </summary>
/// <param name="parA">The vector to be multiplied.</param>
/// <param name="parM">The matrix to multiply the vector with.</param>
/// <returns>A transformed <see cref="Vector4"/> after multiplication and projective division.</returns>
public static Vector4 MulProject(this Vector4 parA, in Matrix4 parM)
{
var result = parA * parM;

View File

@@ -1,35 +1,67 @@
namespace Engine.Util;
public readonly struct NullableObject<T>(T? parValue) : IEquatable<NullableObject<T>>
/// <summary>
/// A struct that wraps nullable reference types, providing a way to check if a value is null or not.
/// </summary>
/// <typeparam name="T">The type of the wrapped object, constrained to be a class type.</typeparam>
public readonly struct NullableObject<T> : IEquatable<NullableObject<T>>
where T : class
{
/// <summary>
/// Determines if the wrapped object is null.
/// </summary>
public bool IsNull => Value == null;
public T? Value { get; } = parValue;
/// <summary>
/// The wrapped object.
/// </summary>
public T? Value { get; }
/// <summary>
/// Constructs a <see cref="NullableObject{T}"/> with a null value.
/// </summary>
public NullableObject() : this(null)
{
}
/// <summary>
/// Constructs a <see cref="NullableObject{T}"/> with the specified value.
/// </summary>
/// <param name="parValue">The value to wrap.</param>
public NullableObject(T? parValue)
{
Value = parValue;
}
/// <summary>
/// Implicitly converts the <see cref="NullableObject{T}"/> to a nullable value of type <typeparamref name="T"/>.
/// </summary>
public static implicit operator T?(NullableObject<T> parNullableObject)
{
return parNullableObject.Value;
}
/// <summary>
/// Implicitly converts a nullable value of type <typeparamref name="T"/> to a <see cref="NullableObject{T}"/>.
/// </summary>
public static implicit operator NullableObject<T>(T? parValue)
{
return new NullableObject<T>(parValue);
}
/// <inheritdoc/>
public override string ToString()
{
return Value?.ToString() ?? "null";
}
/// <inheritdoc/>
public override bool Equals(object? parObj)
{
return parObj is NullableObject<T> other && Equals(other);
}
/// <inheritdoc/>
public override int GetHashCode()
{
if (IsNull)
@@ -47,16 +79,23 @@ public readonly struct NullableObject<T>(T? parValue) : IEquatable<NullableObjec
return hashCode;
}
/// <inheritdoc/>
public bool Equals(NullableObject<T> parOther)
{
return EqualityComparer<T?>.Default.Equals(Value, parOther.Value);
}
/// <summary>
/// Determines whether two <see cref="NullableObject{T}"/> instances are equal.
/// </summary>
public static bool operator ==(NullableObject<T> parLeft, NullableObject<T> parRight)
{
return parLeft.Equals(parRight);
}
/// <summary>
/// Determines whether two <see cref="NullableObject{T}"/> instances are not equal.
/// </summary>
public static bool operator !=(NullableObject<T> parLeft, NullableObject<T> parRight)
{
return !(parLeft == parRight);

View File

@@ -1,12 +1,27 @@
namespace Engine.Util;
using Engine.Scene;
public class TickableTimer
namespace Engine.Util;
public class TickableTimer : IUpdate
{
/// <summary>
/// Event that is invoked on every update, passing the current time.
/// </summary>
public event Action<double>? OnUpdate;
/// <summary>
/// Event that is invoked when the timer reaches zero and is finished.
/// </summary>
public event Action? OnFinished;
/// <summary>
/// Indicates whether the timer has finished.
/// </summary>
public bool IsFinished => _currentTime <= 0;
/// <summary>
/// The current time remaining on the timer.
/// </summary>
public double CurrentTime
{
get => _currentTime;
@@ -37,6 +52,9 @@ public class TickableTimer
}
}
/// <summary>
/// The total duration of the timer.
/// </summary>
public double TotalTime
{
get => _totalTime;
@@ -52,9 +70,20 @@ public class TickableTimer
}
}
/// <summary>
/// The current time remaining on the timer.
/// </summary>
private double _currentTime;
/// <summary>
/// The total duration of the timer.
/// </summary>
private double _totalTime;
/// <summary>
/// Initializes a new <see cref="TickableTimer"/> with a specified total time.
/// </summary>
/// <param name="parTotalTime">The total duration of the timer.</param>
public TickableTimer(double parTotalTime)
{
if (parTotalTime <= 0)
@@ -66,11 +95,15 @@ public class TickableTimer
_currentTime = parTotalTime;
}
/// <inheritdoc/>
public void Update(double parDeltaTime)
{
CurrentTime -= parDeltaTime;
}
/// <summary>
/// Resets the timer back to its total duration.
/// </summary>
public void Reset()
{
CurrentTime = TotalTime;

View File

@@ -7,19 +7,44 @@ using OpenTK.Windowing.GraphicsLibraryFramework;
namespace Engine;
/// <summary>
/// Represents a window used for rendering and interaction with the user.
/// </summary>
public class Window : IPresenter
{
public event Action<ResizeEventArgs>? Resize;
/// <inheritdoc/>
public event Action<ResizeEventArgs>? OnResize;
/// <inheritdoc/>
public int Width { get; private set; }
/// <inheritdoc/>
public int Height { get; private set; }
/// <inheritdoc/>
public bool IsExiting => NativeWindow.IsExiting;
/// <summary>
/// The internal native window instance used for rendering and event handling.
/// </summary>
internal NativeWindow NativeWindow { get; }
/// <summary>
/// The reference to the engine instance associated with this window.
/// </summary>
private readonly Engine _engine;
/// <summary>
/// Indicates whether the window is running in headless mode.
/// </summary>
private readonly bool _headless;
/// <summary>
/// Initializes a new instance of the <see cref="Window"/> class.
/// </summary>
/// <param name="parEngine">The engine instance managing this window.</param>
/// <param name="parWindow">The native window instance.</param>
/// <param name="parHeadless">Indicates whether the window is headless.</param>
public Window(Engine parEngine, NativeWindow parWindow, bool parHeadless)
{
_engine = parEngine;
@@ -33,16 +58,18 @@ public class Window : IPresenter
{
Width = parArgs.Width;
Height = parArgs.Height;
Resize?.Invoke(parArgs);
OnResize?.Invoke(parArgs);
};
NativeWindow.VSync = VSyncMode.On;
}
/// <inheritdoc/>
public void Update(double parDeltaTime)
{
}
/// <inheritdoc/>
public void Render()
{
if (_headless)
@@ -55,6 +82,7 @@ public class Window : IPresenter
NativeWindow.SwapBuffers();
}
/// <inheritdoc/>
public void Present(IConstTexture parTexture)
{
if (_headless)
@@ -70,14 +98,22 @@ public class Window : IPresenter
);
}
/// <inheritdoc/>
public void Exit()
{
NativeWindow.Close();
}
}
/// <summary>
/// Provides extension methods for the <see cref="NativeWindow"/> class.
/// </summary>
public static class NativeWindowExtensions
{
/// <summary>
/// Swaps the buffers of the specified native window.
/// </summary>
/// <param name="parWindow">The native window instance.</param>
public static unsafe void SwapBuffers(this NativeWindow parWindow)
{
GLFW.SwapBuffers(parWindow.WindowPtr);

View File

@@ -14,7 +14,7 @@ namespace PresenterConsole;
public class ConsolePresenter : IPresenter
{
public event Action<ResizeEventArgs>? Resize;
public event Action<ResizeEventArgs>? OnResize;
public int Width { get; private set; } = 2;
public int Height { get; private set; } = 1;
@@ -104,7 +104,7 @@ public class ConsolePresenter : IPresenter
Width = consoleWidth;
Height = consoleHeight;
Resize?.Invoke(new ResizeEventArgs(Width / 2 * 4, Height * 4));
OnResize?.Invoke(new ResizeEventArgs(Width / 2 * 4, Height * 4));
_framebuffer.Resize(Width / 2, Height);
_consoleOutput.Resize(Width, Height);

View File

@@ -91,7 +91,7 @@ public partial class App : Application
private class PresenterWrapper : IPresenter
{
public event Action<ResizeEventArgs>? Resize;
public event Action<ResizeEventArgs>? OnResize;
public IPresenter? Presenter
{
@@ -100,12 +100,12 @@ public partial class App : Application
{
if (_presenter != null)
{
_presenter.Resize -= PresenterResize;
_presenter.OnResize -= PresenterResize;
}
if (value != null)
{
value.Resize += PresenterResize;
value.OnResize += PresenterResize;
}
_presenter = value;
@@ -140,7 +140,7 @@ public partial class App : Application
private void PresenterResize(ResizeEventArgs e)
{
Resize?.Invoke(e);
OnResize?.Invoke(e);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace PresenterWpf;
public partial class MainWindow : Window, IPresenter
{
public event Action<ResizeEventArgs>? Resize;
public event Action<ResizeEventArgs>? OnResize;
public new int Width { get; private set; }
public new int Height { get; private set; }
@@ -46,7 +46,7 @@ public partial class MainWindow : Window, IPresenter
if (_scheduledResize)
{
_scheduledResize = false;
Resize?.Invoke(new ResizeEventArgs(Width, Height));
OnResize?.Invoke(new ResizeEventArgs(Width, Height));
}
}