From 1dc33328b21bbd0ff26c37bfd0aea3b23d862f36 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Fri, 12 Jun 2026 00:40:51 +0300 Subject: [PATCH 01/10] #109 Add initial unit tests --- ObjLoader/DataStore.cs | 4 + ObjLoader/IMaterialDataStore.cs | 13 ++- ObjLoader/Loaders/IFileManager.cs | 13 +++ ObjLoader/Loaders/LoaderBase.cs | 17 +++- .../MaterialLoader/MaterialLibraryLoader.cs | 12 ++- ObjLoader/Loaders/ObjLoader/ObjLoader.cs | 7 +- .../Loaders/ObjLoader/ObjLoaderFactory.cs | 35 ++++++-- ObjLoader/SharpEngine.Core.ObjLoader.csproj | 1 + SharpEngine.Core.ArchitectureTests/Class1.cs | 6 ++ .../SharpEngine.Core.ArchitectureTests.csproj | 9 +++ .../ObjLoader/DataStore/IVertexDataStore.cs | 0 .../Extensions/AssemblyExtensions.cs | 3 +- SharpEngine.Core/Windowing/Window.cs | 5 +- SharpEngine.slnx | 17 +++- Tests/Directory.Build.props | 3 +- .../Loaders/MaterialLibraryLoaderTests.cs | 29 ++++--- .../ObjLoader.Test/Loaders/ObjLoaderTests.cs | 80 ++++++------------- .../SharpEngine.Core.ObjLoader.Tests.csproj | 9 ++- .../TypeParsers/FaceParserTests.cs | 23 +++--- .../TypeParsers/GroupParserTests.cs | 9 +-- .../TypeParsers/MaterialLibraryParserTests.cs | 10 +-- .../TypeParsers/NormalParserTests.cs | 9 +-- .../TypeParsers/TextureParserTests.cs | 9 +-- .../TypeParsers/UseMaterialParserTests.cs | 16 ++-- .../TypeParsers/VertexParserTests.cs | 9 +-- .../Class1.cs | 6 ++ .../SharpEngine.Core.Components.Tests.csproj | 13 +++ .../SharpEngine.Core.Numerics.Tests/Class1.cs | 6 ++ .../SharpEngine.Core.Numerics.Tests.csproj | 13 +++ Tests/SharpEngine.Core.Tests/Class1.cs | 6 -- .../Extensions/AssemblyExtensionsTests.cs | 40 ++++++++++ .../SharpEngine.Core.Tests.csproj | 25 ++++++ .../SharpEngine.Shared.Tests.csproj | 2 +- 33 files changed, 314 insertions(+), 145 deletions(-) create mode 100644 ObjLoader/Loaders/IFileManager.cs create mode 100644 SharpEngine.Core.ArchitectureTests/Class1.cs create mode 100644 SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj delete mode 100644 SharpEngine.Core/Components/ObjLoader/DataStore/IVertexDataStore.cs create mode 100644 Tests/SharpEngine.Core.Components.Tests/Class1.cs create mode 100644 Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj create mode 100644 Tests/SharpEngine.Core.Numerics.Tests/Class1.cs create mode 100644 Tests/SharpEngine.Core.Numerics.Tests/SharpEngine.Core.Numerics.Tests.csproj delete mode 100644 Tests/SharpEngine.Core.Tests/Class1.cs create mode 100644 Tests/SharpEngine.Core.Tests/Extensions/AssemblyExtensionsTests.cs diff --git a/ObjLoader/DataStore.cs b/ObjLoader/DataStore.cs index 5833552..ec82bc1 100644 --- a/ObjLoader/DataStore.cs +++ b/ObjLoader/DataStore.cs @@ -79,5 +79,9 @@ public void AddNormal(Normal normal) /// public void AddVertex(Vertex vertex) => Vertices.Add(vertex); + + /// + public void AddMaterial(Material currentMaterial) + => Materials.Add(currentMaterial); } } \ No newline at end of file diff --git a/ObjLoader/IMaterialDataStore.cs b/ObjLoader/IMaterialDataStore.cs index 38f3168..9424ed5 100644 --- a/ObjLoader/IMaterialDataStore.cs +++ b/ObjLoader/IMaterialDataStore.cs @@ -1,14 +1,21 @@ -namespace SharpEngine.Core.ObjLoader; +using SharpEngine.Core.Components.Properties; + +namespace SharpEngine.Core.ObjLoader; /// /// Contains definitions for the material data store, which provides methods to manage material information during the OBJ file loading process. /// public interface IMaterialDataStore { + /// + /// Adds a material to the material data store. + /// + /// The material to add. + void AddMaterial(Material currentMaterial); + /// /// Sets the material for the current group. /// /// The name of the material to set. - public void SetMaterial(string materialName); - + void SetMaterial(string materialName); } \ No newline at end of file diff --git a/ObjLoader/Loaders/IFileManager.cs b/ObjLoader/Loaders/IFileManager.cs new file mode 100644 index 0000000..21e33eb --- /dev/null +++ b/ObjLoader/Loaders/IFileManager.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace SharpEngine.Core.ObjLoader.Loader.Loaders +{ + /// + /// Represents an abstraction of a stream reader for testability. + /// + public interface IFileManager + { + /// + StreamReader StreamReader(string path); + } +} \ No newline at end of file diff --git a/ObjLoader/Loaders/LoaderBase.cs b/ObjLoader/Loaders/LoaderBase.cs index 4ecbabc..5253b8f 100644 --- a/ObjLoader/Loaders/LoaderBase.cs +++ b/ObjLoader/Loaders/LoaderBase.cs @@ -1,4 +1,5 @@ using System.IO; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Loader.Loaders { @@ -7,6 +8,18 @@ namespace SharpEngine.Core.ObjLoader.Loader.Loaders /// public abstract class LoaderBase { + private readonly IFileStreamFactory _fileStreamFactory; + private readonly IFileManager _fileManager; + + /// + /// Initializes a new instance of the . + /// + protected LoaderBase(IFileStreamFactory fileStreamFactory, IFileManager fileManager) + { + _fileStreamFactory = fileStreamFactory; + _fileManager = fileManager; + } + /// /// Parses the file at the specified path and processes each line via . /// @@ -16,8 +29,8 @@ public abstract class LoaderBase /// The path of the file to open and parse. public void ParseFile(string path) { - using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); - using var lineStreamReader = new StreamReader(fileStream); + using var fileStream = _fileStreamFactory.FileSystem.FileStream.New(path, FileMode.Open, FileAccess.Read); + using var lineStreamReader = _fileManager.StreamReader(path); while (!lineStreamReader.EndOfStream) { diff --git a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs index 39aa7d3..e2dcb91 100644 --- a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs +++ b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs @@ -1,9 +1,11 @@ -using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties; using SharpEngine.Core.ObjLoader.Loader.Loaders; using SharpEngine.Shared.Extensions; using System; using System.Collections.Generic; +using System.IO.Abstractions; using System.Numerics; namespace SharpEngine.Core.ObjLoader.Loaders.MaterialLoader @@ -19,7 +21,7 @@ namespace SharpEngine.Core.ObjLoader.Loaders.MaterialLoader /// public class MaterialLibraryLoader : LoaderBase { - private readonly DataStore _dataStore; + private readonly IMaterialDataStore _dataStore; private readonly Dictionary> _parseActionDictionary = []; private readonly List _unrecognizedLines = []; @@ -35,7 +37,9 @@ public class MaterialLibraryLoader : LoaderBase /// Ns, d, Tr, illum, map_*, bump, disp, decal) to populate Material instances. /// /// DataStore used to register and store parsed materials and texture references. - public MaterialLibraryLoader(DataStore dataStore) + /// Represents a factory for creating file streams. + /// Represents a manager for handling file operations. + public MaterialLibraryLoader(IMaterialDataStore dataStore, IFileStreamFactory fileStreamFactory, IFileManager fileManager) : base(fileStreamFactory, fileManager) { _dataStore = dataStore; @@ -92,7 +96,7 @@ protected override void ParseLine(string keyword, string data) private void PushMaterial(string materialName) { _currentMaterial = new Material(materialName); - _dataStore.Materials.Add(_currentMaterial); + _dataStore.AddMaterial(_currentMaterial); } private static Vector3 ParseVec3(string data) diff --git a/ObjLoader/Loaders/ObjLoader/ObjLoader.cs b/ObjLoader/Loaders/ObjLoader/ObjLoader.cs index cfcd5f0..a79ac5b 100644 --- a/ObjLoader/Loaders/ObjLoader/ObjLoader.cs +++ b/ObjLoader/Loaders/ObjLoader/ObjLoader.cs @@ -1,8 +1,11 @@ using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.ObjLoader.Loader.Loaders; using SharpEngine.Core.ObjLoader.TypeParsers; + using Silk.NET.OpenGL; + using System.Collections.Generic; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader { @@ -21,7 +24,9 @@ public class ObjLoader : LoaderBase /// /// The path to the OBJ file. /// The data store to populate during parsing. - public ObjLoader(string path, DataStore dataStore) + /// Represents a factory for creating file streams. + /// Represents a manager for handling file operations. + public ObjLoader(string path, DataStore dataStore, IFileStreamFactory fileStreamFactory, IFileManager fileManager) : base(fileStreamFactory, fileManager) { _path = path; _dataStore = dataStore; diff --git a/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs b/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs index 5a7d90c..b99400c 100644 --- a/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs +++ b/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs @@ -1,21 +1,42 @@ +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; + using Silk.NET.OpenGL; using System; using System.Collections.Generic; using System.IO; - -using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.Components.Properties.Meshes; -using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; -using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader { + /// + /// Represents a testable wrapper for creating stream file related objects. + /// + public class FileManager : IFileManager + { + /// + public StreamReader StreamReader(string path) + => new(path); + } + /// /// Handles loading 3D models. /// public static class ObjLoaderFactory { + private static readonly FileSystem _fileSystem; + private static readonly FileManager _fileManager; + + static ObjLoaderFactory() + { + _fileSystem = new FileSystem(); + _fileManager = new FileManager(); + } + const string FbxExtension = ".fbx"; const string ObjExtension = ".obj"; @@ -56,13 +77,13 @@ private static List LoadObjMeshes(GL gl, string path) var textureParser = new TextureParser(dataStore); var vertexParser = new VertexParser(dataStore); - var materialLibraryLoader = new MaterialLibraryLoader(dataStore); + var materialLibraryLoader = new MaterialLibraryLoader(dataStore, _fileSystem.FileStream, _fileManager); var materialLoader = new MaterialLibraryLoaderFacade(materialLibraryLoader, path); var materialLibraryParser = new MaterialLibraryParser(materialLoader); var useMaterialParser = new UseMaterialParser(dataStore); - var loader = new ObjLoader(path, dataStore) + var loader = new ObjLoader(path, dataStore, _fileSystem.FileStream, _fileManager) .SetupTypeParsers(faceParser, groupParser, normalParser, textureParser, vertexParser, materialLibraryParser, useMaterialParser); return loader.Load(gl); diff --git a/ObjLoader/SharpEngine.Core.ObjLoader.csproj b/ObjLoader/SharpEngine.Core.ObjLoader.csproj index 7a52e96..04ffde4 100644 --- a/ObjLoader/SharpEngine.Core.ObjLoader.csproj +++ b/ObjLoader/SharpEngine.Core.ObjLoader.csproj @@ -7,6 +7,7 @@ + diff --git a/SharpEngine.Core.ArchitectureTests/Class1.cs b/SharpEngine.Core.ArchitectureTests/Class1.cs new file mode 100644 index 0000000..77198f6 --- /dev/null +++ b/SharpEngine.Core.ArchitectureTests/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Core.ArchitectureTests; + +public class Class1 +{ + +} diff --git a/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj b/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/SharpEngine.Core/Components/ObjLoader/DataStore/IVertexDataStore.cs b/SharpEngine.Core/Components/ObjLoader/DataStore/IVertexDataStore.cs deleted file mode 100644 index e69de29..0000000 diff --git a/SharpEngine.Core/Extensions/AssemblyExtensions.cs b/SharpEngine.Core/Extensions/AssemblyExtensions.cs index 39a3315..2501343 100644 --- a/SharpEngine.Core/Extensions/AssemblyExtensions.cs +++ b/SharpEngine.Core/Extensions/AssemblyExtensions.cs @@ -4,7 +4,7 @@ namespace SharpEngine.Core.Extensions; /// -/// Contains extension methods for the class, providing additional functionality related to assembly metadata and versioning. +/// Contains extension methods for the . /// public static class AssemblyExtensions { @@ -13,6 +13,7 @@ public static class AssemblyExtensions /// /// The assembly to get the version for. /// The version of the assembly. + /// Thrown when the assembly version is null. public static Version GetVersion(this Assembly assembly) => assembly.GetName().Version!; } diff --git a/SharpEngine.Core/Windowing/Window.cs b/SharpEngine.Core/Windowing/Window.cs index 903f3df..f88a071 100644 --- a/SharpEngine.Core/Windowing/Window.cs +++ b/SharpEngine.Core/Windowing/Window.cs @@ -130,7 +130,7 @@ public Window(CameraView camera, Scene scene, IViewSettings settings, ILogger builder.AddConsole()).CreateLogger()) { } + public Window(Scene scene, IViewSettings settings) + : this(scene, settings, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) { } + /// /// Initializes a new instance of . /// diff --git a/SharpEngine.slnx b/SharpEngine.slnx index 4cfa5e7..39b30b0 100644 --- a/SharpEngine.slnx +++ b/SharpEngine.slnx @@ -34,9 +34,23 @@ + + + + + + + + + + + + + + @@ -47,9 +61,6 @@ - - - diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props index 3ad3602..3b85263 100644 --- a/Tests/Directory.Build.props +++ b/Tests/Directory.Build.props @@ -4,7 +4,6 @@ False - - + \ No newline at end of file diff --git a/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs index be3028c..987b111 100644 --- a/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs @@ -1,16 +1,19 @@ using System.Collections.Generic; using System.Linq; -using NUnit.Framework; using FluentAssertions; +using Xunit; + using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties; using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; using System.Diagnostics.CodeAnalysis; +using Moq; +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Tests.Loaders { - [TestFixture] public class MaterialLibraryLoaderTests { private readonly MaterialLibrarySpy _materialLibrarySpy; @@ -24,10 +27,10 @@ public class MaterialLibraryLoaderTests public MaterialLibraryLoaderTests() { _materialLibrarySpy = new MaterialLibrarySpy(); - _materialLibraryLoader = new MaterialLibraryLoader(null!); + _materialLibraryLoader = new MaterialLibraryLoader(_materialLibrarySpy, Mock.Of(), Mock.Of()); } - [Test] + [Fact] public void Adds_correct_materials() { LoadMaterial(); @@ -37,7 +40,7 @@ public void Adds_correct_materials() _secondMaterial.Name.Should().BeEquivalentTo("second_material"); } - [Test] + [Fact] public void Sets_correct_ambient_color() { LoadMaterial(); @@ -47,7 +50,7 @@ public void Sets_correct_ambient_color() _firstMaterial.AmbientColor.Z.Should().BeApproximately(3, Epsilon); } - [Test] + [Fact] public void Sets_correct_diffuse_color() { LoadMaterial(); @@ -57,7 +60,7 @@ public void Sets_correct_diffuse_color() _firstMaterial.DiffuseColor.Z.Should().BeApproximately(0.0314f, Epsilon); } - [Test] + [Fact] public void Sets_correct_specular() { LoadMaterial(); @@ -69,7 +72,7 @@ public void Sets_correct_specular() _firstMaterial.SpecularCoefficient.Should().BeApproximately(32, Epsilon); } - [Test] + [Fact] public void Sets_correct_transparency() { LoadMaterial(); @@ -77,7 +80,7 @@ public void Sets_correct_transparency() _firstMaterial.Transparency.Should().BeApproximately(0.5f, Epsilon); } - [Test] + [Fact] public void Sets_correct_illumination_model() { LoadMaterial(); @@ -85,7 +88,7 @@ public void Sets_correct_illumination_model() _firstMaterial.IlluminationModel.Should().Be(2); } - [Test] + [Fact] public void Sets_correct_texure_maps() { LoadMaterial(); @@ -126,7 +129,7 @@ disp lenna_disp.tga decal lenna_stencil.tga newmtl second_material"; - private class MaterialLibrarySpy : IMaterialLibrary + private class MaterialLibrarySpy : IMaterialDataStore { public List Materials { get; private set; } @@ -135,10 +138,12 @@ public MaterialLibrarySpy() Materials = new List(); } - public void Push(Material material) + public void AddMaterial(Material material) { Materials.Add(material); } + + public void SetMaterial(string materialName) => throw new System.NotImplementedException(); } } } \ No newline at end of file diff --git a/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs b/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs index 56e9765..9cba209 100644 --- a/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs @@ -1,69 +1,59 @@ using System.IO; using System.Text; using System.Linq; -using NUnit.Framework; + using FluentAssertions; +using Xunit; using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.ObjLoader.Loader.TypeParsers; using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; using CoreObjLoader = SharpEngine.Core.ObjLoader.Loaders.ObjLoader.ObjLoader; +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using Moq; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Tests.Loaders { - [TestFixture] public class ObjLoaderTests { - private class MaterialStreamProviderSpy : IMaterialStreamProvider - { - public string RequestedMaterialFilePath { get; private set; } - public Stream StreamToReturn { get; set; } - - public Stream Open(string materialFilePath) - { - RequestedMaterialFilePath = materialFilePath; - return StreamToReturn; - } - } - - private CoreObjLoader _loader; + private readonly CoreObjLoader _loader; private Mesh _loadResult; - private DataStore _textureDataStore; - private FaceParser _faceParser; - private GroupParser _groupParser; - private NormalParser _normalParser; - private TextureParser _textureParser; - private VertexParser _vertexParser; - private MaterialLibraryLoader _materialLibraryLoader; - private MaterialLibraryParser _materialLibraryParser; - private UseMaterialParser _useMaterialParser; - private MaterialLibraryLoaderFacade _materialLibraryLoaderFacade; - private MaterialStreamProviderSpy _materialStreamProviderSpy; - - [SetUp] - public void SetUp() + private readonly DataStore _textureDataStore; + private readonly FaceParser _faceParser; + private readonly GroupParser _groupParser; + private readonly NormalParser _normalParser; + private readonly TextureParser _textureParser; + private readonly VertexParser _vertexParser; + private readonly MaterialLibraryLoader _materialLibraryLoader; + private readonly MaterialLibraryParser _materialLibraryParser; + private readonly UseMaterialParser _useMaterialParser; + private readonly MaterialLibraryLoaderFacade _materialLibraryLoaderFacade; + + public ObjLoaderTests() { _textureDataStore = new DataStore(); + var fileStreamFactory = Mock.Of(); + var fileManager = Mock.Of(); + _faceParser = new FaceParser(_textureDataStore); _groupParser = new GroupParser(_textureDataStore); _normalParser = new NormalParser(_textureDataStore); _textureParser = new TextureParser(_textureDataStore); _vertexParser = new VertexParser(_textureDataStore); - _materialStreamProviderSpy = new MaterialStreamProviderSpy(); - _materialStreamProviderSpy.StreamToReturn = CreateMemoryStreamFromString(MaterialLibraryString); - _materialLibraryLoader = new MaterialLibraryLoader(_textureDataStore); + _materialLibraryLoader = new MaterialLibraryLoader(_textureDataStore, fileStreamFactory, fileManager); _materialLibraryLoaderFacade = new MaterialLibraryLoaderFacade(_materialLibraryLoader, ""); _materialLibraryParser = new MaterialLibraryParser(_materialLibraryLoaderFacade); _useMaterialParser = new UseMaterialParser(_textureDataStore); - _loader = new CoreObjLoader("", _textureDataStore); + _loader = new CoreObjLoader("", _textureDataStore, fileStreamFactory, fileManager); } - [Test] + [Fact] public void Loads_object_and_material_correctly() { Load(); @@ -73,8 +63,6 @@ public void Loads_object_and_material_correctly() _loadResult.Normals.Should().HaveCount(8); _loadResult.Materials.Should().HaveCount(1); - _materialStreamProviderSpy.RequestedMaterialFilePath.Should().BeEquivalentTo("cube.mtl"); - _loadResult.Groups.Should().HaveCount(1); var group = _loadResult.Groups.First(); @@ -82,15 +70,9 @@ public void Loads_object_and_material_correctly() group.Material!.Name.Should().BeEquivalentTo("cube_material"); } - [Test] + [Fact] public void Loads_object_correctly_when_material_is_not_found() { - _materialStreamProviderSpy.StreamToReturn = null!; - var materialLibraryLoaderFacade = new MaterialLibraryLoaderFacade(_materialLibraryLoader, ""); - var materialLibraryParser = new MaterialLibraryParser(_materialLibraryLoaderFacade); - - _loader = new CoreObjLoader("", _textureDataStore); - Load(); _loadResult.Vertices.Should().HaveCount(8); @@ -107,17 +89,9 @@ public void Loads_object_correctly_when_material_is_not_found() private void Load() { - var objectStream = CreateMemoryStreamFromString(ObjectFileString); - _loadResult = _loader.Load(null!).First(); } - private Stream CreateMemoryStreamFromString(string str) - { - var data = Encoding.ASCII.GetBytes(str); - return new MemoryStream(data); - } - private const string ObjectFileString = "#Comment\r\n" + "mtllib\tcube.mtl\r\n" + @@ -185,8 +159,4 @@ map_bump lenna_bump.tga disp lenna_disp.tga decal lenna_stencil.tga"; } - - internal interface IMaterialStreamProvider - { - } } \ No newline at end of file diff --git a/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj b/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj index 6a619d7..3a8519e 100644 --- a/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj +++ b/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj @@ -6,10 +6,11 @@ - - - - + + + + + diff --git a/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs index df5b03a..05636a0 100644 --- a/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; @@ -7,21 +7,18 @@ namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class FaceParserTests { - private FaceGroupSpy _faceGroupSpy; - private FaceParser _faceParser; + private readonly FaceGroupSpy _faceGroupSpy; + private readonly FaceParser _faceParser; - [SetUp] - public void SetUp() + public FaceParserTests() { _faceGroupSpy = new FaceGroupSpy(); - _faceParser = new FaceParser(_faceGroupSpy); } - [Test] + [Fact] public void CanParse_returns_true_on_face_line() { const string groupKeyword = "f"; @@ -30,7 +27,7 @@ public void CanParse_returns_true_on_face_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_normal_line() { const string invalidKeyword = "vt"; @@ -39,7 +36,7 @@ public void CanParse_returns_false_on_non_normal_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_normal_line_correctly_1() { const string faceLine = "1 2 3"; @@ -60,7 +57,7 @@ public void Parses_normal_line_correctly_1() parsedFace[2].NormalIndex.Should().Be(0); } - [Test] + [Fact] public void Parses_normal_line_correctly_2() { const string faceLine = "3/1 4/2 5/3"; @@ -83,7 +80,7 @@ public void Parses_normal_line_correctly_2() parsedFace[2].NormalIndex.Should().Be(0); } - [Test] + [Fact] public void Parses_normal_line_correctly_3() { const string faceLine = "6/4/1 3/5/3 7/6/5"; @@ -106,7 +103,7 @@ public void Parses_normal_line_correctly_3() parsedFace[2].NormalIndex.Should().Be(5); } - [Test] + [Fact] public void Parses_normal_line_correctly_4() { const string faceLine = "6//1 3//3 7//5"; diff --git a/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs index 061dd2d..1e38c77 100644 --- a/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs @@ -1,12 +1,11 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.ObjLoader.Loader.TypeParsers; namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class GroupParserTests { private readonly GroupDataStoreMock _groupDataStoreMock; @@ -18,7 +17,7 @@ public GroupParserTests() _groupParser = new GroupParser(_groupDataStoreMock); } - [Test] + [Fact] public void CanParse_returns_true_on_normal_line() { const string groupKeyword = "g"; @@ -27,7 +26,7 @@ public void CanParse_returns_true_on_normal_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_normal_line() { const string invalidKeyword = "vt"; @@ -36,7 +35,7 @@ public void CanParse_returns_false_on_non_normal_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_normal_line_correctly() { const string normalLine = "test group"; diff --git a/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs index 3c974a6..e1771b6 100644 --- a/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs @@ -1,11 +1,11 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; + using SharpEngine.Core.ObjLoader.Loader.TypeParsers; using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class MaterialLibraryParserTests { private readonly MaterialLibraryLoaderFacadeSpy _materialLibraryLoaderFacadeSpy; @@ -17,7 +17,7 @@ public MaterialLibraryParserTests() _parser = new MaterialLibraryParser(_materialLibraryLoaderFacadeSpy); } - [Test] + [Fact] public void CanParse_returns_true_on_mtlib_line() { const string groupKeyword = "mtllib"; @@ -26,7 +26,7 @@ public void CanParse_returns_true_on_mtlib_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_mtlib_line() { const string invalidKeyword = "vt"; @@ -35,7 +35,7 @@ public void CanParse_returns_false_on_non_mtlib_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_mtlib_line_correctly() { const string faceLine = "cube.mtl"; diff --git a/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs index f047eb5..15bce84 100644 --- a/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; @@ -7,7 +7,6 @@ namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class NormalParserTests { private readonly NormalParser _normalParser; @@ -19,7 +18,7 @@ public NormalParserTests() _normalParser = new NormalParser(_normalDataStoreMock); } - [Test] + [Fact] public void CanParse_returns_true_on_normal_line() { const string normalLine = "vn"; @@ -28,7 +27,7 @@ public void CanParse_returns_true_on_normal_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_normal_line() { const string invalidNormal = "vt"; @@ -37,7 +36,7 @@ public void CanParse_returns_false_on_non_normal_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_normal_line_correctly() { const string normalLine = "1.000000 2.000000 -1.000000"; diff --git a/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs index 4b9ffd9..36402df 100644 --- a/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; @@ -7,7 +7,6 @@ namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class TextureParserTests { private readonly TextureDataStoreMock _textureDataStoreMock; @@ -20,7 +19,7 @@ public TextureParserTests() _textureParser = new TextureParser(_textureDataStoreMock); } - [Test] + [Fact] public void CanParse_returns_true_on_normal_line() { const string textureKeyword = "vt"; @@ -29,7 +28,7 @@ public void CanParse_returns_true_on_normal_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_normal_line() { const string invalidKeyword = "vn"; @@ -38,7 +37,7 @@ public void CanParse_returns_false_on_non_normal_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_normal_line_correctly() { const string textureLine = "0.500 -1.352"; diff --git a/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs index 29cbaa8..5b72637 100644 --- a/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs @@ -1,10 +1,13 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; + using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.Components.Properties; + +using System; namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class UseMaterialParserTests { private readonly ElementGroupSpy _elementGroupSpy; @@ -16,7 +19,7 @@ public UseMaterialParserTests() _parser = new UseMaterialParser(_elementGroupSpy); } - [Test] + [Fact] public void CanParse_returns_true_on_usemtl_line() { const string groupKeyword = "usemtl"; @@ -25,7 +28,7 @@ public void CanParse_returns_true_on_usemtl_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_usemtl_line() { const string invalidKeyword = "vt"; @@ -34,7 +37,7 @@ public void CanParse_returns_false_on_non_usemtl_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_usemtl_line_correctly_1() { const string useMtlLine = "materialName"; @@ -47,6 +50,9 @@ private class ElementGroupSpy : IMaterialDataStore { public string MaterialName { get; private set; } + public void AddMaterial(Material currentMaterial) + => throw new NotImplementedException(); + public void SetMaterial(string materialName) { MaterialName = materialName; diff --git a/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs index 8009a66..dfa5af7 100644 --- a/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; @@ -7,7 +7,6 @@ namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class VertexParserTests { private readonly VertexDataStoreMock _vertexDataStoreMock; @@ -19,7 +18,7 @@ public VertexParserTests() _vertexParser = new VertexParser(_vertexDataStoreMock); } - [Test] + [Fact] public void CanParse_returns_false_on_non_vertex_line() { const string invalidVertex = "vt"; @@ -28,7 +27,7 @@ public void CanParse_returns_false_on_non_vertex_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void CanParse_returns_true_on_vertex_line() { const string vertexLine = "v"; @@ -37,7 +36,7 @@ public void CanParse_returns_true_on_vertex_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void Parses_vertex_line_correctly() { const string vertexLine = "0.123 0.234 0.345"; diff --git a/Tests/SharpEngine.Core.Components.Tests/Class1.cs b/Tests/SharpEngine.Core.Components.Tests/Class1.cs new file mode 100644 index 0000000..cab4a8c --- /dev/null +++ b/Tests/SharpEngine.Core.Components.Tests/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Core.Components.Tests; + +public class Class1 +{ + +} diff --git a/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj b/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj new file mode 100644 index 0000000..a15a71c --- /dev/null +++ b/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/Tests/SharpEngine.Core.Numerics.Tests/Class1.cs b/Tests/SharpEngine.Core.Numerics.Tests/Class1.cs new file mode 100644 index 0000000..580dc2a --- /dev/null +++ b/Tests/SharpEngine.Core.Numerics.Tests/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Core.Numerics.Tests; + +public class Class1 +{ + +} diff --git a/Tests/SharpEngine.Core.Numerics.Tests/SharpEngine.Core.Numerics.Tests.csproj b/Tests/SharpEngine.Core.Numerics.Tests/SharpEngine.Core.Numerics.Tests.csproj new file mode 100644 index 0000000..547dedb --- /dev/null +++ b/Tests/SharpEngine.Core.Numerics.Tests/SharpEngine.Core.Numerics.Tests.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/Tests/SharpEngine.Core.Tests/Class1.cs b/Tests/SharpEngine.Core.Tests/Class1.cs deleted file mode 100644 index ff34b98..0000000 --- a/Tests/SharpEngine.Core.Tests/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SharpEngine.Core.Tests; - -public class Class1 -{ - -} diff --git a/Tests/SharpEngine.Core.Tests/Extensions/AssemblyExtensionsTests.cs b/Tests/SharpEngine.Core.Tests/Extensions/AssemblyExtensionsTests.cs new file mode 100644 index 0000000..a58a131 --- /dev/null +++ b/Tests/SharpEngine.Core.Tests/Extensions/AssemblyExtensionsTests.cs @@ -0,0 +1,40 @@ +using SharpEngine.Core.Extensions; +using System.Reflection; +using Xunit; + +namespace SharpEngine.Core.Tests.Extensions; + +public class AssemblyExtensionsTests +{ + private class AssemblyStub : Assembly + { + public Version Version { get; } + public AssemblyStub(Version version) + { + Version = version; + } + + public override AssemblyName GetName() + { + return new AssemblyName("TestAssembly") + { + Version = Version + }; + } + } + + [Fact] + public void GetVersion_Returns_Correct_Version() + { + var assemblyVersion = new Version(1, 2, 3, 4); + + var assembly = new AssemblyStub(assemblyVersion); + var version = assembly.GetVersion(); + + Assert.NotNull(version); + Assert.Equal(1, version.Major); + Assert.Equal(2, version.Minor); + Assert.Equal(3, version.Build); + Assert.Equal(4, version.Revision); + } +} diff --git a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj index bbcf245..a3a2555 100644 --- a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj +++ b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj @@ -6,4 +6,29 @@ enable + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj b/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj index fa71b7a..bbcf245 100644 --- a/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj +++ b/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + $(DotNetTargetFramework) enable enable From c62c365c449748c839e4f738fd6a019f4cfe2def Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sat, 20 Jun 2026 19:47:27 +0300 Subject: [PATCH 02/10] #109 Audio player updates Added pause, resume and loading new audios on top of an existing player --- SharpEngine.Core/Audio/Audio.cs | 27 ++++++- SharpEngine.Core/Audio/AudioBuffer.cs | 2 +- .../Audio/{WavData.cs => AudioData.cs} | 4 +- SharpEngine.Core/Audio/AudioDevice.cs | 8 ++ SharpEngine.Core/Audio/AudioPlayerBase.cs | 32 ++++++-- SharpEngine.Core/Audio/AudioSource.cs | 23 ++++++ SharpEngine.Core/Audio/Mp3Player.cs | 74 +++++++++++++++++++ SharpEngine.Core/Audio/WavPlayer.cs | 4 +- SharpEngine.Core/SharpEngine.Core.csproj | 2 + 9 files changed, 165 insertions(+), 11 deletions(-) rename SharpEngine.Core/Audio/{WavData.cs => AudioData.cs} (90%) create mode 100644 SharpEngine.Core/Audio/Mp3Player.cs diff --git a/SharpEngine.Core/Audio/Audio.cs b/SharpEngine.Core/Audio/Audio.cs index 7d9d171..7d38c4d 100644 --- a/SharpEngine.Core/Audio/Audio.cs +++ b/SharpEngine.Core/Audio/Audio.cs @@ -8,6 +8,8 @@ namespace SharpEngine.Core.Audio; /// public static class Audio { + private static AudioPlayerBase? audioPlayer = null; + /// /// Plays an audio file if it is in the WAV format. /// @@ -18,11 +20,34 @@ public static void Play(string filePath) switch (Path.GetExtension(filePath)) { case ".wav": - new WavPlayer().Play(filePath); + audioPlayer = new WavPlayer(); + audioPlayer.Play(filePath); + break; + + case ".mp3": + audioPlayer = new Mp3Player(); + audioPlayer.Play(filePath); break; default: throw new NotSupportedException("Unsupported file format."); } } + + /// + public static void Stop() + { + audioPlayer?.Stop(); + audioPlayer = null; + } + + /// + /// Pauses the currently playing audio, if any. + /// + public static void Pause() => audioPlayer?.Pause(); + + /// + /// Resumes the currently paused audio, if any. + /// + public static void Resume() => audioPlayer?.Resume(); } diff --git a/SharpEngine.Core/Audio/AudioBuffer.cs b/SharpEngine.Core/Audio/AudioBuffer.cs index 506f426..6041946 100644 --- a/SharpEngine.Core/Audio/AudioBuffer.cs +++ b/SharpEngine.Core/Audio/AudioBuffer.cs @@ -29,7 +29,7 @@ public AudioBuffer(AL al) /// /// The data to be loaded into the buffer. /// Details about the data. - public void LoadData(ReadOnlySpan data, WavData wavData) + public void LoadData(ReadOnlySpan data, AudioData wavData) { unsafe { diff --git a/SharpEngine.Core/Audio/WavData.cs b/SharpEngine.Core/Audio/AudioData.cs similarity index 90% rename from SharpEngine.Core/Audio/WavData.cs rename to SharpEngine.Core/Audio/AudioData.cs index 80ac937..fa81d42 100644 --- a/SharpEngine.Core/Audio/WavData.cs +++ b/SharpEngine.Core/Audio/AudioData.cs @@ -3,9 +3,9 @@ namespace SharpEngine.Core.Audio; /// -/// Represents audio data in WAV format. +/// Represents the data in a audio file. /// -public class WavData +public class AudioData { /// Gets or sets the number of channels used in the data. public short NumChannels { get; set; } diff --git a/SharpEngine.Core/Audio/AudioDevice.cs b/SharpEngine.Core/Audio/AudioDevice.cs index aa0560d..d5429bc 100644 --- a/SharpEngine.Core/Audio/AudioDevice.cs +++ b/SharpEngine.Core/Audio/AudioDevice.cs @@ -8,6 +8,14 @@ namespace SharpEngine.Core.Audio; /// public unsafe class AudioDevice : IDisposable { + // Shared single AudioDevice instance to ensure a single OpenAL context is used + private static readonly AudioDevice _shared = new AudioDevice(); + + /// + /// Gets the shared audio device instance used by the application. + /// + public static AudioDevice Shared => _shared; + /// Gets the OpenAL context. public AL Al { get; } diff --git a/SharpEngine.Core/Audio/AudioPlayerBase.cs b/SharpEngine.Core/Audio/AudioPlayerBase.cs index 5778672..8df590a 100644 --- a/SharpEngine.Core/Audio/AudioPlayerBase.cs +++ b/SharpEngine.Core/Audio/AudioPlayerBase.cs @@ -1,4 +1,5 @@ using SharpEngine.Core.Scenes; +using Silk.NET.OpenAL; using System; using System.IO; @@ -17,7 +18,7 @@ public abstract class AudioPlayerBase : SceneNode public AudioProperties AudioProperties { get; set; } /// Contains the audio data of the media file. - protected readonly WavData Data; + protected readonly AudioData Data; /// Represents a container for the . protected readonly AudioBuffer AudioBuffer; @@ -30,9 +31,10 @@ public abstract class AudioPlayerBase : SceneNode /// protected AudioPlayerBase() { - Data = new WavData(); + Data = new AudioData(); - var audioDevice = new AudioDevice(); + // Use the shared audio device so multiple players share the same OpenAL context + var audioDevice = AudioDevice.Shared; AudioBuffer = new AudioBuffer(audioDevice.Al); AudioSource = new AudioSource(audioDevice.Al); @@ -61,7 +63,27 @@ protected virtual void ValidateFile(string filePath) } /// - /// Stops the audio playback of the associated AudioSource. + /// Stops the audio playback of the associated . /// - public virtual void Stop() => AudioSource.Stop(); + public virtual void Stop() + { + // Stop playback but do not delete the source here. Deleting the source + // in AudioSource.Stop caused subsequent players to generate new sources + // while buffers still referenced old sources, leading to overlapping audio. + AudioSource.Stop(); + + // Detach buffer from source so it can be reused safely + var al = AudioDevice.Shared.Al; + al.SetSourceProperty(AudioSource.Get(), SourceInteger.Buffer, 0); + } + + /// + /// Pauses the audio playback of the associated . + /// + public virtual void Pause() => AudioSource.Pause(); + + /// + /// Resumes the audio playback of the associated . + /// + public virtual void Resume() => AudioSource.Resume(); } diff --git a/SharpEngine.Core/Audio/AudioSource.cs b/SharpEngine.Core/Audio/AudioSource.cs index d0a3669..6c11cd4 100644 --- a/SharpEngine.Core/Audio/AudioSource.cs +++ b/SharpEngine.Core/Audio/AudioSource.cs @@ -42,6 +42,29 @@ public void Play(AudioBuffer buffer) public void Stop() { _al.SourceStop(_source); + } + + /// + /// Pauses playback of the source. + /// + public void Pause() + { + _al.SourcePause(_source); + } + + /// + /// Resumes playback of the source if it was paused. + /// + public void Resume() + { + _al.SourcePlay(_source); + } + + /// + /// Deletes the underlying OpenAL source. Call when the source will no longer be used. + /// + public void Dispose() + { _al.DeleteSource(_source); } } \ No newline at end of file diff --git a/SharpEngine.Core/Audio/Mp3Player.cs b/SharpEngine.Core/Audio/Mp3Player.cs new file mode 100644 index 0000000..1fe5ff3 --- /dev/null +++ b/SharpEngine.Core/Audio/Mp3Player.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Buffers.Binary; +using System.IO; +using NLayer; +using Silk.NET.OpenAL; + +namespace SharpEngine.Core.Audio; + +internal class Mp3Player : AudioPlayerBase +{ + private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + + /// + protected override string FileExtension => AudioFileExtensions.Mp3Extension; + + public Mp3Player() + { + } + + /// + /// Thrown if the audio data contains more than 2 channels. + /// Thrown if the file path is invalid. + /// Thrown if the file is not found. + public override void Play(string filePath) + { + ValidateFile(filePath); + + using var fs = File.OpenRead(filePath); + using var mpeg = new MpegFile(fs); + + int sampleRate = mpeg.SampleRate; + int channels = mpeg.Channels; + + // Decode to float samples and convert to 16-bit PCM + using var pcmStream = new MemoryStream(); + var floatBuffer = new float[4096]; + int samplesRead; + while ((samplesRead = mpeg.ReadSamples(floatBuffer, 0, floatBuffer.Length)) > 0) + { + // samplesRead is number of float samples (interleaved channels) + var bytes = new byte[samplesRead * 2]; + for (int i = 0; i < samplesRead; i++) + { + float f = floatBuffer[i]; + // clamp and convert + int intVal = (int)MathF.Round(f * 32767f); + if (intVal > short.MaxValue) intVal = short.MaxValue; + else if (intVal < short.MinValue) intVal = short.MinValue; + short s = (short)intVal; + bytes[i * 2] = (byte)(s & 0xFF); + bytes[i * 2 + 1] = (byte)((s >> 8) & 0xFF); + } + + pcmStream.Write(bytes, 0, bytes.Length); + } + + var pcmData = pcmStream.ToArray(); + + // Fill WavData + Data.NumChannels = (short)channels; + Data.SampleRate = sampleRate; + Data.BitsPerSample = 16; + Data.Format = channels switch + { + 1 => BufferFormat.Mono16, + 2 => BufferFormat.Stereo16, + _ => throw new InvalidOperationException($"Can't play audio with {channels} channels.") + }; + + AudioBuffer.LoadData(pcmData, Data); + AudioSource.Play(AudioBuffer); + } +} \ No newline at end of file diff --git a/SharpEngine.Core/Audio/WavPlayer.cs b/SharpEngine.Core/Audio/WavPlayer.cs index 7869f28..33401aa 100644 --- a/SharpEngine.Core/Audio/WavPlayer.cs +++ b/SharpEngine.Core/Audio/WavPlayer.cs @@ -15,7 +15,7 @@ public class WavPlayer : AudioPlayerBase private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); /// - protected override string FileExtension => ".wav"; + protected override string FileExtension => AudioFileExtensions.WavExtension; private static class WavConstants { @@ -90,7 +90,7 @@ private static bool CheckHeader(ReadOnlySpan file, ref int index, ReadOnly return true; } - private static void ParseFormat(ReadOnlySpan formatChunk, WavData wavData) + private static void ParseFormat(ReadOnlySpan formatChunk, AudioData wavData) { int index = 0; var audioFormat = BinaryPrimitives.ReadInt16LittleEndian(formatChunk.Slice(index, WavConstants.AudioFormatLength)); diff --git a/SharpEngine.Core/SharpEngine.Core.csproj b/SharpEngine.Core/SharpEngine.Core.csproj index 04fb6cb..8760f7b 100644 --- a/SharpEngine.Core/SharpEngine.Core.csproj +++ b/SharpEngine.Core/SharpEngine.Core.csproj @@ -18,6 +18,7 @@ true README.md + @@ -26,6 +27,7 @@ + From bbc57edb8ee91ff6b761f8955c9360b7daee9634 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sat, 20 Jun 2026 19:48:07 +0300 Subject: [PATCH 03/10] #109 Fix windowing --- Examples/Minimal/Program.cs | 1 + Examples/MultipleWindows/Program.cs | 9 +- SharpEngine.Core/Handlers/WindowHandler.cs | 16 ++-- .../Renderers/ParticleRenderer.cs | 25 ++++- SharpEngine.Core/Windowing/SilkWindow.cs | 8 ++ SharpEngine.Core/Windowing/Window.cs | 96 ++++++++++++------- 6 files changed, 110 insertions(+), 45 deletions(-) diff --git a/Examples/Minimal/Program.cs b/Examples/Minimal/Program.cs index 26310ec..36a530e 100644 --- a/Examples/Minimal/Program.cs +++ b/Examples/Minimal/Program.cs @@ -16,6 +16,7 @@ public static void Main(string[] _) var game = new Minimal(); using var window = new Window(game); + window.Initialize(); window.Run(); } } diff --git a/Examples/MultipleWindows/Program.cs b/Examples/MultipleWindows/Program.cs index 67751b3..b1dfc16 100644 --- a/Examples/MultipleWindows/Program.cs +++ b/Examples/MultipleWindows/Program.cs @@ -117,10 +117,13 @@ private static Window CreateWindow() private static void EnqueueWindow() { var window = CreateWindow(); - foreach (var mouse in window!.Input!.Mice) - mouse.Click += Mouse_Click; - _inputContexts.Add(window.Input); + // Prefer the higher-level Window event to avoid wiring low-level input handlers. + window.OnButtonMouseDown += (mouse, button) => Mouse_Click(mouse, (MouseButton)button, mouse.Position); + + if (window.Input is not null) + _inputContexts.Add(window.Input); + _windows.Add(window); } diff --git a/SharpEngine.Core/Handlers/WindowHandler.cs b/SharpEngine.Core/Handlers/WindowHandler.cs index ef2d1ec..d82c773 100644 --- a/SharpEngine.Core/Handlers/WindowHandler.cs +++ b/SharpEngine.Core/Handlers/WindowHandler.cs @@ -200,8 +200,10 @@ private static Windowing.Window CreateWindow(WindowOptions options) private static void EnqueueWindow(WindowOptions options) { var window = CreateWindow(options); - foreach (var mouse in window!.Input!.Mice) - mouse.Click += Mouse_Click; + // Subscribe to the window's high-level mouse button event instead of + // attaching to the low-level Input.Mice click handlers. This centralizes + // input handling inside Window and avoids duplicate wiring. + window.OnButtonMouseDown += (mouse, button) => Mouse_Click(mouse, (Silk.NET.Input.MouseButton)button, mouse.Position); _inputContexts.Add(window.Input); _windows.Add(window); @@ -217,13 +219,13 @@ private void RegisterWindow(SilkWindow window, bool isMain = false) if (window is null) return; - if (window.Input is not null) - { - foreach (var mouse in window.Input.Mice) - mouse.Click += Mouse_Click; + // Prefer the higher-level event on the Window class for mouse button + // notifications rather than wiring Input.Mice click events directly. + if (window is Windowing.Window w) + w.OnButtonMouseDown += (mouse, button) => Mouse_Click(mouse, (Silk.NET.Input.MouseButton)button, mouse.Position); + if (window.Input is not null) _inputContexts.Add(window.Input); - } _windows.Add(window); diff --git a/SharpEngine.Core/Renderers/ParticleRenderer.cs b/SharpEngine.Core/Renderers/ParticleRenderer.cs index 53f6d93..e34e2e3 100644 --- a/SharpEngine.Core/Renderers/ParticleRenderer.cs +++ b/SharpEngine.Core/Renderers/ParticleRenderer.cs @@ -1,4 +1,7 @@ -using SharpEngine.Core.Interfaces; +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Scenes; using System; using System.Threading.Tasks; @@ -17,9 +20,27 @@ public ParticleRenderer(ISettings settings) : base(settings) { } + /// + /// Initializes a new instance of . + /// + public ParticleRenderer(CameraView camera, ISettings settings, Scene scene) + : this(camera, settings, scene, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) + { + } + + /// + /// Initializes a new instance of . + /// + public ParticleRenderer(CameraView camera, ISettings settings, Scene scene, ILogger logger) : base(settings) + { + // _scene = scene; + // _camera = camera; + // _logger = logger; + } + /// public override RenderFlags RenderFlag => RenderFlags.All; /// - public override Task Render() => throw new NotImplementedException(); + public override Task Render() => Task.CompletedTask; // => throw new NotImplementedException(); } diff --git a/SharpEngine.Core/Windowing/SilkWindow.cs b/SharpEngine.Core/Windowing/SilkWindow.cs index 1f5e1dd..c7c9fd3 100644 --- a/SharpEngine.Core/Windowing/SilkWindow.cs +++ b/SharpEngine.Core/Windowing/SilkWindow.cs @@ -75,6 +75,14 @@ public Vector2D Size set => CurrentWindow.Size = value; } + /// Gets the width of the window in pixels. + /// Use to change the window size. + public int Width => Size.X; + + /// Gets the height of the window in pixels. + /// Use to change the window size. + public int Height => Size.Y; + /// public string Title { diff --git a/SharpEngine.Core/Windowing/Window.cs b/SharpEngine.Core/Windowing/Window.cs index f88a071..15afa84 100644 --- a/SharpEngine.Core/Windowing/Window.cs +++ b/SharpEngine.Core/Windowing/Window.cs @@ -92,14 +92,29 @@ public static GL SharedGL public GL GetGL() => _gl; private void SetGL(GL gl) => _gl = gl; + /// + /// Initializes a new instance of . + /// + /// The game instance. + public Window(Game game) + : this(game.Scene, game.Camera.Settings, logger: null, renderers: null) { } + + /// + /// Initializes a new instance of . + /// + /// The scene to be rendered. + /// The settings for the window. + public Window(Scene scene, IViewSettings settings) + : this(scene, settings, logger: null, renderers: null) { } + /// /// Initializes a new instance of . /// /// The camera the window should render from. /// Contains the game scene. /// The settings for the window. - public Window(CameraView camera, Scene scene, IViewSettings settings) : - this(camera, scene, settings, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) { } + public Window(CameraView camera, Scene scene, IViewSettings settings) + : this(camera, scene, settings, logger: null, renderers: null) { } /// /// Initializes a new instance of . @@ -107,9 +122,9 @@ public Window(CameraView camera, Scene scene, IViewSettings settings) : /// The camera the window should render from. /// Contains the game scene. /// The settings for the window. - /// The logger for the window. + /// The logger for the window. When , a default logger will be used. /// The renderers for the window. - public Window(CameraView camera, Scene scene, IViewSettings settings, ILogger logger, IEnumerable? renderers = null) + public Window(CameraView camera, Scene scene, IViewSettings settings, ILogger? logger = null, IEnumerable? renderers = null) { // TODO: // Should the developer need to call for a window initialization? @@ -119,34 +134,14 @@ public Window(CameraView camera, Scene scene, IViewSettings settings, ILogger builder.AddConsole()).CreateLogger(); CheckEngineVersion(); - // NOTE: Window initialization is intentionally not performed automatically - // here. Call InitializeWindow() explicitly when ready to create the underlying - // native window and load resources. See the TODO in the project for reasoning. + // NOTE: Window initialization is intentionally not performed automatically here. + // Call InitializeWindow() explicitly when ready to create the underlying native window and load resources. } - private void CheckEngineVersion() - { - var project = new ProjectDto(); - var currentAssemlyVersion = typeof(Window).Assembly.GetVersion(); - - if (currentAssemlyVersion != project.EngineVersion) - _logger.LogWarning("The current engine version ({CurrentVersion}) does not match the project engine version ({ProjectVersion}). This may lead to unexpected behavior.", currentAssemlyVersion, project.EngineVersion); - } - - /// - /// Initializes a new instance of . - /// - /// The game instance. - public Window(Game game) - : this(game.Scene, game.Camera.Settings, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) { } - - public Window(Scene scene, IViewSettings settings) - : this(scene, settings, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) { } - /// /// Initializes a new instance of . /// @@ -155,18 +150,27 @@ public Window(Scene scene, IViewSettings settings) /// /// Contains the game scene. /// The settings for the window. - /// The logger for the window. + /// The logger for the window. When , a default logger will be used. /// The renderers for the window. - public Window(Scene scene, IViewSettings settings, ILogger logger, IEnumerable? renderers = null) + public Window(Scene scene, IViewSettings settings, ILogger? logger = null, IEnumerable? renderers = null) { Scene = scene; Settings = settings; Camera = new(Vector3.One, settings); _registeredRenderers = renderers?.ToArray() ?? []; - _logger = logger; + _logger = logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); - // Initialization is deferred. Call Initialize() when you want the - // native window to be created and resources to be loaded. + // NOTE: Window initialization is intentionally not performed automatically here. + // Call InitializeWindow() explicitly when ready to create the underlying native window and load resources. + } + + private void CheckEngineVersion() + { + var project = new ProjectDto(); + var currentAssemlyVersion = typeof(Window).Assembly.GetVersion(); + + if (currentAssemlyVersion != project.EngineVersion) + _logger.LogWarning("The current engine version ({CurrentVersion}) does not match the project engine version ({ProjectVersion}). This may lead to unexpected behavior.", currentAssemlyVersion, project.EngineVersion); } /// @@ -183,6 +187,11 @@ public override void Initialize() CurrentWindow.Closing += OnClosing; _windowInitialized = true; + + // Ensure the underlying native window is created and initialized. + // SilkWindow.Initialize calls CurrentWindow.Initialize(); call it here to + // complete the initialization sequence (create GL context, input, etc.). + base.Initialize(); } /// @@ -265,7 +274,14 @@ protected void RenderFrame(Frame frame) _gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); ToggleWireFrame(Settings.UseWireFrame); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during pre-rendering."); + } + try + { UseShaders(); var activeRenderers = _renderers.Where(renderer => Settings.RendererFlags.HasFlag(renderer.RenderFlag)) @@ -274,14 +290,28 @@ protected void RenderFrame(Frame frame) foreach (var renderer in activeRenderers) renderer.Render().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during rendering."); + } + try + { AfterRender(frame); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during post render."); + } + try + { _imGuiController?.Render(); } catch (Exception ex) { - _logger.LogInformation("{Message}", ex.Message); + _logger.LogError(ex, "An error occurred while rendering ImGui interface."); } } From d583a4acf73d46a48380f0b348b87a2f98ee9799 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sat, 20 Jun 2026 19:48:26 +0300 Subject: [PATCH 04/10] #109 Update scene calls --- SharpEngine.Core/Scenes/Scene.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/SharpEngine.Core/Scenes/Scene.cs b/SharpEngine.Core/Scenes/Scene.cs index ff4db26..6ab4c91 100644 --- a/SharpEngine.Core/Scenes/Scene.cs +++ b/SharpEngine.Core/Scenes/Scene.cs @@ -1,10 +1,12 @@ using Microsoft.Extensions.Logging; using SharpEngine.Core.Entities.Properties; +using SharpEngine.Telemetry; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace SharpEngine.Core.Scenes; @@ -14,7 +16,7 @@ namespace SharpEngine.Core.Scenes; /// public class Scene { - private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + private static readonly ILogger Logger = LoggingExtensions.CreateLogger(); /// The file extension by which saved scenes are associated with. public const string SceneFileExtension = "sharpscene"; @@ -22,6 +24,7 @@ public class Scene private string? _fileFullPath; /// Gets or sets whether the scene has unsaved changes. + [JsonIgnore] public bool HasUnsavedChanges { get; set; } /// Gets or sets the name of the scene file. @@ -214,21 +217,29 @@ public void SetFileFullPath(string sceneFile) public bool HasSaveFile() => _fileFullPath is not null; + /// + /// Saves the scene. + /// + public void SaveScene() + => SaveSceneAsync().GetAwaiter().GetResult(); + /// /// Saves the scene. /// /// A representing an asynchronous operation. - public async Task SaveScene() - => await SaveScene(_fileFullPath!); + public async Task SaveSceneAsync() + => await SaveSceneAsync(_fileFullPath!); /// /// Saves the scene to the given . /// /// The name of the file to be saved. /// A representing an asynchronous operation. - public async Task SaveScene(string fileName) + public async Task SaveSceneAsync(string fileName) { var json = JsonSerializer.Serialize(this); await System.IO.File.WriteAllTextAsync(fileName, json); + + HasUnsavedChanges = false; } } \ No newline at end of file From 1296f638fe109a035af4025faaa65f9aff03f627 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 21 Jun 2026 13:07:07 +0300 Subject: [PATCH 05/10] #67 Shader improvements (WIP) --- .../Properties/Material.cs | 20 +- .../Properties/Shaders/ShaderProgram.cs | 219 ++++++++++++++++++ .../SharpEngine.Core.Components.csproj | 1 + .../Entities/UI/ShaderParameterAttribute.cs | 19 ++ SharpEngine.Core/Rendering/ShaderParameter.cs | 70 ++++++ SharpEngine.Core/Shaders/DefaultShader.cs | 10 + 6 files changed, 331 insertions(+), 8 deletions(-) create mode 100644 SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs create mode 100644 SharpEngine.Core/Entities/UI/ShaderParameterAttribute.cs create mode 100644 SharpEngine.Core/Rendering/ShaderParameter.cs create mode 100644 SharpEngine.Core/Shaders/DefaultShader.cs diff --git a/SharpEngine.Core.Components/Properties/Material.cs b/SharpEngine.Core.Components/Properties/Material.cs index 5d01401..b1a54b3 100644 --- a/SharpEngine.Core.Components/Properties/Material.cs +++ b/SharpEngine.Core.Components/Properties/Material.cs @@ -10,30 +10,34 @@ namespace SharpEngine.Core.Components.Properties; /// public class Material : ICloneable, IEquatable { + /// + /// Initializes a new instance of . + /// + /// The name assigned to the new material. + public Material(string materialName) + : this(materialName, diffuseMap: null, specularMap: null) { } + /// /// Initializes a new instance of . /// /// The name assigned to the new material. /// The diffuse map texture of the material. /// The specular map texture of the material. Defaults to the diffuse map if not provided. - public Material(string materialName, Texture? diffuseMap = null, Texture? specularMap = null) : this(materialName) + public Material(string materialName, Texture? diffuseMap = null, Texture? specularMap = null) { DiffuseMap = diffuseMap; SpecularMap = specularMap ?? diffuseMap; Specular = new(0.5f, 0.5f, 0.5f); Shininess = 32.0f; - } - /// - /// Initializes a new instance of . - /// - /// The name assigned to the new material. - public Material(string materialName) - { Name = materialName; } + // Resolve dependency issue with GL to fix this. + // public Shader Shader { get; init; } + public Shader Shader { get; set; } + /// Gets or sets the name of the material. public string Name { get; set; } diff --git a/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs new file mode 100644 index 0000000..b0ce786 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs @@ -0,0 +1,219 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; +using Silk.NET.OpenGL; +using System.Text.RegularExpressions; + +namespace SharpEngine.Core.Shaders; + +public abstract class ShaderProgram : IDisposable +{ + protected readonly ILogger _logger; + + protected readonly GL GL; + + protected Dictionary _uniformLocations = []; + private bool disposedValue; + + /// Gets the handle to the shader program. + public uint Handle { get; protected set; } + + protected ShaderProgram(GL gl) + { + GL = gl; + _logger = LoggingExtensions.CreateLogger(); + } + + /// + /// Initializes and compiles vertex and fragment shaders into a shader program. + /// + protected void Initialize(string vertPath, string fragPath) + { + // Load and compile shader + if (!LoadShader(ShaderType.VertexShader, vertPath, out uint vertexShader)) + { + _logger.LogError("Unable to load vertex shader."); + return; + } + + if (!LoadShader(ShaderType.FragmentShader, fragPath, out uint fragmentShader)) + { + _logger.LogError("Unable to load fragment shader."); + return; + } + // These two shaders must then be merged into a shader program, which can then be used by OpenGL. + // To do this, create a program... + + Handle = GL.CreateProgram(); + + // Attach both shaders... + GL.AttachShader(Handle, vertexShader); + GL.AttachShader(Handle, fragmentShader); + + // And then link them together. + bool shaderLinked = LinkProgram(Handle); + + // When the shader program is linked, it no longer needs the individual shaders attached to it; the compiled code is copied into the shader program. + // Detach them, and then delete them. + GL.DetachShader(Handle, vertexShader); + GL.DetachShader(Handle, fragmentShader); + GL.DeleteShader(fragmentShader); + GL.DeleteShader(vertexShader); + + if (!shaderLinked) + { + _logger.LogInformation("Unable to link shader program."); + return; + } + + // The shader is now ready to go, but first, we're going to cache all the shader uniform locations. + // Querying this from the shader is very slow, so we do it once on initialization and reuse those values + // later. + SetUniformLocations(Handle); + + return; + } + + private bool LoadShader(ShaderType shaderType, string shaderPath, out uint shaderProgram) + { + if (!File.Exists(shaderPath)) + { + _logger.LogInformation("Shader file not found: {Path}", shaderPath); + + shaderProgram = 0; + return false; + } + + string shaderSource = File.ReadAllText(shaderPath); + shaderSource = ProcessIncludes(shaderSource, Path.GetDirectoryName(shaderPath)!); + + // GL.CreateShader will create an empty shader (obviously). The ShaderType enum denotes which type of shader will be created. + shaderProgram = GL.CreateShader(shaderType); + GL.ShaderSource(shaderProgram, shaderSource); + + if (!CompileShader(shaderProgram)) + { + _logger.LogInformation("Unable to load {Type} shader from '{Path}'.", shaderType, shaderPath); + return false; + } + + return true; + } + + private void SetUniformLocations(uint shaderProgramHandle) + { + // First, we have to get the number of active uniforms in the shader. + GL.GetProgram(shaderProgramHandle, GLEnum.ActiveUniforms, out var numberOfUniforms); + + Dictionary uniformLocations = []; + + // Loop over all the uniforms, + for (uint i = 0; i < numberOfUniforms; i++) + { + // get the name of this uniform, + var key = GL.GetActiveUniform(shaderProgramHandle, i, out _, out _); + + // get the location, + var location = GL.GetUniformLocation(shaderProgramHandle, key); + + // and then add it to the dictionary. + uniformLocations.Add(key, location); + } + + _uniformLocations = uniformLocations; + } + + private static string ProcessIncludes(string shaderCode, string directory) + { + const string includePattern = @"#include\s+""(.+?)"""; + return Regex.Replace(shaderCode, includePattern, match => + { + string includePath = Path.Combine(directory, match.Groups[1].Value); + string includeCode = File.ReadAllText(includePath); + return ProcessIncludes(includeCode, Path.GetDirectoryName(includePath)!); + }, RegexOptions.NonBacktracking); + } + + private bool CompileShader(uint shaderProgram) + { + // Try to compile the shader + GL.CompileShader(shaderProgram); + + // Check for compilation errors + GL.GetShader(shaderProgram, GLEnum.CompileStatus, out var statusCode); + if (statusCode != (int)GLEnum.True) + { + // We can use `GL.GetShaderInfoLog(shader)` to get information about the error. + var infoLog = GL.GetShaderInfoLog(shaderProgram); + _logger.LogError("Error occurred whilst compiling Shader({Shader}).\n\n{Log}", shaderProgram, infoLog); + + return false; + } + + return true; + } + + private bool LinkProgram(uint program) + { + GL.LinkProgram(program); + GL.GetProgram(program, GLEnum.LinkStatus, out var statusCode); + + if (statusCode != (int)GLEnum.True) + { + string infoLog = GL.GetProgramInfoLog(program); + _logger.LogError("Error occurred whilst linking Program({Program}): {Info}", program, infoLog); + + return false; + } + + return true; + } + + /// + /// Enables the shader program. + /// + public void Use() + => GL.UseProgram(Handle); + + /// + /// Checks if the shader attribute exists within the current shader. + /// + /// The name of the attribute that's being looked for. + /// Outputs the location of the attribute in the shader if found; otherwise -1. + /// If the attribute exists, ; otherwise, . + public bool TryGetAttribLocation(string attribName, out int location) + { + location = GL.GetAttribLocation(Handle, attribName); + if (location == ShaderAttributes.AttributeLocationNotFound) + { + _logger.LogWarning("Attribute '{Attribute}' not found in shader program.", attribName); + return false; + } + + return true; + } + + /// + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + return; + + if (disposing) + { + // TODO: Collect handles in a separate container with proper access to the shared GL context. + //_gl.DeleteProgram(Handle); + Handle = 0; + _uniformLocations.Clear(); + } + + disposedValue = true; + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj index f411581..bff77ad 100644 --- a/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj +++ b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj @@ -14,6 +14,7 @@ + diff --git a/SharpEngine.Core/Entities/UI/ShaderParameterAttribute.cs b/SharpEngine.Core/Entities/UI/ShaderParameterAttribute.cs new file mode 100644 index 0000000..8762a03 --- /dev/null +++ b/SharpEngine.Core/Entities/UI/ShaderParameterAttribute.cs @@ -0,0 +1,19 @@ +using SharpEngine.Core.Rendering; +using System; + +namespace SharpEngine.Core.Entities.UI; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public sealed class ShaderParameterAttribute : Attribute +{ + public string Name { get; } + public ParameterType Type { get; } + public UpdateFrequency Frequency { get; } + + public ShaderParameterAttribute(string name, ParameterType type = ParameterType.Unknown, UpdateFrequency frequency = UpdateFrequency.PerObject) + { + Name = name; + Type = type; + Frequency = frequency; + } +} \ No newline at end of file diff --git a/SharpEngine.Core/Rendering/ShaderParameter.cs b/SharpEngine.Core/Rendering/ShaderParameter.cs new file mode 100644 index 0000000..e4c059b --- /dev/null +++ b/SharpEngine.Core/Rendering/ShaderParameter.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace SharpEngine.Core.Rendering +{ + public enum ParameterType + { + Float, + Vec2, + Vec3, + Vec4, + Mat4, + Texture, + Unknown + } + + public enum UpdateFrequency + { + Static, + PerObject, + PerFrame + } + + public sealed class ParameterDescriptor + { + public string Name { get; } + public ParameterType Type { get; } + public UpdateFrequency Frequency { get; } + + public ParameterDescriptor(string name, ParameterType type, UpdateFrequency frequency = UpdateFrequency.PerObject) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Type = type; + Frequency = frequency; + } + } + + public sealed class ParameterInstance + { + public ParameterDescriptor Descriptor { get; } + + // Boxed value. Use typed accessors/helpers in ShaderProgram to avoid frequent casts. + private object _value; + public object Value => _value; + + public bool Dirty { get; private set; } + + // Used to avoid re-uploading per-frame params multiple times per frame + public int LastUploadedFrame { get; internal set; } = -1; + + public ParameterInstance(ParameterDescriptor descriptor, object initialValue = null) + { + Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); + _value = initialValue; + Dirty = true; // not uploaded yet + } + + public bool SetValue(object newValue, bool forceDirty = false) + { + if (!forceDirty && Equals(_value, newValue)) + return false; + + _value = newValue; + Dirty = true; + return true; + } + + public void ClearDirty() => Dirty = false; + } +} diff --git a/SharpEngine.Core/Shaders/DefaultShader.cs b/SharpEngine.Core/Shaders/DefaultShader.cs new file mode 100644 index 0000000..1338e87 --- /dev/null +++ b/SharpEngine.Core/Shaders/DefaultShader.cs @@ -0,0 +1,10 @@ +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +public class DefaultShader : Shader +{ + public DefaultShader(GL gl) : base(gl, _Resources.Default.VertexShader, _Resources.Default.FragmentShader, "default") { } + + // public override bool SetAttributes() => true; +} From 59f76894c8497c33597661c51ab390f1550bc79c Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 21 Jun 2026 16:11:57 +0300 Subject: [PATCH 06/10] #67 TEmp --- .../Tutorial 4.1 - Model Loading/Program.cs | 2 +- .../Properties/Shaders/Shader.cs | 41 +--- .../Properties/Shaders/ShaderExtensions.cs | 187 +----------------- SharpEngine.Core/Entities/GameObject.cs | 33 ++-- SharpEngine.Core/Shaders/ShaderService.cs | 2 +- 5 files changed, 26 insertions(+), 239 deletions(-) diff --git a/Examples/Tutorial 4.1 - Model Loading/Program.cs b/Examples/Tutorial 4.1 - Model Loading/Program.cs index 742d903..a3e0cad 100644 --- a/Examples/Tutorial 4.1 - Model Loading/Program.cs +++ b/Examples/Tutorial 4.1 - Model Loading/Program.cs @@ -82,7 +82,7 @@ private static void OnLoad() Gl = GL.GetApi(window); - Shader = new Shader(Gl, PathExtensions.GetAssemblyPath("shader2.vert"), Default.LightShader, "test").Initialize(); + Shader = new Shader(Gl, PathExtensions.GetAssemblyPath("shader2.vert"), Default.LightShader, "test"); Texture = new Texture(Gl, "silk.png"); var model = ObjLoaderFactory.Load(Gl, "Untitled2.obj"); diff --git a/SharpEngine.Core.Components/Properties/Shaders/Shader.cs b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs index 06e8322..6a9196e 100644 --- a/SharpEngine.Core.Components/Properties/Shaders/Shader.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs @@ -7,11 +7,8 @@ namespace SharpEngine.Core.Shaders; /// /// Represents a shader program. /// -public partial class Shader : IDisposable +public partial class Shader : ShaderProgram, IDisposable { - /// Gets the handle to the shader program. - public uint Handle { get; private set; } - /// Gets or sets the identifying name of the shader. public string Name { get; set; } @@ -21,12 +18,6 @@ public partial class Shader : IDisposable /// Gets or sets the path to the fragment shader file. public string FragPath { get; set; } - private readonly GL _gl; - private readonly ILogger _logger; - - private Dictionary _uniformLocations = []; - private bool disposedValue; - /// /// Initializes a new instance of . /// @@ -38,13 +29,10 @@ public partial class Shader : IDisposable /// The vertex shader full path. /// The fragment shader full path. /// The identifier name of the shader. - public Shader(GL gl, string vertPath, string fragPath, string name) + public Shader(GL gl, string vertPath, string fragPath, string name) : base(gl) { - _gl = gl; Name = name; - _logger = LoggingExtensions.CreateLogger(); - // There are several different types of shaders, but the only two you need for basic rendering are the vertex and fragment shaders. // The vertex shader is responsible for moving around vertices, and uploading that data to the fragment shader. // The vertex shader won't be too important here, but they'll be more important later. @@ -59,30 +47,7 @@ public Shader(GL gl, string vertPath, string fragPath, string name) VertPath = vertPath; FragPath = fragPath; - } - /// - protected virtual void Dispose(bool disposing) - { - if (disposedValue) - return; - - if (disposing) - { - // TODO: Collect handles in a separate container with proper access to the shared GL context. - //_gl.DeleteProgram(Handle); - Handle = 0; - _uniformLocations.Clear(); - } - - disposedValue = true; - } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + Initialize(vertPath, fragPath); } } diff --git a/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs index f9fb638..b80cbbe 100644 --- a/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs @@ -10,177 +10,6 @@ namespace SharpEngine.Core.Shaders; /// public partial class Shader { - /// - /// Initializes and compiles vertex and fragment shaders into a shader program. - /// - /// The current instance after initialization. - public Shader Initialize() - { - // Load and compile shader - if (!LoadShader(ShaderType.VertexShader, VertPath, out uint vertexShader)) - { - _logger.LogError("Unable to load vertex shader."); - return this; - } - - if (!LoadShader(ShaderType.FragmentShader, FragPath, out uint fragmentShader)) - { - _logger.LogError("Unable to load fragment shader."); - return this; - } - - // These two shaders must then be merged into a shader program, which can then be used by OpenGL. - // To do this, create a program... - - Handle = _gl.CreateProgram(); - - // Attach both shaders... - _gl.AttachShader(Handle, vertexShader); - _gl.AttachShader(Handle, fragmentShader); - - // And then link them together. - bool shaderLinked = LinkProgram(Handle); - - // When the shader program is linked, it no longer needs the individual shaders attached to it; the compiled code is copied into the shader program. - // Detach them, and then delete them. - _gl.DetachShader(Handle, vertexShader); - _gl.DetachShader(Handle, fragmentShader); - _gl.DeleteShader(fragmentShader); - _gl.DeleteShader(vertexShader); - - if (!shaderLinked) - { - _logger.LogInformation("Unable to link shader program."); - return this; - } - - // The shader is now ready to go, but first, we're going to cache all the shader uniform locations. - // Querying this from the shader is very slow, so we do it once on initialization and reuse those values - // later. - SetUniformLocations(); - - return this; - } - - private bool LoadShader(ShaderType shaderType, string shaderPath, out uint shader) - { - if (!File.Exists(shaderPath)) - { - _logger.LogInformation("Shader file not found: {Path}", shaderPath); - - shader = 0; - return false; - } - - string shaderSource = File.ReadAllText(shaderPath); - shaderSource = ProcessIncludes(shaderSource, Path.GetDirectoryName(shaderPath)!); - - // GL.CreateShader will create an empty shader (obviously). The ShaderType enum denotes which type of shader will be created. - shader = _gl.CreateShader(shaderType); - _gl.ShaderSource(shader, shaderSource); - - if (!CompileShader(shader)) - { - _logger.LogInformation("Unable to load {Type} shader from '{Path}'.", shaderType, shaderPath); - return false; - } - - return true; - } - - private void SetUniformLocations() - { - // First, we have to get the number of active uniforms in the shader. - _gl.GetProgram(Handle, GLEnum.ActiveUniforms, out var numberOfUniforms); - - Dictionary uniformLocations = []; - - // Loop over all the uniforms, - for (uint i = 0; i < numberOfUniforms; i++) - { - // get the name of this uniform, - var key = _gl.GetActiveUniform(Handle, i, out _, out _); - - // get the location, - var location = _gl.GetUniformLocation(Handle, key); - - // and then add it to the dictionary. - uniformLocations.Add(key, location); - } - - _uniformLocations = uniformLocations; - } - - private static string ProcessIncludes(string shaderCode, string directory) - { - const string includePattern = @"#include\s+""(.+?)"""; - return Regex.Replace(shaderCode, includePattern, match => - { - string includePath = Path.Combine(directory, match.Groups[1].Value); - string includeCode = File.ReadAllText(includePath); - return ProcessIncludes(includeCode, Path.GetDirectoryName(includePath)!); - }, RegexOptions.NonBacktracking); - } - - private bool CompileShader(uint shader) - { - // Try to compile the shader - _gl.CompileShader(shader); - - // Check for compilation errors - _gl.GetShader(shader, GLEnum.CompileStatus, out var statusCode); - if (statusCode != (int)GLEnum.True) - { - // We can use `GL.GetShaderInfoLog(shader)` to get information about the error. - var infoLog = _gl.GetShaderInfoLog(shader); - _logger.LogError("Error occurred whilst compiling Shader({Shader}).\n\n{Log}", shader, infoLog); - - return false; - } - - return true; - } - - private bool LinkProgram(uint program) - { - _gl.LinkProgram(program); - _gl.GetProgram(program, GLEnum.LinkStatus, out var statusCode); - - if (statusCode != (int)GLEnum.True) - { - string infoLog = _gl.GetProgramInfoLog(program); - _logger.LogError("Error occurred whilst linking Program({Program}): {Info}", program, infoLog); - - return false; - } - - return true; - } - - /// - /// Enables the shader program. - /// - public void Use() - => _gl.UseProgram(Handle); - - /// - /// Checks if the shader attribute exists within the current shader. - /// - /// The name of the attribute that's being looked for. - /// Outputs the location of the attribute in the shader if found; otherwise -1. - /// If the attribute exists, ; otherwise, . - public bool TryGetAttribLocation(string attribName, out int location) - { - location = _gl.GetAttribLocation(Handle, attribName); - if (location == ShaderAttributes.AttributeLocationNotFound) - { - _logger.LogWarning("Attribute '{Attribute}' not found in shader program.", attribName); - return false; - } - - return true; - } - // Uniform setters // Uniforms are variables that can be set by user code, instead of reading them from the VBO. // You use VBOs for vertex-related data, and uniforms for almost everything else. @@ -198,8 +27,8 @@ private bool TrySetUniform(string uniformName, T data, Action setter) return false; } - // TODO: #95 The _gl.UseProgram should be not be called here. Rather the renderer should call it once before rendering. - _gl.UseProgram(Handle); + // TODO: #95 The GL.UseProgram should be not be called here. Rather the renderer should call it once before rendering. + GL.UseProgram(Handle); setter(location, data); return true; @@ -211,7 +40,7 @@ private bool TrySetUniform(string uniformName, T data, Action setter) /// The name of the uniform. /// The data to set. public bool SetInt(string name, int data) - => TrySetUniform(name, data, _gl.Uniform1); + => TrySetUniform(name, data, GL.Uniform1); /// /// Set a uniform float on this shader. @@ -219,7 +48,7 @@ public bool SetInt(string name, int data) /// The name of the uniform. /// The data to set. public bool SetFloat(string name, float data) - => TrySetUniform(name, data, _gl.Uniform1); + => TrySetUniform(name, data, GL.Uniform1); /// /// Set a uniform Matrix4 on this shader @@ -233,7 +62,7 @@ public bool SetFloat(string name, float data) /// /// public bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true) - => TrySetUniform(name, data, (uniform, d) => _gl.UniformMatrix4(uniform, transpose, d.ToSpan())); + => TrySetUniform(name, data, (uniform, d) => GL.UniformMatrix4(uniform, transpose, d.ToSpan())); /// /// Set a uniform Vector2 on this shader. @@ -241,7 +70,7 @@ public bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true) /// The name of the uniform. /// The data to set. public bool SetVector2(string name, Vector2 data) - => TrySetUniform(name, data, _gl.Uniform2); + => TrySetUniform(name, data, GL.Uniform2); /// /// Set a uniform Vector3 on this shader. @@ -249,7 +78,7 @@ public bool SetVector2(string name, Vector2 data) /// The name of the uniform. /// The data to set. public bool SetVector3(string name, Vector3 data) - => TrySetUniform(name, data, _gl.Uniform3); + => TrySetUniform(name, data, GL.Uniform3); /// /// Set a uniform Vector3 on this shader. @@ -257,5 +86,5 @@ public bool SetVector3(string name, Vector3 data) /// The name of the uniform. /// The data to set. public bool SetVector4(string name, Vector4 data) - => TrySetUniform(name, data, _gl.Uniform4); + => TrySetUniform(name, data, GL.Uniform4); } diff --git a/SharpEngine.Core/Entities/GameObject.cs b/SharpEngine.Core/Entities/GameObject.cs index 1b52fea..d164efd 100644 --- a/SharpEngine.Core/Entities/GameObject.cs +++ b/SharpEngine.Core/Entities/GameObject.cs @@ -28,31 +28,21 @@ public class GameObject : EmptyNode, IRenderable private readonly object _modelCacheLock = new(); private readonly Dictionary _modelByShareGroup = []; - private readonly string _shaderVertPath; - private readonly string _shaderFragPath; - private readonly string _shaderName; + private string _shaderVertPath; + private string _shaderFragPath; + private string _shaderName; /// /// Initializes a new instance of the . /// - public GameObject() : base(string.Empty) - { - BoundingBox = BoundingBox.CalculateBoundingBox(Transform); - - _shaderVertPath = _Resources.Default.VertexShader; - _shaderFragPath = _Resources.Default.FragmentShader; - _shaderName = "lighting"; - } + public GameObject() : this(model: null!) { } /// /// Initializes a new instance of the class with a model. /// /// The model. - public GameObject(Model model) : base(string.Empty) + public GameObject(Model model) : this(model, shader: null!) { - Model = model; - BoundingBox = BoundingBox.CalculateBoundingBox(Transform); - _shaderVertPath = _Resources.Default.VertexShader; _shaderFragPath = _Resources.Default.FragmentShader; _shaderName = "lighting"; @@ -61,17 +51,20 @@ public GameObject(Model model) : base(string.Empty) /// /// Initializes a new instance of the with specified textures and shaders. /// - /// The shader to be used by the game object. /// The model of the game object. - public GameObject(Shader shader, Model model) : base(string.Empty) + /// The shader to be used by the game object. + public GameObject(Model model, Shader shader) : base(string.Empty) { Model = model; BoundingBox = BoundingBox.CalculateBoundingBox(Transform); Shader = shader; - _shaderVertPath = shader.VertPath; - _shaderFragPath = shader.FragPath; - _shaderName = shader.Name; + if (shader is not null) + { + _shaderVertPath = shader.VertPath; + _shaderFragPath = shader.FragPath; + _shaderName = shader.Name; + } } /// diff --git a/SharpEngine.Core/Shaders/ShaderService.cs b/SharpEngine.Core/Shaders/ShaderService.cs index 74fee61..be99f9c 100644 --- a/SharpEngine.Core/Shaders/ShaderService.cs +++ b/SharpEngine.Core/Shaders/ShaderService.cs @@ -121,7 +121,7 @@ public Shader LoadShader(GL gl, object shareGroupKey, string vertPath, string fr // Create a new shader instance and add it to the cache. // Shader program objects are shareable across contexts *only* when those contexts share. - var shader = new Shader(gl, vertPath, fragPath, name).Initialize(); + var shader = new Shader(gl, vertPath, fragPath, name); _shaderCache[cacheKey] = shader; HasShadersToLoad = true; From 15c9ec4f6c8f6190212e13abcaa951ba1f5990f1 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 21 Jun 2026 16:19:51 +0300 Subject: [PATCH 07/10] #67 ShaderProgram inheritance --- Examples/MinecraftClone/Minecraft.cs | 10 +++---- .../Properties/Shaders/ShaderProgram.cs | 16 ++++++++++ SharpEngine.Core/Entities/UI/UIElement.cs | 30 +++++++++---------- SharpEngine.Core/Renderers/Renderer.cs | 2 +- SharpEngine.Core/Shaders/LampShader.cs | 20 ++++--------- SharpEngine.Core/Shaders/LightingShader.cs | 16 ++-------- SharpEngine.Core/Shaders/ShaderBase.cs | 18 ----------- SharpEngine.Core/Shaders/UIShader.cs | 17 ++--------- 8 files changed, 45 insertions(+), 84 deletions(-) diff --git a/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs index 1b347e2..e423051 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -75,21 +75,21 @@ public override void Initialize() _lightsNode = _scene.Root.AddChild("lights"); _blocksNode = _scene.Root.AddChild("blocks"); - var gridLayout = new GridLayout(); + // var gridLayout = new GridLayout(); // TODO: #89 Fix UI renderer - _uiElem = new UIElement("uiElement"); + _uiElem = new UIElement(Window.GetGL(), "uiElement"); _scene.UIElements.Add(_uiElem); - var uiElem2 = new UIElement("uiElement"); + var uiElem2 = new UIElement(Window.GetGL(), "uiElement"); uiElem2.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(0.2f, 0.2f); uiElem2.Transform.Position = new SharpEngine.Core.Numerics.Vector2(30, 0); - gridLayout.AddChild(_uiElem, uiElem2); + // gridLayout.AddChild(_uiElem, uiElem2); _scene.UIElements.Add(_uiElem); _scene.UIElements.Add(uiElem2); - _scene.UIElements.Add(gridLayout); + // _scene.UIElements.Add(gridLayout); InitializeWorld(); } diff --git a/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs index b0ce786..10bcdc0 100644 --- a/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs @@ -17,6 +17,17 @@ public abstract class ShaderProgram : IDisposable /// Gets the handle to the shader program. public uint Handle { get; protected set; } + /// Gets or sets the vertex array object. + public uint Vao { get; set; } + + /// + /// Sets the attributes for the shader. + /// + /// + /// if the attributes were set successfully; otherwise . + /// + public virtual bool SetAttributes(GL gl) => true; + protected ShaderProgram(GL gl) { GL = gl; @@ -70,6 +81,11 @@ protected void Initialize(string vertPath, string fragPath) // later. SetUniformLocations(Handle); + Vao = GL.GenVertexArray(); + GL.BindVertexArray(Vao); + + SetAttributes(GL); + return; } diff --git a/SharpEngine.Core/Entities/UI/UIElement.cs b/SharpEngine.Core/Entities/UI/UIElement.cs index 50d498a..f78a597 100644 --- a/SharpEngine.Core/Entities/UI/UIElement.cs +++ b/SharpEngine.Core/Entities/UI/UIElement.cs @@ -25,19 +25,20 @@ public class UIElement : EmptyNode, IRenderable /// /// Initializes a new instance of . /// - public UIElement() : this("UIElement") { } + public UIElement(GL gl) : this(gl, "UIElement") { } /// /// Initializes a new instance of . /// /// The name of the UI element. - public UIElement(string name) : base(name) + public UIElement(GL gl, string name) : base(name) { // TODO: #5 Support custom meshes? Mesh = MeshService.Instance.LoadMesh(nameof(Primitives.Plane), Primitives.Plane.Mesh); + _uiShader = new UIShader(gl); } - private readonly UIShader _uiShader = new(); + private readonly UIShader _uiShader; private readonly Texture _texture = TextureService.Instance.LoadTexture(Default.DebugTexture); /// Gets or sets the width of the ui element. @@ -111,8 +112,7 @@ private ContextState EnsureContextState(GL gl, Window window, SharedBuffers shar gl.BindBuffer(GLEnum.ArrayBuffer, sharedBuffers.Vbo); gl.BindBuffer(GLEnum.ElementArrayBuffer, sharedBuffers.Ebo); - _uiShader.EnsureInitialized(window); - _uiShader.Shader!.Use(); + _uiShader.Use(); _uiShader.SetAttributes(gl); state = new ContextState(vao); @@ -131,12 +131,10 @@ public override Task Render(CameraView camera, Window window) { var gl = window.GetGL(); - _uiShader.EnsureInitialized(window); - var sharedBuffers = EnsureSharedBuffers(gl, window); var contextState = EnsureContextState(gl, window, sharedBuffers); - _uiShader.Shader!.Use(); + _uiShader.Use(); gl.BindVertexArray(contextState.Vao); _texture.Use(TextureUnit.Texture0); @@ -146,14 +144,14 @@ public override Task Render(CameraView camera, Window window) const float screenWidth = 1280; const float screenHeight = 720; - _uiShader.Shader!.SetFloat("width", Width); - _uiShader.Shader!.SetFloat("height", Height); - _uiShader.Shader!.SetVector2("screenSize", new System.Numerics.Vector2(screenWidth, screenHeight)); - _uiShader.Shader!.SetVector2("position", (System.Numerics.Vector2)Transform.Position); - _uiShader.Shader!.SetFloat("rotation", Math.DegreesToRadians(Transform.Rotation.Angle)); - _uiShader.Shader!.SetInt("texture1", 0); - _uiShader.Shader!.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); - _uiShader.Shader!.SetMatrix4("orthoMatrix", OrthoMatrix); // Pass the orthographic matrix to the shader + _uiShader.SetFloat("width", Width); + _uiShader.SetFloat("height", Height); + _uiShader.SetVector2("screenSize", new System.Numerics.Vector2(screenWidth, screenHeight)); + _uiShader.SetVector2("position", (System.Numerics.Vector2)Transform.Position); + _uiShader.SetFloat("rotation", Math.DegreesToRadians(Transform.Rotation.Angle)); + _uiShader.SetInt("texture1", 0); + _uiShader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); + _uiShader.SetMatrix4("orthoMatrix", OrthoMatrix); // Pass the orthographic matrix to the shader gl.DrawElements(PrimitiveType.Triangles, (uint)sharedBuffers.IndexCount, DrawElementsType.UnsignedInt, []); diff --git a/SharpEngine.Core/Renderers/Renderer.cs b/SharpEngine.Core/Renderers/Renderer.cs index 873a359..67249b6 100644 --- a/SharpEngine.Core/Renderers/Renderer.cs +++ b/SharpEngine.Core/Renderers/Renderer.cs @@ -79,7 +79,7 @@ public override Task Render() _gl.Enable(EnableCap.Blend); _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - _camera.SetShaderUniforms(_lightingShader.Shader!); + _camera.SetShaderUniforms(_lightingShader); _gl.BindVertexArray(_lightingShader.Vao); var lightRenderTasks = _scene.IterateAsync(_scene.Root.Children, RenderLight); diff --git a/SharpEngine.Core/Shaders/LampShader.cs b/SharpEngine.Core/Shaders/LampShader.cs index f7ce4e1..b95f1a2 100644 --- a/SharpEngine.Core/Shaders/LampShader.cs +++ b/SharpEngine.Core/Shaders/LampShader.cs @@ -5,24 +5,14 @@ namespace SharpEngine.Core.Shaders; -internal class LampShader : ShaderBase +internal class LampShader : Shader { - private readonly GL _gl; - /// /// Initializes a new instance of using the provided OpenGL context. /// /// The OpenGL context to use for shader and VAO creation. - public LampShader(GL gl) + public LampShader(GL gl) : base(gl, Default.VertexShader, Default.LightShader, nameof(LampShader)) { - _gl = gl; - - Shader = ShaderService.Instance.LoadShader(_gl, Default.VertexShader, Default.LightShader, "lamp"); - - Vao = _gl.GenVertexArray(); - _gl.BindVertexArray(Vao); - - SetAttributes(_gl); } /// @@ -31,12 +21,12 @@ public override bool SetAttributes(GL gl) if (!base.SetAttributes(gl)) return false; - if (!Shader!.TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) + if (!TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) return false; var positionLocationUint = (uint)positionLocation; - _gl.EnableVertexAttribArray(positionLocationUint); - _gl.VertexAttribPointer(positionLocationUint, 3, VertexAttribPointerType.Float, false, VertexData.Stride, 0); + GL.EnableVertexAttribArray(positionLocationUint); + GL.VertexAttribPointer(positionLocationUint, 3, VertexAttribPointerType.Float, false, VertexData.Stride, 0); return true; } diff --git a/SharpEngine.Core/Shaders/LightingShader.cs b/SharpEngine.Core/Shaders/LightingShader.cs index 55b5e5e..4681f5b 100644 --- a/SharpEngine.Core/Shaders/LightingShader.cs +++ b/SharpEngine.Core/Shaders/LightingShader.cs @@ -4,25 +4,13 @@ namespace SharpEngine.Core.Shaders; -internal class LightingShader : ShaderBase +internal class LightingShader : Shader { - private readonly GL _gl; - /// /// Initializes a new instance of . /// /// The OpenGL context used to create and configure the shader program. - public LightingShader(GL gl) - { - _gl = gl; - - Shader = ShaderService.Instance.LoadShader(_gl, Default.VertexShader, Default.FragmentShader, "lighting"); - - Vao = _gl.GenVertexArray(); - _gl.BindVertexArray(Vao); - - SetAttributes(_gl); - } + public LightingShader(GL gl) : base(gl, Default.VertexShader, Default.FragmentShader, nameof(LightingShader)) { } /// public override bool SetAttributes(GL gl) diff --git a/SharpEngine.Core/Shaders/ShaderBase.cs b/SharpEngine.Core/Shaders/ShaderBase.cs index 492217a..4c72847 100644 --- a/SharpEngine.Core/Shaders/ShaderBase.cs +++ b/SharpEngine.Core/Shaders/ShaderBase.cs @@ -13,23 +13,5 @@ public abstract class ShaderBase /// Gets the shader. public Shader? Shader { get; protected set; } - /// Gets or sets the vertex array object. - public uint Vao { get; set; } - /// - /// Sets the attributes for the shader. - /// - /// - /// if the attributes were set successfully; otherwise . - /// - public virtual bool SetAttributes(GL gl) - { - if (Shader is null) - { - Logger.LogError("Unable to set shader attributes, shader not found."); - return false; - } - - return true; - } } diff --git a/SharpEngine.Core/Shaders/UIShader.cs b/SharpEngine.Core/Shaders/UIShader.cs index 50e80a6..3a4cb2d 100644 --- a/SharpEngine.Core/Shaders/UIShader.cs +++ b/SharpEngine.Core/Shaders/UIShader.cs @@ -13,22 +13,9 @@ namespace SharpEngine.Core.Shaders; /// This shader is responsible for rendering 2D UI components on the screen, such as buttons, panels, and other interface elements. /// It is designed to work with the specific vertex and fragment shaders defined for UI rendering in the game engine. /// -internal class UIShader : ShaderBase +internal class UIShader : Shader { - /// - /// Ensures that the shader is initialized. - /// - /// - /// This method should be called before using the shader to ensure that it is properly loaded and ready for use. - /// - /// The window where the shader will be used. - public void EnsureInitialized(Window window) - { - if (Shader is not null) - return; - - Shader = ShaderService.Instance.LoadShader(window, Default.UIVertexShader, Default.UIFragmentShader, nameof(UIShader)); - } + public UIShader(GL gl) : base(gl, Default.UIVertexShader, Default.UIFragmentShader, nameof(UIShader)) { } /// public override bool SetAttributes(GL gl) From 6d425492153c4d84a9ffd113155fd0eefacfdb1b Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 21 Jun 2026 16:45:05 +0300 Subject: [PATCH 08/10] #67 Progress --- SharpEngine.Core/Entities/GameObject.cs | 14 ++++++++------ SharpEngine.Core/Renderers/Renderer.cs | 11 ++++++++--- SharpEngine.Core/Shaders/ShaderBase.cs | 17 ----------------- .../UI => Shaders}/ShaderParameterAttribute.cs | 2 +- 4 files changed, 17 insertions(+), 27 deletions(-) delete mode 100644 SharpEngine.Core/Shaders/ShaderBase.cs rename SharpEngine.Core/{Entities/UI => Shaders}/ShaderParameterAttribute.cs (93%) diff --git a/SharpEngine.Core/Entities/GameObject.cs b/SharpEngine.Core/Entities/GameObject.cs index d164efd..6af78b5 100644 --- a/SharpEngine.Core/Entities/GameObject.cs +++ b/SharpEngine.Core/Entities/GameObject.cs @@ -1,22 +1,21 @@ using SharpEngine.Core.Attributes; +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.Entities.Views; -using SharpEngine.Core.Components.Properties; -using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Interfaces; using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; -using Shader = SharpEngine.Core.Shaders.Shader; -using EngineTexture = SharpEngine.Core.Components.Properties.Textures.Texture; - using Silk.NET.OpenGL; -using System.Threading.Tasks; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using EngineTexture = SharpEngine.Core.Components.Properties.Textures.Texture; +using Shader = SharpEngine.Core.Shaders.Shader; namespace SharpEngine.Core.Entities; @@ -43,6 +42,9 @@ public GameObject() : this(model: null!) { } /// The model. public GameObject(Model model) : this(model, shader: null!) { + // Ensure this instance is using a shader compiled for the correct context/share-group. + // Shader = ShaderService.Instance.LoadShader(Window.MainWindow, _shaderVertPath, _shaderFragPath, _shaderName); + _shaderVertPath = _Resources.Default.VertexShader; _shaderFragPath = _Resources.Default.FragmentShader; _shaderName = "lighting"; diff --git a/SharpEngine.Core/Renderers/Renderer.cs b/SharpEngine.Core/Renderers/Renderer.cs index 67249b6..55c0046 100644 --- a/SharpEngine.Core/Renderers/Renderer.cs +++ b/SharpEngine.Core/Renderers/Renderer.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using SharpEngine.Core.Entities; using SharpEngine.Core.Entities.Lights; using SharpEngine.Core.Entities.Properties; @@ -6,10 +7,8 @@ using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; - -using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; using Silk.NET.OpenGL; - using System; using System.Numerics; using System.Threading.Tasks; @@ -54,6 +53,12 @@ public Renderer(CameraView camera, ISettings settings, Scene scene, ILogger + /// Initializes a new instance of . + /// + public Renderer(CameraView camera, ISettings settings, Scene scene) + : this(camera, settings, scene, LoggingExtensions.CreateLogger()) { } + /// protected override void OnWindowAttached(Window window) { diff --git a/SharpEngine.Core/Shaders/ShaderBase.cs b/SharpEngine.Core/Shaders/ShaderBase.cs deleted file mode 100644 index 4c72847..0000000 --- a/SharpEngine.Core/Shaders/ShaderBase.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.Logging; -using Silk.NET.OpenGL; - -namespace SharpEngine.Core.Shaders; - -/// -/// Represents the base class for all shaders. -/// -public abstract class ShaderBase -{ - private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); - - /// Gets the shader. - public Shader? Shader { get; protected set; } - - -} diff --git a/SharpEngine.Core/Entities/UI/ShaderParameterAttribute.cs b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs similarity index 93% rename from SharpEngine.Core/Entities/UI/ShaderParameterAttribute.cs rename to SharpEngine.Core/Shaders/ShaderParameterAttribute.cs index 8762a03..73a60a3 100644 --- a/SharpEngine.Core/Entities/UI/ShaderParameterAttribute.cs +++ b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs @@ -1,7 +1,7 @@ using SharpEngine.Core.Rendering; using System; -namespace SharpEngine.Core.Entities.UI; +namespace SharpEngine.Core.Shaders; [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] public sealed class ShaderParameterAttribute : Attribute From f0db2f718cfd44f47efe33a14eaebbda466597de Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 12:02:59 +0300 Subject: [PATCH 09/10] #67 Remove duplicates and cleanup --- .../MaterialLoader/MaterialLibraryLoader.cs | 18 +- .../Properties/Material.cs | 99 ++--- .../Properties/Meshes/Mesh.cs | 37 ++ .../Properties/Meshes/Model.cs | 101 +++-- .../Properties/Shaders/IShader.cs | 50 +++ .../Properties/Shaders/Shader.cs | 2 +- .../Properties/Shaders/ShaderExtensions.cs | 15 + .../Properties/Shaders/ShaderProgram.cs | 3 + .../Properties/Transform2D.cs | 5 +- .../SharpEngine.Core.Components.csproj | 6 + SharpEngine.Core/Entities/GameObject.cs | 140 ++----- .../Entities/Lights/DirectionalLight.cs | 2 +- SharpEngine.Core/Entities/Lights/Light.cs | 2 +- .../Entities/Lights/PointLight.cs | 7 +- SharpEngine.Core/Entities/Lights/SpotLight.cs | 5 +- SharpEngine.Core/Entities/UI/MeshRenderer.cs | 25 ++ SharpEngine.Core/Entities/UI/UIElement.cs | 150 ++------ SharpEngine.Core/Primitives/Plane.cs | 38 +- .../Primitives/PrimitiveFactory.cs | 5 +- .../Rendering/ParameterDescriptor.cs | 18 + SharpEngine.Core/Rendering/ShaderParameter.cs | 33 -- .../Rendering/ShaderParameterBinder.cs | 188 +++++++++ .../Rendering/ShaderParameterType.cs | 14 + .../Rendering/ShaderUpdateFrequency.cs | 9 + SharpEngine.Core/Scenes/Scene.cs | 4 +- .../Shaders/ShaderParameterAttribute.cs | 6 +- SharpEngine.Core/Shaders/ShaderService.cs | 5 +- SharpEngine.Core/Windowing/SilkWindow.cs | 2 +- SharpEngine.Core/Windowing/Window.cs | 6 +- .../Windowing/WindowExtensions.cs | 12 + .../Meshes/ModelTests.cs | 98 +++++ .../SharpEngine.Core.Components.Tests.csproj | 9 +- .../Shaders/ShaderParameterBinderTests.cs | 360 ++++++++++++++++++ .../SharpEngine.Core.Tests.csproj | 1 - 34 files changed, 1034 insertions(+), 441 deletions(-) create mode 100644 SharpEngine.Core.Components/Properties/Shaders/IShader.cs create mode 100644 SharpEngine.Core/Entities/UI/MeshRenderer.cs create mode 100644 SharpEngine.Core/Rendering/ParameterDescriptor.cs create mode 100644 SharpEngine.Core/Rendering/ShaderParameterBinder.cs create mode 100644 SharpEngine.Core/Rendering/ShaderParameterType.cs create mode 100644 SharpEngine.Core/Rendering/ShaderUpdateFrequency.cs create mode 100644 SharpEngine.Core/Windowing/WindowExtensions.cs create mode 100644 Tests/SharpEngine.Core.Components.Tests/Meshes/ModelTests.cs create mode 100644 Tests/SharpEngine.Core.Tests/Shaders/ShaderParameterBinderTests.cs diff --git a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs index e2dcb91..9861031 100644 --- a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs +++ b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs @@ -54,20 +54,20 @@ public MaterialLibraryLoader(IMaterialDataStore dataStore, IFileStreamFactory fi AddParseAction("illum", i => _currentMaterial.IlluminationModel = i.ParseInvariantInt()); - AddParseAction("map_Ka", m => _currentMaterial.AmbientTextureMap = m); - AddParseAction("map_Kd", m => _currentMaterial.DiffuseTextureMap = m); + AddParseAction("map_Ka", m => _currentMaterial.AmbientTextureMap = new TextureDto(m)); + AddParseAction("map_Kd", m => _currentMaterial.DiffuseMap = new TextureDto(m)); - AddParseAction("map_Ks", m => _currentMaterial.SpecularTextureMap = m); - AddParseAction("map_Ns", m => _currentMaterial.SpecularHighlightTextureMap = m); + AddParseAction("map_Ks", m => _currentMaterial.SpecularMap = new TextureDto(m)); + AddParseAction("map_Ns", m => _currentMaterial.SpecularHighlightTextureMap = new TextureDto(m)); - AddParseAction("map_d", m => _currentMaterial.AlphaTextureMap = m); + AddParseAction("map_d", m => _currentMaterial.AlphaTextureMap = new TextureDto(m)); - AddParseAction("map_bump", m => _currentMaterial.BumpMap = m); - AddParseAction("bump", m => _currentMaterial.BumpMap = m); + AddParseAction("map_bump", m => _currentMaterial.BumpMap = new TextureDto(m)); + AddParseAction("bump", m => _currentMaterial.BumpMap = new TextureDto(m)); - AddParseAction("disp", m => _currentMaterial.DisplacementMap = m); + AddParseAction("disp", m => _currentMaterial.DisplacementMap = new TextureDto(m)); - AddParseAction("decal", m => _currentMaterial.StencilDecalMap = m); + AddParseAction("decal", m => _currentMaterial.StencilDecalMap = new TextureDto(m)); } private void AddParseAction(string key, Action action) diff --git a/SharpEngine.Core.Components/Properties/Material.cs b/SharpEngine.Core.Components/Properties/Material.cs index b1a54b3..3028aa2 100644 --- a/SharpEngine.Core.Components/Properties/Material.cs +++ b/SharpEngine.Core.Components/Properties/Material.cs @@ -5,10 +5,31 @@ namespace SharpEngine.Core.Components.Properties; +public record TextureDto +{ + public TextureDto(string path, Texture? texture = null) + { + Path = path; + Texture = texture; + } + + public string Path { get; set; } + public Texture? Texture { get; set; } +} + +public enum TextureUnitIndex : int +{ + /// The texture unit for the diffuse map. + DIFFUSE_UNIT = 0, + + /// The texture unit for the specular map. + SPECULAR_UNIT = 1 +} + /// /// Represents the material rendered onto a game object. /// -public class Material : ICloneable, IEquatable +public class Material { /// /// Initializes a new instance of . @@ -25,8 +46,8 @@ public Material(string materialName) /// The specular map texture of the material. Defaults to the diffuse map if not provided. public Material(string materialName, Texture? diffuseMap = null, Texture? specularMap = null) { - DiffuseMap = diffuseMap; - SpecularMap = specularMap ?? diffuseMap; + DiffuseMap = new TextureDto(diffuseMap?.Path ?? string.Empty, diffuseMap); + SpecularMap = new TextureDto(specularMap?.Path ?? string.Empty, specularMap); Specular = new(0.5f, 0.5f, 0.5f); Shininess = 32.0f; @@ -42,25 +63,10 @@ public Material(string materialName, Texture? diffuseMap = null, Texture? specul public string Name { get; set; } /// Gets or sets the diffuse map texture. - public Texture? DiffuseMap { get; set; } - - /// Gets or sets the path to the diffuse texture map. - public string? DiffuseTextureMap { get; set; } + public TextureDto? DiffuseMap { get; set; } /// Gets or sets the specular map texture. - public Texture? SpecularMap { get; set; } - - /// Gets or sets the path to the specular texture map. - public string? SpecularTextureMap { get; set; } - - /// Gets a value indicating whether the material uses a specular map. - public bool UseSpecularMap => SpecularMap is not null && SpecularMap?.Handle != DiffuseMap?.Handle; - - /// The texture unit for the diffuse map. - public const int DIFFUSE_UNIT = 0; - - /// The texture unit for the specular map. - public const int SPECULAR_UNIT = 1; + public TextureDto? SpecularMap { get; set; } /// Gets or sets the specular color of the material. public Vector3 Specular { get; set; } @@ -87,22 +93,22 @@ public Material(string materialName, Texture? diffuseMap = null, Texture? specul public int IlluminationModel { get; set; } /// Gets or sets the path to the ambient texture map. - public string? AmbientTextureMap { get; set; } + public TextureDto? AmbientTextureMap { get; set; } /// Gets or sets the path to the specular highlight texture map. - public string? SpecularHighlightTextureMap { get; set; } + public TextureDto? SpecularHighlightTextureMap { get; set; } /// Gets or sets the path to the bump map. - public string? BumpMap { get; set; } + public TextureDto? BumpMap { get; set; } /// Gets or sets the path to the displacement map. - public string? DisplacementMap { get; set; } + public TextureDto? DisplacementMap { get; set; } /// Gets or sets the path to the stencil decal map. - public string? StencilDecalMap { get; set; } + public TextureDto? StencilDecalMap { get; set; } /// Gets or sets the path to the alpha texture map. - public string? AlphaTextureMap { get; set; } + public TextureDto? AlphaTextureMap { get; set; } /// /// Sets the uniform values for the material in the specified shader. @@ -114,48 +120,19 @@ public void SetUniformValues(Shader shader) if (DiffuseMap is not null) { - DiffuseMap.Use(TextureUnit.Texture0); - shader.SetInt("material.diffuse", DIFFUSE_UNIT); + DiffuseMap.Texture?.Use(TextureUnit.Texture0); + shader.SetTextureUnit("material.diffuse", TextureUnitIndex.DIFFUSE_UNIT); } if (SpecularMap is not null) { - SpecularMap.Use(TextureUnit.Texture1); - shader.SetInt("material.specular", SPECULAR_UNIT); - shader.SetFloat("material.shininess", Shininess); + SpecularMap.Texture?.Use(TextureUnit.Texture1); + shader.SetTextureUnit("material.specular", TextureUnitIndex.SPECULAR_UNIT); } else - { - shader.SetInt("material.specular", DIFFUSE_UNIT); - shader.SetFloat("material.shininess", 0); - } - } + shader.SetTextureUnit("material.specular", TextureUnitIndex.DIFFUSE_UNIT); - /// - public object Clone() - => MemberwiseClone(); + shader.SetFloat("material.shininess", Shininess); - /// - public override bool Equals(object? obj) - { - if (obj is null) - return false; - - if (ReferenceEquals(this, obj)) - return true; - - if (GetType() != obj.GetType()) - return false; - - return true; - } - - /// - public bool Equals(Material? other) => Equals(other); - - /// - public override int GetHashCode() - { - throw new NotImplementedException(); } } diff --git a/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs b/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs index bf63131..2d5a8c5 100644 --- a/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs +++ b/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs @@ -131,6 +131,20 @@ public Mesh(GL gl, float[] vertices, uint[] indices, List textures) SetupMesh(); } + /// + /// Initializes a new instance of the class with the specified OpenGL context, interleaved vertices, and indices. + /// + /// The OpenGL context. + /// The interleaved vertex data (position, normal, uv per vertex). + /// The indices of the mesh. + public Mesh(GL gl, float[] vertices, uint[] indices) + { + GL = gl; + Vertices = vertices; + Indices = indices; + SetupMesh(); + } + /// /// Initializes a new instance of the class with the specified OpenGL context. /// @@ -141,6 +155,21 @@ public Mesh(GL gl) SetupMesh(); } + /// + /// Disposes the existing GPU buffers and reinitializes them using the current and data. + /// + /// + /// Call this method after updating or on a mesh that was already + /// set up, for example after in-place processing via . + /// + public void ReinitializeGpuBuffers() + { + VAO.Dispose(); + VBO.Dispose(); + EBO.Dispose(); + SetupMesh(); + } + /// /// Allocates the required memory for the mesh and sets up the Vertex Array Object (VAO), Vertex Buffer Object (VBO), and Element Buffer Object (EBO). /// @@ -166,6 +195,14 @@ public void SetupMesh() public void Bind() => VAO.Bind(); + public void Draw() + { + if (Indices.Length > 0) + GL.DrawElements(PrimitiveType.Triangles, (uint)Indices.Length, DrawElementsType.UnsignedInt, []); + else + GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)(Vertices.Length / (VertexData.VerticesSize + VertexData.NormalsSize + VertexData.TexCoordsSize))); + } + /// protected virtual void Dispose(bool disposing) { diff --git a/SharpEngine.Core.Components/Properties/Meshes/Model.cs b/SharpEngine.Core.Components/Properties/Meshes/Model.cs index e5ba6b5..360b321 100644 --- a/SharpEngine.Core.Components/Properties/Meshes/Model.cs +++ b/SharpEngine.Core.Components/Properties/Meshes/Model.cs @@ -60,54 +60,43 @@ public void Dispose() } /// - /// Produces a GPU-ready Mesh from the provided mesh by cloning when already processed or by expanding vertices and constructing indices, materials, and textures. + /// Processes the provided mesh in place, updating its vertex data, materials, textures, and GPU buffers so it is ready for rendering. /// - /// Source mesh to process. Must not be null. + /// The mesh to process. Must not be null. /// - /// A Mesh prepared for rendering: either a cloned processed mesh if the input already contains vertex buffer - /// data, or a newly constructed Mesh with expanded vertices, generated index buffer, and associated textures - /// and materials. + /// The same instance with updated , , + /// , , and reinitialized GPU buffers. + /// When the mesh already carries GPU-ready vertex data ( is non-empty) its + /// materials and textures are refreshed in place; otherwise vertices are expanded from face groups and all + /// data properties are computed and assigned. /// - /// Thrown if the input mesh is null. + /// Thrown if is null. public Mesh ProcessMesh(Mesh mesh) { ArgumentNullException.ThrowIfNull(mesh); - // TODO: Instead of cloning the object, we should just update the values. if (mesh.Vertices.Length > 0) - return CloneProcessedMesh(mesh); + { + var existingMaterials = mesh.Materials.Select(CloneMaterial).ToList(); + var existingTextures = (existingMaterials.Count > 0 ? BuildTextures(existingMaterials) : CloneTextures(mesh.Textures)).ToList(); + mesh.Materials = existingMaterials; + mesh.Textures = existingTextures; + mesh.ReinitializeGpuBuffers(); + return mesh; + } var vertices = ExpandVertices(mesh); var indices = Enumerable.Range(0, vertices.Count).Select(index => (uint)index).ToArray(); var materials = BuildMaterials(mesh).ToList(); var textures = BuildTextures(materials).ToList(); - // TODO: Instead of cloning the object, we should just update the values. - return new Mesh(_gl, BuildVertices(vertices), indices, textures) - { - Name = mesh.Name, - Vertices2 = vertices, - Normals2 = mesh.Normals2, - TextureCoordinates2 = mesh.TextureCoordinates2, - Groups = mesh.Groups, - Materials = materials, - }; - } - - private Mesh CloneProcessedMesh(Mesh template) - { - var materials = template.Materials.Select(CloneMaterial).ToList(); - var textures = materials.Count > 0 ? BuildTextures(materials) : CloneTextures(template.Textures); - - return new Mesh(_gl, template.Vertices, template.Indices, textures.ToList()) - { - Name = template.Name, - Vertices2 = template.Vertices2, - Normals2 = template.Normals2, - TextureCoordinates2 = template.TextureCoordinates2, - Groups = template.Groups, - Materials = materials, - }; + mesh.Vertices = BuildVertices(vertices); + mesh.Indices = indices; + mesh.Textures = textures; + mesh.Vertices2 = vertices; + mesh.Materials = materials; + mesh.ReinitializeGpuBuffers(); + return mesh; } private static List ExpandVertices(Mesh mesh) @@ -187,14 +176,14 @@ private IEnumerable BuildMaterials(Mesh mesh) private Material? CreateRuntimeMaterial(Material definition) { - if (string.IsNullOrWhiteSpace(definition.DiffuseTextureMap)) + if (string.IsNullOrWhiteSpace(definition.DiffuseMap?.Path)) return null; - var diffuseTexture = new EngineTexture(_gl, ResolveAssetPath(definition.DiffuseTextureMap), EngineTextureType.Diffuse); + var diffuseTexture = new EngineTexture(_gl, ResolveAssetPath(definition.DiffuseMap.Path), EngineTextureType.Diffuse); EngineTexture? specularTexture = null; - if (!string.IsNullOrWhiteSpace(definition.SpecularTextureMap)) - specularTexture = new EngineTexture(_gl, ResolveAssetPath(definition.SpecularTextureMap), EngineTextureType.Specular); + if (!string.IsNullOrWhiteSpace(definition.SpecularMap?.Path)) + specularTexture = new EngineTexture(_gl, ResolveAssetPath(definition.SpecularMap.Path), EngineTextureType.Specular); return new Material(definition.Name, diffuseTexture, specularTexture) { @@ -205,8 +194,8 @@ private IEnumerable BuildMaterials(Mesh mesh) SpecularCoefficient = definition.SpecularCoefficient, Transparency = definition.Transparency, IlluminationModel = definition.IlluminationModel, - DiffuseTextureMap = definition.DiffuseTextureMap, - SpecularTextureMap = definition.SpecularTextureMap, + DiffuseMap = definition.DiffuseMap, + SpecularMap = definition.SpecularMap, AmbientTextureMap = definition.AmbientTextureMap, SpecularHighlightTextureMap = definition.SpecularHighlightTextureMap, AlphaTextureMap = definition.AlphaTextureMap, @@ -222,12 +211,12 @@ private IEnumerable CloneTextures(IReadOnlyList te private Material CloneMaterial(Material material) { EngineTexture? diffuseTexture = null; - if (material.DiffuseMap != null) - diffuseTexture = new EngineTexture(_gl, material.DiffuseMap.Path, material.DiffuseMap.Type); - + if (material.DiffuseMap?.Texture is not null) + diffuseTexture = new EngineTexture(_gl, material.DiffuseMap.Path, material.DiffuseMap.Texture.Type); + EngineTexture? specularTexture = null; - if (material.UseSpecularMap) - specularTexture = new EngineTexture(_gl, material!.SpecularMap!.Path, material.SpecularMap.Type); + if (material.SpecularMap?.Texture is not null) + specularTexture = new EngineTexture(_gl, material.SpecularMap.Path, material.SpecularMap.Texture.Type); return new Material(material.Name, diffuseTexture, specularTexture) { @@ -238,8 +227,8 @@ private Material CloneMaterial(Material material) SpecularCoefficient = material.SpecularCoefficient, Transparency = material.Transparency, IlluminationModel = material.IlluminationModel, - DiffuseTextureMap = material.DiffuseTextureMap, - SpecularTextureMap = material.SpecularTextureMap, + DiffuseMap = material.DiffuseMap, + SpecularMap = material.SpecularMap, AmbientTextureMap = material.AmbientTextureMap, SpecularHighlightTextureMap = material.SpecularHighlightTextureMap, AlphaTextureMap = material.AlphaTextureMap, @@ -260,11 +249,11 @@ private static EngineTexture[] GetMaterialEngineTextures(Material material) ArgumentNullException.ThrowIfNull(material); var materials = new List(); - if (material.UseSpecularMap) - materials.Add(material.SpecularMap!); - - if (material.DiffuseMap != null) - materials.Add(material.DiffuseMap); + if (material.SpecularMap?.Texture is not null) + materials.Add(material.SpecularMap.Texture!); + + if (material.DiffuseMap?.Texture is not null) + materials.Add(material.DiffuseMap.Texture!); return [.. materials]; } @@ -280,7 +269,13 @@ private string ResolveAssetPath(string assetPath) assetPath : System.IO.Path.Combine(directory, assetPath); } - private static float[] BuildVertices(IEnumerable vertexCollection) + /// + /// Converts a collection of objects into an interleaved float array + /// (position XYZ, normal XYZ, texture-coordinate UV per vertex) suitable for uploading to a GPU vertex buffer. + /// + /// The vertices to convert. + /// A flat array with eight elements per vertex. + internal static float[] BuildVertices(IEnumerable vertexCollection) { var vertices = new List(); diff --git a/SharpEngine.Core.Components/Properties/Shaders/IShader.cs b/SharpEngine.Core.Components/Properties/Shaders/IShader.cs new file mode 100644 index 0000000..3c34378 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Shaders/IShader.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace SharpEngine.Core.Shaders; + +/// +/// Defines the contract for setting shader uniform parameters and querying their locations. +/// +public interface IShader +{ + /// Gets the uniform name-to-location map cached at shader initialization. + IReadOnlyDictionary UniformLocations { get; } + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetInt(string name, int data); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetFloat(string name, float data); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// Whether to transpose the matrix before uploading. Defaults to . + /// if the uniform was found and set; otherwise . + bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetVector2(string name, Vector2 data); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetVector3(string name, Vector3 data); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetVector4(string name, Vector4 data); +} diff --git a/SharpEngine.Core.Components/Properties/Shaders/Shader.cs b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs index 6a9196e..8bee87e 100644 --- a/SharpEngine.Core.Components/Properties/Shaders/Shader.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs @@ -7,7 +7,7 @@ namespace SharpEngine.Core.Shaders; /// /// Represents a shader program. /// -public partial class Shader : ShaderProgram, IDisposable +public partial class Shader : ShaderProgram, IShader, IDisposable { /// Gets or sets the identifying name of the shader. public string Name { get; set; } diff --git a/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs index b80cbbe..eaa5390 100644 --- a/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using SharpEngine.Core.Components.Properties; using Silk.NET.OpenGL; using System.Numerics; using System.Text.RegularExpressions; @@ -42,6 +43,9 @@ private bool TrySetUniform(string uniformName, T data, Action setter) public bool SetInt(string name, int data) => TrySetUniform(name, data, GL.Uniform1); + public bool SetTextureUnit(string name, TextureUnitIndex textureUnit) + => TrySetUniform(name, (int)textureUnit, GL.Uniform1); + /// /// Set a uniform float on this shader. /// @@ -64,6 +68,14 @@ public bool SetFloat(string name, float data) public bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true) => TrySetUniform(name, data, (uniform, d) => GL.UniformMatrix4(uniform, transpose, d.ToSpan())); + /// + /// Set a uniform Vector2 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetVector2(string name, SharpEngine.Core.Numerics.Vector2 data) + => TrySetUniform(name, (Vector2)data, GL.Uniform2); + /// /// Set a uniform Vector2 on this shader. /// @@ -80,6 +92,9 @@ public bool SetVector2(string name, Vector2 data) public bool SetVector3(string name, Vector3 data) => TrySetUniform(name, data, GL.Uniform3); + public bool SetVector3(string name, SharpEngine.Core.Numerics.Vector3 data) + => TrySetUniform(name, (Vector3)data, GL.Uniform3); + /// /// Set a uniform Vector3 on this shader. /// diff --git a/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs index 10bcdc0..2249dee 100644 --- a/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs @@ -20,6 +20,9 @@ public abstract class ShaderProgram : IDisposable /// Gets or sets the vertex array object. public uint Vao { get; set; } + /// Gets the uniform name-to-location map cached at shader initialization. + public IReadOnlyDictionary UniformLocations => _uniformLocations; + /// /// Sets the attributes for the shader. /// diff --git a/SharpEngine.Core.Components/Properties/Transform2D.cs b/SharpEngine.Core.Components/Properties/Transform2D.cs index 8a44d56..0809d3e 100644 --- a/SharpEngine.Core.Components/Properties/Transform2D.cs +++ b/SharpEngine.Core.Components/Properties/Transform2D.cs @@ -19,8 +19,11 @@ public class Transform2D : ITransform public Vector2 Scale { get; set; } = new(1, 1); /// - /// Gets or sets the rotation of the game object. + /// Gets or sets the quaternion rotation of the game object. /// + /// + /// To get the rotation angle in degrees, use . + /// public Quaternion Rotation { get; set; } = new() { Angle = 0 }; /// diff --git a/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj index bff77ad..99cc5f5 100644 --- a/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj +++ b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj @@ -23,4 +23,10 @@ + + + <_Parameter1>SharpEngine.Core.Components.Tests + + + diff --git a/SharpEngine.Core/Entities/GameObject.cs b/SharpEngine.Core/Entities/GameObject.cs index 6af78b5..99b0a14 100644 --- a/SharpEngine.Core/Entities/GameObject.cs +++ b/SharpEngine.Core/Entities/GameObject.cs @@ -3,6 +3,7 @@ using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Entities.UI; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; using SharpEngine.Core.Numerics; @@ -24,13 +25,13 @@ namespace SharpEngine.Core.Entities; /// public class GameObject : EmptyNode, IRenderable { + // TODO: Remove these two. + protected record struct TempShaderDataContainer(string shaderVertPath, string shaderFragPath, string shaderName); + protected readonly TempShaderDataContainer _tempShaderData; + private readonly object _modelCacheLock = new(); private readonly Dictionary _modelByShareGroup = []; - private string _shaderVertPath; - private string _shaderFragPath; - private string _shaderName; - /// /// Initializes a new instance of the . /// @@ -42,12 +43,7 @@ public GameObject() : this(model: null!) { } /// The model. public GameObject(Model model) : this(model, shader: null!) { - // Ensure this instance is using a shader compiled for the correct context/share-group. - // Shader = ShaderService.Instance.LoadShader(Window.MainWindow, _shaderVertPath, _shaderFragPath, _shaderName); - - _shaderVertPath = _Resources.Default.VertexShader; - _shaderFragPath = _Resources.Default.FragmentShader; - _shaderName = "lighting"; + // Shader = ShaderService.Instance.LoadShader(Window.SharedGL, _shaderVertPath, _shaderFragPath, _shaderName); } /// @@ -62,13 +58,20 @@ public GameObject(Model model, Shader shader) : base(string.Empty) Shader = shader; if (shader is not null) - { - _shaderVertPath = shader.VertPath; - _shaderFragPath = shader.FragPath; - _shaderName = shader.Name; - } + _tempShaderData = new TempShaderDataContainer(shader.VertPath, shader.FragPath, shader.Name); + else + _tempShaderData = new TempShaderDataContainer(_Resources.Default.VertexShader, _Resources.Default.FragmentShader, "lighting"); } + /// + /// Gets or sets the mesh renderer for this game object. + /// + /// + /// When set, takes precedence over the property during rendering. + /// TODO: #53 This should be coming from the entity component system. + /// + public MeshRenderer MeshRenderer { get; set; } + /// /// Gets or sets the shader of the game object. /// @@ -120,19 +123,13 @@ protected virtual void SetShaderUniforms(CameraView camera) public override Task Render(CameraView camera, Window window) { // TODO: This needs to removed later once fixed. - if (Model is null || Model.Meshes is null) + if (Model is null || Model.Meshes is null || !Model.Meshes.Any()) return Task.CompletedTask; - var glInstance = window.GetGL(); - - // Ensure this instance is using a shader compiled for the correct context/share-group. - Shader = ShaderService.Instance.LoadShader(window, _shaderVertPath, _shaderFragPath, _shaderName); - - var modelToRender = GetOrCreateModelForWindow(window, glInstance); - if (modelToRender is null) - return Task.CompletedTask; + if (Shader is null) + Shader = ShaderService.Instance.LoadShader(window, _tempShaderData.shaderVertPath, _tempShaderData.shaderFragPath, _tempShaderData.shaderName); - foreach (var mesh in modelToRender.Meshes) + foreach (var mesh in Model.Meshes) { mesh.Bind(); @@ -145,100 +142,9 @@ public override Task Render(CameraView camera, Window window) foreach (var material in mesh.Materials) material.SetUniformValues(Shader); - if (mesh.Indices.Length > 0) - glInstance.DrawElements(PrimitiveType.Triangles, (uint)mesh.Indices.Length, DrawElementsType.UnsignedInt, []); - else - glInstance.DrawArrays(PrimitiveType.Triangles, 0, (uint)(mesh.Vertices.Length / (VertexData.VerticesSize + VertexData.NormalsSize + VertexData.TexCoordsSize))); + mesh.Draw(); } return Task.CompletedTask; } - - private static object GetShareGroupKey(Window window) - => (object?)window.SharedContext ?? (object?)window.GLContext ?? window; - - private Model? GetOrCreateModelForWindow(Window window, GL gl) - { - if (Model is null) - return null; - - var shareGroupKey = GetShareGroupKey(window); - - lock (_modelCacheLock) - { - if (_modelByShareGroup.TryGetValue(shareGroupKey, out var cachedModel)) - return cachedModel; - - // If this model was created using the same GL instance, we can reuse it for this share group. - // Otherwise, build a copy of the model bound to the GL instance for this window. - var canReuseModel = Model.Meshes.Count > 0 && ReferenceEquals(Model.Meshes[0].GL, gl); - - var modelForWindow = canReuseModel ? Model : CloneModel(gl, Model); - _modelByShareGroup[shareGroupKey] = modelForWindow; - - return modelForWindow; - } - } - - private static Model CloneModel(GL gl, Model template) - { - // Create an empty model and populate it with meshes recreated against the provided GL instance. - // This allows rendering on windows that don't share the original context. - var clone = new Model(gl, string.Empty); - - foreach (var mesh in template.Meshes) - clone.Meshes.Add(CloneMesh(gl, mesh)); - - return clone; - } - - private static Mesh CloneMesh(GL gl, Mesh template) - { - var textures = template.Textures is null ? - new List() : - [.. template.Textures.Select(t => new EngineTexture(gl, t.Path, t.Type))]; - - var clone = new Mesh(gl, template.Vertices, template.Indices, textures) - { - Name = template.Name, - }; - - if (template.Materials is { Count: > 0 }) - clone.Materials = [.. template.Materials.Select(m => CloneMaterial(gl, m))]; - - return clone; - } - - private static Material CloneMaterial(GL gl, Material template) - { - ArgumentNullException.ThrowIfNull(template); - var diffuse = new EngineTexture(gl, template.DiffuseMap!.Path, template.DiffuseMap!.Type); - - EngineTexture? specular = null; - if (template.UseSpecularMap) - specular = new EngineTexture(gl, template.SpecularMap!.Path, template.SpecularMap!.Type); - - var clone = new Material(template.Name, diffuse, specular) - { - Name = template.Name, - DiffuseTextureMap = template.DiffuseTextureMap, - SpecularTextureMap = template.SpecularTextureMap, - Specular = template.Specular, - Shininess = template.Shininess, - AmbientColor = template.AmbientColor, - DiffuseColor = template.DiffuseColor, - SpecularColor = template.SpecularColor, - SpecularCoefficient = template.SpecularCoefficient, - Transparency = template.Transparency, - IlluminationModel = template.IlluminationModel, - AmbientTextureMap = template.AmbientTextureMap, - SpecularHighlightTextureMap = template.SpecularHighlightTextureMap, - BumpMap = template.BumpMap, - DisplacementMap = template.DisplacementMap, - StencilDecalMap = template.StencilDecalMap, - AlphaTextureMap = template.AlphaTextureMap, - }; - - return clone; - } } diff --git a/SharpEngine.Core/Entities/Lights/DirectionalLight.cs b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs index b37be9a..11f4d68 100644 --- a/SharpEngine.Core/Entities/Lights/DirectionalLight.cs +++ b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs @@ -1,9 +1,9 @@ using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; -using System.Numerics; using System.Threading.Tasks; namespace SharpEngine.Core.Entities.Lights; diff --git a/SharpEngine.Core/Entities/Lights/Light.cs b/SharpEngine.Core/Entities/Lights/Light.cs index dfd2585..d56422d 100644 --- a/SharpEngine.Core/Entities/Lights/Light.cs +++ b/SharpEngine.Core/Entities/Lights/Light.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using SharpEngine.Core.Numerics; namespace SharpEngine.Core.Entities.Lights; diff --git a/SharpEngine.Core/Entities/Lights/PointLight.cs b/SharpEngine.Core/Entities/Lights/PointLight.cs index 0b6ef21..0dec2fc 100644 --- a/SharpEngine.Core/Entities/Lights/PointLight.cs +++ b/SharpEngine.Core/Entities/Lights/PointLight.cs @@ -1,10 +1,9 @@ -using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; using System; -using System.Numerics; using System.Threading.Tasks; namespace SharpEngine.Core.Entities.Lights; @@ -59,7 +58,7 @@ protected override void SetShaderUniforms(CameraView camera) if (Shader == null) throw new NullReferenceException(nameof(Shader)); - Shader.SetVector3($"pointLights[{_index}].position", (Vector3)Transform.Position); + Shader.SetVector3($"pointLights[{_index}].position", Transform.Position); Shader.SetVector3($"pointLights[{_index}].ambient", Ambient); Shader.SetVector3($"pointLights[{_index}].diffuse", Diffuse); Shader.SetVector3($"pointLights[{_index}].specular", Specular); @@ -73,7 +72,7 @@ protected override void SetShaderUniforms(CameraView camera) /// public override Task Render(CameraView camera, Window window) { - Shader = ShaderService.Instance.LoadShader(window, Default.VertexShader, Default.FragmentShader, "lighting"); + Shader = ShaderService.Instance.LoadShader(window, _tempShaderData.shaderVertPath, _tempShaderData.shaderFragPath, _tempShaderData.shaderName); SetShaderUniforms(camera); return Task.CompletedTask; diff --git a/SharpEngine.Core/Entities/Lights/SpotLight.cs b/SharpEngine.Core/Entities/Lights/SpotLight.cs index 1da0b41..9c98412 100644 --- a/SharpEngine.Core/Entities/Lights/SpotLight.cs +++ b/SharpEngine.Core/Entities/Lights/SpotLight.cs @@ -1,4 +1,3 @@ -using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; @@ -82,9 +81,9 @@ protected override void SetShaderUniforms(CameraView camera) /// public override Task Render(CameraView camera, Window window) { - Shader = ShaderService.Instance.LoadShader(window, Default.VertexShader, Default.FragmentShader, "lighting"); + Shader = ShaderService.Instance.LoadShader(window, _tempShaderData.shaderVertPath, _tempShaderData.shaderFragPath, _tempShaderData.shaderName); SetShaderUniforms(camera); - + return Task.CompletedTask; } } diff --git a/SharpEngine.Core/Entities/UI/MeshRenderer.cs b/SharpEngine.Core/Entities/UI/MeshRenderer.cs new file mode 100644 index 0000000..7995074 --- /dev/null +++ b/SharpEngine.Core/Entities/UI/MeshRenderer.cs @@ -0,0 +1,25 @@ +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Entities.Properties.Meshes; + +namespace SharpEngine.Core.Entities.UI; + +public class MeshRenderer +{ + public MeshRenderer(Mesh mesh, Material material) + { + Mesh = mesh; + Material = material; + } + + // TODO: This should be the correct structure for the MeshRenderer, but for now we will keep these separate + public MeshRenderer(Model model, Material material) + { + Model = model; + Material = material; + } + + public Model Model { get; } + public Mesh Mesh { get; } + public Material Material { get; } +} \ No newline at end of file diff --git a/SharpEngine.Core/Entities/UI/UIElement.cs b/SharpEngine.Core/Entities/UI/UIElement.cs index f78a597..305ba60 100644 --- a/SharpEngine.Core/Entities/UI/UIElement.cs +++ b/SharpEngine.Core/Entities/UI/UIElement.cs @@ -2,18 +2,20 @@ using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Rendering; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; using SharpEngine.Core.Textures; using SharpEngine.Core.Windowing; using SharpEngine.Core._Resources; +using SharpEngine.Telemetry; using Vector2 = SharpEngine.Core.Numerics.Vector2; using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; using Silk.NET.OpenGL; -using System.Collections.Generic; using System.Numerics; using System.Threading.Tasks; +using SharpEngine.Core.Components.Properties; namespace SharpEngine.Core.Entities.UI; @@ -22,138 +24,62 @@ namespace SharpEngine.Core.Entities.UI; /// public class UIElement : EmptyNode, IRenderable { - /// - /// Initializes a new instance of . - /// - public UIElement(GL gl) : this(gl, "UIElement") { } + // TODO: #53 This should be coming from the entity component system + public MeshRenderer MeshRenderer { get; set; } - /// - /// Initializes a new instance of . - /// - /// The name of the UI element. - public UIElement(GL gl, string name) : base(name) - { - // TODO: #5 Support custom meshes? - Mesh = MeshService.Instance.LoadMesh(nameof(Primitives.Plane), Primitives.Plane.Mesh); - _uiShader = new UIShader(gl); - } - - private readonly UIShader _uiShader; - private readonly Texture _texture = TextureService.Instance.LoadTexture(Default.DebugTexture); + private readonly ShaderParameterBinder _paramBinder; /// Gets or sets the width of the ui element. + [ShaderParameter("width", ShaderParameterType.Float)] public float Width { get; set; } = 10; /// Gets or sets the height of the ui element. + [ShaderParameter("height", ShaderParameterType.Float)] public float Height { get; set; } = 10; - /// Gets or sets the mesh of the UI element. - public Mesh Mesh { get; set; } + [ShaderParameter("orthoMatrix", ShaderParameterType.Mat4)] + private Matrix4x4 OrthoMatrix = Matrix4x4.CreateOrthographicOffCenter(-1, 1, -1, 1, -1, 1); /// - /// Gets the most recently used VAO for this element. + /// Initializes a new instance of . /// - /// - /// UIElement maintains VAOs per OpenGL context. This property is updated on render. - /// - public uint VAO { get; private set; } - - private sealed record SharedBuffers(uint Vbo, uint Ebo, int IndexCount); - private sealed record ContextState(uint Vao); - - private readonly object _gpuLock = new(); - private readonly Dictionary _sharedBuffersByShareGroup = []; - private readonly Dictionary _contextStateByContext = []; - - private static object GetShareGroupKey(Window window) - => (object?)window.SharedContext ?? (object?)window.GLContext ?? (object)window; - - private static object GetContextKey(Window window) - => (object?)window.GLContext ?? (object)window; - - private SharedBuffers EnsureSharedBuffers(GL gl, Window window) - { - var shareGroupKey = GetShareGroupKey(window); - - lock (_gpuLock) - { - if (_sharedBuffersByShareGroup.TryGetValue(shareGroupKey, out var buffers)) - return buffers; - - // Buffers are shareable across contexts in the same share group. - var vbo = gl.GenBuffer(); - gl.BindBuffer(GLEnum.ArrayBuffer, vbo); - gl.BufferData(GLEnum.ArrayBuffer, Mesh.GetVertices(), GLEnum.StaticDraw); - - var ebo = gl.GenBuffer(); - gl.BindBuffer(GLEnum.ElementArrayBuffer, ebo); - gl.BufferData(GLEnum.ElementArrayBuffer, Mesh.Indices, GLEnum.StaticDraw); - - buffers = new SharedBuffers(vbo, ebo, Mesh.Indices.Length); - _sharedBuffersByShareGroup[shareGroupKey] = buffers; - - return buffers; - } - } + public UIElement(GL gl) : this(gl, "UIElement") { } - private ContextState EnsureContextState(GL gl, Window window, SharedBuffers sharedBuffers) + /// + /// Initializes a new instance of . + /// + /// The name of the UI element. + public UIElement(GL gl, string name) : base(name) { - var contextKey = GetContextKey(window); - - lock (_gpuLock) - { - if (_contextStateByContext.TryGetValue(contextKey, out var state)) - return state; - - // VAOs are generally not shared between contexts. - var vao = gl.GenVertexArray(); - gl.BindVertexArray(vao); - - gl.BindBuffer(GLEnum.ArrayBuffer, sharedBuffers.Vbo); - gl.BindBuffer(GLEnum.ElementArrayBuffer, sharedBuffers.Ebo); - - _uiShader.Use(); - _uiShader.SetAttributes(gl); - - state = new ContextState(vao); - _contextStateByContext[contextKey] = state; + // TODO: #5 Support custom meshes? + var mesh = MeshService.Instance.LoadMesh(nameof(Primitives.Plane), Primitives.Plane.Mesh); + var debugTexture = TextureService.Instance.LoadTexture(Default.DebugTexture); + var material = new Material("defaultUiMaterial", debugTexture) { Shader = new UIShader(gl) }; + MeshRenderer = new MeshRenderer(mesh, material); - return state; - } + _paramBinder = new ShaderParameterBinder(LoggingExtensions.CreateLogger()); + _paramBinder.Bind(this, material.Shader); } - Matrix4x4 OrthoMatrix = Matrix4x4.CreateOrthographicOffCenter(-1, 1, -1, 1, -1, 1); - /// /// Render the UI element. /// + /// The camera view. + /// The window where the UI element is rendered. public override Task Render(CameraView camera, Window window) { - var gl = window.GetGL(); - - var sharedBuffers = EnsureSharedBuffers(gl, window); - var contextState = EnsureContextState(gl, window, sharedBuffers); - - _uiShader.Use(); - gl.BindVertexArray(contextState.Vao); - _texture.Use(TextureUnit.Texture0); - - VAO = contextState.Vao; - - // TODO: #75 These should come from somewhere else. - const float screenWidth = 1280; - const float screenHeight = 720; - - _uiShader.SetFloat("width", Width); - _uiShader.SetFloat("height", Height); - _uiShader.SetVector2("screenSize", new System.Numerics.Vector2(screenWidth, screenHeight)); - _uiShader.SetVector2("position", (System.Numerics.Vector2)Transform.Position); - _uiShader.SetFloat("rotation", Math.DegreesToRadians(Transform.Rotation.Angle)); - _uiShader.SetInt("texture1", 0); - _uiShader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); - _uiShader.SetMatrix4("orthoMatrix", OrthoMatrix); // Pass the orthographic matrix to the shader - - gl.DrawElements(PrimitiveType.Triangles, (uint)sharedBuffers.IndexCount, DrawElementsType.UnsignedInt, []); + MeshRenderer.Mesh.Bind(); + MeshRenderer.Material.Shader.Use(); + MeshRenderer.Material.DiffuseMap!.Texture!.Use(TextureUnit.Texture0); + + _paramBinder.Apply(this, MeshRenderer.Material.Shader); + MeshRenderer.Material.Shader.SetVector2("screenSize", (System.Numerics.Vector2)window.Size); + MeshRenderer.Material.Shader.SetVector2("position", (System.Numerics.Vector2)Transform.Position); + MeshRenderer.Material.Shader.SetFloat("rotation", Math.DegreesToRadians(Transform.Rotation.Angle)); + MeshRenderer.Material.Shader.SetInt("texture1", 0); + MeshRenderer.Material.Shader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); + + MeshRenderer.Mesh.Draw(); return Task.CompletedTask; } diff --git a/SharpEngine.Core/Primitives/Plane.cs b/SharpEngine.Core/Primitives/Plane.cs index 854ae7a..3eae8b1 100644 --- a/SharpEngine.Core/Primitives/Plane.cs +++ b/SharpEngine.Core/Primitives/Plane.cs @@ -9,33 +9,17 @@ namespace SharpEngine.Core.Primitives; public static class Plane { /// The plane mesh. - public static Mesh Mesh { get; } = new(Window.SharedGL) - { - Vertices = + /// + /// Vertices are stored in interleaved format: position (x, y, z), normal (nx, ny, nz), UV (u, v). + /// + public static Mesh Mesh { get; } = new(Window.SharedGL, + vertices: [ - 1f, 1f, 0.0f, // top right - 1f, -1f, 0.0f, // bottom right - -1f, -1f, 0.0f, // bottom left - -1f, 1f, 0.0f, // top left + // pos.x, pos.y, pos.z, norm.x, norm.y, norm.z, uv.u, uv.v + 1f, 1f, 0.0f, 0f, 0f, 1f, 1.0f, 0.0f, // top right + 1f, -1f, 0.0f, 0f, 0f, 1f, 1.0f, 0.0f, // bottom right + -1f, -1f, 0.0f, 0f, 0f, 1f, 0.0f, 1.0f, // bottom left + -1f, 1f, 0.0f, 0f, 0f, 1f, 0.0f, 1.0f, // top left ], - Normals = - [ - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - ], - TextureCoordinates = - [ - 1.0f, 0.0f, - 1.0f, 0.0f, - 0.0f, 1.0f, - 0.0f, 1.0f, - ], - Indices = - [ - 0, 1, 3, // first triangle - 1, 2, 3, // second triangle - ] - }; + indices: [0u, 1u, 3u, 1u, 2u, 3u]); } diff --git a/SharpEngine.Core/Primitives/PrimitiveFactory.cs b/SharpEngine.Core/Primitives/PrimitiveFactory.cs index 1148e59..60777f2 100644 --- a/SharpEngine.Core/Primitives/PrimitiveFactory.cs +++ b/SharpEngine.Core/Primitives/PrimitiveFactory.cs @@ -1,9 +1,8 @@ using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Entities; using SharpEngine.Core.Entities.Properties; - +using SharpEngine.Core.Numerics; using System; -using System.Numerics; namespace SharpEngine.Core.Primitives; @@ -42,7 +41,7 @@ public static GameObject Create(PrimitiveType primitiveType, Vector3 position, s return new GameObject() { Model = model, - Transform = new Transform((Numerics.Vector3)position), + Transform = new Transform(position), }; } diff --git a/SharpEngine.Core/Rendering/ParameterDescriptor.cs b/SharpEngine.Core/Rendering/ParameterDescriptor.cs new file mode 100644 index 0000000..1964741 --- /dev/null +++ b/SharpEngine.Core/Rendering/ParameterDescriptor.cs @@ -0,0 +1,18 @@ +using System; + +namespace SharpEngine.Core.Rendering +{ + public sealed class ParameterDescriptor + { + public string Name { get; } + public ShaderParameterType Type { get; } + public ShaderUpdateFrequency Frequency { get; } + + public ParameterDescriptor(string name, ShaderParameterType type, ShaderUpdateFrequency frequency = ShaderUpdateFrequency.PerObject) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Type = type; + Frequency = frequency; + } + } +} diff --git a/SharpEngine.Core/Rendering/ShaderParameter.cs b/SharpEngine.Core/Rendering/ShaderParameter.cs index e4c059b..6ee9682 100644 --- a/SharpEngine.Core/Rendering/ShaderParameter.cs +++ b/SharpEngine.Core/Rendering/ShaderParameter.cs @@ -1,40 +1,7 @@ using System; -using System.Collections.Generic; namespace SharpEngine.Core.Rendering { - public enum ParameterType - { - Float, - Vec2, - Vec3, - Vec4, - Mat4, - Texture, - Unknown - } - - public enum UpdateFrequency - { - Static, - PerObject, - PerFrame - } - - public sealed class ParameterDescriptor - { - public string Name { get; } - public ParameterType Type { get; } - public UpdateFrequency Frequency { get; } - - public ParameterDescriptor(string name, ParameterType type, UpdateFrequency frequency = UpdateFrequency.PerObject) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - Type = type; - Frequency = frequency; - } - } - public sealed class ParameterInstance { public ParameterDescriptor Descriptor { get; } diff --git a/SharpEngine.Core/Rendering/ShaderParameterBinder.cs b/SharpEngine.Core/Rendering/ShaderParameterBinder.cs new file mode 100644 index 0000000..a2551cd --- /dev/null +++ b/SharpEngine.Core/Rendering/ShaderParameterBinder.cs @@ -0,0 +1,188 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Shaders; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Numerics; +using System.Reflection; + +namespace SharpEngine.Core.Rendering; + +/// +/// Binds C# properties and fields decorated with to their +/// corresponding GLSL uniforms, validating coverage at bind time and dispatching values at render time. +/// +/// +/// Call once when initializing an object against a shader to validate the +/// attribute-to-uniform mapping. Then call every frame (or whenever values change) +/// to upload the current property values to the shader without writing individual Set* calls. +/// Uniforms that have no matching attribute are reported at Debug level; attributes that +/// reference a non-existent uniform are reported at Warning level. +/// +public class ShaderParameterBinder +{ + private readonly ILogger _logger; + + private readonly record struct MemberBinding( + MemberInfo Member, + ShaderParameterAttribute Attribute, + ShaderParameterType ResolvedType); + + // Reflection metadata is expensive; cache per source type across all instances. + private static readonly ConcurrentDictionary _memberCache = new(); + + private MemberBinding[] _bindings = []; + + /// + /// Initializes a new instance of . + /// + /// Logger used to report validation results and dispatch warnings. + public ShaderParameterBinder(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Scans for -decorated members, + /// validates them against the shader's active uniforms, and caches the resolved bindings for + /// use by . + /// + /// The object whose type is inspected for shader parameter attributes. + /// The shader whose are validated against. + public void Bind(object source, IShader shader) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(shader); + + var type = source.GetType(); + var discovered = _memberCache.GetOrAdd(type, DiscoverMembers); + + var parameterNames = new HashSet(discovered.Length, StringComparer.Ordinal); + foreach (var (_, attr) in discovered) + parameterNames.Add(attr.Name); + + // Uniforms with no matching [ShaderParameter] attribute — developer may be setting them manually. + foreach (var uniformName in shader.UniformLocations.Keys) + { + if (!parameterNames.Contains(uniformName)) + _logger.LogDebug("Shader uniform '{Uniform}' has no [ShaderParameter] mapping on '{Type}'.", uniformName, type.Name); + } + + var resolved = new List(discovered.Length); + foreach (var (member, attr) in discovered) + { + if (!shader.UniformLocations.ContainsKey(attr.Name)) + { + _logger.LogWarning( + "[ShaderParameter(\"{Name}\")] on '{Member}' of '{Type}' has no matching uniform in the shader.", + attr.Name, member.Name, type.Name); + continue; + } + + var resolvedType = attr.Type != ShaderParameterType.Unknown ? attr.Type : InferType(member); + if (resolvedType == ShaderParameterType.Unknown) + { + _logger.LogWarning( + "Cannot determine ParameterType for [ShaderParameter(\"{Name}\")] on '{Member}'. Specify the type explicitly.", + attr.Name, member.Name); + continue; + } + + resolved.Add(new MemberBinding(member, attr, resolvedType)); + } + + _bindings = [.. resolved]; + } + + /// + /// Reads the current values of all bound members from and uploads + /// them to using the appropriate Set* method. + /// + /// The object from which property/field values are read. + /// The shader that receives the uniform values. + public void Apply(object source, IShader shader) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(shader); + + foreach (var binding in _bindings) + { + var value = GetMemberValue(binding.Member, source); + if (value is null) + continue; + + var uploaded = binding.ResolvedType switch + { + ShaderParameterType.Int when value is int i => shader.SetInt(binding.Attribute.Name, i), + ShaderParameterType.Float when value is float f => shader.SetFloat(binding.Attribute.Name, f), + ShaderParameterType.Vec2 when value is Vector2 v2 => shader.SetVector2(binding.Attribute.Name, v2), + ShaderParameterType.Vec3 when value is Vector3 v3 => shader.SetVector3(binding.Attribute.Name, v3), + ShaderParameterType.Vec4 when value is Vector4 v4 => shader.SetVector4(binding.Attribute.Name, v4), + ShaderParameterType.Mat4 when value is Matrix4x4 m => shader.SetMatrix4(binding.Attribute.Name, m), + ShaderParameterType.Texture when value is int slot => shader.SetInt(binding.Attribute.Name, slot), + _ => LogTypeMismatch(binding, value) + }; + + _ = uploaded; // result intentionally unused; Set* implementations log their own failures + } + } + + private bool LogTypeMismatch(MemberBinding binding, object value) + { + _logger.LogWarning("Value type '{ValueType}' is incompatible with ParameterType '{ParamType}' for uniform '{Name}'.", + value.GetType().Name, binding.ResolvedType, binding.Attribute.Name); + + return false; + } + + private static (MemberInfo Member, ShaderParameterAttribute Attribute)[] DiscoverMembers(Type type) + { + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + var results = new List<(MemberInfo, ShaderParameterAttribute)>(); + + foreach (var prop in type.GetProperties(flags)) + { + var attr = prop.GetCustomAttribute(); + if (attr is not null) + results.Add((prop, attr)); + } + + foreach (var field in type.GetFields(flags)) + { + var attr = field.GetCustomAttribute(); + if (attr is not null) + results.Add((field, attr)); + } + + return [.. results]; + } + + private static object? GetMemberValue(MemberInfo member, object source) + => member switch + { + PropertyInfo prop => prop.GetValue(source), + FieldInfo field => field.GetValue(source), + _ => null + }; + + private static ShaderParameterType InferType(MemberInfo member) + { + var memberType = member switch + { + PropertyInfo prop => prop.PropertyType, + FieldInfo field => field.FieldType, + _ => null + }; + + return memberType switch + { + not null when memberType == typeof(int) => ShaderParameterType.Int, + not null when memberType == typeof(float) => ShaderParameterType.Float, + not null when memberType == typeof(Vector2) => ShaderParameterType.Vec2, + not null when memberType == typeof(Vector3) => ShaderParameterType.Vec3, + not null when memberType == typeof(Vector4) => ShaderParameterType.Vec4, + not null when memberType == typeof(Matrix4x4) => ShaderParameterType.Mat4, + _ => ShaderParameterType.Unknown + }; + } +} diff --git a/SharpEngine.Core/Rendering/ShaderParameterType.cs b/SharpEngine.Core/Rendering/ShaderParameterType.cs new file mode 100644 index 0000000..1ede3b8 --- /dev/null +++ b/SharpEngine.Core/Rendering/ShaderParameterType.cs @@ -0,0 +1,14 @@ +namespace SharpEngine.Core.Rendering +{ + public enum ShaderParameterType + { + Int, + Float, + Vec2, + Vec3, + Vec4, + Mat4, + Texture, + Unknown + } +} diff --git a/SharpEngine.Core/Rendering/ShaderUpdateFrequency.cs b/SharpEngine.Core/Rendering/ShaderUpdateFrequency.cs new file mode 100644 index 0000000..311164b --- /dev/null +++ b/SharpEngine.Core/Rendering/ShaderUpdateFrequency.cs @@ -0,0 +1,9 @@ +namespace SharpEngine.Core.Rendering +{ + public enum ShaderUpdateFrequency + { + Static, + PerObject, + PerFrame + } +} diff --git a/SharpEngine.Core/Scenes/Scene.cs b/SharpEngine.Core/Scenes/Scene.cs index 6ab4c91..670ff38 100644 --- a/SharpEngine.Core/Scenes/Scene.cs +++ b/SharpEngine.Core/Scenes/Scene.cs @@ -63,11 +63,13 @@ public Scene(string sceneFile) /// Adds an empty node to the scene root. /// /// The name of the new empty node. - public void AddNode(string name) + public SceneNode AddNode(string name) { var node = new EmptyNode(name); Nodes.Add(node); Root.Children.Add(node); + + return node; } /// diff --git a/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs index 73a60a3..8b925b0 100644 --- a/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs +++ b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs @@ -7,10 +7,10 @@ namespace SharpEngine.Core.Shaders; public sealed class ShaderParameterAttribute : Attribute { public string Name { get; } - public ParameterType Type { get; } - public UpdateFrequency Frequency { get; } + public ShaderParameterType Type { get; } + public ShaderUpdateFrequency Frequency { get; } - public ShaderParameterAttribute(string name, ParameterType type = ParameterType.Unknown, UpdateFrequency frequency = UpdateFrequency.PerObject) + public ShaderParameterAttribute(string name, ShaderParameterType type = ShaderParameterType.Unknown, ShaderUpdateFrequency frequency = ShaderUpdateFrequency.PerObject) { Name = name; Type = type; diff --git a/SharpEngine.Core/Shaders/ShaderService.cs b/SharpEngine.Core/Shaders/ShaderService.cs index be99f9c..5832856 100644 --- a/SharpEngine.Core/Shaders/ShaderService.cs +++ b/SharpEngine.Core/Shaders/ShaderService.cs @@ -73,7 +73,7 @@ public Shader GetByName(string name) /// A shader with the given name. /// Thrown when either the vertex or fragment shader is not found. public Shader LoadShader(Window window, string vertPath, string fragPath, string name) - => LoadShader(window.GetGL(), GetShareGroupKey(window), vertPath, fragPath, name); + => LoadShader(window.GetGL(), window.GetShareGroupKey(), vertPath, fragPath, name); /// /// Loads a shader from the specified vertex and fragment paths.
@@ -128,7 +128,4 @@ public Shader LoadShader(GL gl, object shareGroupKey, string vertPath, string fr return shader; } - - private static object GetShareGroupKey(Window window) - => (object?)window.SharedContext ?? (object?)window.GLContext ?? (object)window; } diff --git a/SharpEngine.Core/Windowing/SilkWindow.cs b/SharpEngine.Core/Windowing/SilkWindow.cs index c7c9fd3..2c5cc08 100644 --- a/SharpEngine.Core/Windowing/SilkWindow.cs +++ b/SharpEngine.Core/Windowing/SilkWindow.cs @@ -369,4 +369,4 @@ protected virtual void PreRender(Frame frame) { } /// Handles closing the application. ///
public virtual void OnClosing() { } -} +} \ No newline at end of file diff --git a/SharpEngine.Core/Windowing/Window.cs b/SharpEngine.Core/Windowing/Window.cs index 15afa84..fca661d 100644 --- a/SharpEngine.Core/Windowing/Window.cs +++ b/SharpEngine.Core/Windowing/Window.cs @@ -260,10 +260,8 @@ public override void OnLoad() /// Contains information about the previous frame. protected void RenderFrame(Frame frame) { - while (!_initialized) - { - // Wait for the window to be initialized. - } + if (!_initialized) + throw new InvalidOperationException("Window has not been initialized. Call Initialize() before rendering."); try { diff --git a/SharpEngine.Core/Windowing/WindowExtensions.cs b/SharpEngine.Core/Windowing/WindowExtensions.cs new file mode 100644 index 0000000..ccb5b1b --- /dev/null +++ b/SharpEngine.Core/Windowing/WindowExtensions.cs @@ -0,0 +1,12 @@ +namespace SharpEngine.Core.Windowing; + +public static class WindowExtensions +{ + /// + /// Gets a key representing the share group for the window. + /// + /// The window to get the share group key for. + /// A key representing the share group for the window. + public static object GetShareGroupKey(this Window window) + => (object?)window.SharedContext ?? (object?)window.GLContext ?? window; +} \ No newline at end of file diff --git a/Tests/SharpEngine.Core.Components.Tests/Meshes/ModelTests.cs b/Tests/SharpEngine.Core.Components.Tests/Meshes/ModelTests.cs new file mode 100644 index 0000000..a9bdcae --- /dev/null +++ b/Tests/SharpEngine.Core.Components.Tests/Meshes/ModelTests.cs @@ -0,0 +1,98 @@ +using FluentAssertions; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using System.Numerics; +using Xunit; + +namespace SharpEngine.Core.Components.Tests.Meshes; + +/// +/// Unit tests for covering the pure, GL-independent methods. +/// +/// +/// Tests that require a real OpenGL context (e.g., ) need an +/// integration test environment with a live windowing / GL context and are therefore not +/// covered here. +/// +public class ModelTests +{ + // ------------------------------------------------------------------------- + // BuildVertices + // ------------------------------------------------------------------------- + + [Fact] + public void BuildVertices_EmptyCollection_ReturnsEmptyArray() + { + var result = Model.BuildVertices([]); + + result.Should().BeEmpty(); + } + + [Fact] + public void BuildVertices_SingleVertex_ReturnsEightFloatsInCorrectOrder() + { + var vertex = new Vertex + { + Position = new Vector3(1f, 2f, 3f), + Normal = new Vector3(0f, 1f, 0f), + TexCoords = new Vector2(0.5f, 0.25f) + }; + + var result = Model.BuildVertices([vertex]); + + result.Should().HaveCount(8); + result.Should().ContainInOrder( + 1f, 2f, 3f, // Position + 0f, 1f, 0f, // Normal + 0.5f, 0.25f); // TexCoords + } + + [Fact] + public void BuildVertices_MultipleVertices_ProducesContiguousFloatLayout() + { + var v1 = new Vertex { Position = new Vector3(1f, 0f, 0f), Normal = new Vector3(0f, 0f, 1f), TexCoords = new Vector2(0f, 0f) }; + var v2 = new Vertex { Position = new Vector3(0f, 1f, 0f), Normal = new Vector3(0f, 0f, 1f), TexCoords = new Vector2(1f, 1f) }; + + var result = Model.BuildVertices([v1, v2]); + + result.Should().HaveCount(16); + + // First vertex + result[0].Should().Be(1f); + result[1].Should().Be(0f); + result[2].Should().Be(0f); + result[3].Should().Be(0f); + result[4].Should().Be(0f); + result[5].Should().Be(1f); + result[6].Should().Be(0f); + result[7].Should().Be(0f); + + // Second vertex + result[8].Should().Be(0f); + result[9].Should().Be(1f); + result[10].Should().Be(0f); + result[11].Should().Be(0f); + result[12].Should().Be(0f); + result[13].Should().Be(1f); + result[14].Should().Be(1f); + result[15].Should().Be(1f); + } + + [Fact] + public void BuildVertices_VertexWithNegativeCoordinates_PreservesSignedValues() + { + var vertex = new Vertex + { + Position = new Vector3(-0.5f, -0.5f, -0.5f), + Normal = new Vector3(-1f, 0f, 0f), + TexCoords = new Vector2(0f, 1f) + }; + + var result = Model.BuildVertices([vertex]); + + result.Should().ContainInOrder( + -0.5f, -0.5f, -0.5f, + -1f, 0f, 0f, + 0f, 1f); + } +} diff --git a/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj b/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj index a15a71c..0e0a454 100644 --- a/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj +++ b/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj @@ -1,11 +1,18 @@  - net10.0 + $(DotNetTargetFramework) enable enable + + + + + + + diff --git a/Tests/SharpEngine.Core.Tests/Shaders/ShaderParameterBinderTests.cs b/Tests/SharpEngine.Core.Tests/Shaders/ShaderParameterBinderTests.cs new file mode 100644 index 0000000..cb88507 --- /dev/null +++ b/Tests/SharpEngine.Core.Tests/Shaders/ShaderParameterBinderTests.cs @@ -0,0 +1,360 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using SharpEngine.Core.Rendering; +using SharpEngine.Core.Shaders; +using System.Collections.Generic; +using System.Numerics; +using Xunit; + +namespace SharpEngine.Core.Tests.Shaders; + +public class ShaderParameterBinderTests +{ + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + private static ShaderParameterBinder CreateBinder() + => new(NullLogger.Instance); + + private static Mock CreateShaderMock(params string[] uniformNames) + { + var dict = new Dictionary(uniformNames.Length); + for (int i = 0; i < uniformNames.Length; i++) + dict[uniformNames[i]] = i; + + var mock = new Mock(); + mock.Setup(s => s.UniformLocations).Returns(dict); + mock.Setup(s => s.SetFloat(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetInt(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetVector2(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetVector3(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetVector4(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetMatrix4(It.IsAny(), It.IsAny(), It.IsAny())).Returns(true); + return mock; + } + + // --------------------------------------------------------------------------- + // Test objects + // --------------------------------------------------------------------------- + + private class FloatSource + { + [ShaderParameter("myFloat", ShaderParameterType.Float)] + public float Value { get; set; } = 3.14f; + } + + private class IntSource + { + [ShaderParameter("myInt", ShaderParameterType.Int)] + public int Value { get; set; } = 7; + } + + private class Vec2Source + { + [ShaderParameter("myVec2", ShaderParameterType.Vec2)] + public Vector2 Value { get; set; } = new(1f, 2f); + } + + private class Vec3Source + { + [ShaderParameter("myVec3", ShaderParameterType.Vec3)] + public Vector3 Value { get; set; } = new(1f, 2f, 3f); + } + + private class Vec4Source + { + [ShaderParameter("myVec4", ShaderParameterType.Vec4)] + public Vector4 Value { get; set; } = new(1f, 2f, 3f, 4f); + } + + private class Mat4Source + { + [ShaderParameter("myMat4", ShaderParameterType.Mat4)] + public Matrix4x4 Value { get; set; } = Matrix4x4.Identity; + } + + private class TextureSource + { + [ShaderParameter("tex", ShaderParameterType.Texture)] + public int Slot { get; set; } = 0; + } + + private class PrivateFieldSource + { + [ShaderParameter("fieldUniform", ShaderParameterType.Float)] +#pragma warning disable CS0414 + private float _field = 9.9f; +#pragma warning restore CS0414 + } + + private class InferredFloatSource + { + // No explicit ParameterType — should be inferred from C# float + [ShaderParameter("inferredFloat")] + public float Value { get; set; } = 1.5f; + } + + private class InferredMat4Source + { + // No explicit ParameterType — should be inferred from Matrix4x4 + [ShaderParameter("inferredMat4")] + public Matrix4x4 Value { get; set; } = Matrix4x4.Identity; + } + + private class MismatchSource + { + // Attribute says Vec2 but C# type is float — Apply should not call any Set* + [ShaderParameter("mismatch", ShaderParameterType.Vec2)] + public float Value { get; set; } = 1f; + } + + private class UnmappedAttributeSource + { + [ShaderParameter("doesNotExistInShader", ShaderParameterType.Float)] + public float Value { get; set; } = 1f; + } + + private class MultiPropertySource + { + [ShaderParameter("width", ShaderParameterType.Float)] + public float Width { get; set; } = 100f; + + [ShaderParameter("height", ShaderParameterType.Float)] + public float Height { get; set; } = 200f; + } + + // --------------------------------------------------------------------------- + // Bind – validation + // --------------------------------------------------------------------------- + + [Fact] + public void Bind_AttributeWithNoMatchingUniform_DoesNotAddBinding() + { + var binder = CreateBinder(); + var shader = CreateShaderMock("otherUniform"); // no "doesNotExistInShader" + var source = new UnmappedAttributeSource(); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void Bind_NullSource_ThrowsArgumentNullException() + { + var binder = CreateBinder(); + var shader = CreateShaderMock(); + var act = () => binder.Bind(null!, shader.Object); + act.Should().Throw(); + } + + [Fact] + public void Bind_NullShader_ThrowsArgumentNullException() + { + var binder = CreateBinder(); + var act = () => binder.Bind(new FloatSource(), null!); + act.Should().Throw(); + } + + // --------------------------------------------------------------------------- + // Apply – correct dispatch per ParameterType + // --------------------------------------------------------------------------- + + [Fact] + public void Apply_Float_CallsSetFloat() + { + var binder = CreateBinder(); + var source = new FloatSource { Value = 3.14f }; + var shader = CreateShaderMock("myFloat"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("myFloat", 3.14f), Times.Once); + } + + [Fact] + public void Apply_Int_CallsSetInt() + { + var binder = CreateBinder(); + var source = new IntSource { Value = 7 }; + var shader = CreateShaderMock("myInt"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetInt("myInt", 7), Times.Once); + } + + [Fact] + public void Apply_Vec2_CallsSetVector2() + { + var binder = CreateBinder(); + var source = new Vec2Source { Value = new Vector2(1f, 2f) }; + var shader = CreateShaderMock("myVec2"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetVector2("myVec2", new Vector2(1f, 2f)), Times.Once); + } + + [Fact] + public void Apply_Vec3_CallsSetVector3() + { + var binder = CreateBinder(); + var source = new Vec3Source { Value = new Vector3(1f, 2f, 3f) }; + var shader = CreateShaderMock("myVec3"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetVector3("myVec3", new Vector3(1f, 2f, 3f)), Times.Once); + } + + [Fact] + public void Apply_Vec4_CallsSetVector4() + { + var binder = CreateBinder(); + var source = new Vec4Source { Value = new Vector4(1f, 2f, 3f, 4f) }; + var shader = CreateShaderMock("myVec4"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetVector4("myVec4", new Vector4(1f, 2f, 3f, 4f)), Times.Once); + } + + [Fact] + public void Apply_Mat4_CallsSetMatrix4() + { + var binder = CreateBinder(); + var source = new Mat4Source { Value = Matrix4x4.Identity }; + var shader = CreateShaderMock("myMat4"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetMatrix4("myMat4", Matrix4x4.Identity, true), Times.Once); + } + + [Fact] + public void Apply_Texture_CallsSetInt() + { + var binder = CreateBinder(); + var source = new TextureSource { Slot = 2 }; + var shader = CreateShaderMock("tex"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetInt("tex", 2), Times.Once); + } + + [Fact] + public void Apply_PrivateField_IsDispatched() + { + var binder = CreateBinder(); + var source = new PrivateFieldSource(); + var shader = CreateShaderMock("fieldUniform"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("fieldUniform", 9.9f), Times.Once); + } + + [Fact] + public void Apply_TypeMismatch_DoesNotCallAnySetMethod() + { + var binder = CreateBinder(); + var source = new MismatchSource { Value = 1f }; + var shader = CreateShaderMock("mismatch"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat(It.IsAny(), It.IsAny()), Times.Never); + shader.Verify(s => s.SetVector2(It.IsAny(), It.IsAny()), Times.Never); + } + + // --------------------------------------------------------------------------- + // Apply – type inference (ParameterType.Unknown) + // --------------------------------------------------------------------------- + + [Fact] + public void Apply_InferredFloat_CallsSetFloat() + { + var binder = CreateBinder(); + var source = new InferredFloatSource { Value = 1.5f }; + var shader = CreateShaderMock("inferredFloat"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("inferredFloat", 1.5f), Times.Once); + } + + [Fact] + public void Apply_InferredMat4_CallsSetMatrix4() + { + var binder = CreateBinder(); + var source = new InferredMat4Source { Value = Matrix4x4.Identity }; + var shader = CreateShaderMock("inferredMat4"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetMatrix4("inferredMat4", Matrix4x4.Identity, true), Times.Once); + } + + // --------------------------------------------------------------------------- + // Apply – multiple properties + // --------------------------------------------------------------------------- + + [Fact] + public void Apply_MultipleProperties_EachDispatchedSeparately() + { + var binder = CreateBinder(); + var source = new MultiPropertySource { Width = 100f, Height = 200f }; + var shader = CreateShaderMock("width", "height"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("width", 100f), Times.Once); + shader.Verify(s => s.SetFloat("height", 200f), Times.Once); + } + + [Fact] + public void Apply_BeforeBind_DoesNotCallAnySetMethod() + { + var binder = CreateBinder(); + var source = new FloatSource(); + var shader = CreateShaderMock("myFloat"); + + // Apply without calling Bind first — _bindings is empty, nothing should be dispatched + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void Apply_ReflectsCurrentPropertyValues() + { + var binder = CreateBinder(); + var source = new FloatSource { Value = 1f }; + var shader = CreateShaderMock("myFloat"); + + binder.Bind(source, shader.Object); + + source.Value = 42f; + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("myFloat", 42f), Times.Once); + shader.Verify(s => s.SetFloat("myFloat", 1f), Times.Never); + } +} diff --git a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj index a3a2555..2c3bedd 100644 --- a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj +++ b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj @@ -21,7 +21,6 @@ - From 4dbe4c19bb7d1bc5fdaa4e1019880669cf6c25d7 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 16:12:33 +0300 Subject: [PATCH 10/10] #67 Solution builds --- Examples/MinecraftClone/Block/BlockBase.cs | 2 +- Examples/MinecraftClone/Minecraft.cs | 14 +++++++------- .../Loaders/MaterialLibraryLoaderTests.cs | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Examples/MinecraftClone/Block/BlockBase.cs b/Examples/MinecraftClone/Block/BlockBase.cs index 1c62d38..e10a295 100644 --- a/Examples/MinecraftClone/Block/BlockBase.cs +++ b/Examples/MinecraftClone/Block/BlockBase.cs @@ -42,7 +42,7 @@ protected BlockBase(Vector3 position, string name, string diffuseMapFile, string private void Initialize(Vector3 position, string name, string diffuseMapFile, string specularMapFile, string vertShaderFile, string fragShaderFile) { - var cube = PrimitiveFactory.Create(PrimitiveType.Cube, position, diffuseMapFile, specularMapFile, vertShaderFile, fragShaderFile); + var cube = PrimitiveFactory.Create(PrimitiveType.Cube, (SharpEngine.Core.Numerics.Vector3)position, diffuseMapFile, specularMapFile, vertShaderFile, fragShaderFile); AssignProperties(cube, name); } diff --git a/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs index e423051..6819f78 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -172,17 +172,17 @@ private void InitializeLights() _lightsNode.AddChild(new DirectionalLight()); _lightsNode.AddChild( - new PointLight(new Vector3(0.7f, 0.2f, 2.0f), 0), - new PointLight(new Vector3(2.3f, -3.3f, -4.0f), 1), - new PointLight(new Vector3(-4.0f, 2.0f, -12.0f), 2), - new PointLight(new Vector3(0.0f, 0.0f, -3.0f), 3) + new PointLight(new SharpEngine.Core.Numerics.Vector3(0.7f, 0.2f, 2.0f), 0), + new PointLight(new SharpEngine.Core.Numerics.Vector3(2.3f, -3.3f, -4.0f), 1), + new PointLight(new SharpEngine.Core.Numerics.Vector3(-4.0f, 2.0f, -12.0f), 2), + new PointLight(new SharpEngine.Core.Numerics.Vector3(0.0f, 0.0f, -3.0f), 3) ); _lightsNode.AddChild(new SpotLight() { - Ambient = new Vector3(0.0f, 0.0f, 0.0f), - Diffuse = new Vector3(1.0f, 1.0f, 1.0f), - Specular = new Vector3(1.0f, 1.0f, 1.0f), + Ambient = new SharpEngine.Core.Numerics.Vector3(0.0f, 0.0f, 0.0f), + Diffuse = new SharpEngine.Core.Numerics.Vector3(1.0f, 1.0f, 1.0f), + Specular = new SharpEngine.Core.Numerics.Vector3(1.0f, 1.0f, 1.0f), }); } diff --git a/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs index 987b111..6e8cf13 100644 --- a/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs @@ -94,8 +94,8 @@ public void Sets_correct_texure_maps() LoadMaterial(); _firstMaterial.AmbientTextureMap.Should().BeEquivalentTo("lenna1.tga"); - _firstMaterial.DiffuseTextureMap.Should().BeEquivalentTo("lenna2.tga"); - _firstMaterial.SpecularTextureMap.Should().BeEquivalentTo("lenna3.tga"); + _firstMaterial.DiffuseMap.Should().BeEquivalentTo("lenna2.tga"); + _firstMaterial.SpecularMap.Should().BeEquivalentTo("lenna3.tga"); _firstMaterial.SpecularHighlightTextureMap.Should().BeEquivalentTo("lenna_spec.tga"); _firstMaterial.AlphaTextureMap.Should().BeEquivalentTo("lenna_alpha.tga"); _firstMaterial.BumpMap.Should().BeEquivalentTo("lenna_bump.tga");