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 1b347e2..6819f78 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(); } @@ -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/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/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 5d01401..3028aa2 100644 --- a/SharpEngine.Core.Components/Properties/Material.cs +++ b/SharpEngine.Core.Components/Properties/Material.cs @@ -5,58 +5,68 @@ 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 . + /// + /// 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; + 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; - } - /// - /// 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; } /// 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; } @@ -83,22 +93,22 @@ public Material(string materialName) 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. @@ -110,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); - } - } - - /// - public object Clone() - => MemberwiseClone(); - - /// - public override bool Equals(object? obj) - { - if (obj is null) - return false; + shader.SetTextureUnit("material.specular", TextureUnitIndex.DIFFUSE_UNIT); - if (ReferenceEquals(this, obj)) - return true; + shader.SetFloat("material.shininess", Shininess); - 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 06e8322..8bee87e 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, IShader, 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..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; @@ -10,177 +11,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 +28,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 +41,10 @@ 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); + + public bool SetTextureUnit(string name, TextureUnitIndex textureUnit) + => TrySetUniform(name, (int)textureUnit, GL.Uniform1); /// /// Set a uniform float on this shader. @@ -219,7 +52,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 +66,15 @@ 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. + /// + /// 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. @@ -241,7 +82,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 +90,10 @@ 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); + + public bool SetVector3(string name, SharpEngine.Core.Numerics.Vector3 data) + => TrySetUniform(name, (Vector3)data, GL.Uniform3); /// /// Set a uniform Vector3 on this shader. @@ -257,5 +101,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.Components/Properties/Shaders/ShaderProgram.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs new file mode 100644 index 0000000..2249dee --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs @@ -0,0 +1,238 @@ +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; } + + /// 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. + /// + /// + /// if the attributes were set successfully; otherwise . + /// + public virtual bool SetAttributes(GL gl) => true; + + 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); + + Vao = GL.GenVertexArray(); + GL.BindVertexArray(Vao); + + SetAttributes(GL); + + 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/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 f411581..99cc5f5 100644 --- a/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj +++ b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj @@ -14,6 +14,7 @@ + @@ -22,4 +23,10 @@ + + + <_Parameter1>SharpEngine.Core.Components.Tests + + + diff --git a/SharpEngine.Core/Entities/GameObject.cs b/SharpEngine.Core/Entities/GameObject.cs index 1b52fea..99b0a14 100644 --- a/SharpEngine.Core/Entities/GameObject.cs +++ b/SharpEngine.Core/Entities/GameObject.cs @@ -1,22 +1,22 @@ 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.UI; 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; @@ -25,55 +25,53 @@ 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 readonly string _shaderVertPath; - private readonly string _shaderFragPath; - private readonly 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"; + // Shader = ShaderService.Instance.LoadShader(Window.SharedGL, _shaderVertPath, _shaderFragPath, _shaderName); } /// /// 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) + _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. /// @@ -125,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(); @@ -150,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 50d498a..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,140 +24,62 @@ namespace SharpEngine.Core.Entities.UI; /// public class UIElement : EmptyNode, IRenderable { - /// - /// Initializes a new instance of . - /// - public UIElement() : this("UIElement") { } - - /// - /// Initializes a new instance of . - /// - /// The name of the UI element. - public UIElement(string name) : base(name) - { - // TODO: #5 Support custom meshes? - Mesh = MeshService.Instance.LoadMesh(nameof(Primitives.Plane), Primitives.Plane.Mesh); - } + // TODO: #53 This should be coming from the entity component system + public MeshRenderer MeshRenderer { get; set; } - private readonly UIShader _uiShader = new(); - 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); + public UIElement(GL gl) : this(gl, "UIElement") { } - buffers = new SharedBuffers(vbo, ebo, Mesh.Indices.Length); - _sharedBuffersByShareGroup[shareGroupKey] = buffers; - - return buffers; - } - } - - 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.EnsureInitialized(window); - _uiShader.Shader!.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(); - - _uiShader.EnsureInitialized(window); - - var sharedBuffers = EnsureSharedBuffers(gl, window); - var contextState = EnsureContextState(gl, window, sharedBuffers); - - _uiShader.Shader!.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.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 - - 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/Renderers/Renderer.cs b/SharpEngine.Core/Renderers/Renderer.cs index 873a359..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) { @@ -79,7 +84,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/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 new file mode 100644 index 0000000..6ee9682 --- /dev/null +++ b/SharpEngine.Core/Rendering/ShaderParameter.cs @@ -0,0 +1,37 @@ +using System; + +namespace SharpEngine.Core.Rendering +{ + 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/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/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; +} 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 deleted file mode 100644 index 492217a..0000000 --- a/SharpEngine.Core/Shaders/ShaderBase.cs +++ /dev/null @@ -1,35 +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; } - - /// 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/ShaderParameterAttribute.cs b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs new file mode 100644 index 0000000..8b925b0 --- /dev/null +++ b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs @@ -0,0 +1,19 @@ +using SharpEngine.Core.Rendering; +using System; + +namespace SharpEngine.Core.Shaders; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public sealed class ShaderParameterAttribute : Attribute +{ + public string Name { get; } + public ShaderParameterType Type { get; } + public ShaderUpdateFrequency Frequency { get; } + + public ShaderParameterAttribute(string name, ShaderParameterType type = ShaderParameterType.Unknown, ShaderUpdateFrequency frequency = ShaderUpdateFrequency.PerObject) + { + Name = name; + Type = type; + Frequency = frequency; + } +} \ No newline at end of file diff --git a/SharpEngine.Core/Shaders/ShaderService.cs b/SharpEngine.Core/Shaders/ShaderService.cs index 74fee61..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.
@@ -121,14 +121,11 @@ 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; return shader; } - - private static object GetShareGroupKey(Window window) - => (object?)window.SharedContext ?? (object?)window.GLContext ?? (object)window; } 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) 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/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"); 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..1a08f02 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..397a4c5 100644 --- a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj +++ b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj @@ -1,4 +1,4 @@ - + $(DotNetTargetFramework) @@ -21,7 +21,6 @@ -