diff --git a/Asset Store/AssetStore.Database/AssetStore.Database.csproj b/Asset Store/AssetStore.Database/AssetStore.Database.csproj new file mode 100644 index 0000000..247bc9d --- /dev/null +++ b/Asset Store/AssetStore.Database/AssetStore.Database.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Asset Store/AssetStore.Database/DatabaseContext.cs b/Asset Store/AssetStore.Database/DatabaseContext.cs new file mode 100644 index 0000000..344ae80 --- /dev/null +++ b/Asset Store/AssetStore.Database/DatabaseContext.cs @@ -0,0 +1,44 @@ +using AssetStore.Database.Models; +using Microsoft.EntityFrameworkCore; +using SharpEngine.Shared.Dto.Primitives; + +namespace AssetStore.Database; + +public class DatabaseContext : DbContext +{ + public DbSet Assets { get; set; } + public DbSet Comments { get; set; } + + public DatabaseContext() + { + } + + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreExampleDB;Trusted_Connection=True;"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.HasMany(asset => asset.Comments); + + entity.Property(u => u.Id) + .HasConversion( + id => id.Value, + value => new AssetId(value)); + }); + + modelBuilder.Entity(entity => + { + entity.Property(u => u.Id) + .HasConversion( + id => id.Value, + value => new CommentId(value)); + + }); + } +} \ No newline at end of file diff --git a/Asset Store/AssetStore.Database/Models/Asset.cs b/Asset Store/AssetStore.Database/Models/Asset.cs new file mode 100644 index 0000000..0ae9119 --- /dev/null +++ b/Asset Store/AssetStore.Database/Models/Asset.cs @@ -0,0 +1,36 @@ +using SharpEngine.Shared.Dto.AssetStore; +using SharpEngine.Shared.Dto.Primitives; + +namespace AssetStore.Database.Models; + +public class Asset +{ + public AssetId Id { get; set; } + public required string Name { get; set; } + public string Description { get; set; } = string.Empty; + public decimal Price { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow; + public IReadOnlyList KeyWords { get; set; } = []; + public IReadOnlyList Comments { get; set; } = []; + public UserId AuthorId { get; set; } + public bool Tombstoned { get; set; } = false; + public Version Version { get; set; } + // (e.g., model, texture, sound, etc.) + // public AssetType + + public required string BlobUri { get; set; } + public required string FileSize { get; set; } + + public required string Checksum { get; set; } + + public static explicit operator Asset(AssetDto dto) + { + throw new NotImplementedException(); + } + + public static explicit operator AssetDto(Asset? model) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Asset Store/AssetStore.Database/Models/Comment.cs b/Asset Store/AssetStore.Database/Models/Comment.cs new file mode 100644 index 0000000..ef189ca --- /dev/null +++ b/Asset Store/AssetStore.Database/Models/Comment.cs @@ -0,0 +1,14 @@ +using SharpEngine.Shared.Dto.Primitives; + +namespace AssetStore.Database.Models; + +public class Comment +{ + public CommentId Id { get; set; } + public AssetId AssetId { get; set; } + public UserId UserId { get; set; } + public required string Content { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public Comment? ParentComment { get; set; } + public IReadOnlyList Replies { get; set; } = []; +} diff --git a/Asset Store/AssetStore.slnx b/Asset Store/AssetStore.slnx new file mode 100644 index 0000000..268f27a --- /dev/null +++ b/Asset Store/AssetStore.slnx @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/Asset Store/AssetStore/AssetStore.Api.csproj b/Asset Store/AssetStore/AssetStore.Api.csproj new file mode 100644 index 0000000..5fa94a2 --- /dev/null +++ b/Asset Store/AssetStore/AssetStore.Api.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/Asset Store/AssetStore/AssetStore.http b/Asset Store/AssetStore/AssetStore.http new file mode 100644 index 0000000..2e4293e --- /dev/null +++ b/Asset Store/AssetStore/AssetStore.http @@ -0,0 +1,6 @@ +@AssetStore_HostAddress = http://localhost:5274 + +GET {{AssetStore_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Asset Store/AssetStore/Controllers/AssetController.cs b/Asset Store/AssetStore/Controllers/AssetController.cs new file mode 100644 index 0000000..4ea29c8 --- /dev/null +++ b/Asset Store/AssetStore/Controllers/AssetController.cs @@ -0,0 +1,112 @@ +using AssetStore.Database.Models; +using AssetStore.Services.Asset; +using Microsoft.AspNetCore.Mvc; +using SharpEngine.Shared.Dto.AssetStore; + +namespace AssetStore.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AssetController : ControllerBase + { + private readonly ILogger _logger; + private readonly IAssetService _service; + + public AssetController(ILogger logger, IAssetService service) + { + _logger = logger; + _service = service; + } + + [HttpGet] + public ActionResult> GetAllAssets() + { + _logger.LogDebug("Retrieving assets."); + + var assets = _service.GetAssets(); + _logger.LogDebug("Found {Count} assets.", assets.Count()); + + return StatusCode(StatusCodes.Status200OK, assets); + } + + [HttpGet("{assetId}")] + public ActionResult GetById(Guid assetId) + { + _logger.LogDebug("Retrieving asset with id '{assetId}'.", assetId); + var asset = _service.GetAsset(assetId); + + if (asset is null) + return StatusCode(StatusCodes.Status204NoContent, "Unable to find asset."); + + return StatusCode(StatusCodes.Status200OK, asset); + } + + [HttpGet("/keyword")] + public ActionResult> GetAllAssetsByKeyWord([FromBody] IReadOnlyList keywords) + { + var joinedKeyWords = string.Join(", ", keywords); + _logger.LogDebug("Retrieving assets for the given keywords: {KeyWords}.", joinedKeyWords); + + var assets = _service.GetAssets(keywords); + _logger.LogDebug("Found {Count} assets for the given keywords: {KeyWords}.", assets.Count(), joinedKeyWords); + + if (!assets.Any()) + return StatusCode(StatusCodes.Status204NoContent, $"Unable to find assets with keywords '{joinedKeyWords}'."); + + return StatusCode(StatusCodes.Status200OK, assets); + } + + [HttpGet("/author/{author}")] + public ActionResult> GetAllAssetsByAuthor(Guid authorId) + { + _logger.LogDebug("Retrieving assets for author with id: '{Author}'.", authorId); + + var assets = _service.GetAssets(authorId); + _logger.LogDebug("Found {Count} assets for the given author: '{AuthorId}'.", assets.Count(), authorId); + + if (!assets.Any()) + return StatusCode(StatusCodes.Status204NoContent, $"Unable to find assets by author with id '{authorId}'."); + + return StatusCode(StatusCodes.Status200OK, assets); + } + + [HttpPost] + public async Task> CreateAsset([FromBody] AssetDto asset) + { + _logger.LogDebug("Checking if asset already exists."); + if (asset.Id.Value != Guid.Empty) + { + _logger.LogDebug("Asset already exists."); + return ReviseAsset(asset); + } + + _logger.LogDebug("Creating asset '{Asset}'. Published by '{Author}'.", asset.Name, asset.AuthorId); + var result = await _service.CreateAsset(asset); + + _logger.LogDebug("Successfully created asset '{Asset}'. Published by '{Author}'.", asset.Name, asset.AuthorId); + return result; + } + + [HttpPut] + public AssetDto ReviseAsset([FromBody] AssetDto asset) + { + // TODO: versioning logic + + // TODO: Replace existing with revised asset + var result = _service.ReviseAsset(asset); + + _logger.LogDebug("Successfully revised asset '{Asset}'. Published by '{Author}'.", asset.Name, asset.AuthorId); + return asset; + } + + [HttpDelete] + public async Task DeleteAsset(Guid assetId) + { + bool removed = await _service.DeleteAsset(assetId); + if (!removed) + return StatusCode(StatusCodes.Status404NotFound); + + return StatusCode(StatusCodes.Status200OK, "Asset successfully deleted."); + } + } +} diff --git a/Asset Store/AssetStore/Controllers/CommentController.cs b/Asset Store/AssetStore/Controllers/CommentController.cs new file mode 100644 index 0000000..ca4235c --- /dev/null +++ b/Asset Store/AssetStore/Controllers/CommentController.cs @@ -0,0 +1,30 @@ +using AssetStore.Database.Models; +using Microsoft.AspNetCore.Mvc; +using SharpEngine.Shared.Dto.AssetStore; + +namespace AssetStore.Controllers +{ + [ApiController] + [Route("[controller]")] + public class CommentController : ControllerBase + { + private readonly ILogger _logger; + + public CommentController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + public IEnumerable GetAssetComments(Guid assetId) + { + return new List(); + } + + [HttpPost] + public CommentDto PostComment(Guid assetId, CommentDto comment) + { + return new CommentDto(); + } + } +} diff --git a/Asset Store/AssetStore/Program.cs b/Asset Store/AssetStore/Program.cs new file mode 100644 index 0000000..666a9c5 --- /dev/null +++ b/Asset Store/AssetStore/Program.cs @@ -0,0 +1,23 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Asset Store/AssetStore/Properties/launchSettings.json b/Asset Store/AssetStore/Properties/launchSettings.json new file mode 100644 index 0000000..4c87c24 --- /dev/null +++ b/Asset Store/AssetStore/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5274", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7215;http://localhost:5274", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Asset Store/AssetStore/Services/Asset/AssetService.cs b/Asset Store/AssetStore/Services/Asset/AssetService.cs new file mode 100644 index 0000000..a11c7e6 --- /dev/null +++ b/Asset Store/AssetStore/Services/Asset/AssetService.cs @@ -0,0 +1,88 @@ +using AssetStore.Database.Models; +using Microsoft.EntityFrameworkCore; +using SharpEngine.Shared.Dto.AssetStore; +using SharpEngine.Shared.Dto.Primitives; + +namespace AssetStore.Services.Asset; + +public class AssetService : IAssetService +{ + private readonly ILogger _logger; + private readonly Database.DatabaseContext _db; + + public AssetService(ILogger _logger, Database.DatabaseContext db) + { + _db = db; + } + + /// + /// + public async Task CreateAsset(AssetDto asset) + { + _logger.LogDebug("Creating asset."); + + var model = (Asset)asset; + await _db.Assets.AddAsync(model); + await _db.SaveChangesAsync(); + + return asset; + } + + /// + public async Task DeleteAsset(AssetId assetId) + { + var asset = await _db.Assets.FirstOrDefaultAsync(asset => asset.Id.Value == assetId.Value); + if (asset is null) + return false; + + _db.Assets.Remove(asset); + await _db.SaveChangesAsync(); + + return true; + } + + /// + public async Task GetAsset(AssetId assetId) + { + var asset = await _db.Assets.FirstOrDefaultAsync(asset => asset.Id.Value == assetId.Value); + return (AssetDto?)asset; + } + + /// + public IEnumerable GetAssets() + { + var assets = _db.Assets.AsNoTracking().Select(asset => (AssetDto)asset); + return assets; + } + + /// + public IEnumerable GetAssets(IReadOnlyList keywords) + { + var assets = _db.Assets.AsNoTracking() + .Where(asset => asset.KeyWords.Any(keyword => keywords.Contains(keyword))) + .Select(asset => (AssetDto)asset); + + return assets; + } + + /// + public IEnumerable GetAssets(UserId authorId) + { + var assets = _db.Assets.AsNoTracking() + .Where(asset => asset.AuthorId.Value == authorId.Value) + .Select(asset => (AssetDto)asset); + + return assets; + } + + /// + public AssetDto GetByChecksum(string checksum) => throw new NotImplementedException(); + + /// + public AssetDto ReviseAsset(AssetDto asset) + { + asset.Version = new Version(asset.Version.Major, asset.Version.Minor + 1); + + throw new NotImplementedException(); + } +} diff --git a/Asset Store/AssetStore/Services/Asset/IAssetService.cs b/Asset Store/AssetStore/Services/Asset/IAssetService.cs new file mode 100644 index 0000000..ad4df4c --- /dev/null +++ b/Asset Store/AssetStore/Services/Asset/IAssetService.cs @@ -0,0 +1,17 @@ +using AssetStore.Database.Models; +using SharpEngine.Shared.Dto.AssetStore; +using SharpEngine.Shared.Dto.Primitives; + +namespace AssetStore.Services.Asset; + +public interface IAssetService +{ + Task CreateAsset(AssetDto asset); + Task DeleteAsset(AssetId assetId); + Task GetAsset(AssetId assetId); + IEnumerable GetAssets(); + IEnumerable GetAssets(IReadOnlyList keywords); + IEnumerable GetAssets(UserId authorId); + AssetDto GetByChecksum(string checksum); + AssetDto ReviseAsset(AssetDto asset); +} \ No newline at end of file diff --git a/Asset Store/AssetStore/appsettings.Development.json b/Asset Store/AssetStore/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Asset Store/AssetStore/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Asset Store/AssetStore/appsettings.json b/Asset Store/AssetStore/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Asset Store/AssetStore/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Asset Store/Directory.Build.props b/Asset Store/Directory.Build.props new file mode 100644 index 0000000..e69de29 diff --git a/Asset Store/Readme.md b/Asset Store/Readme.md new file mode 100644 index 0000000..e69de29 diff --git a/Asset Store/Tests/AssetStore.API.Tests/AssetStore.Api.Tests.csproj b/Asset Store/Tests/AssetStore.API.Tests/AssetStore.Api.Tests.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/Asset Store/Tests/AssetStore.API.Tests/AssetStore.Api.Tests.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/Asset Store/Tests/AssetStore.API.Tests/Controllers/AssetControllerTests.cs b/Asset Store/Tests/AssetStore.API.Tests/Controllers/AssetControllerTests.cs new file mode 100644 index 0000000..48fe198 --- /dev/null +++ b/Asset Store/Tests/AssetStore.API.Tests/Controllers/AssetControllerTests.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AssetStore.Api.Tests.Controllers; + +internal class AssetControllerTests +{ +} diff --git a/Asset Store/Tests/AssetStore.API.Tests/Controllers/CommentControllerTests.cs b/Asset Store/Tests/AssetStore.API.Tests/Controllers/CommentControllerTests.cs new file mode 100644 index 0000000..1180378 --- /dev/null +++ b/Asset Store/Tests/AssetStore.API.Tests/Controllers/CommentControllerTests.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AssetStore.Api.Tests.Controllers; + +internal class CommentControllerTests +{ +} diff --git a/Asset Store/Tests/AssetStore.API.Tests/Services/AssetServiceTests.cs b/Asset Store/Tests/AssetStore.API.Tests/Services/AssetServiceTests.cs new file mode 100644 index 0000000..44d8123 --- /dev/null +++ b/Asset Store/Tests/AssetStore.API.Tests/Services/AssetServiceTests.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AssetStore.Api.Tests.Services; + +internal class AssetServiceTests +{ +} diff --git a/Asset Store/Tests/AssetStore.API.Tests/Services/CommentServiceTests.cs b/Asset Store/Tests/AssetStore.API.Tests/Services/CommentServiceTests.cs new file mode 100644 index 0000000..93698f0 --- /dev/null +++ b/Asset Store/Tests/AssetStore.API.Tests/Services/CommentServiceTests.cs @@ -0,0 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace AssetStore.Api.Tests.Services; + +internal class CommentServiceTests +{ +} diff --git a/Asset Store/Tests/Directory.Build.props b/Asset Store/Tests/Directory.Build.props new file mode 100644 index 0000000..e69de29 diff --git a/Core/Debug.cs b/Core/Debug.cs deleted file mode 100644 index e3fa033..0000000 --- a/Core/Debug.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Serilog; -using System; - -namespace SharpEngine.Core; - -/// -/// Contains methods for debugging the application. -/// -public static class Debug -{ - /// - /// Logs an information message. - /// - /// The message to be logged. - /// The possible exception. - /// Determines whether the log should be written to a file. - public static void LogInformation(string message, Exception? ex = null, bool writeToFile = false) - { - Console.WriteLine(message); - - if (!writeToFile) - return; - - var log = new LoggerConfiguration() - .WriteTo.Console() - .CreateLogger(); - - log.Information(ex, message); - } -} \ No newline at end of file diff --git a/Core/Entities/GameObject.cs b/Core/Entities/GameObject.cs deleted file mode 100644 index b69e257..0000000 --- a/Core/Entities/GameObject.cs +++ /dev/null @@ -1,106 +0,0 @@ -using SharpEngine.Core.Attributes; -using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.Extensions; -using SharpEngine.Core.Scenes; -using SharpEngine.Core.Shaders; -using SharpEngine.Core.Textures; - -using Silk.NET.OpenGL; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using System.Threading.Tasks; - -namespace SharpEngine.Core.Entities; - -/// -/// Represents a game object in the scene. -/// -public class GameObject : SceneNode -{ - /// - /// Initializes a new instance of the . - /// - public GameObject() - { - var shader = ShaderService.Instance.LoadShader(PathExtensions.GetAssemblyPath("Shaders/shader.vert"), PathExtensions.GetAssemblyPath("Shaders/lighting.frag"), "lighting"); - var diffuse = TextureService.Instance.LoadTexture(PathExtensions.GetAssemblyPath("Textures/DefaultTextures/debug.JPG")); - Material = new(shader, diffuse); - - BoundingBox = BoundingBox.CalculateBoundingBox(Transform); - } - - /// - /// Initializes a new instance of the with specified textures and shaders. - /// - /// The file path of the diffuse map texture. - /// The file path of the specular map texture. - /// The file path of the vertex shader. - /// The file path of the fragment shader. - public GameObject(string diffuseMapFile, string specularMapFile, string vertShaderFile, string fragShaderFile) - { - var diffuse = TextureService.Instance.LoadTexture(diffuseMapFile); - var specularMap = TextureService.Instance.LoadTexture(specularMapFile); - var shader = ShaderService.Instance.LoadShader(vertShaderFile, fragShaderFile, "lighting"); - - Material = new Material(shader, diffuse) - { - SpecularMap = specularMap - }; - - BoundingBox = BoundingBox.CalculateBoundingBox(Transform); - } - - /// - /// Gets or sets the mesh of the game object. - /// - public List Mesh { get; set; } = []; - - /// - /// Gets or sets the material of the game object. - /// - [Inspector(DisplayInInspector = false)] - public Material Material { get; set; } - - private Transform _transform = new(); - - /// - /// Gets or sets the transform of the game object. - /// - public Transform Transform - { - get => _transform; - set - { - _transform = value; - BoundingBox = BoundingBox.CalculateBoundingBox(_transform); - } - } - - /// - /// Gets the bounding box of the game object. - /// - [Inspector(DisplayInInspector = false)] - public BoundingBox BoundingBox { get; set; } - - /// - public override Task Render() - { - Material.DiffuseMap.Use(TextureUnit.Texture0); - Material.Shader.SetInt("material.diffuse", Material.diffuseUnit); - - if (Material.SpecularMap is not null) - { - Material.SpecularMap.Use(TextureUnit.Texture1); - Material.Shader.SetInt("material.specular", Material.specularUnit); - Material.Shader.SetVector3("material.specular", Material.Specular); - Material.Shader.SetFloat("material.shininess", Material.Shininess); - } - - Material.Shader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); - Window.GL.DrawArrays(PrimitiveType.Triangles, 0, 36); - - return Task.CompletedTask; - } -} diff --git a/Core/Entities/Properties/Material.cs b/Core/Entities/Properties/Material.cs deleted file mode 100644 index b30f02b..0000000 --- a/Core/Entities/Properties/Material.cs +++ /dev/null @@ -1,51 +0,0 @@ -using SharpEngine.Core.Shaders; -using SharpEngine.Core.Textures; -using System.Numerics; - -namespace SharpEngine.Core.Entities.Properties; - -/// -/// Represents the material rendered onto a game object. -/// -public class Material -{ - /// - /// Initializes a new instance of . - /// - public Material(Shader shader, Texture diffuseMap) - { - Shader = shader; - DiffuseMap = diffuseMap; - - Specular = new(0.5f, 0.5f, 0.5f); - Shininess = 32.0f; - } - - /// - /// Gets or sets the diffuse map texture. - /// - public Texture DiffuseMap { get; set; } - - /// - /// Gets or sets the specular map texture. - /// - public Texture? SpecularMap { get; set; } - - /// - /// Gets or sets the shader used to render the material. - /// - public Shader Shader { get; set; } - - internal readonly int diffuseUnit = 0; - internal readonly int specularUnit = 1; - - /// - /// Gets or sets the ambient color of the material. - /// - public Vector3 Specular { get; set; } - - /// - /// Gets or sets the shininess of the material. - /// - public float Shininess { get; set; } -} diff --git a/Core/Entities/Properties/Meshes/Mesh.cs b/Core/Entities/Properties/Meshes/Mesh.cs deleted file mode 100644 index 5685204..0000000 --- a/Core/Entities/Properties/Meshes/Mesh.cs +++ /dev/null @@ -1,30 +0,0 @@ -using SharpEngine.Core.Attributes; - -namespace SharpEngine.Core.Entities.Properties.Meshes; - -/// -/// Represents a game object mesh. -/// -public class Mesh -{ - /// Gets or sets a indetifying name for the mesh. - public string Name { get; set; } = "New Mesh"; - - /// - /// Gets or sets the mesh vertices. - /// - [Inspector(DisplayInInspector = false)] - public float[] Vertices { get; set; } = []; - - /// - /// Gets or sets the mesh normals. - /// - [Inspector(DisplayInInspector = false)] - public float[] Normals { get; set; } = []; - - /// - /// Gets or sets the mesh texture UV coordinates. - /// - [Inspector(DisplayInInspector = false)] - public float[] TextureCoordinates { get; set; } = []; -} \ No newline at end of file diff --git a/Core/Entities/UIElement.cs b/Core/Entities/UIElement.cs deleted file mode 100644 index 2ba6372..0000000 --- a/Core/Entities/UIElement.cs +++ /dev/null @@ -1,89 +0,0 @@ -using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.Scenes; -using SharpEngine.Core.Shaders; - -using Silk.NET.OpenGL; -using System.Threading.Tasks; - -namespace SharpEngine.Core.Entities; - -/// -/// Represents a User Interface entity. -/// -public class UIElement : SceneNode -{ - /// - /// Initializes a new instance of . - /// - /// The name of the UI element. - public UIElement(string name) - { - Name = name; - Mesh = MeshService.Instance.LoadMesh("plane", Primitives.Plane.Mesh); - } - - private readonly UIShader _uIShader = new(); - - /// Gets or sets the 2D space transformation of the UI element. - public Transform2D Transform { get; set; } = new() - { - Rotation = 180 - }; - - // TODO: Use actual mesh - /// Gets or sets the mesh of the UI element. - public Mesh Mesh { get; set; } = new(); - - private readonly float[] _vertices = - [ - // Position Texture coordinates - 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // top right - 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom right - -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom left - -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // top left - ]; - - private readonly uint[] _indices = - [ - 0, 1, 3, - 1, 2, 3 - ]; - - private uint _vertexArrayObject; - - /// - public void Initialize() - { - _vertexArrayObject = Window.GL.GenVertexArray(); - Window.GL.BindVertexArray(_vertexArrayObject); - - InitializeBuffers(); - } - - private void InitializeBuffers() - { - var vertexBufferObject = Window.GL.GenBuffer(); - Window.GL.BindBuffer(GLEnum.ArrayBuffer, vertexBufferObject); - Window.GL.BufferData(GLEnum.ArrayBuffer, (uint)_vertices.Length * sizeof(float), _vertices, GLEnum.StaticDraw); - - var elementBufferObject = Window.GL.GenBuffer(); - Window.GL.BindBuffer(GLEnum.ElementArrayBuffer, elementBufferObject); - Window.GL.BufferData(GLEnum.ElementArrayBuffer, (uint)_indices.Length * sizeof(uint), _indices, GLEnum.StaticDraw); - } - - /// - /// Render the UI element. - /// - public override Task Render() - { - Window.GL.BindVertexArray(_vertexArrayObject); - - _uIShader.Shader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); - - uint a = 0; - Window.GL.DrawElements(PrimitiveType.Triangles, (uint)_indices.Length, DrawElementsType.UnsignedInt, ref a); - - return Task.CompletedTask; - } -} diff --git a/Core/GlobalSuppressions.cs b/Core/GlobalSuppressions.cs deleted file mode 100644 index b944386..0000000 --- a/Core/GlobalSuppressions.cs +++ /dev/null @@ -1,8 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Minor Code Smell", "S2344:Enumeration type names should not have \"Flags\" or \"Enum\" suffixes", Justification = "", Scope = "type", Target = "~T:SharpEngine.Core.Renderers.RenderFlags")] diff --git a/Core/Primitives/Cube.cs b/Core/Primitives/Cube.cs deleted file mode 100644 index 6db45cc..0000000 --- a/Core/Primitives/Cube.cs +++ /dev/null @@ -1,146 +0,0 @@ -using SharpEngine.Core.Entities.Properties.Meshes; - -namespace SharpEngine.Core.Primitives; - -/// -/// Used to create a primitive cube object. -/// -public static class Cube -{ - /// The cube mesh. - public static readonly Mesh Mesh = new() - { - Vertices = - [ - -0.5f, -0.5f, -0.5f, - 0.5f, -0.5f, -0.5f, - 0.5f, 0.5f, -0.5f, - 0.5f, 0.5f, -0.5f, - -0.5f, 0.5f, -0.5f, - -0.5f, -0.5f, -0.5f, - - -0.5f, -0.5f, 0.5f, - 0.5f, -0.5f, 0.5f, - 0.5f, 0.5f, 0.5f, - 0.5f, 0.5f, 0.5f, - -0.5f, 0.5f, 0.5f, - -0.5f, -0.5f, 0.5f, - - -0.5f, 0.5f, 0.5f, - -0.5f, 0.5f, -0.5f, - -0.5f, -0.5f, -0.5f, - -0.5f, -0.5f, -0.5f, - -0.5f, -0.5f, 0.5f, - -0.5f, 0.5f, 0.5f, - - 0.5f, 0.5f, 0.5f, - 0.5f, 0.5f, -0.5f, - 0.5f, -0.5f, -0.5f, - 0.5f, -0.5f, -0.5f, - 0.5f, -0.5f, 0.5f, - 0.5f, 0.5f, 0.5f, - - -0.5f, -0.5f, -0.5f, - 0.5f, -0.5f, -0.5f, - 0.5f, -0.5f, 0.5f, - 0.5f, -0.5f, 0.5f, - -0.5f, -0.5f, 0.5f, - -0.5f, -0.5f, -0.5f, - - -0.5f, 0.5f, -0.5f, - 0.5f, 0.5f, -0.5f, - 0.5f, 0.5f, 0.5f, - 0.5f, 0.5f, 0.5f, - -0.5f, 0.5f, 0.5f, - -0.5f, 0.5f, -0.5f, - ], - 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, - 0.0f, 0.0f, -1.0f, - 0.0f, 0.0f, -1.0f, - - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - - -1.0f, 0.0f, 0.0f, - -1.0f, 0.0f, 0.0f, - -1.0f, 0.0f, 0.0f, - -1.0f, 0.0f, 0.0f, - -1.0f, 0.0f, 0.0f, - -1.0f, 0.0f, 0.0f, - - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - - 0.0f, -1.0f, 0.0f, - 0.0f, -1.0f, 0.0f, - 0.0f, -1.0f, 0.0f, - 0.0f, -1.0f, 0.0f, - 0.0f, -1.0f, 0.0f, - 0.0f, -1.0f, 0.0f, - - 0.0f, 1.0f, 0.0f, - 0.0f, 1.0f, 0.0f, - 0.0f, 1.0f, 0.0f, - 0.0f, 1.0f, 0.0f, - 0.0f, 1.0f, 0.0f, - 0.0f, 1.0f, 0.0f, - ], - TextureCoordinates = - [ - 0.0f, 0.0f, - 1.0f, 0.0f, - 1.0f, 1.0f, - 1.0f, 1.0f, - 0.0f, 1.0f, - 0.0f, 0.0f, - - 0.0f, 0.0f, - 1.0f, 0.0f, - 1.0f, 1.0f, - 1.0f, 1.0f, - 0.0f, 1.0f, - 0.0f, 0.0f, - - 1.0f, 0.0f, - 1.0f, 1.0f, - 0.0f, 1.0f, - 0.0f, 1.0f, - 0.0f, 0.0f, - 1.0f, 0.0f, - - 1.0f, 0.0f, - 1.0f, 1.0f, - 0.0f, 1.0f, - 0.0f, 1.0f, - 0.0f, 0.0f, - 1.0f, 0.0f, - - 0.0f, 1.0f, - 1.0f, 1.0f, - 1.0f, 0.0f, - 1.0f, 0.0f, - 0.0f, 0.0f, - 0.0f, 1.0f, - - 0.0f, 1.0f, - 1.0f, 1.0f, - 1.0f, 0.0f, - 1.0f, 0.0f, - 0.0f, 0.0f, - 0.0f, 1.0f - ] - }; -} diff --git a/Core/Scenes/SceneNode.cs b/Core/Scenes/SceneNode.cs deleted file mode 100644 index 5644ac3..0000000 --- a/Core/Scenes/SceneNode.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace SharpEngine.Core.Scenes; - -/// -/// Represents a node in the scene. -/// -public class SceneNode -{ - /// - /// Gets or sets the name of the node. - /// - public string Name { get; set; } = "New Object"; - - /// - /// Gets or sets the children of the node. - /// - public List Children { get; set; } = new(); - - /// - /// Initializes a new empty . - /// - public SceneNode() { } - - /// - /// Initializes a new instance of the with the specified . - /// - /// The name of the new empty node. - public SceneNode(string name) - { - Name = name; - } - - /// - /// Adds an empty child node to this node by the given . - /// - /// The name of the empty node to be added. - /// The created node. - public SceneNode AddChild(string name) - { - var node = new SceneNode(name); - Children.Add(node); - - return node; - } - - /// - /// Adds a child node to this node. - /// - /// The nodes to be added. - /// The current node. - public virtual SceneNode AddChild(params SceneNode[] nodes) - { - foreach (var node in nodes) - Children.Add(node); - - return this; - } - - /// - /// Removes a child node from this node. - /// - /// The node to be removed. - public void RemoveChild(SceneNode node) - { - Children.Remove(node); - } - - /// - /// Renders the current object to the screen. - /// - /// A representing an asynchronous operation. - public virtual Task Render() => Task.CompletedTask; -} diff --git a/Core/Shaders/LampShader.cs b/Core/Shaders/LampShader.cs deleted file mode 100644 index ced37b6..0000000 --- a/Core/Shaders/LampShader.cs +++ /dev/null @@ -1,31 +0,0 @@ -using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Extensions; -using Silk.NET.OpenGL; - -namespace SharpEngine.Core.Shaders; - -internal class LampShader : ShaderBase -{ - public LampShader() - { - Shader = ShaderService.Instance.LoadShader(PathExtensions.GetAssemblyPath("Shaders/shader.vert"), PathExtensions.GetAssemblyPath("Shaders/shader.frag"), "lamp"); - - Vao = Window.GL.GenVertexArray(); - Window.GL.BindVertexArray(Vao); - - SetAttributes(); - } - - /// - public override bool SetAttributes() - { - if (!base.SetAttributes()) - return false; - - var positionLocation = (uint)Shader!.GetAttribLocation(ShaderAttributes.Pos); - Window.GL.EnableVertexAttribArray(positionLocation); - Window.GL.VertexAttribPointer(positionLocation, 3, VertexAttribPointerType.Float, false, VertexData.Stride, 0); - - return true; - } -} diff --git a/Core/Shaders/LightingShader.cs b/Core/Shaders/LightingShader.cs deleted file mode 100644 index 340a003..0000000 --- a/Core/Shaders/LightingShader.cs +++ /dev/null @@ -1,39 +0,0 @@ -using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Extensions; -using Silk.NET.OpenGL; - -namespace SharpEngine.Core.Shaders; - -internal class LightingShader : ShaderBase -{ - public LightingShader() - { - Shader = ShaderService.Instance.LoadShader(PathExtensions.GetAssemblyPath("Shaders/shader.vert"), PathExtensions.GetAssemblyPath("Shaders/lighting.frag"), "lighting"); - - Vao = Window.GL.GenVertexArray(); - Window.GL.BindVertexArray(Vao); - - SetAttributes(); - } - - /// - public override bool SetAttributes() - { - if (!base.SetAttributes()) - return false; - - var positionLocation = (uint)Shader!.GetAttribLocation(ShaderAttributes.Pos); - Window.GL.EnableVertexAttribArray(positionLocation); - Window.GL.VertexAttribPointer(positionLocation, VertexData.VerticesSize, VertexAttribPointerType.Float, false, VertexData.Stride, 0); - - var normalLocation = (uint)Shader.GetAttribLocation(ShaderAttributes.Normal); - Window.GL.EnableVertexAttribArray(normalLocation); - Window.GL.VertexAttribPointer(normalLocation, VertexData.NormalsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.NormalsOffset); - - var texCoordLocation = (uint)Shader.GetAttribLocation(ShaderAttributes.TexCoords); - Window.GL.EnableVertexAttribArray(texCoordLocation); - Window.GL.VertexAttribPointer(texCoordLocation, VertexData.TexCoordsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.TexCoordsOffset); - - return true; - } -} diff --git a/Core/Shaders/Shader.cs b/Core/Shaders/Shader.cs deleted file mode 100644 index 4c7b94a..0000000 --- a/Core/Shaders/Shader.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.IO; -using System.Collections.Generic; - -using System.Text.RegularExpressions; -using System.Numerics; - -using Silk.NET.OpenGL; - -namespace SharpEngine.Core.Shaders; - -/// -/// Represents a shader program. -/// -public class Shader -{ - /// Gets the handle to the shader program. - public readonly uint Handle; - - /// Gets or sets the identifying name of the shader. - public string Name { get; set; } - - private Dictionary _uniformLocations = []; - - /// - /// Initializes a new instance of . - /// - /// - /// Shaders are written in GLSL, which is a language very similar to C in its semantics. - /// The GLSL source is compiled *at runtime*, so it can optimize itself for the graphics card it's currently being used on. - /// - /// The vertex shader full path. - /// The fragment shader full path. - /// The identfier name of the shader. - public Shader(string vertPath, string fragPath, string name) - { - Name = name; - - // 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. - // The fragment shader is responsible for then converting the vertices to "fragments", which represent all the data OpenGL needs to draw a pixel. - // The fragment shader is what we'll be using the most here. - - // Load and compile shader - if (!LoadShader(ShaderType.VertexShader, vertPath, out uint vertexShader)) - return; - - if (!LoadShader(ShaderType.FragmentShader, fragPath, out uint fragmentShader)) - 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 = Window.GL.CreateProgram(); - - // Attach both shaders... - Window.GL.AttachShader(Handle, vertexShader); - Window.GL.AttachShader(Handle, fragmentShader); - - // And then link them together. - 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. - Window.GL.DetachShader(Handle, vertexShader); - Window.GL.DetachShader(Handle, fragmentShader); - Window.GL.DeleteShader(fragmentShader); - Window.GL.DeleteShader(vertexShader); - - // 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. - - GetUniformLocations(); - } - - private static bool LoadShader(ShaderType shaderType, string shaderPath, out uint shader) - { - if (!File.Exists(shaderPath)) - { - Console.WriteLine($"Shader file not found: {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 = Window.GL.CreateShader(shaderType); - Window.GL.ShaderSource(shader, shaderSource); - CompileShader(shader); - - return true; - } - - private void GetUniformLocations() - { - // First, we have to get the number of active uniforms in the shader. - Window.GL.GetProgram(Handle, GLEnum.ActiveUniforms, out var numberOfUniforms); - - // Next, allocate the dictionary to hold the locations. - _uniformLocations = new Dictionary(); - - // Loop over all the uniforms, - for (uint i = 0; i < numberOfUniforms; i++) - { - // get the name of this uniform, - var key = Window.GL.GetActiveUniform(Handle, i, out _, out _); - - // get the location, - var location = Window.GL.GetUniformLocation(Handle, key); - - // and then add it to the dictionary. - _uniformLocations.Add(key, location); - } - } - - private static string ProcessIncludes(string shaderCode, string directory) - { - 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)!); - }); - } - - private static void CompileShader(uint shader) - { - // Try to compile the shader - Window.GL.CompileShader(shader); - - // Check for compilation errors - Window.GL.GetShader(shader, GLEnum.CompileStatus, out var code); - if (code != (int)GLEnum.True) - { - // We can use `GL.GetShaderInfoLog(shader)` to get information about the error. - var infoLog = Window.GL.GetShaderInfoLog(shader); - throw new InvalidOperationException($"Error occurred whilst compiling Shader({shader}).\n\n{infoLog}"); - } - } - - private static void LinkProgram(uint program) - { - Window.GL.LinkProgram(program); - - Window.GL.GetProgram(program, GLEnum.LinkStatus, out var code); - if (code != (int)GLEnum.True) - { - string infoLog = Window.GL.GetProgramInfoLog(program); - throw new InvalidOperationException($"Error occurred whilst linking Program({program}): {infoLog}"); - } - } - - /// - /// Enables the shader program. - /// - public void Use() - => Window.GL.UseProgram(Handle); - - /// - /// Checks if the shader attribute exists within the current shader. - /// - /// The name of the attribute that's being looked for. - /// If the attribute exists, the location of the attribute in the shader; otherwise -1. - public int GetAttribLocation(string attribName) - { - int location = Window.GL.GetAttribLocation(Handle, attribName); - if (location == -1) - Console.WriteLine($"Attribute '{attribName}' not found in shader program."); - - return location; - } - - // 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. - - // Setting a uniform is almost always the exact same, so I'll explain it here once, instead of in every method: - // 1. Bind the program you want to set the uniform on - // 2. Get a handle to the location of the uniform with GL.GetUniformLocation. - // 3. Use the appropriate GL.Uniform* function to set the uniform. - - /// - /// Set a uniform int on this shader. - /// - /// The name of the uniform. - /// The data to set. - public void SetInt(string name, int data) - { - Window.GL.UseProgram(Handle); - Window.GL.Uniform1(_uniformLocations[name], data); - } - - /// - /// Set a uniform float on this shader. - /// - /// The name of the uniform. - /// The data to set. - public void SetFloat(string name, float data) - { - Window.GL.UseProgram(Handle); - Window.GL.Uniform1(_uniformLocations[name], data); - } - - /// - /// Set a uniform Matrix4 on this shader - /// - /// The name of the uniform. - /// The data to set. - /// Determines whether or not the matrix should be transposed. Defaults to . - /// - /// - /// The matrix is transposed before being sent to the shader unless is set to . - /// - /// - public void SetMatrix4(string name, Matrix4x4 data, bool transpose = true) - { - Window.GL.UseProgram(Handle); - Window.GL.UniformMatrix4(_uniformLocations[name], transpose, data.ToSpan()); - } - - /// - /// Set a uniform Vector3 on this shader. - /// - /// The name of the uniform. - /// The data to set. - public void SetVector3(string name, Vector3 data) - { - Window.GL.UseProgram(Handle); - Window.GL.Uniform3(_uniformLocations[name], data); - } -} diff --git a/Core/Shaders/UIShader.cs b/Core/Shaders/UIShader.cs deleted file mode 100644 index 9e049a1..0000000 --- a/Core/Shaders/UIShader.cs +++ /dev/null @@ -1,29 +0,0 @@ -using SharpEngine.Core.Extensions; -using Silk.NET.OpenGL; - -namespace SharpEngine.Core.Shaders; - -internal class UIShader : ShaderBase -{ - public UIShader() - { - Shader = new Shader(PathExtensions.GetAssemblyPath("Shaders/uiShader.vert"), PathExtensions.GetAssemblyPath("Shaders/uiShader.frag"), "ui"); - } - - /// - public override bool SetAttributes() - { - if (!base.SetAttributes()) - return false; - - var vertexLocation = Shader!.GetAttribLocation(ShaderAttributes.Pos); - if (vertexLocation == ShaderAttributes.AttributeLocationNotFound) - return false; - - uint vertexLocationUInt = (uint)vertexLocation; - Window.GL.EnableVertexAttribArray(vertexLocationUInt); - Window.GL.VertexAttribPointer(vertexLocationUInt, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), 0); - - return true; - } -} diff --git a/Core/Shaders/shader.frag b/Core/Shaders/shader.frag deleted file mode 100644 index 1c649f4..0000000 --- a/Core/Shaders/shader.frag +++ /dev/null @@ -1,7 +0,0 @@ -#version 330 core -out vec4 FragColor; - -void main() -{ - FragColor = vec4(1.0); // set all 4 vector values to 1.0 -} \ No newline at end of file diff --git a/Core/Shaders/uiShader.frag b/Core/Shaders/uiShader.frag deleted file mode 100644 index fe3175f..0000000 --- a/Core/Shaders/uiShader.frag +++ /dev/null @@ -1,7 +0,0 @@ -#version 330 core -out vec4 FragColor; - -void main() -{ - FragColor = vec4(1.0, 0.0, 0.0, 1.0); // set all 4 vector values to 1.0 -} \ No newline at end of file diff --git a/Core/Shaders/uiShader.vert b/Core/Shaders/uiShader.vert deleted file mode 100644 index aa9fdcc..0000000 --- a/Core/Shaders/uiShader.vert +++ /dev/null @@ -1,15 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 aPos; - -uniform mat4 model; - -out vec3 FragPos; - -void main() -{ - vec4 pos = vec4(aPos, 1.0); - - gl_Position = model * pos; - FragPos = vec3(model * pos); -} \ No newline at end of file diff --git a/Core/SharpEngine.Core.csproj b/Core/SharpEngine.Core.csproj deleted file mode 100644 index e93fd8f..0000000 --- a/Core/SharpEngine.Core.csproj +++ /dev/null @@ -1,54 +0,0 @@ - - - - Everything needed to build games using a purely C# game engine. - - - AnyCPU;x64;x86 - $(GenerateNuGetPackages) - https://github.com/Av3boy/SharpEngine - game-engine; - True - - - $(AssemblyName).NET - Debug;Release;LauncherNonWindows - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - PreserveNewest - - - - \ No newline at end of file diff --git a/Core/Textures/Texture.cs b/Core/Textures/Texture.cs deleted file mode 100644 index 689b1a7..0000000 --- a/Core/Textures/Texture.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Silk.NET.OpenGL; - -namespace SharpEngine.Core.Textures; - -/// -/// Represents a texture program. -/// -public class Texture -{ - /// The OpenGL handle for the texture. - public readonly uint Handle; - - /// - /// Initializes a new instance of . - /// - /// The handle to the texture. - public Texture(uint glHandle) - { - Handle = glHandle; - } - - // Multiple textures can be bound, if your shader needs more than just one. - // If you want to do that, use GL.ActiveTexture to set which slot GL.BindTexture binds to. - // The OpenGL standard requires that there be at least 16, but there can be more depending on your graphics card. - - /// - /// Activate the texture. - /// - /// The bound texture location. - public void Use(TextureUnit unit) - { - Window.GL.ActiveTexture(unit); - Window.GL.BindTexture(TextureTarget.Texture2D, Handle); - } -} diff --git a/Core/Textures/TextureService.cs b/Core/Textures/TextureService.cs deleted file mode 100644 index ae0e056..0000000 --- a/Core/Textures/TextureService.cs +++ /dev/null @@ -1,74 +0,0 @@ -using StbImageSharp; - -using System.IO; -using System.Collections.Generic; -using System; - -using Silk.NET.OpenGL; - -namespace SharpEngine.Core.Textures; - -/// -/// Represents a service for loading textures. -/// -public class TextureService -{ - /// - /// Gets the singleton instance of . - /// - public static TextureService Instance { get; } = new TextureService(); - private readonly Dictionary _textureCache = []; - - // Private constructor to prevent instantiation - private TextureService() { } - - /// - /// Loads a texture from the specified path. - /// - /// the full path to the texture. - /// The loaded texture program. - public Texture LoadTexture(string path) - { - if (string.IsNullOrWhiteSpace(path)) - throw new FileNotFoundException("No texture file provided."); - - // Check if the texture is already in the cache - if (_textureCache.TryGetValue(path, out var cachedTexture)) - return cachedTexture; - - // Generate handle - uint handle = Window.GL.GenTexture(); - - // Bind the handle - Window.GL.ActiveTexture(TextureUnit.Texture0); - Window.GL.BindTexture(TextureTarget.Texture2D, handle); - - // OpenGL has its texture origin in the lower left corner instead of the top left corner, - // so we tell StbImageSharp to flip the image when loading. - StbImage.stbi_set_flip_vertically_on_load(1); - - // Here we open a stream to the file and pass it to StbImageSharp to load. - using (Stream stream = File.OpenRead(path)) - { - var image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); - - // Generate a texture - Window.GL.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, (uint)image.Width, (uint)image.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, image.Data.AsSpan()); - } - - // Set texture parameters - Window.GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); - Window.GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); - Window.GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); - Window.GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); - - // Generate mipmaps - Window.GL.GenerateMipmap(GLEnum.Texture2D); - - // Create a new texture instance and add it to the cache - var texture = new Texture(handle); - _textureCache[path] = texture; - - return texture; - } -} diff --git a/Editor/EditorWindow.cs b/Editor/EditorWindow.cs deleted file mode 100644 index 92fe757..0000000 --- a/Editor/EditorWindow.cs +++ /dev/null @@ -1,129 +0,0 @@ -using ImGuiNET; -using Launcher.UI; -using Silk.NET.Input; - -using SharpEngine.Core; -using SharpEngine.Core.Entities.Views.Settings; -using SharpEngine.Core.Primitives; -using SharpEngine.Core.Scenes; -using SharpEngine.Editor.Windows; - -using System.Numerics; -using System.Reflection; - -namespace SharpEngine.Editor; - -/// -/// Represents an editor window. -/// -public class EditorWindow : Window -{ - private readonly Project _project; - private readonly List _windows = []; - - private ContextMenuWindow? _contextMenuWindow; - private ActionsMenuWindow? _actionsMenuWindow; - - /// - /// Initializes an editor window with a specified scene and view settings. - /// - /// Represents the current scene to be displayed in the editor window. - /// The project loaded to the editor. - /// Contains configuration options for how the view should be rendered. - public EditorWindow(Scene scene, Project project, IViewSettings settings) : base(scene, settings) - { - _project = project; - } - - /// - public override void OnLoad() - { - base.OnLoad(); - - // Test only: remove later. - var cube = PrimitiveFactory.Create(PrimitiveType.Cube, new Vector3(3, 1, 1)); - cube.Name = "test"; - - Scene.Root.Children.Add(cube); - - // Get all types that inherit from ImGuiWindowBase - var windowTypes = Assembly.GetExecutingAssembly() - .GetTypes() - .Where(t => t.IsSubclassOf(typeof(ImGuiWindowBase)) && !t.IsAbstract) - .ToArray(); - - // Initialize all ImGuiWindowBase implementations - for (int i = 0; i < windowTypes.Length; i++) - { - try - { - var type = windowTypes[i]; - var window = Activator.CreateInstance(type) ?? - throw new InvalidOperationException($"Error initializing window: {type}"); - - var windowBase = (ImGuiWindowBase)window; - windowBase.SetScene(Scene); - windowBase.SetProject(_project); - - _windows.Add(windowBase); - } - catch (Exception e) - { - Debug.LogInformation(e.Message, e); - } - } - - _contextMenuWindow = (ContextMenuWindow?)_windows.FirstOrDefault(w => w.GetType() == typeof(ContextMenuWindow)); - if (_contextMenuWindow is null) - Debug.LogInformation("ContextMenuWindow not found."); - - _actionsMenuWindow = (ActionsMenuWindow?)_windows.FirstOrDefault(w => w.GetType() == typeof(ActionsMenuWindow)); - if (_actionsMenuWindow is null) - Debug.LogInformation("ActionsMenuWindow not found."); - else - _actionsMenuWindow.OnSceneLoaded += SetScene; - } - - /// - protected override void AfterRender(double deltaTime) - { - base.AfterRender(deltaTime); - - // Enable docking - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable; - - // Create a docking space - var viewport = ImGui.GetMainViewport(); - ImGui.SetNextWindowPos(viewport.Pos); - ImGui.SetNextWindowSize(viewport.Size); - ImGui.SetNextWindowViewport(viewport.ID); - ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0.0f); - ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0.0f); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - - ImGui.Begin("DockSpace", ImGuiWindowFlags.NoTitleBar | - ImGuiWindowFlags.NoCollapse | - ImGuiWindowFlags.NoResize | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoBringToFrontOnFocus | - ImGuiWindowFlags.NoNavFocus | ImGuiWindowFlags.AlwaysAutoResize); - ImGui.PopStyleVar(3); - - var dockSpaceId = ImGui.GetID("DockSpace"); - ImGui.DockSpace(dockSpaceId, Vector2.Zero, ImGuiDockNodeFlags.PassthruCentralNode); - - ImGui.End(); - - foreach (var window in _windows) - window.RenderWindow(); - } - - /// - public override void OnMouseClick(IMouse mouse, MouseButton button, Vector2 vector) - { - base.OnMouseClick(mouse, button, vector); - - if (button == MouseButton.Right) - _contextMenuWindow?.ShowContextMenu(); - } -} diff --git a/Editor/Windows/ContextMenuWindow.cs b/Editor/Windows/ContextMenuWindow.cs deleted file mode 100644 index d7ea5ad..0000000 --- a/Editor/Windows/ContextMenuWindow.cs +++ /dev/null @@ -1,61 +0,0 @@ -using ImGuiNET; -using Launcher.UI; -using SharpEngine.Core.Primitives; -using SharpEngine.Core.Scenes; -using System.Numerics; - -namespace SharpEngine.Editor.Windows -{ - /// - /// Represents the context menu window. - /// - public class ContextMenuWindow : ImGuiWindowBase - { - private bool _showContextMenu; - private bool _updateContextMenuLocation; - - /// - public override string Name => "Context Menu"; - - /// - public override ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; - - /// - /// Shows the context menu. - /// - public void ShowContextMenu() - { - _updateContextMenuLocation = true; - _showContextMenu = true; - } - - /// - public override void Render() - { - if (!_showContextMenu) - return; - - if (Scene is null) - throw new InvalidOperationException("Scene is not set."); - - if (_updateContextMenuLocation) - { - ImGui.SetNextWindowPos(ImGui.GetMousePos()); - _updateContextMenuLocation = false; - } - - ImGui.Begin("Context Menu", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove); - - if (ImGui.Button("Create cube")) - { - var cube = PrimitiveFactory.Create(PrimitiveType.Cube, new Vector3(0, 0, 0)); - cube.Name = "Cube" + Scene.Root.Children.Count; - - Scene.ActiveElement = cube; - Scene.Root.AddChild(cube); - } - - ImGui.End(); - } - } -} diff --git a/.editorconfig b/Engine/.editorconfig similarity index 100% rename from .editorconfig rename to Engine/.editorconfig diff --git a/Directory.Build.props b/Engine/Directory.Build.props similarity index 75% rename from Directory.Build.props rename to Engine/Directory.Build.props index ca5e202..c13bb19 100644 --- a/Directory.Build.props +++ b/Engine/Directory.Build.props @@ -1,13 +1,20 @@ + net8.0 + + + enable latest True + + + + Antti Veikkolainen - README.md https://github.com/Av3boy/SharpEngine @@ -23,11 +30,8 @@ True - - - True - \ - - + + 2.22.0 + \ No newline at end of file diff --git a/Editor/EditorSettings.cs b/Engine/Editor/EditorSettings.cs similarity index 75% rename from Editor/EditorSettings.cs rename to Engine/Editor/EditorSettings.cs index 12e447e..6b92693 100644 --- a/Editor/EditorSettings.cs +++ b/Engine/Editor/EditorSettings.cs @@ -1,5 +1,6 @@ using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Renderers; +using Silk.NET.Input; namespace SharpEngine.Editor; @@ -22,4 +23,10 @@ public class EditorSettings : IViewSettings /// public float MouseSensitivity { get; set; } + + /// + public MouseButton PrimaryButton { get; set; } = MouseButton.Left; + + /// + public MouseButton SecondaryButton { get; set; } = MouseButton.Right; } \ No newline at end of file diff --git a/Engine/Editor/EditorWindow.cs b/Engine/Editor/EditorWindow.cs new file mode 100644 index 0000000..7eae472 --- /dev/null +++ b/Engine/Editor/EditorWindow.cs @@ -0,0 +1,270 @@ +using ImGuiNET; + +using SharpEngine.Core; +using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Primitives; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Entities; + +using SharpEngine.Editor.Windows; + +using Silk.NET.Input; + +using System.Numerics; +using System.Reflection; +using SharpEngine.Core.Windowing; +using SharpEngine.Shared.Dto; +using SharpEngine.Shared; + +namespace SharpEngine.Editor; + +/// +/// Represents an editor window. +/// +public class EditorWindow : Window +{ + private readonly Project _project; + private readonly List _windows = []; + + private ContextMenuWindow? _contextMenuWindow; + private ActionsMenuWindow? _actionsMenuWindow; + + /// + /// Initializes an editor window with a specified scene and view settings. + /// + /// Represents the current scene to be displayed in the editor window. + /// The project loaded to the editor. + /// Contains configuration options for how the view should be rendered. + public EditorWindow(Scene scene, Project project, IViewSettings settings) : base(scene, settings) + { + _project = project; + } + + /// + public override void OnLoad() + { + base.OnLoad(); + + try + { + // Test only: remove later. + var cube = PrimitiveFactory.Create(PrimitiveType.Cube, new Vector3(3, 1, 1)); + cube.Name = "test"; + + Scene.Root.Children.Add(cube); + + // Get all types that inherit from ImGuiWindowBase + var windowTypes = Assembly.GetExecutingAssembly() + .GetTypes() + .Where(t => t.IsSubclassOf(typeof(ImGuiWindowBase)) && !t.IsAbstract) + .ToArray(); + + // Initialize all ImGuiWindowBase implementations + for (int i = 0; i < windowTypes.Length; i++) + { + try + { + var type = windowTypes[i]; + var window = Activator.CreateInstance(type) ?? + throw new InvalidOperationException($"Error initializing window: {type}"); + + var windowBase = (ImGuiWindowBase)window; + windowBase.SetScene(Scene); + windowBase.SetProject(_project); + + _windows.Add(windowBase); + } + catch (Exception ex) + { + Debug.Log.Error(ex.Message); + } + } + + _contextMenuWindow = (ContextMenuWindow?)_windows.FirstOrDefault(w => w.GetType() == typeof(ContextMenuWindow)); + if (_contextMenuWindow is null) + Debug.Log.Error("ContextMenuWindow not found."); + + _actionsMenuWindow = (ActionsMenuWindow?)_windows.FirstOrDefault(w => w.GetType() == typeof(ActionsMenuWindow)); + if (_actionsMenuWindow is null) + Debug.Log.Error("ActionsMenuWindow not found."); + else + _actionsMenuWindow.OnSceneLoaded += SetScene; + } + catch (Exception ex) + { + Debug.Log.Error(ex, ex.Message); + } + } + + private bool showUnsavedChangesDialog = true; + + /// + protected override void AfterRender(Frame frame) + { + base.AfterRender(frame); + + EnableDocking(); + RenderMenuBar(); + + ImGui.Begin("Unsaved changes", ref showUnsavedChangesDialog, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove); + ImGui.Text("There are unsaved changes in the project. Are you sure you want to exist without saving?"); + + bool cancelled = false; + if (ImGui.Button("Cancel")) + cancelled = true; + + if (!cancelled && ImGui.Button("Save & close")) + { + Save(); + CurrentWindow.Close(); + } + + if (!cancelled && ImGui.Button("Exit without saving")) + CurrentWindow.Close(); + + ImGui.End(); + + foreach (var window in _windows) + window.RenderWindow(); + } + + private void Save() + { + // TODO: #84 Save all changes. + } + + private static void EnableDocking() + { + // Enable docking + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable; + + // Create a docking space + var viewport = ImGui.GetMainViewport(); + ImGui.SetNextWindowPos(viewport.Pos); + ImGui.SetNextWindowSize(viewport.Size); + ImGui.SetNextWindowViewport(viewport.ID); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0.0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0.0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + + ImGui.Begin("DockSpace", ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoCollapse | + ImGuiWindowFlags.NoResize | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.NoNavFocus | ImGuiWindowFlags.AlwaysAutoResize); + ImGui.PopStyleVar(3); + + var dockSpaceId = ImGui.GetID("DockSpace"); + ImGui.DockSpace(dockSpaceId, Vector2.Zero, ImGuiDockNodeFlags.PassthruCentralNode); + + ImGui.End(); + } + + private void RenderMenuBar() + { + // Add a main menu bar + if (ImGui.BeginMainMenuBar()) + { + RenderFileMenu(); + + if (ImGui.BeginMenu("Edit")) + { + if (ImGui.MenuItem("Settings")) + { + // Handle New action + + // TODO: #76 Select preferred editor + // Required fields: + // radio button, each defined editor (store as json) + // textbox,Path to executable + // textbox, cli argument to open a file + } + + ImGui.EndMenu(); + } + + if (ImGui.BeginMenu("Windows")) + { + foreach (var window in _windows) + { + if (ImGui.MenuItem(window.Name)) + window.Open(); + } + + ImGui.EndMenu(); + } + + ImGui.EndMainMenuBar(); + } + } + + private void RenderFileMenu() + { + if (ImGui.BeginMenu("File")) + { + if (ImGui.MenuItem("New")) + { + // TODO: #83 Handle New action + } + + if (ImGui.MenuItem("Open")) + { + // TODO: #82 Handle Open action + } + + if (ImGui.MenuItem("Save")) + Save(); + + if (ImGui.MenuItem("Exit")) + { + // TODO: #81 Handle Exit action + } + + if (ImGui.MenuItem("Publish")) + { + // TODO: #80 Handle Publish action + } + + ImGui.EndMenu(); + } + } + + /// + public override void OnMouseClick(IMouse mouse, MouseButton button, Vector2 vector) + { + base.OnMouseClick(mouse, button, vector); + + if (button == Settings.PrimaryButton) + { + // TODO: #79 This probably isn't doable using ImGui. Figure out a new way or wait until UI is written using the Core 2D renderer. + if (ImGui.IsItemClicked((ImGuiMouseButton)button)) + { + // Clicked on an ImGui component + uint hoveredItemId = ImGui.GetID(""); + Debug.Log.Information("Clicked on ImGui component with ID: {HoveredItemId}", hoveredItemId); + } + else + { + Camera.IsInView(Scene, out GameObject? intersectingObject, out Vector3 hitPosition); + _contextMenuWindow?.ShowContextMenu(intersectingObject); + } + } + + if (button == Settings.SecondaryButton) + { + // TODO: #79 Make this work. + if (ImGui.IsItemClicked((ImGuiMouseButton)button)) + { + // Clicked on an ImGui component + uint hoveredItemId = ImGui.GetID(""); + Debug.Log.Information("Clicked on ImGui component with ID: {HoveredItemId}", hoveredItemId); + } + else + { + Camera.IsInView(Scene, out GameObject? intersectingObject, out Vector3 hitPosition); + Scene.ActiveElement = intersectingObject; + } + } + } +} diff --git a/Editor/Program.cs b/Engine/Editor/Program.cs similarity index 85% rename from Editor/Program.cs rename to Engine/Editor/Program.cs index 1dd9670..08cfcbb 100644 --- a/Editor/Program.cs +++ b/Engine/Editor/Program.cs @@ -1,9 +1,9 @@ -using Launcher.UI; -using SharpEngine.Core; +using SharpEngine.Core; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Scenes; using SharpEngine.Editor.Windows; using SharpEngine.Shared; +using SharpEngine.Shared.Dto; using System.Reflection; namespace SharpEngine.Editor; @@ -41,19 +41,18 @@ public static void Main(string[] args) var scene = !string.IsNullOrEmpty(sceneFile) ? Scene.LoadScene(sceneFile) : new Scene(); var project = !string.IsNullOrEmpty(projectFile) ? Project.LoadProject(projectFile) #if DEBUG - // TODO: We should create an additional getter for the .sharpproject file. : new Project() { Path = Path.Combine(Directory.GetCurrentDirectory(), @"..\..\..\..\Examples\Minimal"), Name = "Minimal" }; #else : new Project(); #endif using var window = new EditorWindow(scene, project!, new DefaultViewSettings()); - window.OnLoaded += () => Console.WriteLine("test"); + window.OnLoaded += () => Debug.Log.Information("test"); window.Run(); } - catch (Exception e) + catch (Exception ex) { - Debug.LogInformation($"Failed to start the editor: {e.Message}", e, true); + Debug.Log.Information(ex, "Failed to start the editor: {Message}", ex.Message); } } } \ No newline at end of file diff --git a/Editor/SharpEngine.Editor.csproj b/Engine/Editor/SharpEngine.Editor.csproj similarity index 88% rename from Editor/SharpEngine.Editor.csproj rename to Engine/Editor/SharpEngine.Editor.csproj index 2fc55d0..4045738 100644 --- a/Editor/SharpEngine.Editor.csproj +++ b/Engine/Editor/SharpEngine.Editor.csproj @@ -19,12 +19,6 @@ icon.png - - - - - - True @@ -33,7 +27,7 @@ - + diff --git a/Editor/SharpEngine.Editor.sln b/Engine/Editor/SharpEngine.Editor.sln similarity index 100% rename from Editor/SharpEngine.Editor.sln rename to Engine/Editor/SharpEngine.Editor.sln diff --git a/Editor/Windows/ActionsMenuWindow.cs b/Engine/Editor/Windows/ActionsMenuWindow.cs similarity index 61% rename from Editor/Windows/ActionsMenuWindow.cs rename to Engine/Editor/Windows/ActionsMenuWindow.cs index 7fb73bd..4cbbf1c 100644 --- a/Editor/Windows/ActionsMenuWindow.cs +++ b/Engine/Editor/Windows/ActionsMenuWindow.cs @@ -1,8 +1,10 @@ using ImGuiNET; -using Launcher.UI; using SharpEngine.Core; +using SharpEngine.Core.Audio; using SharpEngine.Core.Scenes; using SharpEngine.Shared; +using SharpEngine.Shared.Extensions; +using System.Text; namespace SharpEngine.Editor.Windows { @@ -22,8 +24,6 @@ public class ActionsMenuWindow : ImGuiWindowBase /// public override void Render() { - ImGui.Text("some text"); - if (ImGui.Button("Save")) SaveScene(Scene); @@ -32,31 +32,59 @@ public override void Render() if (ImGui.Button("Play")) StartGame(); + + const uint size = 256; + var buffer = new byte[size]; + + ImGui.InputText("Audio path", buffer, size); + if (ImGui.Button("Test audio")) + PlayTestAudio(Encoding.Default.GetString(buffer)); + } + + private void PlayTestAudio(string filePath) + { + if (!File.Exists(filePath)) + { + Debug.Log.Warning("Audio file '{FilePath}' does not exist.", filePath); + return; + } + + try + { + var audioPlayer = new WavPlayer(); + audioPlayer.AudioProperties.IsLooping = false; + + audioPlayer.Play(filePath); + } + catch (Exception ex) + { + Debug.Log.Error(ex, "Error during audio playback: {Message}", ex.Message); + } } private void StartGame() { if (Project is null) { - // TODO: This should never be possible. If the Project is null when it's loaded we should make sure it's initialized. + // TODO: #78 This should never be possible. If the Project is null when it's loaded we should make sure it's initialized. // Editor service probably needs to be made shared so that we can initialize the project / solution when the editor is opened. - // TODO: Do we want to save here automatically or prompt the user to save? - Debug.LogInformation("The project was null"); + // TODO: #77 Save automatically + Debug.Log.Information("The project was null"); return; } - Debug.LogInformation($"Starting {Project.Name}."); - ProcessExtensions.RunProcess($"dotnet run --project {Project.Path}/{Project.Name}.csproj", true, msg => Debug.LogInformation(msg)); + Debug.Log.Information("Starting {ProjectName}.", Project.Name); + ProcessExtensions.RunProcess($"dotnet run --project {Project.Path}/{Project.Name}.csproj", true, Debug.Log.Information); } private void SaveScene(Scene scene) { - Console.WriteLine("Saving.."); + Debug.Log.Information("Saving.."); if (!scene.HasUnsavedChanges && scene.HasSaveFile()) { - Console.WriteLine("No changes to save."); + Debug.Log.Information("No changes to save."); return; } @@ -64,7 +92,7 @@ private void SaveScene(Scene scene) { scene.SaveScene().Wait(); OnSceneLoaded?.Invoke(scene); - Console.WriteLine("File saved successfully!"); + Debug.Log.Information("File saved successfully!"); return; } @@ -83,15 +111,15 @@ private void SaveScene(Scene scene) scene.SaveScene(dialog.FileName).Wait(); Scene.SetFileFullPath(dialog.FileName); - Console.WriteLine("File saved successfully!"); + Debug.Log.Information("File saved successfully!"); } else - Console.WriteLine("File save cancelled."); + Debug.Log.Information("File save cancelled."); } private static bool SelectFile(out string selectedFile) { - Console.WriteLine("Selecting file.."); + Debug.Log.Information("Selecting file.."); using var dialog = new OpenFileDialog() { @@ -113,12 +141,12 @@ private static bool SelectFile(out string selectedFile) private void LoadScene(string sceneFile) { - Console.WriteLine($"Loading scene from file: {sceneFile}."); + Debug.Log.Information($"Loading scene from file: {sceneFile}."); var newScene = Scene.LoadScene(sceneFile); SetScene(newScene); - Console.WriteLine("Scene loaded successfully!"); + Debug.Log.Information("Scene loaded successfully!"); } } } diff --git a/Editor/Windows/AssetsWindow.cs b/Engine/Editor/Windows/AssetsWindow.cs similarity index 87% rename from Editor/Windows/AssetsWindow.cs rename to Engine/Editor/Windows/AssetsWindow.cs index d8f3dd9..f3c79ba 100644 --- a/Editor/Windows/AssetsWindow.cs +++ b/Engine/Editor/Windows/AssetsWindow.cs @@ -1,7 +1,4 @@ using ImGuiNET; -using Launcher.UI; -using SharpEngine.Core; -using SharpEngine.Core.Scenes; namespace SharpEngine.Editor.Windows { @@ -35,6 +32,9 @@ private static void RenderDirectory(string path) foreach (var file in files) if (ImGui.Selectable(Path.GetFileName(file))) { + // TODO: #76 Open the file in the IDE + // Process.Start(Settings.PreferredIDE.Name + Settings.PreferredIDE.CLIArguments + file); + // Scene.ActiveElement = file; // Set the file as the active element } } diff --git a/Engine/Editor/Windows/ContextMenuWindow.cs b/Engine/Editor/Windows/ContextMenuWindow.cs new file mode 100644 index 0000000..1233710 --- /dev/null +++ b/Engine/Editor/Windows/ContextMenuWindow.cs @@ -0,0 +1,96 @@ +using ImGuiNET; +using SharpEngine.Core.Entities; +using SharpEngine.Core.Primitives; +using SharpEngine.Core.Scenes; +using System.Numerics; + +namespace SharpEngine.Editor.Windows +{ + /// + /// Represents the context menu window. + /// + public class ContextMenuWindow : ImGuiWindowBase + { + private bool _showContextMenu; + private bool _updateContextMenuLocation; + private GameObject? IntersectedObject; + + /// + public override string Name => "Context Menu"; + + /// + public override ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoResize | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDocking; + + /// + /// Shows the context menu. + /// + /// The object the cursor right-clicked on. + public void ShowContextMenu(GameObject? intersectedObject = null) + { + IntersectedObject = intersectedObject; + _updateContextMenuLocation = true; + _showContextMenu = true; + } + + /// + public override void PreRender() + { + if (_updateContextMenuLocation) + { + ImGui.SetNextWindowPos(ImGui.GetMousePos()); + _updateContextMenuLocation = false; + } + } + + /// + public override void Render() + { + if (!_showContextMenu) + { + IntersectedObject = null; + return; + } + + if (Scene is not null) + RenderSceneActions(); + + if (IntersectedObject is not null) + RenderIntersectedObjectActions(); + } + + private void RenderIntersectedObjectActions() + { + if (ImGui.Button("Delete")) + { + Scene.Root.RemoveChild(IntersectedObject!); + IntersectedObject = null; + } + } + + private void RenderSceneActions() + { + var values = Enum.GetValues(); + foreach (var value in values) + { + if (ImGui.Button($"Create {value}")) + { + var cube = PrimitiveFactory.Create(value, new Vector3(0, 0, 0)); + cube.Name = value.ToString() + Scene.Root.Children.Count; + + Scene.ActiveElement = cube; + Scene.Root.AddChild(cube); + } + } + + if (ImGui.Button("Empty")) + { + var element = Scene.ActiveElement ?? Scene.Root; + element.AddChild(SceneNode.Empty); + } + } + } +} diff --git a/Editor/Windows/ImGuiWindowBase.cs b/Engine/Editor/Windows/ImGuiWindowBase.cs similarity index 61% rename from Editor/Windows/ImGuiWindowBase.cs rename to Engine/Editor/Windows/ImGuiWindowBase.cs index d1579bb..4210f05 100644 --- a/Editor/Windows/ImGuiWindowBase.cs +++ b/Engine/Editor/Windows/ImGuiWindowBase.cs @@ -1,7 +1,9 @@ using ImGuiNET; -using Launcher.UI; + +using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Scenes; -using Silk.NET.Windowing; +using SharpEngine.Shared; +using SharpEngine.Shared.Dto; namespace SharpEngine.Editor.Windows { @@ -16,6 +18,15 @@ public abstract class ImGuiWindowBase /// Gets or sets the currently active scene. public Scene Scene { get; private set; } + /// Gets the name of the window. + public abstract string Name { get; } + + /// + public virtual ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.None; + + private readonly Dictionary _previousDockingStates = []; + private bool _isVisible = true; + /// /// Initializes a new instance of . /// @@ -25,14 +36,6 @@ public ImGuiWindowBase() Project = new Project(); } - private readonly Dictionary _previousDockingStates = []; - - /// Gets the name of the window. - public abstract string Name { get; } - - /// - public virtual ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.None; - /// /// Sets the current scene. /// @@ -45,25 +48,34 @@ public ImGuiWindowBase() /// The new project. public void SetProject(Project project) => Project = project; + /// + /// Makes the ImGui window visible. + /// + public void Open() => _isVisible = true; + + /// + /// Executes operations required before rendering the ImGui window. + /// + public virtual void PreRender() { } + /// /// Renders a new ImGui window to the main window. /// public void RenderWindow() { - ImGui.Begin(Name, ImGuiWindowFlags); + if (!_isVisible) + return; + + PreRender(); + + ImGui.Begin(Name, ref _isVisible, ImGuiWindowFlags); Render(); // Check docking state bool isDocked = ImGui.IsWindowDocked(); - if (_previousDockingStates.TryGetValue(Name, out bool wasDocked)) - { - if (wasDocked && !isDocked) - { - // Window was undocked - OnWindowUndocked(); - } - } + if (_previousDockingStates.TryGetValue(Name, out bool wasDocked) && wasDocked && !isDocked) + OnWindowUndocked(); // Update previous docking state _previousDockingStates[Name] = isDocked; @@ -76,26 +88,38 @@ public void RenderWindow() /// protected virtual void OnWindowUndocked() { - Console.WriteLine($"Window {Name} was undocked."); - CreateSilkWindow(); + Debug.Log.Information($"Window {Name} was undocked."); + + // TODO: #75 This method of doing this does not work (at least in .Net 8). + //CreateSilkWindow(); } private void CreateSilkWindow() { - // TODO: Use the Core window. - var window = Window.Create(WindowOptions.Default with + try { - Title = Name - }); + var thread = new Thread(() => + { - window.Load += () => - { - // TODO: Initialize the ImGui context like we do in the Core window. - }; + var window = new Core.Windowing.Window(new(), new DefaultViewSettings()); - window.Render += delta => Render(); + window.Closing += () => _previousDockingStates[Name] = false; + window.OnAfterRender += deltaTime => + { + ImGui.Begin(Name, ImGuiWindowFlags); + Render(); + ImGui.End(); + }; - window.Run(); + window.Run(); + }); + + thread.Start(); + } + catch (Exception ex) + { + Debug.Log.Information($"Failed to create window: {ex.Message}"); + } } /// diff --git a/Editor/Windows/PropertiesWindow.cs b/Engine/Editor/Windows/PropertiesWindow.cs similarity index 88% rename from Editor/Windows/PropertiesWindow.cs rename to Engine/Editor/Windows/PropertiesWindow.cs index 9faf495..038a01f 100644 --- a/Editor/Windows/PropertiesWindow.cs +++ b/Engine/Editor/Windows/PropertiesWindow.cs @@ -1,5 +1,6 @@ using ImGuiNET; using SharpEngine.Core.Attributes; +using SharpEngine.Shared; using System.Numerics; using System.Reflection; @@ -11,13 +12,20 @@ namespace SharpEngine.Editor.Windows; public class PropertiesWindow : ImGuiWindowBase { /// - public override string Name => "Properties"; + public override string Name => Scene.ActiveElement is null ? "Properties" : Scene.ActiveElement!.Name + " properties"; /// public override void Render() { if (Scene.ActiveElement is not null) RenderActiveElementProperties(Scene.ActiveElement); + + if (Scene.ActiveElement is null) + { + ImGui.Text("No properties to display."); + ImGui.End(); + return; + } } /// @@ -28,16 +36,6 @@ public override void Render() /// Marks the starting level or the currently rendered level of properties. public void RenderActiveElementProperties(object obj, int maxRecursionDepth = 5, int currentDepth = 0) { - if (currentDepth == 0) - ImGui.Begin(obj is null ? "Properties" : Scene.ActiveElement!.Name + " properties"); - - if (obj is null) - { - ImGui.Text("No properties to display."); - ImGui.End(); - return; - } - if (currentDepth > maxRecursionDepth) return; @@ -49,6 +47,10 @@ public void RenderActiveElementProperties(object obj, int maxRecursionDepth = 5, foreach (var property in properties) { + // Skip properties with index parameters + if (property.GetIndexParameters().Length > 0) + continue; + var inspectorAttribute = property.GetCustomAttribute(); if (inspectorAttribute != null && !inspectorAttribute.DisplayInInspector) { @@ -79,12 +81,9 @@ public void RenderActiveElementProperties(object obj, int maxRecursionDepth = 5, } } } - catch (Exception e) + catch (Exception ex) { - Console.WriteLine(e); + Debug.Log.Error(ex, "{Message}", ex.Message); } - - if (currentDepth == 0) - ImGui.End(); } } diff --git a/Editor/Windows/SceneWindow.cs b/Engine/Editor/Windows/SceneWindow.cs similarity index 98% rename from Editor/Windows/SceneWindow.cs rename to Engine/Editor/Windows/SceneWindow.cs index aa4275c..e47065a 100644 --- a/Editor/Windows/SceneWindow.cs +++ b/Engine/Editor/Windows/SceneWindow.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using Launcher.UI; using SharpEngine.Core.Scenes; namespace SharpEngine.Editor.Windows diff --git a/Examples/MinecraftClone/Block/BlockBase.cs b/Engine/Examples/MinecraftClone/Block/BlockBase.cs similarity index 97% rename from Examples/MinecraftClone/Block/BlockBase.cs rename to Engine/Examples/MinecraftClone/Block/BlockBase.cs index 15c7a93..1c62d38 100644 --- a/Examples/MinecraftClone/Block/BlockBase.cs +++ b/Engine/Examples/MinecraftClone/Block/BlockBase.cs @@ -56,7 +56,6 @@ private void AssignProperties(GameObject cube, string name) }; Name = name; - Material = cube.Material; - Mesh = cube.Mesh; + Model = cube.Model; } } diff --git a/Examples/MinecraftClone/Block/Dirt.cs b/Engine/Examples/MinecraftClone/Block/Dirt.cs similarity index 72% rename from Examples/MinecraftClone/Block/Dirt.cs rename to Engine/Examples/MinecraftClone/Block/Dirt.cs index 8aada39..183d5d8 100644 --- a/Examples/MinecraftClone/Block/Dirt.cs +++ b/Engine/Examples/MinecraftClone/Block/Dirt.cs @@ -1,4 +1,5 @@ -using SharpEngine.Core.Extensions; +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; using System.Numerics; namespace Minecraft.Block; @@ -13,10 +14,10 @@ internal class Dirt : BlockBase /// /// The position where the block should be initialized. /// The name of the block to initialize. - public Dirt(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources/grass.jpg"), - PathExtensions.GetAssemblyPath("Resources/container2_specular.png"), - PathExtensions.GetAssemblyPath("Shaders/shader.vert"), - PathExtensions.GetAssemblyPath("Shaders/lighting.frag")) { } + public Dirt(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources\\grass.jpg"), + PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"), + Default.VertexShader, + Default.FragmentShader) { } /// public override BlockType BlockType => BlockType.Dirt; diff --git a/Examples/MinecraftClone/Block/Stone.cs b/Engine/Examples/MinecraftClone/Block/Stone.cs similarity index 63% rename from Examples/MinecraftClone/Block/Stone.cs rename to Engine/Examples/MinecraftClone/Block/Stone.cs index 08dc773..13352d3 100644 --- a/Examples/MinecraftClone/Block/Stone.cs +++ b/Engine/Examples/MinecraftClone/Block/Stone.cs @@ -1,22 +1,23 @@ -using SharpEngine.Core.Extensions; +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; using System.Numerics; namespace Minecraft.Block; internal class Stone : BlockBase { - // TODO: Create record for textures files for less parameters - // TODO: Create const files for these file strings + // TODO: #91 Create record for textures files for less parameters + // TODO: #91 Create const files for these file strings /// /// Initializes a new block. /// /// The position where the block is created. /// The name of the object in the scene. - public Stone(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources/container2.png"), - PathExtensions.GetAssemblyPath("Resources/container2_specular.png"), - PathExtensions.GetAssemblyPath("Shaders/shader.vert"), - PathExtensions.GetAssemblyPath("Shaders/lighting.frag")) { } + public Stone(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources\\container2.png"), + PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"), + Default.VertexShader, + Default.FragmentShader) { } /// public override BlockType BlockType => BlockType.Stone; diff --git a/Examples/MinecraftClone/BlockFactory.cs b/Engine/Examples/MinecraftClone/BlockFactory.cs similarity index 100% rename from Examples/MinecraftClone/BlockFactory.cs rename to Engine/Examples/MinecraftClone/BlockFactory.cs diff --git a/Examples/MinecraftClone/Input.cs b/Engine/Examples/MinecraftClone/Input.cs similarity index 100% rename from Examples/MinecraftClone/Input.cs rename to Engine/Examples/MinecraftClone/Input.cs diff --git a/Examples/MinecraftClone/Inventory.cs b/Engine/Examples/MinecraftClone/Inventory.cs similarity index 97% rename from Examples/MinecraftClone/Inventory.cs rename to Engine/Examples/MinecraftClone/Inventory.cs index 7379b8f..ed8b7eb 100644 --- a/Examples/MinecraftClone/Inventory.cs +++ b/Engine/Examples/MinecraftClone/Inventory.cs @@ -14,13 +14,13 @@ public class Inventory /// Gets or sets the selected slot. public ToolbarSlot SelectedSlot { get; set; } = new(1); - // TODO: After first version + // TODO: #90 After first version // public List Items { get; set; } = new(); /// Gets or sets the toolbar slots. public ToolbarSlot[] Toolbar { get; set; } = new ToolbarSlot[10]; - // TODO: Use later to optimize inventory logic + // TODO: #90 Use later to optimize inventory logic // private bool _toolbarSlotsAvailable; /// diff --git a/Examples/MinecraftClone/Minecraft.cs b/Engine/Examples/MinecraftClone/Minecraft.cs similarity index 56% rename from Examples/MinecraftClone/Minecraft.cs rename to Engine/Examples/MinecraftClone/Minecraft.cs index 4c6ebf7..92494da 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Engine/Examples/MinecraftClone/Minecraft.cs @@ -1,15 +1,25 @@ +using ImGuiNET; +using Minecraft.Block; +using ObjLoader.Loaders.ObjLoader; using SharpEngine.Core; +using SharpEngine.Core.Entities; +using SharpEngine.Core.Entities.Lights; +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Entities.UI; +using SharpEngine.Core.Entities.UI.Layouts; using SharpEngine.Core.Enums; using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; -using SharpEngine.Core.Entities; -using SharpEngine.Core.Entities.Lights; - -using Minecraft.Block; +using SharpEngine.Core.Windowing; +using SharpEngine.Shared; +using Silk.NET.Core.Native; using Silk.NET.Input; - using System; +using System.Collections.Generic; +using System.Linq; using System.Numerics; +using Tutorial; namespace Minecraft; @@ -28,7 +38,12 @@ public class Minecraft : Game private Input _input; private readonly Inventory _inventory; - private UIElement uiElem; + private UIElement _uiElem; + + /// + /// Gets the main window. + /// + public Window? Window { get; set; } /// /// Initializes a new instance of the . @@ -39,6 +54,7 @@ public Minecraft(Scene scene, ISettings settings) CoreSettings = settings; _inventory = new Inventory(); + } /// @@ -54,22 +70,96 @@ public override void Initialize() _lightsNode = _scene.Root.AddChild("lights"); _blocksNode = _scene.Root.AddChild("blocks"); - // TODO: Fix UI renderer - uiElem = new UIElement("uiElement"); - _scene.UIElements.Add(uiElem); + var gridLayout = new GridLayout(); + + // TODO: #89 Fix UI renderer + _uiElem = new UIElement("uiElement"); + _scene.UIElements.Add(_uiElem); + + //var uiElem2 = new UIElement("uiElement"); + //uiElem2.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(0.2f, 0.2f); + //uiElem2.Transform.Position = new Vector2(30, 0); + + // gridLayout.AddChild(_uiElem, uiElem2); + // _scene.UIElements.Add(_uiElem); + // _scene.UIElements.Add(uiElem2); + + //_scene.UIElements.Add(gridLayout); InitializeWorld(); } catch (Exception ex) { - Console.WriteLine(ex); + Debug.Log.Information(ex.Message, "{Message}", ex.Message); } } + /// + /// Handles rendering after the frame is drawn. + /// + /// Information about the frame. + public void OnAfterRender(Frame frame) + { + var x = _uiElem.Transform.Position.X; + var y = _uiElem.Transform.Position.Y; + + var sx = _uiElem.Transform.Scale.X; + var sy = _uiElem.Transform.Scale.Y; + + var rotation = _uiElem.Transform.Rotation.Angle; + + var height = _uiElem.Height; + var width = _uiElem.Width; + + ImGui.Begin("Debug"); + ImGui.Text($"FPS: {frame.FrameRate}"); + ImGui.Text($"Camera position: {Camera.Position}"); + ImGui.Text($"UI Element position: {_uiElem.Transform.Position}"); + + ImGui.SliderFloat("X", ref x, -2000, 2000); + ImGui.SliderFloat("Y", ref y, -2000, 2000); + + ImGui.Text($"UI Element scale: {_uiElem.Transform.Scale}"); + + ImGui.SliderFloat("sX", ref sx, 0, 1); + ImGui.SliderFloat("sY", ref sy, 0, 1); + + ImGui.Text($"UI Element scale: {_uiElem.Transform.Scale}"); + ImGui.SliderFloat("rotation", ref rotation, 0, 360); + + ImGui.Text($"UI Element width: {_uiElem.Width}"); + ImGui.SliderFloat("width", ref width, 0, 100); + + ImGui.Text($"UI Element height: {_uiElem.Height}"); + ImGui.SliderFloat("height", ref height, 0, 100); + + ImGui.End(); + + _uiElem.Transform.Position = new SharpEngine.Core.Numerics.Vector2(x, y); + _uiElem.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(sx, sy); + _uiElem.Transform.Rotation.Angle = rotation; + + _uiElem.Height = height; + _uiElem.Width = width; + } + private void InitializeWorld() { InitializeLights(); InitializeChunks(); + + // TODO: #2 Does not work yet. + // var torus = MeshService.Instance.LoadMesh("torus", @"C:\Users\antti\Documents\Untitled2.obj"); + + var model = ObjLoaderFactory.Load(Window.GL, @"C:\Users\antti\Documents\Untitled2.obj"); + var go = new GameObject(model); + var go2 = new GameObject(model) + { + Transform = new Transform(new SharpEngine.Core.Numerics.Vector3(0, 0, 30)) + }; + + _scene.Root.AddChild(go); + _scene.Root.AddChild(go2); } private void InitializeLights() @@ -89,21 +179,17 @@ private void InitializeLights() Diffuse = new Vector3(1.0f, 1.0f, 1.0f), Specular = new Vector3(1.0f, 1.0f, 1.0f), }); - - // var _lightingShader = ShaderService.Instance.LoadShader("Shaders/shader.vert", "Shaders/lighting.frag", "lighting"); - // _lightingShader.SetInt("numDirLights", 1); - // _lightingShader.SetInt("numPointLights", 4); - // _lightingShader.SetInt("numSpotLights", 1); } private void InitializeChunks() { - // TODO: Generate chunks when player moves + // TODO: #88 Generate chunks when player moves - // TODO: Generate chunks using 3d Perlin noise + // TODO: #87 Generate chunks using 3d Perlin noise const int chunkSize = 16; - const int numChunks = 3; + const int numChunks = 1; + // const int numChunks = 3; for (int i = 0; i < numChunks; i++) { @@ -136,14 +222,14 @@ private void GenerateChunk(int chunkSize, Vector3 chunkPos) /// public override void Update(double deltaTime, IInputContext input) { - UpdateUI(); + //UpdateUI(); _input.HandleKeyboard(input.Keyboards[0], (float)deltaTime); } private void UpdateUI() - => uiElem.Transform.Rotation += 0.01f; + => _uiElem.Transform.Rotation.Angle += 0.01f; - // TODO: Input system to let users change change key bindings? + // TODO: #21 Input system to let users change change key bindings? /// public override void HandleKeyboard(IKeyboard input, double deltaTime) { @@ -152,7 +238,7 @@ public override void HandleKeyboard(IKeyboard input, double deltaTime) if (input.IsKeyPressed(Key.Number0 + i)) { _inventory.SetSelectedSlot(i); - Console.WriteLine($"Selected slot: {i} ({_inventory.SelectedSlot.Items.Type})"); + Debug.Log.Information("Selected slot: {I} ({Type})", i, _inventory.SelectedSlot.Items.Type); } } @@ -172,9 +258,6 @@ private void Input_KeyUp(IKeyboard arg1, Key arg2, int arg3) } } - /// - public override void HandleMouse(IMouse mouse) { } - /// public override void HandleMouseDown(IMouse mouse, MouseButton button) { @@ -190,7 +273,7 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) } else { - Console.WriteLine($"No more {_inventory.SelectedSlot.Items.Type}s."); + Debug.Log.Information("No more {Type}s.", _inventory.SelectedSlot.Items.Type); } } @@ -199,9 +282,9 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) var destroyedBlockType = DestroyBlock(); if (destroyedBlockType != BlockType.None) { - // TODO: The block should be added to the slot so that 0 is the last slot instead of 9. - // TODO: The first block destroyed doesn't seem to be added to the inventory. - Console.WriteLine($"Block destroyed: {destroyedBlockType}."); + // TODO: #86 The block should be added to the slot so that 0 is the last slot instead of 9. + // TODO: #86 The first block destroyed doesn't seem to be added to the inventory. + Debug.Log.Information("Block destroyed: {DestroyedBlockType}.", destroyedBlockType); _inventory.AddToolbarItem(destroyedBlockType); } } @@ -209,7 +292,7 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) private BlockType DestroyBlock() { - if (!IsBlockInView(out GameObject? intersectingObject, out Vector3 _)) + if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out Vector3 _, allowedTypes: typeof(BlockBase))) return BlockType.None; var block = (BlockBase)intersectingObject!; @@ -220,7 +303,7 @@ private BlockType DestroyBlock() private void PlaceBlock() { - if (!IsBlockInView(out GameObject? intersectingObject, out Vector3 hitPosition)) + if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out Vector3 hitPosition, allowedTypes: typeof(BlockBase))) return; var newBlockPosition = GetNewBlockPosition(hitPosition, intersectingObject!); @@ -230,26 +313,13 @@ private void PlaceBlock() var newBlock = BlockFactory.CreateBlock(_inventory.SelectedSlot.Items.Type, newBlockPosition, $"Dirt ({_blocksNode.Children.Count})"); _blocksNode.AddChild(newBlock); - Console.WriteLine($"New block created: {newBlock.Transform.Position}, block in view location: {intersectingObject!.Transform.Position}"); - + Debug.Log.Information("New block created: {Pos}, block in view location: {IntersectingPos}", newBlock.Transform.Position, intersectingObject!.Transform.Position); } private static Vector3 GetNewBlockPosition(Vector3 hitPosition, GameObject intersectingObject) { Vector3 normal = Ray.GetClosestFaceNormal(hitPosition, intersectingObject); - return intersectingObject.Transform.Position + (normal * intersectingObject.Transform.Scale); - } - - /// - /// Checks whether a block is in view of the camera. - /// - /// - /// - /// - public bool IsBlockInView(out GameObject? intersectingObject, out Vector3 hitPosition) - { - Ray ray = new Ray(Camera.Position, Camera.Front); - return ray.IsGameObjectInView(_scene, out intersectingObject, out hitPosition, allowedTypes: typeof(BlockBase)); + return (System.Numerics.Vector3)intersectingObject.Transform.Position + (normal * (System.Numerics.Vector3)intersectingObject.Transform.Scale); } /// @@ -268,7 +338,7 @@ public override void HandleMouseWheel(MouseWheelScrollDirection direction, Scrol slotIndex = 0; _inventory.SetSelectedSlot(slotIndex); - Console.WriteLine($"Selected slot: {slotIndex}"); + Debug.Log.Information("Selected slot: {Index}", slotIndex); } } diff --git a/Examples/MinecraftClone/Minecraft.csproj b/Engine/Examples/MinecraftClone/Minecraft.csproj similarity index 74% rename from Examples/MinecraftClone/Minecraft.csproj rename to Engine/Examples/MinecraftClone/Minecraft.csproj index 89889c9..d3d98c6 100644 --- a/Examples/MinecraftClone/Minecraft.csproj +++ b/Engine/Examples/MinecraftClone/Minecraft.csproj @@ -3,11 +3,11 @@ Exe AnyCPU;x64;x86 - Debug;Release;LauncherNonWindows - + + diff --git a/Examples/MinecraftClone/Minecraft.sln b/Engine/Examples/MinecraftClone/Minecraft.sln similarity index 100% rename from Examples/MinecraftClone/Minecraft.sln rename to Engine/Examples/MinecraftClone/Minecraft.sln diff --git a/Examples/MinecraftClone/Program.cs b/Engine/Examples/MinecraftClone/Program.cs similarity index 70% rename from Examples/MinecraftClone/Program.cs rename to Engine/Examples/MinecraftClone/Program.cs index 376ee7a..727a8f4 100644 --- a/Examples/MinecraftClone/Program.cs +++ b/Engine/Examples/MinecraftClone/Program.cs @@ -1,5 +1,6 @@ using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; +using SharpEngine.Core.Windowing; namespace Minecraft; @@ -18,13 +19,18 @@ private static void Main() Scene scene = new Scene(); Minecraft game = new Minecraft(scene, gameSettings); - using var window = new SharpEngine.Core.Window(game.Camera, scene, game.Camera.Settings); - window.OnLoaded += () => game.Initialize(); + using var window = new Window(game.Camera, scene, game.Camera.Settings); + window.OnLoaded += game.Initialize; window.OnHandleMouse += game.HandleMouse; window.OnUpdate += game.Update; window.OnHandleKeyboard += game.HandleKeyboard; window.OnButtonMouseDown += game.HandleMouseDown; window.HandleMouseWheel += game.HandleMouseWheel; + window.OnAfterRender += frame => game.OnAfterRender(frame); + + // TODO: #85 This needs to be streamlined. + game.Window = window; + window.Run(); } } diff --git a/Examples/MinecraftClone/Resources/container2.png b/Engine/Examples/MinecraftClone/Resources/container2.png similarity index 100% rename from Examples/MinecraftClone/Resources/container2.png rename to Engine/Examples/MinecraftClone/Resources/container2.png diff --git a/Examples/MinecraftClone/Resources/container2_specular.png b/Engine/Examples/MinecraftClone/Resources/container2_specular.png similarity index 100% rename from Examples/MinecraftClone/Resources/container2_specular.png rename to Engine/Examples/MinecraftClone/Resources/container2_specular.png diff --git a/Examples/MinecraftClone/Resources/grass.jpg b/Engine/Examples/MinecraftClone/Resources/grass.jpg similarity index 100% rename from Examples/MinecraftClone/Resources/grass.jpg rename to Engine/Examples/MinecraftClone/Resources/grass.jpg diff --git a/Examples/Minimal/Minimal.csproj b/Engine/Examples/Minimal/Minimal.csproj similarity index 66% rename from Examples/Minimal/Minimal.csproj rename to Engine/Examples/Minimal/Minimal.csproj index 2821290..8bbdf5a 100644 --- a/Examples/Minimal/Minimal.csproj +++ b/Engine/Examples/Minimal/Minimal.csproj @@ -5,11 +5,10 @@ net8.0 enable enable - Debug;Release;LauncherNonWindows - + diff --git a/Examples/Minimal/Minimal.sln b/Engine/Examples/Minimal/Minimal.sln similarity index 100% rename from Examples/Minimal/Minimal.sln rename to Engine/Examples/Minimal/Minimal.sln diff --git a/Examples/Minimal/Program.cs b/Engine/Examples/Minimal/Program.cs similarity index 84% rename from Examples/Minimal/Program.cs rename to Engine/Examples/Minimal/Program.cs index b3c78f4..9751459 100644 --- a/Examples/Minimal/Program.cs +++ b/Engine/Examples/Minimal/Program.cs @@ -1,8 +1,6 @@ using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; - -using Silk.NET.Maths; -using Silk.NET.Windowing; +using SharpEngine.Core.Windowing; namespace Minimal; @@ -19,7 +17,7 @@ public static void Main(string[] _) var game = new Minimal(new DefaultSettings()); var scene = new Scene(); - using var window = new SharpEngine.Core.Window(game.Camera, scene, game.Camera.Settings); + using var window = new Window(game.Camera, scene, game.Camera.Settings); window.Run(); } } diff --git a/Engine/Examples/MultipleWindows/MultipleWindows.csproj b/Engine/Examples/MultipleWindows/MultipleWindows.csproj new file mode 100644 index 0000000..e48ece3 --- /dev/null +++ b/Engine/Examples/MultipleWindows/MultipleWindows.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + + diff --git a/Engine/Examples/MultipleWindows/Program.cs b/Engine/Examples/MultipleWindows/Program.cs new file mode 100644 index 0000000..788b808 --- /dev/null +++ b/Engine/Examples/MultipleWindows/Program.cs @@ -0,0 +1,130 @@ +using Silk.NET.Input; +using Silk.NET.Maths; +using Silk.NET.Windowing; + +using System.Collections.Concurrent; +using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Shared; + +namespace SharpEngine.Examples.MultipleWindows; + +// An example provided a lovely person in this thread: +// https://github.com/dotnet/Silk.NET/issues/2436#issuecomment-2752966073 + +/// +/// Represents the main entry point of the application. +/// +public static partial class Program +{ + private static readonly List _windows = []; + private static readonly List _inputContexts = []; + private static readonly ConcurrentQueue _windowQueue = []; + private static readonly CancellationTokenSource _cancellationTokenSource = new(); + + /// + /// The main entry point of the application. + /// + /// Arguments discarded. + public static void Main(string[] _) + { + StartWindowQueueTask(); + + try + { + while (!_cancellationTokenSource.IsCancellationRequested) + { + for (int i = 0; i < _windows.Count; i++) + UpdateWindow(ref i); + + DequeueWindows(); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + _cancellationTokenSource.Dispose(); + } + } + + private static void UpdateWindow(ref int i) + { + var window = _windows[i]; + if (window is null) + return; + + window.DoEvents(); + window.DoUpdate(); + window.DoRender(); + + if (window.IsClosing) + { + window.Reset(); + window.Dispose(); + + _windows.RemoveAt(i); + i--; // Adjust the index to account for the removed item + + if (_windows.Count == 0) + _cancellationTokenSource.Cancel(); + } + } + + private static void DequeueWindows() + { + if (_cancellationTokenSource.IsCancellationRequested) + return; + + while (_windowQueue.TryDequeue(out var _)) + EnqueueWindow(); + } + + private static void StartWindowQueueTask() + => Task.Run(async () => + { + _windowQueue.Enqueue(WindowOptions.Default); + while (!_cancellationTokenSource.IsCancellationRequested) + { + await Task.Delay(1000); + Debug.Log.Information("Running loop on background thread..."); + } + }); + + private static SharpEngine.Core.Windowing.Window CreateWindow() + { + var options = new DefaultViewSettings() with + { + WindowOptions = WindowOptions.Default with + { + Title = "Window" + _windows.Count, + + // This is to make sure the windows don't overlap + Position = new Vector2D( + x: 500 + (50 * _windows.Count), + y: 400 + (50 * _windows.Count)) + } + }; + + var window = new SharpEngine.Core.Windowing.Window(new(), options); + window.Initialize(); + + return window; + } + + private static void EnqueueWindow() + { + var window = CreateWindow(); + foreach (var mouse in window!.Input!.Mice) + mouse.Click += Mouse_Click; + + _inputContexts.Add(window.Input); + _windows.Add(window); + } + + private static void Mouse_Click(IMouse args1, Silk.NET.Input.MouseButton arg2, System.Numerics.Vector2 arg3) + { + _windowQueue.Enqueue(WindowOptions.Default); + } +} diff --git a/Engine/Examples/Tutorial 4.1 - Model Loading/Program.cs b/Engine/Examples/Tutorial 4.1 - Model Loading/Program.cs new file mode 100644 index 0000000..1bffe63 --- /dev/null +++ b/Engine/Examples/Tutorial 4.1 - Model Loading/Program.cs @@ -0,0 +1,201 @@ +using ObjLoader.Loaders.ObjLoader; +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; +using Shader = SharpEngine.Core.Shaders.Shader; +using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; + +using Silk.NET.Input; +using Silk.NET.Maths; +using Silk.NET.OpenGL; +using Silk.NET.Windowing; + +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace Tutorial +{ + public static class Program + { + #region fields + private static IWindow window; + private static GL Gl; + private static IKeyboard primaryKeyboard; + + private static Texture Texture; + private static Shader Shader; + private static List Models = []; + + //Setup the camera's location, directions, and movement speed + private static Vector3 CameraPosition = new(0.0f, 0.0f, 3.0f); + private static Vector3 CameraFront = new(0.0f, 0.0f, -1.0f); + private static Vector3 CameraUp = Vector3.UnitY; + private static Vector3 CameraDirection = Vector3.Zero; + private static float CameraYaw = -90f; + private static float CameraPitch = 0f; + private static float CameraZoom = 45f; + + //Used to track change in mouse movement to allow for moving of the Camera + private static Vector2 LastMousePosition; + #endregion + + private static void Main(string[] _) + { + var options = WindowOptions.Default; + options.Size = new Vector2D(800, 600); + options.Title = "LearnOpenGL with Silk.NET"; + window = Window.Create(options); + + window.Load += OnLoad; + window.Update += OnUpdate; + window.Render += OnRender; + window.FramebufferResize += OnFramebufferResize; + window.Closing += OnClose; + + window.Run(); + + window.Dispose(); + } + + private static void OnLoad() + { + IInputContext input = window.CreateInput(); + primaryKeyboard = input.Keyboards[0]; + + if (primaryKeyboard != null) + primaryKeyboard.KeyDown += KeyDown; + + for (int i = 0; i < input.Mice.Count; i++) + { + input.Mice[i].Cursor.CursorMode = CursorMode.Raw; + input.Mice[i].MouseMove += OnMouseMove; + input.Mice[i].Scroll += OnMouseWheel; + } + + Gl = GL.GetApi(window); + + Shader = new Shader(Gl, PathExtensions.GetAssemblyPath("shader2.vert"), Default.LightShader, "test").Initialize(); + Texture = new Texture(Gl, "silk.png"); + + var model = ObjLoaderFactory.Load(Gl, "Untitled2.obj"); + + Models.Add(model); + Models.Add(model); + } + + private static void OnUpdate(double deltaTime) + { + var moveSpeed = 2.5f * (float) deltaTime; + + //Move forwards + if (primaryKeyboard.IsKeyPressed(Key.W)) + CameraPosition += moveSpeed * CameraFront; + + //Move backwards + if (primaryKeyboard.IsKeyPressed(Key.S)) + CameraPosition -= moveSpeed * CameraFront; + + //Move left + if (primaryKeyboard.IsKeyPressed(Key.A)) + CameraPosition -= Vector3.Normalize(Vector3.Cross(CameraFront, CameraUp)) * moveSpeed; + + //Move right + if (primaryKeyboard.IsKeyPressed(Key.D)) + CameraPosition += Vector3.Normalize(Vector3.Cross(CameraFront, CameraUp)) * moveSpeed; + } + + private static void OnRender(double deltaTime) + { + Gl.Enable(EnableCap.DepthTest); + Gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + + //Use elapsed time to convert to radians to allow our cube to rotate over time + var difference = (float) (window.Time * 100); + + var size = window.FramebufferSize; + + var modelMatrix = Matrix4x4.CreateRotationY(SharpEngine.Core.Math.DegreesToRadians(difference)) * + Matrix4x4.CreateRotationX(SharpEngine.Core.Math.DegreesToRadians(difference)); + + var view = Matrix4x4.CreateLookAt(CameraPosition, CameraPosition + CameraFront, CameraUp); + //Note that the aspect ratio calculation must be performed as a float, otherwise integer division will be performed (truncating the result). + var projection = Matrix4x4.CreatePerspectiveFieldOfView(SharpEngine.Core.Math.DegreesToRadians(CameraZoom), (float)size.X / size.Y, 0.1f, 100.0f); + + for (int i = 0; i < Models.Count; i++) + { + if (i >= 1) + modelMatrix = Matrix4x4.CreateTranslation(0, -30.0f, 0.0f) * modelMatrix; + + RenderModel(Models[i], modelMatrix, view, projection); + } + } + + private static void RenderModel(Model_Old model, Matrix4x4 modelMatrix, Matrix4x4 view, Matrix4x4 projection) + { + foreach (var mesh in model.Meshes) + { + mesh.Bind(); + + Texture.Use(); + + Shader.Use(); + Shader.SetInt("uTexture0", 0); + Shader.SetMatrix4("uModel", modelMatrix, false); + Shader.SetMatrix4("uView", view, false); + Shader.SetMatrix4("uProjection", projection, false); + + Gl.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Vertices.Length); + } + } + + private static void OnFramebufferResize(Vector2D newSize) => Gl.Viewport(newSize); + + private static void OnMouseMove(IMouse mouse, Vector2 position) + { + var lookSensitivity = 0.1f; + if (LastMousePosition == default) + { + LastMousePosition = position; + } + else + { + var xOffset = (position.X - LastMousePosition.X) * lookSensitivity; + var yOffset = (position.Y - LastMousePosition.Y) * lookSensitivity; + LastMousePosition = position; + + CameraYaw += xOffset; + CameraPitch -= yOffset; + + //We don't want to be able to look behind us by going over our head or under our feet so make sure it stays within these bounds + CameraPitch = Math.Clamp(CameraPitch, -89.0f, 89.0f); + + CameraDirection.X = MathF.Cos(SharpEngine.Core.Math.DegreesToRadians(CameraYaw)) * MathF.Cos(SharpEngine.Core.Math.DegreesToRadians(CameraPitch)); + CameraDirection.Y = MathF.Sin(SharpEngine.Core.Math.DegreesToRadians(CameraPitch)); + CameraDirection.Z = MathF.Sin(SharpEngine.Core.Math.DegreesToRadians(CameraYaw)) * MathF.Cos(SharpEngine.Core.Math.DegreesToRadians(CameraPitch)); + CameraFront = Vector3.Normalize(CameraDirection); + } + } + + //We don't want to be able to zoom in too close or too far away so clamp to these values + + private static void OnMouseWheel(IMouse mouse, ScrollWheel scrollWheel) + => CameraZoom = Math.Clamp(CameraZoom - scrollWheel.Y, 1.0f, 45f); + + private static void OnClose() + { + foreach (var model in Models) + model.Dispose(); + + Shader.Dispose(); + Texture.Dispose(); + } + + private static void KeyDown(IKeyboard keyboard, Key key, int arg3) + { + if (key == Key.Escape) + { + window.Close(); + } + } + } +} diff --git a/Engine/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj b/Engine/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj new file mode 100644 index 0000000..079bf1a --- /dev/null +++ b/Engine/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj @@ -0,0 +1,42 @@ + + + + Exe + net8.0 + Tutorial + + + + + + + + + + + + + + + + + + Always + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + \ No newline at end of file diff --git a/Engine/Examples/Tutorial 4.1 - Model Loading/shader2.vert b/Engine/Examples/Tutorial 4.1 - Model Loading/shader2.vert new file mode 100644 index 0000000..fc46c04 --- /dev/null +++ b/Engine/Examples/Tutorial 4.1 - Model Loading/shader2.vert @@ -0,0 +1,16 @@ +#version 330 core +layout (location = 0) in vec3 vPos; +layout (location = 1) in vec2 vUv; + +uniform mat4 uModel; +uniform mat4 uView; +uniform mat4 uProjection; + +out vec2 fUv; + +void main() +{ + //Multiplying our uniform with the vertex position, the multiplication order here does matter. + gl_Position = uProjection * uView * uModel * vec4(vPos, 1.0); + fUv = vUv; +} \ No newline at end of file diff --git a/Engine/Examples/Tutorial 4.1 - Model Loading/silk.png b/Engine/Examples/Tutorial 4.1 - Model Loading/silk.png new file mode 100644 index 0000000..a82d3ab Binary files /dev/null and b/Engine/Examples/Tutorial 4.1 - Model Loading/silk.png differ diff --git a/Launcher/App.xaml b/Engine/Launcher/App.xaml similarity index 100% rename from Launcher/App.xaml rename to Engine/Launcher/App.xaml diff --git a/Launcher/App.xaml.cs b/Engine/Launcher/App.xaml.cs similarity index 100% rename from Launcher/App.xaml.cs rename to Engine/Launcher/App.xaml.cs diff --git a/Launcher/Attributes/FileAttribute.cs b/Engine/Launcher/Attributes/FileAttribute.cs similarity index 89% rename from Launcher/Attributes/FileAttribute.cs rename to Engine/Launcher/Attributes/FileAttribute.cs index 0c748ea..fe43381 100644 --- a/Launcher/Attributes/FileAttribute.cs +++ b/Engine/Launcher/Attributes/FileAttribute.cs @@ -12,7 +12,7 @@ public class FileAttribute : ValidationAttribute public override bool IsValid(object? value) { if (value is string path) - return System.IO.File.Exists(path); + return File.Exists(path); return false; } diff --git a/Engine/Launcher/Enums/SvgIcon.cs b/Engine/Launcher/Enums/SvgIcon.cs new file mode 100644 index 0000000..650ac01 --- /dev/null +++ b/Engine/Launcher/Enums/SvgIcon.cs @@ -0,0 +1,31 @@ +namespace Launcher.Enums; + +/// +/// Contains the supported SVG icons. +/// +public enum SvgIcon +{ + /// Indicates no icon. + None, + + /// Indicates the home icon. + Home, + + /// Indicates the settings icon. + Cog, + + /// Indicates the plus icon. + Discard, + + /// Indicates the search lens icon. + Lens, + + /// Indicates the close 'x' icon. + Close, + + /// Indicates the chevron '<' down icon. + ChevronDown, + + /// Indicates the chevron '<' up icon. + ChevronUp, +} diff --git a/Launcher/Launcher.csproj b/Engine/Launcher/Launcher.csproj similarity index 100% rename from Launcher/Launcher.csproj rename to Engine/Launcher/Launcher.csproj diff --git a/Launcher/Launcher.sln b/Engine/Launcher/Launcher.sln similarity index 100% rename from Launcher/Launcher.sln rename to Engine/Launcher/Launcher.sln diff --git a/Launcher/MainPage.xaml b/Engine/Launcher/MainPage.xaml similarity index 100% rename from Launcher/MainPage.xaml rename to Engine/Launcher/MainPage.xaml diff --git a/Launcher/MainPage.xaml.cs b/Engine/Launcher/MainPage.xaml.cs similarity index 100% rename from Launcher/MainPage.xaml.cs rename to Engine/Launcher/MainPage.xaml.cs diff --git a/Launcher/MauiProgram.cs b/Engine/Launcher/MauiProgram.cs similarity index 100% rename from Launcher/MauiProgram.cs rename to Engine/Launcher/MauiProgram.cs diff --git a/Launcher/Platforms/Android/AndroidManifest.xml b/Engine/Launcher/Platforms/Android/AndroidManifest.xml similarity index 100% rename from Launcher/Platforms/Android/AndroidManifest.xml rename to Engine/Launcher/Platforms/Android/AndroidManifest.xml diff --git a/Launcher/Platforms/Android/MainActivity.cs b/Engine/Launcher/Platforms/Android/MainActivity.cs similarity index 100% rename from Launcher/Platforms/Android/MainActivity.cs rename to Engine/Launcher/Platforms/Android/MainActivity.cs diff --git a/Launcher/Platforms/Android/MainApplication.cs b/Engine/Launcher/Platforms/Android/MainApplication.cs similarity index 100% rename from Launcher/Platforms/Android/MainApplication.cs rename to Engine/Launcher/Platforms/Android/MainApplication.cs diff --git a/Launcher/Platforms/Android/Resources/values/colors.xml b/Engine/Launcher/Platforms/Android/Resources/values/colors.xml similarity index 100% rename from Launcher/Platforms/Android/Resources/values/colors.xml rename to Engine/Launcher/Platforms/Android/Resources/values/colors.xml diff --git a/Launcher/Platforms/MacCatalyst/AppDelegate.cs b/Engine/Launcher/Platforms/MacCatalyst/AppDelegate.cs similarity index 100% rename from Launcher/Platforms/MacCatalyst/AppDelegate.cs rename to Engine/Launcher/Platforms/MacCatalyst/AppDelegate.cs diff --git a/Launcher/Platforms/MacCatalyst/Entitlements.plist b/Engine/Launcher/Platforms/MacCatalyst/Entitlements.plist similarity index 100% rename from Launcher/Platforms/MacCatalyst/Entitlements.plist rename to Engine/Launcher/Platforms/MacCatalyst/Entitlements.plist diff --git a/Launcher/Platforms/MacCatalyst/Info.plist b/Engine/Launcher/Platforms/MacCatalyst/Info.plist similarity index 100% rename from Launcher/Platforms/MacCatalyst/Info.plist rename to Engine/Launcher/Platforms/MacCatalyst/Info.plist diff --git a/Launcher/Platforms/MacCatalyst/Program.cs b/Engine/Launcher/Platforms/MacCatalyst/Program.cs similarity index 100% rename from Launcher/Platforms/MacCatalyst/Program.cs rename to Engine/Launcher/Platforms/MacCatalyst/Program.cs diff --git a/Launcher/Platforms/Tizen/Main.cs b/Engine/Launcher/Platforms/Tizen/Main.cs similarity index 100% rename from Launcher/Platforms/Tizen/Main.cs rename to Engine/Launcher/Platforms/Tizen/Main.cs diff --git a/Launcher/Platforms/Tizen/tizen-manifest.xml b/Engine/Launcher/Platforms/Tizen/tizen-manifest.xml similarity index 100% rename from Launcher/Platforms/Tizen/tizen-manifest.xml rename to Engine/Launcher/Platforms/Tizen/tizen-manifest.xml diff --git a/Launcher/Platforms/Windows/App.xaml b/Engine/Launcher/Platforms/Windows/App.xaml similarity index 100% rename from Launcher/Platforms/Windows/App.xaml rename to Engine/Launcher/Platforms/Windows/App.xaml diff --git a/Launcher/Platforms/Windows/App.xaml.cs b/Engine/Launcher/Platforms/Windows/App.xaml.cs similarity index 100% rename from Launcher/Platforms/Windows/App.xaml.cs rename to Engine/Launcher/Platforms/Windows/App.xaml.cs diff --git a/Launcher/Platforms/Windows/Package.appxmanifest b/Engine/Launcher/Platforms/Windows/Package.appxmanifest similarity index 100% rename from Launcher/Platforms/Windows/Package.appxmanifest rename to Engine/Launcher/Platforms/Windows/Package.appxmanifest diff --git a/Launcher/Platforms/Windows/app.manifest b/Engine/Launcher/Platforms/Windows/app.manifest similarity index 100% rename from Launcher/Platforms/Windows/app.manifest rename to Engine/Launcher/Platforms/Windows/app.manifest diff --git a/Launcher/Platforms/iOS/AppDelegate.cs b/Engine/Launcher/Platforms/iOS/AppDelegate.cs similarity index 100% rename from Launcher/Platforms/iOS/AppDelegate.cs rename to Engine/Launcher/Platforms/iOS/AppDelegate.cs diff --git a/Launcher/Platforms/iOS/Info.plist b/Engine/Launcher/Platforms/iOS/Info.plist similarity index 100% rename from Launcher/Platforms/iOS/Info.plist rename to Engine/Launcher/Platforms/iOS/Info.plist diff --git a/Launcher/Platforms/iOS/Program.cs b/Engine/Launcher/Platforms/iOS/Program.cs similarity index 100% rename from Launcher/Platforms/iOS/Program.cs rename to Engine/Launcher/Platforms/iOS/Program.cs diff --git a/Launcher/Platforms/iOS/Resources/PrivacyInfo.xcprivacy b/Engine/Launcher/Platforms/iOS/Resources/PrivacyInfo.xcprivacy similarity index 100% rename from Launcher/Platforms/iOS/Resources/PrivacyInfo.xcprivacy rename to Engine/Launcher/Platforms/iOS/Resources/PrivacyInfo.xcprivacy diff --git a/Launcher/Properties/launchSettings.json b/Engine/Launcher/Properties/launchSettings.json similarity index 100% rename from Launcher/Properties/launchSettings.json rename to Engine/Launcher/Properties/launchSettings.json diff --git a/Launcher/Resources/AppIcon/appicon.svg b/Engine/Launcher/Resources/AppIcon/appicon.svg similarity index 100% rename from Launcher/Resources/AppIcon/appicon.svg rename to Engine/Launcher/Resources/AppIcon/appicon.svg diff --git a/Launcher/Resources/AppIcon/appiconfg.svg b/Engine/Launcher/Resources/AppIcon/appiconfg.svg similarity index 100% rename from Launcher/Resources/AppIcon/appiconfg.svg rename to Engine/Launcher/Resources/AppIcon/appiconfg.svg diff --git a/Launcher/Resources/AppIcon/icon.ico b/Engine/Launcher/Resources/AppIcon/icon.ico similarity index 100% rename from Launcher/Resources/AppIcon/icon.ico rename to Engine/Launcher/Resources/AppIcon/icon.ico diff --git a/Core/_Resources/icon.png b/Engine/Launcher/Resources/AppIcon/icon.png similarity index 100% rename from Core/_Resources/icon.png rename to Engine/Launcher/Resources/AppIcon/icon.png diff --git a/Launcher/Resources/AppIcon/icon.svg b/Engine/Launcher/Resources/AppIcon/icon.svg similarity index 100% rename from Launcher/Resources/AppIcon/icon.svg rename to Engine/Launcher/Resources/AppIcon/icon.svg diff --git a/Launcher/Resources/Fonts/OpenSans-Regular.ttf b/Engine/Launcher/Resources/Fonts/OpenSans-Regular.ttf similarity index 100% rename from Launcher/Resources/Fonts/OpenSans-Regular.ttf rename to Engine/Launcher/Resources/Fonts/OpenSans-Regular.ttf diff --git a/Launcher/Resources/Images/dotnet_bot.svg b/Engine/Launcher/Resources/Images/dotnet_bot.svg similarity index 100% rename from Launcher/Resources/Images/dotnet_bot.svg rename to Engine/Launcher/Resources/Images/dotnet_bot.svg diff --git a/Launcher/Resources/Raw/AboutAssets.txt b/Engine/Launcher/Resources/Raw/AboutAssets.txt similarity index 100% rename from Launcher/Resources/Raw/AboutAssets.txt rename to Engine/Launcher/Resources/Raw/AboutAssets.txt diff --git a/Launcher/Resources/Splash/splash.svg b/Engine/Launcher/Resources/Splash/splash.svg similarity index 100% rename from Launcher/Resources/Splash/splash.svg rename to Engine/Launcher/Resources/Splash/splash.svg diff --git a/Launcher/Services/EditorService.cs b/Engine/Launcher/Services/EditorService.cs similarity index 94% rename from Launcher/Services/EditorService.cs rename to Engine/Launcher/Services/EditorService.cs index 22142f6..b6d6b85 100644 --- a/Launcher/Services/EditorService.cs +++ b/Engine/Launcher/Services/EditorService.cs @@ -1,5 +1,6 @@ -using Launcher.UI; -using SharpEngine.Shared; +using SharpEngine.Shared; +using SharpEngine.Shared.Dto; +using SharpEngine.Shared.Extensions; using System.Diagnostics; using System.Runtime.InteropServices; @@ -85,7 +86,7 @@ private void CreateSolution(Project project) string createProject = $"dotnet new console -n {projectName} -o {project.Path} -f {FRAMEWORK}"; string addProjectToSolution = $"dotnet sln {project.Path}/{projectName}.sln add {project.Path}/{projectName}.csproj"; - // TODO: Add the NuGet package after it's been published + // TODO: #34 Add the NuGet package after it's been published //string installNuGetPackage = $"dotnet add {project.Path}/{projectName}.csproj package {SHARP_ENGINE_CORE_NUGET_PACKAGE}"; string installNuGetPackage = "echo done"; @@ -108,9 +109,9 @@ private void CreateSolution(Project project) else _notificationService.Show("Solution and project created successfully!"); - // TODO: Set example program.cs content with the minimal projects content. + // TODO: #34 Set example program.cs content with the minimal projects content. // string programFilePath = Path.Combine(project.Path!, "Program.cs"); - // string programContent = ""; // TODO: Replace with program content + // string programContent = ""; // // File.WriteAllText(programFilePath, programContent); } @@ -159,7 +160,10 @@ public List LoadProjects(string projectsFile) var projects = System.Text.Json.JsonSerializer.Deserialize>(json); if (projects is not null) + { + // TOOD: Resolve last modified return projects; + } _notificationService.Show($"Unable to load projects.", false, json, projects?.ToArray()); return []; diff --git a/Launcher/Services/NotificationService.cs b/Engine/Launcher/Services/NotificationService.cs similarity index 100% rename from Launcher/Services/NotificationService.cs rename to Engine/Launcher/Services/NotificationService.cs diff --git a/Engine/Launcher/TestData/TestData.cs b/Engine/Launcher/TestData/TestData.cs new file mode 100644 index 0000000..0643b85 --- /dev/null +++ b/Engine/Launcher/TestData/TestData.cs @@ -0,0 +1,22 @@ +using SharpEngine.Shared.Dto; + +namespace Launcher.TestData; + +/// +/// Contains test data for the launcher. +/// +public static class TestData +{ + /// Indicates whether test data should be loaded. + public const bool LoadTestData = true; + + /// Indicates whether to simulate loading. + public const bool SimulateLoading = false; + + /// A collection of test data projects. + public static IEnumerable Projects = Enumerable.Range(0, 25).Select(i => new Project() + { + Name = $"Project {i}", + Path = @"C:\test", + }); +} diff --git a/Engine/Launcher/UI/Components/ContextMenu.razor b/Engine/Launcher/UI/Components/ContextMenu.razor new file mode 100644 index 0000000..6f3d22f --- /dev/null +++ b/Engine/Launcher/UI/Components/ContextMenu.razor @@ -0,0 +1,33 @@ +@if (showMenu) +{ +
+ @ChildContent(_context!) +
+} + +@code { + [Inject] + private IJSRuntime JS { get; init; } = default!; + + [Parameter] + public RenderFragment ChildContent { get; set; } + + private object? _context; + private bool showMenu = false; + private int menuX, menuY; + + private string menuXpx => $"{menuX}px"; + private string menuYpx => $"{menuY}px"; + + public void Open(MouseEventArgs e, object context) + { + menuX = (int)e.ClientX; + menuY = (int)e.ClientY; + showMenu = true; + + _context = context; + + // Prevent the default browser context menu + JS.InvokeVoidAsync("preventContextMenu"); + } +} diff --git a/Engine/Launcher/UI/Components/ContextMenu.razor.css b/Engine/Launcher/UI/Components/ContextMenu.razor.css new file mode 100644 index 0000000..d0f597f --- /dev/null +++ b/Engine/Launcher/UI/Components/ContextMenu.razor.css @@ -0,0 +1,9 @@ +.context-menu { + position: absolute; + z-index: 1000; + background-color: white; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 8px; +} diff --git a/Engine/Launcher/UI/Components/ContextMenu.razor.min.css b/Engine/Launcher/UI/Components/ContextMenu.razor.min.css new file mode 100644 index 0000000..c5c1a48 --- /dev/null +++ b/Engine/Launcher/UI/Components/ContextMenu.razor.min.css @@ -0,0 +1 @@ +.context-menu{position:absolute;z-index:1000;background-color:#fff;border:1px solid #ccc;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,.1);padding:8px;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/ContextMenu.razor.scss b/Engine/Launcher/UI/Components/ContextMenu.razor.scss new file mode 100644 index 0000000..580b5a7 --- /dev/null +++ b/Engine/Launcher/UI/Components/ContextMenu.razor.scss @@ -0,0 +1,9 @@ +.context-menu { + position: absolute; + z-index: 1000; + background-color: white; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 8px; +} diff --git a/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor b/Engine/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor similarity index 94% rename from Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor rename to Engine/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor index 52ed7ad..53bdec2 100644 --- a/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor +++ b/Engine/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor @@ -22,7 +22,7 @@ @code { private string _confirmDeleteVisibleClass => Visible ? "confirm-delete-visible" : "confirm-delete-hidden"; - /// Gets or sets the event exectued when the project deletion is confirmed. + /// Gets or sets the event executed when the project deletion is confirmed. [Parameter] public EventCallback OnDeleteProjectConfirmed { get; set; } diff --git a/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.css b/Engine/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.css similarity index 100% rename from Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.css rename to Engine/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.css diff --git a/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.min.css b/Engine/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.min.css similarity index 100% rename from Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.min.css rename to Engine/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.min.css diff --git a/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.scss b/Engine/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.scss similarity index 100% rename from Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.scss rename to Engine/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor.scss diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor similarity index 91% rename from Launcher/UI/Components/Dialogs/CreateProjectDialog.razor rename to Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor index 27d5313..5f3275d 100644 --- a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor +++ b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor @@ -10,7 +10,7 @@ -
+
@@ -18,7 +18,7 @@
-
+
diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs similarity index 97% rename from Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs rename to Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs index ad2eaff..7d1b324 100644 --- a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs +++ b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Components; using Launcher.Services; using CommunityToolkit.Maui.Storage; +using SharpEngine.Shared.Dto; namespace Launcher.UI.Components.Dialogs { diff --git a/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.css b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.css new file mode 100644 index 0000000..a76d394 --- /dev/null +++ b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.css @@ -0,0 +1,9 @@ +.field-path-container { + display: flex; + align-items: center; + gap: 8px; +} + +.field-name-container { + margin-bottom: 8px; +} diff --git a/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.min.css b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.min.css new file mode 100644 index 0000000..0ee1ee3 --- /dev/null +++ b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.min.css @@ -0,0 +1 @@ +.field-path-container{display:flex;align-items:center;gap:8px;}.field-name-container{margin-bottom:8px;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.scss b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.scss new file mode 100644 index 0000000..6457dd0 --- /dev/null +++ b/Engine/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.scss @@ -0,0 +1,9 @@ +.field-path-container { + display: flex; + align-items: center; + gap: 8px; +} + +.field-name-container { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/Launcher/UI/Components/Dialogs/DialogBase.cs b/Engine/Launcher/UI/Components/Dialogs/DialogBase.cs similarity index 100% rename from Launcher/UI/Components/Dialogs/DialogBase.cs rename to Engine/Launcher/UI/Components/Dialogs/DialogBase.cs diff --git a/Engine/Launcher/UI/Components/Flyouts/Flyout.razor b/Engine/Launcher/UI/Components/Flyouts/Flyout.razor new file mode 100644 index 0000000..5fca179 --- /dev/null +++ b/Engine/Launcher/UI/Components/Flyouts/Flyout.razor @@ -0,0 +1,56 @@ +@using Launcher.Enums +
+
+
+ @if (Header is not null) + { + @Header + } + else + { +

@Title

+ } +
+ +
+
+ @Content +
+ +
+ +@code { + /// Gets or sets the header content. + [Parameter] + public RenderFragment Header { get; set; } + + /// Gets or sets the main content. + [Parameter] + public RenderFragment Content { get; set; } + + /// Gets or sets the footer content. + [Parameter] + public RenderFragment Footer { get; set; } + + /// Gets or sets the title. + [Parameter] + public string Title { get; set; } = default!; + + /// Gets or sets whether the flyout is visible. + [Parameter] + public bool IsVisible { get; set; } + + /// Gets or sets the event executed when the visibility changes. + [Parameter] + public EventCallback IsVisibleChanged { get; set; } + + private string _isVisibleStyle => IsVisible ? "grid" : "none"; + + private async Task Close() + { + IsVisible = false; + await IsVisibleChanged.InvokeAsync(IsVisible); + } +} diff --git a/Engine/Launcher/UI/Components/Flyouts/Flyout.razor.css b/Engine/Launcher/UI/Components/Flyouts/Flyout.razor.css new file mode 100644 index 0000000..82882fa --- /dev/null +++ b/Engine/Launcher/UI/Components/Flyouts/Flyout.razor.css @@ -0,0 +1,38 @@ +.flyout { + display: none; + position: absolute; + top: 0; + width: 50%; + height: 100vh; + z-index: 9999; + background: white; + border-right: 1px solid black; + left: 50px; + grid-template-rows: 70px auto 70px; +} + +.flyout-header { + display: flex; + justify-content: space-between; +} +.flyout-header button { + border: none; + background: none; +} + +.flyout-content { + overflow-y: scroll; + padding-left: 1em; +} + +.flyout-footer { + border-top: 1px solid black; + padding: 5px; + display: flex; + justify-content: end; + gap: 8px; +} + +.flyout-footer, .flyout-header, .flyout-content { + padding: 1em; +} diff --git a/Engine/Launcher/UI/Components/Flyouts/Flyout.razor.min.css b/Engine/Launcher/UI/Components/Flyouts/Flyout.razor.min.css new file mode 100644 index 0000000..f1bb45f --- /dev/null +++ b/Engine/Launcher/UI/Components/Flyouts/Flyout.razor.min.css @@ -0,0 +1 @@ +.flyout{display:none;position:absolute;top:0;width:50%;height:100vh;z-index:9999;background:#fff;border-right:1px solid #000;left:50px;grid-template-rows:70px auto 70px;}.flyout-header{display:flex;justify-content:space-between;}.flyout-header button{border:0;background:none;}.flyout-content{overflow-y:scroll;padding-left:1em;}.flyout-footer{border-top:1px solid #000;padding:5px;display:flex;justify-content:end;gap:8px;}.flyout-footer,.flyout-header,.flyout-content{padding:1em;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Flyouts/Flyout.razor.scss b/Engine/Launcher/UI/Components/Flyouts/Flyout.razor.scss new file mode 100644 index 0000000..decc8e0 --- /dev/null +++ b/Engine/Launcher/UI/Components/Flyouts/Flyout.razor.scss @@ -0,0 +1,39 @@ +.flyout { + display: none; + position: absolute; + top: 0; + width: 50%; + height: 100vh; + z-index: 9999; + background: white; + border-right: 1px solid black; + left: 50px; + grid-template-rows: 70px auto 70px; +} + +.flyout-header { + display: flex; + justify-content: space-between; + + button { + border: none; + background: none; + } +} + +.flyout-content { + overflow-y: scroll; + padding-left: 1em; +} + +.flyout-footer { + border-top: 1px solid black; + padding: 5px; + display: flex; + justify-content: end; + gap: 8px; +} + +.flyout-footer, .flyout-header, .flyout-content { + padding: 1em; +} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Flyouts/FlyoutBase.cs b/Engine/Launcher/UI/Components/Flyouts/FlyoutBase.cs new file mode 100644 index 0000000..0d0d302 --- /dev/null +++ b/Engine/Launcher/UI/Components/Flyouts/FlyoutBase.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Components; + +namespace Launcher.UI.Components.Flyouts; + +/// +/// Represents a base class for flyout components. +/// +public class FlyoutBase : ComponentBase +{ + /// Gets or sets whether the flyout is visible. + [Parameter] + public bool IsVisible { get; set; } + + /// Gets or sets the event executed when the visibility changes. + [Parameter] + public EventCallback IsVisibleChanged { get; set; } + + /// Gets or sets the event executed when the flyout is closed. + [Parameter] + public EventCallback OnClose { get; set; } + + /// + /// Handles changes in visibility asynchronously and updates the visibility state. + /// + /// Indicates whether the element is currently visible or not. + /// + /// A representing an asynchronous operation. + /// + public virtual async Task OnIsVisibleChangedAsync(bool visibility) + { + IsVisible = visibility; + await IsVisibleChanged.InvokeAsync(visibility); + } + + /// + /// Closes the flyout. + /// + /// + /// A representing an asynchronous operation. + /// + public virtual async Task Close() + { + await OnClose.InvokeAsync(); + await OnIsVisibleChangedAsync(false); + } +} diff --git a/Engine/Launcher/UI/Components/Flyouts/SettingsFlyout.razor b/Engine/Launcher/UI/Components/Flyouts/SettingsFlyout.razor new file mode 100644 index 0000000..e8c7930 --- /dev/null +++ b/Engine/Launcher/UI/Components/Flyouts/SettingsFlyout.razor @@ -0,0 +1,19 @@ +@inherits FlyoutBase + + +
+ + +
+
+ +@code { + [Inject] + private INotificationService _notificationService { get; init; } = default!; + + private async Task SaveSettings() + { + _notificationService.Show("Settings saved."); + await Close(); + } +} diff --git a/Engine/Launcher/UI/Components/Grid/Grid.razor b/Engine/Launcher/UI/Components/Grid/Grid.razor new file mode 100644 index 0000000..2b23f84 --- /dev/null +++ b/Engine/Launcher/UI/Components/Grid/Grid.razor @@ -0,0 +1,84 @@ +@using System.ComponentModel +@using SharpEngine.Shared.Attributes + +@typeparam TItem + +
+ + + + @foreach (var header in _headers) + { + + } + + +
+
+ + +
+ + + @foreach (var item in Items) + { + + + @GridRowTemplate(item) + + + } + +
+
+ +@code { + /// + /// Gets or sets the items to be rendered onto the grid. + /// + [Parameter] + public List Items { get; set; } = []; + + /// + /// Gets or sets a template by which each row should be rendered. + /// + [Parameter] + public RenderFragment GridRowTemplate { get; set; } + + [Parameter] + public EventCallback> FilterChanged { get; set; } + + private List _headers { get; set; } = []; + private Dictionary _filterModes { get; set; } = new(); + + /// + protected override Task OnInitializedAsync() + { + GetHeaders(); + return base.OnInitializedAsync(); + } + + private void GetHeaders() + { + var props = typeof(TItem).GetProperties(); + var headers = props.Select(prop => + { + var displayNameAttribute = (DisplayNameAttribute?)prop.GetCustomAttributes(typeof(DisplayNameAttribute), false).FirstOrDefault(); + var filterAttribute = (FilterAttribute?)prop.GetCustomAttributes(typeof(FilterAttribute), false).FirstOrDefault(); + + return new GridHeaderInfo(displayNameAttribute?.DisplayName ?? prop.Name, filterAttribute?.IsFilterable ?? true); + }); + + _headers = headers.ToList(); + } + + private async Task OnFilterChanged(FilterMode filterMode, string header) + { + if (_filterModes.TryGetValue(header, out var mode)) + _filterModes[header] = filterMode; + else + _filterModes.Add(header, filterMode); + + await FilterChanged.InvokeAsync(_filterModes); + } +} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Grid/Grid.razor.css b/Engine/Launcher/UI/Components/Grid/Grid.razor.css new file mode 100644 index 0000000..96e0633 --- /dev/null +++ b/Engine/Launcher/UI/Components/Grid/Grid.razor.css @@ -0,0 +1,19 @@ +.auto-width-table { + table-layout: auto; + width: 100%; +} + +.auto-width-table th, .auto-width-table td { + white-space: nowrap; + padding: 8px; + text-align: left; +} + +.table-header { + border-bottom: 1px solid black; +} + +.table-content { + overflow-y: scroll; + height: 250px; +} diff --git a/Engine/Launcher/UI/Components/Grid/Grid.razor.min.css b/Engine/Launcher/UI/Components/Grid/Grid.razor.min.css new file mode 100644 index 0000000..74f740a --- /dev/null +++ b/Engine/Launcher/UI/Components/Grid/Grid.razor.min.css @@ -0,0 +1 @@ +.auto-width-table{table-layout:auto;width:100%;}.auto-width-table th,.auto-width-table td{white-space:nowrap;padding:8px;text-align:left;}.table-header{border-bottom:1px solid #000;}.table-content{overflow-y:scroll;height:250px;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Grid/Grid.razor.scss b/Engine/Launcher/UI/Components/Grid/Grid.razor.scss new file mode 100644 index 0000000..daf24c1 --- /dev/null +++ b/Engine/Launcher/UI/Components/Grid/Grid.razor.scss @@ -0,0 +1,19 @@ +.auto-width-table { + table-layout: auto; + width: 100%; +} + +.auto-width-table th, .auto-width-table td { + white-space: nowrap; + padding: 8px; + text-align: left; +} + +.table-header { + border-bottom: 1px solid black; +} + +.table-content { + overflow-y: scroll; + height: 250px; +} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Grid/GridHeader.cs b/Engine/Launcher/UI/Components/Grid/GridHeader.cs new file mode 100644 index 0000000..91e07ec --- /dev/null +++ b/Engine/Launcher/UI/Components/Grid/GridHeader.cs @@ -0,0 +1,24 @@ +namespace Launcher.UI.Components.Grid; + +/// +/// Contains information about a grid header. +/// +public class GridHeaderInfo +{ + /// Gets or sets the title of the column. + public string Title { get; set; } + + /// Gets or sets whether the column allows filtering. + public bool AllowFiltering { get; set; } + + /// + /// Initializes a new instance of the . + /// + /// Specifies the header title for the grid. + /// Indicates whether filtering is permitted on the grid header. + public GridHeaderInfo(string title, bool allowFiltering) + { + Title = title; + AllowFiltering = allowFiltering; + } +} diff --git a/Engine/Launcher/UI/Components/Grid/GridHeader.razor b/Engine/Launcher/UI/Components/Grid/GridHeader.razor new file mode 100644 index 0000000..e5a8481 --- /dev/null +++ b/Engine/Launcher/UI/Components/Grid/GridHeader.razor @@ -0,0 +1,47 @@ + + + + +@code { + /// Gets or sets the contents of the grid row header. + [Parameter] + public string Title { get; set; } = default!; + + /// Gets or sets whether filtering is allowed for the header. + [Parameter] + public bool AllowFiltering { get; set; } + + /// Gets or sets the event executed when the filter mode changes. + [Parameter] + public EventCallback FilterChanged { get; set; } + + /// Gets or sets the filter mode of the header. + [Parameter] + public FilterMode FilterMode { get; set; } = FilterMode.None; + + private string _filterClass => AllowFiltering ? "filter-btn" : "filter-disabled"; + + private SvgIcon _icon => this.FilterMode switch + { + FilterMode.Ascending => SvgIcon.ChevronUp, + FilterMode.Descending => SvgIcon.ChevronDown, + _ => SvgIcon.None + }; + + private async Task OnFilterClicked() + { + FilterMode = FilterMode switch + { + FilterMode.None => FilterMode.Ascending, + FilterMode.Ascending => FilterMode.Descending, + FilterMode.Descending => FilterMode.None, + _ => FilterMode.None + }; + + if (AllowFiltering) + await FilterChanged.InvokeAsync(FilterMode); + } +} diff --git a/Engine/Launcher/UI/Components/Grid/GridHeader.razor.css b/Engine/Launcher/UI/Components/Grid/GridHeader.razor.css new file mode 100644 index 0000000..642257e --- /dev/null +++ b/Engine/Launcher/UI/Components/Grid/GridHeader.razor.css @@ -0,0 +1,15 @@ +.btn { + background: transparent; + border: none; + padding: 0; +} + +.filter-btn { + display: flex; + align-items: center; +} + +.filter-disabled { + cursor: none; + pointer-events: none; +} diff --git a/Engine/Launcher/UI/Components/Grid/GridHeader.razor.min.css b/Engine/Launcher/UI/Components/Grid/GridHeader.razor.min.css new file mode 100644 index 0000000..b5f165e --- /dev/null +++ b/Engine/Launcher/UI/Components/Grid/GridHeader.razor.min.css @@ -0,0 +1 @@ +.btn{background:transparent;border:0;padding:0;}.filter-btn{display:flex;align-items:center;}.filter-disabled{cursor:none;pointer-events:none;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Grid/GridHeader.razor.scss b/Engine/Launcher/UI/Components/Grid/GridHeader.razor.scss new file mode 100644 index 0000000..e0f25a0 --- /dev/null +++ b/Engine/Launcher/UI/Components/Grid/GridHeader.razor.scss @@ -0,0 +1,15 @@ +.btn { + background: transparent; + border: none; + padding: 0; +} + +.filter-btn { + display: flex; + align-items: center; +} + +.filter-disabled { + cursor: none; + pointer-events: none; +} \ No newline at end of file diff --git a/Launcher/UI/Components/GridRow.razor b/Engine/Launcher/UI/Components/Grid/GridRow.razor similarity index 100% rename from Launcher/UI/Components/GridRow.razor rename to Engine/Launcher/UI/Components/Grid/GridRow.razor diff --git a/Launcher/UI/Components/DiscardIcon.razor b/Engine/Launcher/UI/Components/Icons/DiscardIcon.razor similarity index 54% rename from Launcher/UI/Components/DiscardIcon.razor rename to Engine/Launcher/UI/Components/Icons/DiscardIcon.razor index 0b36549..ee38ca5 100644 --- a/Launcher/UI/Components/DiscardIcon.razor +++ b/Engine/Launcher/UI/Components/Icons/DiscardIcon.razor @@ -1,11 +1,10 @@ -✖ + + + @code { /// Gets or sets the event executed when the discard icon is clicked. [Parameter] public EventCallback DiscardClicked { get; set; } - - private async Task OnDiscardClicked() - => await DiscardClicked.InvokeAsync(); } diff --git a/Engine/Launcher/UI/Components/Icons/Icon.razor b/Engine/Launcher/UI/Components/Icons/Icon.razor new file mode 100644 index 0000000..4c2b415 --- /dev/null +++ b/Engine/Launcher/UI/Components/Icons/Icon.razor @@ -0,0 +1,73 @@ +@using Launcher.Enums + +@switch (IconType) +{ + case SvgIcon.Home: + + + + + + + + break; + case SvgIcon.Cog: + + + + + + + + break; + case SvgIcon.Lens: + + + + + + + break; + case SvgIcon.Discard: + ✖ + break; + case SvgIcon.Close: + + + + + + + + break; + case SvgIcon.ChevronDown: + + + + + + + break; + case SvgIcon.ChevronUp: + + + + + + + + break; + default: + break; +} + +@code { + + /// Gets or sets the type of the icon. + [Parameter] + public SvgIcon IconType { get; set; } + + /// Gets or sets the size of the icon in pixels. + [Parameter] + public int SizePx { get; set; } = 16; +} diff --git a/Engine/Launcher/UI/Components/Layout.razor b/Engine/Launcher/UI/Components/Layout.razor new file mode 100644 index 0000000..507dd2d --- /dev/null +++ b/Engine/Launcher/UI/Components/Layout.razor @@ -0,0 +1,23 @@ +@inherits LayoutComponentBase + +
+ + + + + + + +
+ @Body +
+
+ + + +@code { + private bool _settingsVisible; + + private void OpenSettingsFlyout() + => _settingsVisible = true; +} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Layout.razor.css b/Engine/Launcher/UI/Components/Layout.razor.css new file mode 100644 index 0000000..cb2bbbb --- /dev/null +++ b/Engine/Launcher/UI/Components/Layout.razor.css @@ -0,0 +1,6 @@ +.content { + margin: 0 1em; + padding: 0; + display: grid; + grid-template-rows: 100px auto; +} diff --git a/Engine/Launcher/UI/Components/Layout.razor.min.css b/Engine/Launcher/UI/Components/Layout.razor.min.css new file mode 100644 index 0000000..aee90ef --- /dev/null +++ b/Engine/Launcher/UI/Components/Layout.razor.min.css @@ -0,0 +1 @@ +.content{margin:0 1em;padding:0;display:grid;grid-template-rows:100px auto;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Layout.razor.scss b/Engine/Launcher/UI/Components/Layout.razor.scss new file mode 100644 index 0000000..6a00208 --- /dev/null +++ b/Engine/Launcher/UI/Components/Layout.razor.scss @@ -0,0 +1,6 @@ +.content { + margin: 0 1em; + padding: 0; + display: grid; + grid-template-rows: 100px auto; +} diff --git a/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor b/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor new file mode 100644 index 0000000..fe8bc2b --- /dev/null +++ b/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor @@ -0,0 +1,14 @@ + + +@code { + /// Gets or set the icon for the sidebar item. + [Parameter] + public SvgIcon Icon { get; set; } + + /// Gets or sets the event executed when the sidebar item is clicked. + [Parameter] + public EventCallback OnClick { get; set; } + + private async Task OnItemClicked() + => await OnClick.InvokeAsync(); +} diff --git a/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor.css b/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor.css new file mode 100644 index 0000000..63ddf80 --- /dev/null +++ b/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor.css @@ -0,0 +1,5 @@ +.sidebar-item { + cursor: pointer; + list-style: none; + padding: 0.5rem 1em; +} diff --git a/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor.min.css b/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor.min.css new file mode 100644 index 0000000..38c8234 --- /dev/null +++ b/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor.min.css @@ -0,0 +1 @@ +.sidebar-item{cursor:pointer;list-style:none;padding:.5rem 1em;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor.scss b/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor.scss new file mode 100644 index 0000000..73c8089 --- /dev/null +++ b/Engine/Launcher/UI/Components/Navigation/SideBarItem.razor.scss @@ -0,0 +1,5 @@ +.sidebar-item { + cursor: pointer; + list-style: none; + padding: 0.5rem 1em; +} diff --git a/Engine/Launcher/UI/Components/Navigation/Sidebar.razor b/Engine/Launcher/UI/Components/Navigation/Sidebar.razor new file mode 100644 index 0000000..bc3ca2c --- /dev/null +++ b/Engine/Launcher/UI/Components/Navigation/Sidebar.razor @@ -0,0 +1,11 @@ + + +@code { + /// Gets or sets the sidebar items to be rendered within the sidebar. + [Parameter, EditorRequired] + public RenderFragment ChildContent { get; set; } +} diff --git a/Engine/Launcher/UI/Components/Navigation/Sidebar.razor.css b/Engine/Launcher/UI/Components/Navigation/Sidebar.razor.css new file mode 100644 index 0000000..8d28b22 --- /dev/null +++ b/Engine/Launcher/UI/Components/Navigation/Sidebar.razor.css @@ -0,0 +1,6 @@ +.sidebar { + border-right: 1px solid black; +} +.sidebar ul { + padding: 0; +} diff --git a/Engine/Launcher/UI/Components/Navigation/Sidebar.razor.min.css b/Engine/Launcher/UI/Components/Navigation/Sidebar.razor.min.css new file mode 100644 index 0000000..d701475 --- /dev/null +++ b/Engine/Launcher/UI/Components/Navigation/Sidebar.razor.min.css @@ -0,0 +1 @@ +.sidebar{border-right:1px solid #000;}.sidebar ul{padding:0;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Navigation/Sidebar.razor.scss b/Engine/Launcher/UI/Components/Navigation/Sidebar.razor.scss new file mode 100644 index 0000000..6a71104 --- /dev/null +++ b/Engine/Launcher/UI/Components/Navigation/Sidebar.razor.scss @@ -0,0 +1,7 @@ +.sidebar { + border-right: 1px solid black; + + ul { + padding: 0; + } +} diff --git a/Engine/Launcher/UI/Components/Notification/Notification.razor b/Engine/Launcher/UI/Components/Notification/Notification.razor new file mode 100644 index 0000000..a46aee5 --- /dev/null +++ b/Engine/Launcher/UI/Components/Notification/Notification.razor @@ -0,0 +1,30 @@ +@inherits DialogBase + +@inject INotificationService NotificationService + +
+
+ + @foreach (var notification in NotificationService.Notifications) + { + + } + +
+
+ +@code { + /// Gets or sets the type of the notification. + [Parameter] + public NotificationType Type { get; set; } = NotificationType.Warning; + + /// + protected override void OnInitialized() + { + NotificationService.SetInvokeAsync(InvokeAsync); + NotificationService.SetOnNotificationsChanged(StateHasChanged); + } +} diff --git a/Engine/Launcher/UI/Components/Notification/Notification.razor.css b/Engine/Launcher/UI/Components/Notification/Notification.razor.css new file mode 100644 index 0000000..e12e310 --- /dev/null +++ b/Engine/Launcher/UI/Components/Notification/Notification.razor.css @@ -0,0 +1,18 @@ +.notification-container { + width: 100%; + height: 100%; + position: absolute; + pointer-events: none; +} + +.notification { + position: relative; + display: flex; + top: 50px; + flex-direction: column; + align-items: end; +} + +.notification-content { + pointer-events: auto; +} diff --git a/Engine/Launcher/UI/Components/Notification/Notification.razor.min.css b/Engine/Launcher/UI/Components/Notification/Notification.razor.min.css new file mode 100644 index 0000000..c478afd --- /dev/null +++ b/Engine/Launcher/UI/Components/Notification/Notification.razor.min.css @@ -0,0 +1 @@ +.notification-container{width:100%;height:100%;position:absolute;pointer-events:none;}.notification{position:relative;display:flex;top:50px;flex-direction:column;align-items:end;}.notification-content{pointer-events:auto;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Notification/Notification.razor.scss b/Engine/Launcher/UI/Components/Notification/Notification.razor.scss new file mode 100644 index 0000000..f44c2d1 --- /dev/null +++ b/Engine/Launcher/UI/Components/Notification/Notification.razor.scss @@ -0,0 +1,18 @@ +.notification-container { + width: 100%; + height: 100%; + position: absolute; + pointer-events: none; +} + +.notification { + position: relative; + display: flex; + top: 50px; + flex-direction: column; + align-items: end; +} + +.notification-content { + pointer-events: auto; +} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Notification/NotificationType.cs b/Engine/Launcher/UI/Components/Notification/NotificationType.cs new file mode 100644 index 0000000..a5fd255 --- /dev/null +++ b/Engine/Launcher/UI/Components/Notification/NotificationType.cs @@ -0,0 +1,32 @@ +namespace Launcher.UI.Components.Notification +{ + /// + /// Represents the type of notification. + /// + public enum NotificationType + { + /// Primary notification type. + Primary, + + /// Secondary notification type. + Secondary, + + /// Success notification type. + Success, + + /// Danger notification type. + Danger, + + /// Warning notification type. + Warning, + + /// Info notification type. + Info, + + /// Light notification type. + Light, + + /// Dark notification type. + Dark, + } +} diff --git a/Launcher/UI/Components/Dialogs/NotificationType.cs b/Engine/Launcher/UI/Components/Notification/NotificationTypeExtensions.cs similarity index 59% rename from Launcher/UI/Components/Dialogs/NotificationType.cs rename to Engine/Launcher/UI/Components/Notification/NotificationTypeExtensions.cs index 9f34c0a..7762cf4 100644 --- a/Launcher/UI/Components/Dialogs/NotificationType.cs +++ b/Engine/Launcher/UI/Components/Notification/NotificationTypeExtensions.cs @@ -1,35 +1,5 @@ -namespace Launcher.UI.Components +namespace Launcher.UI.Components.Notification { - /// - /// Represents the type of notification. - /// - public enum NotificationType - { - /// Primary notification type. - Primary, - - /// Secondary notification type. - Secondary, - - /// Success notification type. - Success, - - /// Danger notification type. - Danger, - - /// Warning notification type. - Warning, - - /// Info notification type. - Info, - - /// Light notification type. - Light, - - /// Dark notification type. - Dark, - } - /// /// Extension methods for . /// diff --git a/Engine/Launcher/UI/Components/Searchbox.razor b/Engine/Launcher/UI/Components/Searchbox.razor new file mode 100644 index 0000000..2417a3e --- /dev/null +++ b/Engine/Launcher/UI/Components/Searchbox.razor @@ -0,0 +1,35 @@ + + +@code { + /// Gets or sets the value of the search box. + [Parameter] + public string Value { get; set; } = default!; + + /// Gets or sets the event executed when the value of the search box changes. + [Parameter] + public EventCallback ValueChanged { get; set; } + + /// Gets or sets the delay in milliseconds before the value is updated after the user stops typing. + [Parameter] + public int DebounceDelayMilliseconds { get; set; } = 1000; + + /// Gets or sets any extra CSS classes to apply to the search box. + [Parameter] + public string? CssClass { get; set; } + + private CancellationTokenSource _tokenSource { get; set; } = new(); + + private async Task OnChangeAsync(ChangeEventArgs e) + { + _tokenSource = new CancellationTokenSource(); + var token = _tokenSource.Token; + + await Task.Delay(DebounceDelayMilliseconds, token); + + Value = e.Value?.ToString() ?? string.Empty; + await ValueChanged.InvokeAsync(Value); + } +} diff --git a/Engine/Launcher/UI/Components/Searchbox.razor.css b/Engine/Launcher/UI/Components/Searchbox.razor.css new file mode 100644 index 0000000..9f2bc34 --- /dev/null +++ b/Engine/Launcher/UI/Components/Searchbox.razor.css @@ -0,0 +1,14 @@ +.searchbox { + border: 1px solid black; + width: 40%; + justify-self: end; +} + +.searchbox-btn { + border: none; + background: none; +} + +.searchbox-input { + border: none; +} diff --git a/Engine/Launcher/UI/Components/Searchbox.razor.min.css b/Engine/Launcher/UI/Components/Searchbox.razor.min.css new file mode 100644 index 0000000..2641fc8 --- /dev/null +++ b/Engine/Launcher/UI/Components/Searchbox.razor.min.css @@ -0,0 +1 @@ +.searchbox{border:1px solid #000;width:40%;justify-self:end;}.searchbox-btn{border:0;background:none;}.searchbox-input{border:0;} \ No newline at end of file diff --git a/Engine/Launcher/UI/Components/Searchbox.razor.scss b/Engine/Launcher/UI/Components/Searchbox.razor.scss new file mode 100644 index 0000000..4dd8607 --- /dev/null +++ b/Engine/Launcher/UI/Components/Searchbox.razor.scss @@ -0,0 +1,14 @@ +.searchbox { + border: 1px solid black; + width: 40%; + justify-self: end; +} + +.searchbox-btn { + border: none; + background: none; +} + +.searchbox-input { + border: none; +} \ No newline at end of file diff --git a/Engine/Launcher/UI/Home.razor b/Engine/Launcher/UI/Home.razor new file mode 100644 index 0000000..0eae1e4 --- /dev/null +++ b/Engine/Launcher/UI/Home.razor @@ -0,0 +1,41 @@ +@page "/" + + + + +
+

Projects

+ +
+ + +
+
+ + +
+ + + + +

@context.Name

+

@context.Path

+ + @context.LastModified + + + + +
+
+
+ + + @{ + var project = (Project)context; + + + + + } + \ No newline at end of file diff --git a/Launcher/UI/Home.razor.cs b/Engine/Launcher/UI/Home.razor.cs similarity index 61% rename from Launcher/UI/Home.razor.cs rename to Engine/Launcher/UI/Home.razor.cs index 71c52fb..5fe6573 100644 --- a/Launcher/UI/Home.razor.cs +++ b/Engine/Launcher/UI/Home.razor.cs @@ -1,11 +1,17 @@ using Launcher.Services; +using Launcher.UI.Components; using Microsoft.AspNetCore.Components; + +using SharpEngine.Shared.Dto; +using System.Diagnostics; using System.Text.Json; -using System.Text.Json.Serialization; + +using FilterMode = SharpEngine.Shared.Enums.FilterMode; namespace Launcher.UI { /// Represents the home page of the launcher. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S6966:Awaitable method should be used", Justification = "")] public partial class Home : ComponentBase { [Inject] @@ -17,12 +23,26 @@ public partial class Home : ComponentBase private bool _showConfirmDelete; private bool _showCreateProjectDialog; - private List Projects { get; set; } = []; + private ContextMenu? _contextMenu; + + private List Projects + { + get => _allProjects; + set + { + _allProjects = value; + _filteredProjects = value; + } + } + + private List _allProjects = []; + private List _filteredProjects = []; + private Project _selectedProject = new(); private const string _projectsFile = "projects.json"; private static readonly string[] _sharpProjectFilePickerExtension = [".sharpproject"]; -#if DEBUG +#if !DEBUG private readonly string _projectsFilePath = Path.Join(Path.GetTempPath(), _projectsFile); #else private readonly string _projectsFilePath = Path.Join(AppContext.BaseDirectory, _projectsFile); @@ -32,17 +52,35 @@ protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - // TODO: Get current launcher version - // TODO: Check if GitHub has a new version released. + // TODO: #96 Get current launcher version + // TODO: #96 Check if GitHub has a new version released. + +#if DEBUG + if (TestData.TestData.SimulateLoading) + { + _notificationService.Show("Simulating loading projects."); + await Task.Delay(10000); + } + + if (TestData.TestData.LoadTestData) + { + _notificationService.Show("Using test data."); + Projects = [.. TestData.TestData.Projects]; + return; + } +#endif var projects = _editorService.LoadProjects(_projectsFilePath); - + if (projects is not null) Projects.AddRange(projects); else _notificationService.Show($"Unable to load project file.", false, projects); } + private void SearchProjects(string searchValue) + => _filteredProjects = [.. Projects.Where(p => p.Name!.Contains(searchValue, StringComparison.OrdinalIgnoreCase))]; + private void OnCreateProjectClicked() => _showCreateProjectDialog = true; @@ -58,7 +96,7 @@ private void OnDeleteProjectConfirm() if (project is not null) { Projects.Remove(project); - // TODO: Delete project files + // TODO: #26 Delete project files } else _notificationService.Show($"Unable to delete project '{project?.Name}'.", false, _selectedProject, Projects); @@ -87,7 +125,7 @@ private async Task OpenExistingFileAsync() if (pickedFile == null) return; - if (pickedFile.FileName.EndsWith(".sharpproject") == false) + if (!pickedFile.FileName.EndsWith(".sharpproject")) { _notificationService.Show("Invalid file type selected. Please select a .sharpproject file."); return; @@ -113,5 +151,28 @@ private void SetProjects(Project project) string json = JsonSerializer.Serialize(Projects); File.WriteAllText(_projectsFilePath, json); } + + // If the user sets the project path as an executable command, this will execute that command. + // In most cases that should be fine as the developer has the launcher installed on their machine, however this still should be resolved soon. + private static void OpenInExplorer(string projectPath) + => Process.Start("explorer.exe", projectPath); + + private void RemoveFromList(Project project) + { + var foundProject = Projects.Find(p => p.Id == project.Id); + if (foundProject is null) + { + _notificationService.Show($"Unable to remove project '{project.Name}', project not found.", false, project); + return; + } + + Projects.Remove(foundProject); + } + + private void OnFiltersChanged(Dictionary filters) + { + _filteredProjects = [.. Projects.FilterBy(filters)]; + StateHasChanged(); + } } } diff --git a/Launcher/UI/Home.razor.css b/Engine/Launcher/UI/Home.razor.css similarity index 98% rename from Launcher/UI/Home.razor.css rename to Engine/Launcher/UI/Home.razor.css index ffb5a06..dfc3ab4 100644 --- a/Launcher/UI/Home.razor.css +++ b/Engine/Launcher/UI/Home.razor.css @@ -34,7 +34,7 @@ display: flex; justify-content: space-between; align-items: center; - margin: 25px; + margin: 25px 0; } .alert-header { diff --git a/Launcher/UI/Home.razor.min.css b/Engine/Launcher/UI/Home.razor.min.css similarity index 50% rename from Launcher/UI/Home.razor.min.css rename to Engine/Launcher/UI/Home.razor.min.css index 384ee6f..cb60f75 100644 --- a/Launcher/UI/Home.razor.min.css +++ b/Engine/Launcher/UI/Home.razor.min.css @@ -1 +1 @@ -.toast{right:0;transition:transform .5s ease-in-out;}.dialog{left:0;right:0;margin-left:auto;margin-right:auto;width:auto;max-width:90%;}.toast,.dialog{position:absolute;top:50px;}.notification-visible{transform:translateX(0);}.notification-hidden{transform:translateX(100%);}.create-project-dialog-hidden,.confirm-delete-hidden{display:none;}.header{display:flex;justify-content:space-between;align-items:center;margin:25px;}.alert-header{display:flex;justify-content:space-between;}.alert-header h4{padding:0;}.flex-end{justify-content:end;display:flex;gap:8px;}.custom-file-upload{display:inline-block;padding:8px 12px;background-color:#007bff;color:#fff;border-radius:4px;cursor:pointer;}.hidden-input{position:absolute;width:0;height:0;opacity:0;} \ No newline at end of file +.toast{right:0;transition:transform .5s ease-in-out;}.dialog{left:0;right:0;margin-left:auto;margin-right:auto;width:auto;max-width:90%;}.toast,.dialog{position:absolute;top:50px;}.notification-visible{transform:translateX(0);}.notification-hidden{transform:translateX(100%);}.create-project-dialog-hidden,.confirm-delete-hidden{display:none;}.header{display:flex;justify-content:space-between;align-items:center;margin:25px 0;}.alert-header{display:flex;justify-content:space-between;}.alert-header h4{padding:0;}.flex-end{justify-content:end;display:flex;gap:8px;}.custom-file-upload{display:inline-block;padding:8px 12px;background-color:#007bff;color:#fff;border-radius:4px;cursor:pointer;}.hidden-input{position:absolute;width:0;height:0;opacity:0;} \ No newline at end of file diff --git a/Launcher/UI/Home.razor.scss b/Engine/Launcher/UI/Home.razor.scss similarity index 97% rename from Launcher/UI/Home.razor.scss rename to Engine/Launcher/UI/Home.razor.scss index be215ee..bbdf065 100644 --- a/Launcher/UI/Home.razor.scss +++ b/Engine/Launcher/UI/Home.razor.scss @@ -34,7 +34,7 @@ display: flex; justify-content: space-between; align-items: center; - margin: 25px; + margin: 25px 0 ; } .alert-header { diff --git a/Launcher/UI/Routes.razor b/Engine/Launcher/UI/Routes.razor similarity index 100% rename from Launcher/UI/Routes.razor rename to Engine/Launcher/UI/Routes.razor diff --git a/Engine/Launcher/UI/_Imports.razor b/Engine/Launcher/UI/_Imports.razor new file mode 100644 index 0000000..ea37d20 --- /dev/null +++ b/Engine/Launcher/UI/_Imports.razor @@ -0,0 +1,25 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using System.Diagnostics + +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop + +@using Launcher +@using Launcher.Services +@using Launcher.Enums + +@using Launcher.UI +@using Launcher.UI.Components +@using Launcher.UI.Components.Grid +@using Launcher.UI.Components.Notification +@using Launcher.UI.Components.Icons +@using Launcher.UI.Components.Dialogs +@using Launcher.UI.Components.Flyouts +@using Launcher.UI.Components.Navigation + +@using SharpEngine.Shared.Dto +@using SharpEngine.Shared.Enums \ No newline at end of file diff --git a/Engine/Launcher/compilerconfig.json b/Engine/Launcher/compilerconfig.json new file mode 100644 index 0000000..fa73a0f --- /dev/null +++ b/Engine/Launcher/compilerconfig.json @@ -0,0 +1,62 @@ +[ + { + "outputFile": "UI/Home.css", + "inputFile": "UI/Home.scss" + }, + { + "outputFile": "UI/Home.razor.css", + "inputFile": "UI/Home.razor.scss" + }, + { + "outputFile": "wwwroot/css/additional.razor.css", + "inputFile": "wwwroot/css/additional.razor.scss" + }, + { + "outputFile": "wwwroot/css/additional.css", + "inputFile": "wwwroot/css/additional.scss" + }, + { + "outputFile": "UI/Components/Dialogs/ConfirmDeleteDialog.razor.css", + "inputFile": "UI/Components/Dialogs/ConfirmDeleteDialog.razor.scss" + }, + { + "outputFile": "UI/Components/Navigation/SideBarItem.razor.css", + "inputFile": "UI/Components/Navigation/SideBarItem.razor.scss" + }, + { + "outputFile": "UI/Components/Flyouts/Flyout.razor.css", + "inputFile": "UI/Components/Flyouts/Flyout.razor.scss" + }, + { + "outputFile": "UI/Components/Grid.razor.css", + "inputFile": "UI/Components/Grid.razor.scss" + }, + { + "outputFile": "UI/Components/Grid/GridHeader.razor.css", + "inputFile": "UI/Components/Grid/GridHeader.razor.scss" + }, + { + "outputFile": "UI/Components/Dialogs/CreateProjectDialog.razor.css", + "inputFile": "UI/Components/Dialogs/CreateProjectDialog.razor.scss" + }, + { + "outputFile": "UI/Components/Navigation/Sidebar.razor.css", + "inputFile": "UI/Components/Navigation/Sidebar.razor.scss" + }, + { + "outputFile": "UI/Components/Notification/Notification.razor.css", + "inputFile": "UI/Components/Notification/Notification.razor.scss" + }, + { + "outputFile": "UI/Components/ContextMenu.razor.css", + "inputFile": "UI/Components/ContextMenu.razor.scss" + }, + { + "outputFile": "UI/Components/Layout.razor.css", + "inputFile": "UI/Components/Layout.razor.scss" + }, + { + "outputFile": "UI/Components/Searchbox.razor.css", + "inputFile": "UI/Components/Searchbox.razor.scss" + } +] \ No newline at end of file diff --git a/Launcher/compilerconfig.json.defaults b/Engine/Launcher/compilerconfig.json.defaults similarity index 100% rename from Launcher/compilerconfig.json.defaults rename to Engine/Launcher/compilerconfig.json.defaults diff --git a/Launcher/wwwroot/css/additional.css b/Engine/Launcher/wwwroot/css/additional.css similarity index 100% rename from Launcher/wwwroot/css/additional.css rename to Engine/Launcher/wwwroot/css/additional.css diff --git a/Launcher/wwwroot/css/additional.min.css b/Engine/Launcher/wwwroot/css/additional.min.css similarity index 100% rename from Launcher/wwwroot/css/additional.min.css rename to Engine/Launcher/wwwroot/css/additional.min.css diff --git a/Launcher/wwwroot/css/additional.scss b/Engine/Launcher/wwwroot/css/additional.scss similarity index 100% rename from Launcher/wwwroot/css/additional.scss rename to Engine/Launcher/wwwroot/css/additional.scss diff --git a/Launcher/wwwroot/css/app.css b/Engine/Launcher/wwwroot/css/app.css similarity index 97% rename from Launcher/wwwroot/css/app.css rename to Engine/Launcher/wwwroot/css/app.css index aad5a54..2196041 100644 --- a/Launcher/wwwroot/css/app.css +++ b/Engine/Launcher/wwwroot/css/app.css @@ -100,5 +100,11 @@ p, h1, h4, a, td, tr, div, .prevent-select { .page { height: 100%; - display: flow-root; + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 50px auto; +} + +p { + margin: 0; } \ No newline at end of file diff --git a/Launcher/wwwroot/css/bootstrap/bootstrap.min.css b/Engine/Launcher/wwwroot/css/bootstrap/bootstrap.min.css similarity index 100% rename from Launcher/wwwroot/css/bootstrap/bootstrap.min.css rename to Engine/Launcher/wwwroot/css/bootstrap/bootstrap.min.css diff --git a/Launcher/wwwroot/css/bootstrap/bootstrap.min.css.map b/Engine/Launcher/wwwroot/css/bootstrap/bootstrap.min.css.map similarity index 100% rename from Launcher/wwwroot/css/bootstrap/bootstrap.min.css.map rename to Engine/Launcher/wwwroot/css/bootstrap/bootstrap.min.css.map diff --git a/Launcher/wwwroot/favicon.png b/Engine/Launcher/wwwroot/favicon.png similarity index 100% rename from Launcher/wwwroot/favicon.png rename to Engine/Launcher/wwwroot/favicon.png diff --git a/Launcher/wwwroot/index.html b/Engine/Launcher/wwwroot/index.html similarity index 100% rename from Launcher/wwwroot/index.html rename to Engine/Launcher/wwwroot/index.html diff --git a/Engine/ObjLoader/DataStore.cs b/Engine/ObjLoader/DataStore.cs new file mode 100644 index 0000000..5ee2b5c --- /dev/null +++ b/Engine/ObjLoader/DataStore.cs @@ -0,0 +1,52 @@ +using ObjLoader.Loader.Common; +using ObjLoader.Loader.Data.Elements; + +using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using System.Collections.Generic; +using System.Linq; + +namespace ObjLoader +{ + public class DataStore : IGroupDataStore, IFaceGroup + { + private Group _currentGroup; + + private readonly List _groups = []; + private readonly List _materials = []; + + public List Vertices { get; } = []; + + public List Textures { get; } = []; + + public List Normals { get; } = []; + + public List Materials { get; } = []; + + public List Groups { get; } = []; + + public DataStore() + { + PushGroup("default"); + } + + public void AddFace(Face face) + { + _currentGroup.AddFace(face); + } + + public void PushGroup(string groupName) + { + _currentGroup = new Group(groupName); + _groups.Add(_currentGroup); + } + + /// + public void SetMaterial(string materialName) + { + var material = _materials.SingleOrDefault(x => x.Name.EqualsOrdinalIgnoreCase(materialName)); + _currentGroup.Material = material; + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/Loaders/LoaderBase.cs b/Engine/ObjLoader/Loaders/LoaderBase.cs new file mode 100644 index 0000000..0de39a2 --- /dev/null +++ b/Engine/ObjLoader/Loaders/LoaderBase.cs @@ -0,0 +1,38 @@ +using System.IO; + +namespace ObjLoader.Loader.Loaders +{ + public abstract class LoaderBase + { + private StreamReader _lineStreamReader; + + protected void StartLoad(Stream lineStream) + { + _lineStreamReader = new StreamReader(lineStream); + + while (!_lineStreamReader.EndOfStream) + { + ParseLine(); + } + } + + private void ParseLine() + { + var currentLine = _lineStreamReader.ReadLine(); + + if (string.IsNullOrWhiteSpace(currentLine)) + return; + + if (currentLine[0] == '#') + return; + + var fields = currentLine.Trim().Split(null, 2); + var keyword = fields[0].Trim(); + var data = fields[1].Trim(); + + ParseLine(keyword, data); + } + + protected abstract void ParseLine(string keyword, string data); + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs b/Engine/ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs new file mode 100644 index 0000000..4417065 --- /dev/null +++ b/Engine/ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs @@ -0,0 +1,7 @@ +namespace ObjLoader.Loaders.MaterialLoader +{ + public interface IMaterialLibraryLoaderFacade + { + void Load(string materialFileName); + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs b/Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs new file mode 100644 index 0000000..90ebb1b --- /dev/null +++ b/Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs @@ -0,0 +1,107 @@ +using ObjLoader.Loader.Common; +using ObjLoader.Loader.Loaders; +using SharpEngine.Core.Components.Properties; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; + +namespace ObjLoader.Loaders.MaterialLoader +{ + // https://paulbourke.net/dataformats/mtl/ + + public class MaterialLibraryLoader : LoaderBase + { + private string _objPath; + private readonly DataStore _dataStore; + private readonly Dictionary> _parseActionDictionary = []; + private readonly List _unrecognizedLines = []; + + private Material CurrentMaterial { get; set; } + + // TODO: #2 See Model class. This support both materials and textures + + public MaterialLibraryLoader(string objPath, DataStore dataStore) + { + _objPath = objPath; + _dataStore = dataStore; + + AddParseAction("newmtl", PushMaterial); + AddParseAction("Ka", d => CurrentMaterial.AmbientColor = ParseVec3(d)); + AddParseAction("Kd", d => CurrentMaterial.DiffuseColor = ParseVec3(d)); + AddParseAction("Ks", d => CurrentMaterial.SpecularColor = ParseVec3(d)); + AddParseAction("Ns", d => CurrentMaterial.SpecularCoefficient = d.ParseInvariantFloat()); + + AddParseAction("d", d => CurrentMaterial.Transparency = d.ParseInvariantFloat()); + AddParseAction("Tr", d => CurrentMaterial.Transparency = d.ParseInvariantFloat()); + + AddParseAction("illum", i => CurrentMaterial.IlluminationModel = i.ParseInvariantInt()); + + AddParseAction("map_Ka", m => CurrentMaterial.AmbientTextureMap = m); + AddParseAction("map_Kd", m => CurrentMaterial.DiffuseTextureMap = m); + + AddParseAction("map_Ks", m => CurrentMaterial.SpecularTextureMap = m); + AddParseAction("map_Ns", m => CurrentMaterial.SpecularHighlightTextureMap = m); + + AddParseAction("map_d", m => CurrentMaterial.AlphaTextureMap = m); + + AddParseAction("map_bump", m => CurrentMaterial.BumpMap = m); + AddParseAction("bump", m => CurrentMaterial.BumpMap = m); + + AddParseAction("disp", m => CurrentMaterial.DisplacementMap = m); + + AddParseAction("decal", m => CurrentMaterial.StencilDecalMap = m); + } + + private void AddParseAction(string key, Action action) => _parseActionDictionary.Add(key.ToLowerInvariant(), action); + + /// + protected override void ParseLine(string keyword, string data) + { + var parseAction = GetKeywordAction(keyword); + + if (parseAction == null) + { + _unrecognizedLines.Add(keyword + " " + data); + return; + } + + parseAction(data); + } + + private Action GetKeywordAction(string keyword) + { + _parseActionDictionary.TryGetValue(keyword.ToLowerInvariant(), out var action); + + return action; + } + + private void PushMaterial(string materialName) + { + CurrentMaterial = new Material(materialName); + _dataStore.Materials.Add(CurrentMaterial); + } + + private static Vector3 ParseVec3(string data) + { + string[] parts = data.Split(' '); + + float x = parts[0].ParseInvariantFloat(); + float y = parts[1].ParseInvariantFloat(); + float z = parts[2].ParseInvariantFloat(); + + return new Vector3(x, y, z); + } + + public void Load(Stream lineStream) => StartLoad(lineStream); + + /// + public Stream Open(string materialFilePath) + { + var dir = Path.GetDirectoryName(_objPath); + var path = Path.Combine(dir, materialFilePath); + return File.Open(path, FileMode.Open, FileAccess.Read); + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs b/Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs new file mode 100644 index 0000000..c6f62af --- /dev/null +++ b/Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs @@ -0,0 +1,34 @@ +using SharpEngine.Shared; +using System.IO; + +namespace ObjLoader.Loaders.MaterialLoader +{ + public class MaterialLibraryLoaderFacade : IMaterialLibraryLoaderFacade + { + private readonly MaterialLibraryLoader _loader; + + /// + /// Initializes a new instance of . + /// + /// Provides the functionality to load material libraries. + public MaterialLibraryLoaderFacade(MaterialLibraryLoader loader) + { + _loader = loader; + } + + /// + public void Load(string materialFileName) + { + if (!File.Exists(materialFileName)) + { + Debug.Log.Warning("Material file '{MaterialFileName}' doesn't exist.", materialFileName); + return; + } + + using var stream = _loader.Open(materialFileName); + + if (stream != null) + _loader.Load(stream); + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/Loaders/ObjLoader/ObjLoader.cs b/Engine/ObjLoader/Loaders/ObjLoader/ObjLoader.cs new file mode 100644 index 0000000..798bc3c --- /dev/null +++ b/Engine/ObjLoader/Loaders/ObjLoader/ObjLoader.cs @@ -0,0 +1,64 @@ +using ObjLoader.Loader.Loaders; +using ObjLoader.TypeParsers; +using SharpEngine.Core.Entities.Properties.Meshes; +using Silk.NET.Core.Contexts; +using Silk.NET.OpenGL; +using System.Collections.Generic; +using System.IO; +using Tutorial; + +namespace ObjLoader.Loaders.ObjLoader +{ + public class ObjLoader : LoaderBase + { + private readonly string _path; + private readonly DataStore _dataStore; + private readonly List _typeParsers = []; + private readonly List _unrecognizedLines = []; + + public ObjLoader(string path, DataStore dataStore) + { + _path = path; + _dataStore = dataStore; + } + + public ObjLoader SetupTypeParsers(params ITypeParser[] parsers) + { + foreach (var parser in parsers) + _typeParsers.Add(parser); + + return this; + } + + /// + protected override void ParseLine(string keyword, string data) + { + foreach (var typeParser in _typeParsers) + if (typeParser.CanParse(keyword)) + { + typeParser.Parse(data); + return; + } + + _unrecognizedLines.Add(keyword + " " + data); + } + + public List Load(GL gl) + { + var fileStream = new FileStream(_path, FileMode.Open, FileAccess.Read); + StartLoad(fileStream); + + return + [ + new(gl) + { + Vertices2 = _dataStore.Vertices, + TextureCoordinates2 = _dataStore.Textures, + Normals2 = _dataStore.Normals, + Groups = _dataStore.Groups, + Materials = _dataStore.Materials + } + ]; + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs b/Engine/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs new file mode 100644 index 0000000..408855c --- /dev/null +++ b/Engine/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs @@ -0,0 +1,65 @@ +using ObjLoader.Loader.TypeParsers; +using ObjLoader.Loaders.MaterialLoader; + +using SharpEngine.Core.Entities.Properties.Meshes; +using Silk.NET.OpenGL; +using System; +using System.Collections.Generic; +using System.IO; +using Tutorial; + +namespace ObjLoader.Loaders.ObjLoader +{ + /// + /// Handles loading 3D models. + /// + public static class ObjLoaderFactory + { + const string FbxExtension = ".fbx"; + const string ObjExtension = ".obj"; + + /// + /// Loads a mesh based on the file extension of the provided path. + /// + /// The OpenGL context where the model should be bound. + /// Specifies the file path of the mesh to be loaded, which determines the loading method based on its extension. + /// The model loaded from the file. + /// Thrown when the file extension of the provided path is not recognized as a supported mesh format. + public static Model_Old Load(GL gl, string path) + => Path.GetExtension(path) switch + { + FbxExtension => LoadFbx("", path), + ObjExtension => LoadObj(gl, path), + _ => throw new NotSupportedException($"The file extension {Path.GetExtension(path)} is not a supported mesh file.") + }; + + // TODO: #3 Load fbx mesh from file + private static Model_Old LoadFbx(string identifier, string meshFilePath) + { + throw new NotImplementedException(); + } + + private static Model_Old LoadObj(GL gl, string path) + { + return new Model_Old(gl, path); + + // TOOD: Use the ObjLoader to load the model instead of the external library. + + /*var dataStore = new DataStore(); + + var faceParser = new FaceParser(dataStore); + var groupParser = new GroupParser(dataStore); + var normalParser = new NormalParser(dataStore); + var textureParser = new TextureParser(dataStore); + var vertexParser = new VertexParser(dataStore); + + var materialLibraryLoader = new MaterialLibraryLoader(path, dataStore); + var materialLibraryParser = new MaterialLibraryParser(materialLibraryLoader); + var useMaterialParser = new UseMaterialParser(dataStore); + + var loader = new ObjLoader(path, dataStore).SetupTypeParsers(faceParser, groupParser, normalParser, textureParser, vertexParser, materialLibraryParser, useMaterialParser); + return loader.Load(gl); + */ + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/SharpEngine.Core.ObjLoader.csproj b/Engine/ObjLoader/SharpEngine.Core.ObjLoader.csproj new file mode 100644 index 0000000..44cd0f4 --- /dev/null +++ b/Engine/ObjLoader/SharpEngine.Core.ObjLoader.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + + + + + + + + + + + + \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/FaceParser.cs b/Engine/ObjLoader/TypeParsers/FaceParser.cs new file mode 100644 index 0000000..c7328f7 --- /dev/null +++ b/Engine/ObjLoader/TypeParsers/FaceParser.cs @@ -0,0 +1,59 @@ +using ObjLoader.Loader.Common; +using ObjLoader.Loader.Data.Elements; +using ObjLoader.TypeParsers; +using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using System; + +namespace ObjLoader.Loader.TypeParsers +{ + public class FaceParser : TypeParserBase, ITypeParser + { + private readonly IFaceGroup _faceGroup; + + public FaceParser(IFaceGroup faceGroup) + { + _faceGroup = faceGroup; + } + + /// + protected override string Keyword => "f"; + + /// + public override void Parse(string line) + { + string[] vertices = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + var face = new Face(); + + foreach (string vertexString in vertices) + { + var faceVertex = ParseFaceVertex(vertexString); + face.AddVertex(faceVertex); + } + + _faceGroup.AddFace(face); + } + + private static FaceVertex ParseFaceVertex(string vertexString) + { + string[] fields = vertexString.Split('/', StringSplitOptions.None); + + int vertexIndex = fields[0].ParseInvariantInt(); + var faceVertex = new FaceVertex(vertexIndex, 0, 0); + + if (fields.Length > 1) + { + int textureIndex = fields[1].Length == 0 ? 0 : fields[1].ParseInvariantInt(); + faceVertex.TextureIndex = textureIndex; + } + + if (fields.Length > 2) + { + int normalIndex = fields.Length > 2 && fields[2].Length == 0 ? 0 : fields[2].ParseInvariantInt(); + faceVertex.NormalIndex = normalIndex; + } + + return faceVertex; + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/GroupParser.cs b/Engine/ObjLoader/TypeParsers/GroupParser.cs new file mode 100644 index 0000000..b8d1512 --- /dev/null +++ b/Engine/ObjLoader/TypeParsers/GroupParser.cs @@ -0,0 +1,21 @@ +using ObjLoader.TypeParsers; + +namespace ObjLoader.Loader.TypeParsers +{ + public class GroupParser : TypeParserBase, ITypeParser + { + private readonly DataStore _dataStore; + + public GroupParser(DataStore dataStore) + { + _dataStore = dataStore; + } + + /// + protected override string Keyword => "g"; + + /// + public override void Parse(string line) + => _dataStore.PushGroup(line); + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/ITypeParser.cs b/Engine/ObjLoader/TypeParsers/ITypeParser.cs new file mode 100644 index 0000000..9cb2d7e --- /dev/null +++ b/Engine/ObjLoader/TypeParsers/ITypeParser.cs @@ -0,0 +1,8 @@ +namespace ObjLoader.TypeParsers +{ + public interface ITypeParser + { + bool CanParse(string keyword); + void Parse(string line); + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/MaterialLibraryParser.cs b/Engine/ObjLoader/TypeParsers/MaterialLibraryParser.cs new file mode 100644 index 0000000..7b7c2b4 --- /dev/null +++ b/Engine/ObjLoader/TypeParsers/MaterialLibraryParser.cs @@ -0,0 +1,22 @@ +using ObjLoader.Loader.Loaders; +using ObjLoader.Loaders.MaterialLoader; +using ObjLoader.TypeParsers; + +namespace ObjLoader.Loader.TypeParsers +{ + public class MaterialLibraryParser : TypeParserBase, ITypeParser + { + private readonly IMaterialLibraryLoaderFacade _libraryLoaderFacade; + + public MaterialLibraryParser(MaterialLibraryLoader loader) + { + _libraryLoaderFacade = new MaterialLibraryLoaderFacade(loader); + } + + /// + protected override string Keyword => "mtllib"; + + /// + public override void Parse(string line) => _libraryLoaderFacade.Load(line); + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/NormalParser.cs b/Engine/ObjLoader/TypeParsers/NormalParser.cs new file mode 100644 index 0000000..49af71a --- /dev/null +++ b/Engine/ObjLoader/TypeParsers/NormalParser.cs @@ -0,0 +1,32 @@ +using ObjLoader.Loader.Common; +using ObjLoader.TypeParsers; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace ObjLoader.Loader.TypeParsers +{ + public class NormalParser : TypeParserBase, ITypeParser + { + private readonly DataStore _dataStore; + + public NormalParser(DataStore dataStore) + { + _dataStore = dataStore; + } + + /// + protected override string Keyword => "vn"; + + /// + public override void Parse(string line) + { + string[] parts = line.Split(' '); + + float x = parts[0].ParseInvariantFloat(); + float y = parts[1].ParseInvariantFloat(); + float z = parts[2].ParseInvariantFloat(); + + var normal = new Normal(x, y, z); + _dataStore.Normals.Add(normal); + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/TextureParser.cs b/Engine/ObjLoader/TypeParsers/TextureParser.cs new file mode 100644 index 0000000..0c596ba --- /dev/null +++ b/Engine/ObjLoader/TypeParsers/TextureParser.cs @@ -0,0 +1,31 @@ +using ObjLoader.Loader.Common; +using ObjLoader.TypeParsers; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace ObjLoader.Loader.TypeParsers +{ + public class TextureParser : TypeParserBase, ITypeParser + { + private readonly DataStore _dataStore; + + public TextureParser(DataStore dataStore) + { + _dataStore = dataStore; + } + + /// + protected override string Keyword => "vt"; + + /// + public override void Parse(string line) + { + string[] parts = line.Split(' '); + + float x = parts[0].ParseInvariantFloat(); + float y = parts[1].ParseInvariantFloat(); + + var texture = new TextureCoordinate(x, y); + _dataStore.Textures.Add(texture); + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/TypeParserBase.cs b/Engine/ObjLoader/TypeParsers/TypeParserBase.cs new file mode 100644 index 0000000..7880db8 --- /dev/null +++ b/Engine/ObjLoader/TypeParsers/TypeParserBase.cs @@ -0,0 +1,14 @@ +using ObjLoader.Loader.Common; +using ObjLoader.TypeParsers; + +namespace ObjLoader.Loader.TypeParsers +{ + public abstract class TypeParserBase : ITypeParser + { + protected abstract string Keyword { get; } + + public bool CanParse(string keyword) => keyword.EqualsOrdinalIgnoreCase(Keyword); + + public abstract void Parse(string line); + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/UseMaterialParser.cs b/Engine/ObjLoader/TypeParsers/UseMaterialParser.cs new file mode 100644 index 0000000..72f33d8 --- /dev/null +++ b/Engine/ObjLoader/TypeParsers/UseMaterialParser.cs @@ -0,0 +1,20 @@ +using ObjLoader.TypeParsers; + +namespace ObjLoader.Loader.TypeParsers +{ + public class UseMaterialParser : TypeParserBase, ITypeParser + { + private readonly DataStore _dataStore; + + public UseMaterialParser(DataStore dataStore) + { + _dataStore = dataStore; + } + + /// + protected override string Keyword => "usemtl"; + + /// + public override void Parse(string line) => _dataStore.SetMaterial(line); + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/VertexParser.cs b/Engine/ObjLoader/TypeParsers/VertexParser.cs new file mode 100644 index 0000000..d6b4fcf --- /dev/null +++ b/Engine/ObjLoader/TypeParsers/VertexParser.cs @@ -0,0 +1,37 @@ +using ObjLoader.Loader.Common; +using ObjLoader.TypeParsers; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using System; +using Tutorial; + +namespace ObjLoader.Loader.TypeParsers +{ + public class VertexParser : TypeParserBase, ITypeParser + { + private readonly DataStore _dataStore; + + public VertexParser(DataStore dataStore) + { + _dataStore = dataStore; + } + + /// + protected override string Keyword => "v"; + + /// + public override void Parse(string line) + { + string[] parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + float x = parts[0].ParseInvariantFloat(); + float y = parts[1].ParseInvariantFloat(); + float z = parts[2].ParseInvariantFloat(); + + var vertex = new Vertex() + { + Position = new System.Numerics.Vector3(x, y, z), + }; + _dataStore.Vertices.Add(vertex); + } + } +} \ No newline at end of file diff --git a/Resources/Screenshots/editor.png b/Engine/Resources/Screenshots/editor.png similarity index 100% rename from Resources/Screenshots/editor.png rename to Engine/Resources/Screenshots/editor.png diff --git a/Resources/Screenshots/launcher.png b/Engine/Resources/Screenshots/launcher.png similarity index 100% rename from Resources/Screenshots/launcher.png rename to Engine/Resources/Screenshots/launcher.png diff --git a/Resources/icon.ico b/Engine/Resources/icon.ico similarity index 100% rename from Resources/icon.ico rename to Engine/Resources/icon.ico diff --git a/Launcher/Resources/AppIcon/icon.png b/Engine/Resources/icon.png similarity index 100% rename from Launcher/Resources/AppIcon/icon.png rename to Engine/Resources/icon.png diff --git a/Resources/icon.svg b/Engine/Resources/icon.svg similarity index 100% rename from Resources/icon.svg rename to Engine/Resources/icon.svg diff --git a/Resources/icon.xcf b/Engine/Resources/icon.xcf similarity index 100% rename from Resources/icon.xcf rename to Engine/Resources/icon.xcf diff --git a/Engine/SharpEngine.Core.Components/IComponent.cs b/Engine/SharpEngine.Core.Components/IComponent.cs new file mode 100644 index 0000000..fdeaa85 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/IComponent.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Components; + +// TODO: #53 Entity Component System + +internal interface IComponent +{ +} diff --git a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IFaceGroup.cs b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IFaceGroup.cs new file mode 100644 index 0000000..e6c0922 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IFaceGroup.cs @@ -0,0 +1,9 @@ +using ObjLoader.Loader.Data.Elements; + +namespace SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore +{ + public interface IFaceGroup + { + void AddFace(Face face); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IGroupDataStore.cs b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IGroupDataStore.cs new file mode 100644 index 0000000..3fe9d69 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IGroupDataStore.cs @@ -0,0 +1,7 @@ +namespace SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore +{ + public interface IGroupDataStore + { + void PushGroup(string groupName); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IMaterialLibrary.cs b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IMaterialLibrary.cs new file mode 100644 index 0000000..5652449 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IMaterialLibrary.cs @@ -0,0 +1,9 @@ +using SharpEngine.Core.Components.Properties; + +namespace SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore +{ + public interface IMaterialLibrary + { + void Push(Material material); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/INormalDataStore.cs b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/INormalDataStore.cs new file mode 100644 index 0000000..10ae87d --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/INormalDataStore.cs @@ -0,0 +1,9 @@ +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore +{ + public interface INormalDataStore + { + void AddNormal(Normal normal); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/ITextureDataStore.cs b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/ITextureDataStore.cs new file mode 100644 index 0000000..a1591ff --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/ITextureDataStore.cs @@ -0,0 +1,9 @@ +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore +{ + public interface ITextureDataStore + { + void AddTexture(TextureCoordinate texture); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IVertexDataStore.cs b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IVertexDataStore.cs new file mode 100644 index 0000000..6667f06 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IVertexDataStore.cs @@ -0,0 +1,9 @@ +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore +{ + public interface IVertexDataStore + { + void AddVertex(Vertex vertex); + } +} \ No newline at end of file diff --git a/Core/Entities/Properties/BoundingBox.cs b/Engine/SharpEngine.Core.Components/Properties/BoundingBox.cs similarity index 84% rename from Core/Entities/Properties/BoundingBox.cs rename to Engine/SharpEngine.Core.Components/Properties/BoundingBox.cs index 1119c28..e774eaa 100644 --- a/Core/Entities/Properties/BoundingBox.cs +++ b/Engine/SharpEngine.Core.Components/Properties/BoundingBox.cs @@ -28,14 +28,15 @@ public BoundingBox(Vector3 min, Vector3 max) Max = max; } + // TODO: #5 This should be calculated by the mesh. /// /// Calculates the bounding box of the game object. /// /// The bounding box of the game object. - public static BoundingBox CalculateBoundingBox(Transform transform) + public static BoundingBox CalculateBoundingBox(ITransform transform) { var min = transform.Position - (transform.Scale / 2); var max = transform.Position + (transform.Scale / 2); - return new BoundingBox(min, max); + return new BoundingBox((Vector3)min, (Vector3)max); } } diff --git a/Engine/SharpEngine.Core.Components/Properties/ITransform.cs b/Engine/SharpEngine.Core.Components/Properties/ITransform.cs new file mode 100644 index 0000000..a3b5dbd --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/ITransform.cs @@ -0,0 +1,23 @@ +using SharpEngine.Core.Numerics; +using System.Numerics; + +namespace SharpEngine.Core.Entities.Properties; + +/// +/// Represents the transform of an object in dimensions. +/// +/// +public interface ITransform where TVector : IVector, new() +{ + /// Gets or set the the position of the object. + public TVector Position { get; set; } + + /// Gets or set the the scale of the object. + public TVector Scale { get; set; } + + /// Gets or set the the rotation of the object. + public Quaternion Rotation { get; set; } + + /// Gets or set the the model matrix of the object. + public Matrix4x4 ModelMatrix { get; } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Properties/Material.cs b/Engine/SharpEngine.Core.Components/Properties/Material.cs new file mode 100644 index 0000000..07c21be --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Material.cs @@ -0,0 +1,127 @@ +using Silk.NET.OpenGL; +using System.Numerics; +using Shader = SharpEngine.Core.Shaders.Shader; +using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; + +namespace SharpEngine.Core.Components.Properties; + +/// +/// Represents the material rendered onto a game object. +/// +public class Material +{ + /// + /// Initializes a new instance of . + /// + /// The diffuse map texture of the material. + /// The specular map texture of the material. Defaults to the diffuse map if not provided. + public Material(Texture diffuseMap, Texture? specularMap = null) + { + DiffuseMap = diffuseMap; + SpecularMap = specularMap ?? diffuseMap; + + Specular = new(0.5f, 0.5f, 0.5f); + Shininess = 32.0f; + } + + /// + /// Initializes a new instance of . + /// + /// The name assigned to the new material. + public Material(string materialName) + { + Name = materialName; + } + + /// 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; } + + /// 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.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; + + /// Gets or sets the specular color of the material. + public Vector3 Specular { get; set; } + + /// Gets or sets the shininess of the material. + public float Shininess { get; set; } + + /// Gets or sets the ambient color of the material. + public Vector3 AmbientColor { get; set; } + + /// Gets or sets the diffuse color of the material. + public Vector3 DiffuseColor { get; set; } + + /// Gets or sets the specular color of the material. + public Vector3 SpecularColor { get; set; } + + /// Gets or sets the specular coefficient of the material. + public float SpecularCoefficient { get; set; } + + /// Gets or sets the transparency of the material. + public float Transparency { get; set; } + + /// Gets or sets the illumination model of the material. + public int IlluminationModel { get; set; } + + /// Gets or sets the path to the ambient texture map. + public string AmbientTextureMap { get; set; } + + /// Gets or sets the path to the specular highlight texture map. + public string SpecularHighlightTextureMap { get; set; } + + /// Gets or sets the path to the bump map. + public string BumpMap { get; set; } + + /// Gets or sets the path to the displacement map. + public string DisplacementMap { get; set; } + + /// Gets or sets the path to the stencil decal map. + public string StencilDecalMap { get; set; } + + /// Gets or sets the path to the alpha texture map. + public string AlphaTextureMap { get; set; } + + /// + /// Sets the uniform values for the material in the specified shader. + /// + /// The shader to set the uniform values in. + public void SetUniformValues(Shader shader) + { + // TODO: Get all shader uniforms and set their values automatically + + DiffuseMap.Use(TextureUnit.Texture0); + shader.SetInt("material.diffuse", DIFFUSE_UNIT); + + if (UseSpecularMap) + { + SpecularMap.Use(TextureUnit.Texture1); + shader.SetInt("material.specular", SPECULAR_UNIT); + shader.SetVector3("material.specular", Specular); + shader.SetFloat("material.shininess", Shininess); + } + else + { + shader.SetInt("material.specular", 0); + shader.SetVector3("material.specular", Vector3.Zero); + shader.SetFloat("material.shininess", 0); + } + } +} diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/BufferObject.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/BufferObject.cs new file mode 100644 index 0000000..1279b1d --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/BufferObject.cs @@ -0,0 +1,60 @@ +using Silk.NET.OpenGL; +using System.Runtime.InteropServices; + +namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +{ + /// + /// Represents a buffer object in OpenGL that manages memory allocation and data transfer for a specific data type. + /// + /// Specifies the type of data stored in the buffer, which must be an unmanaged type for proper memory handling. + public class BufferObject : IDisposable where TDataType : unmanaged + { + private readonly uint _handle; + private readonly BufferTargetARB _bufferType; + private readonly GL _gl; + private bool disposedValue; + + /// + /// Initializes a new instance of . + /// + /// The OpenGL context where the memory should be allocated. + /// The data buffer. + /// The type of buffer target. + public BufferObject(GL gl, Span data, BufferTargetARB bufferType) + { + _gl = gl; + _bufferType = bufferType; + + _handle = _gl.GenBuffer(); + Bind(); + + var size = (nuint)(data.Length * Marshal.SizeOf()); + _gl.BufferData(bufferType, size, data, BufferUsageARB.StaticDraw); + } + + /// + /// Binds the buffer object. + /// + public void Bind() => _gl.BindBuffer(_bufferType, _handle); + + /// + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + return; + + if (disposing) + _gl.DeleteBuffer(_handle); + + 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/Engine/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs new file mode 100644 index 0000000..e6f66fc --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs @@ -0,0 +1,192 @@ +using ObjLoader.Loader.Data.Elements; +using SharpEngine.Core.Attributes; +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using Silk.NET.OpenGL; +using Tutorial; + +using Texture2 = SharpEngine.Core.Components.Properties.Textures.Texture; + +namespace SharpEngine.Core.Entities.Properties.Meshes; + +/// +/// Represents a game object mesh, which is a collection of vertices, normals, texture coordinates, and indices +/// used to define the shape and appearance of a 3D object. +/// +public class Mesh : IDisposable +{ + private bool disposedValue; + + /// + /// Gets or sets an identifying name for the mesh. + /// + public string Name { get; set; } = "New Mesh"; + + /// + /// Gets or sets the mesh vertices as an array of floats. + /// + [Inspector(DisplayInInspector = false)] + public float[] Vertices { get; set; } = []; + + /// + /// Gets or sets the mesh vertices as a list of objects. + /// + public List Vertices2 { get; set; } = []; + + /// + /// Gets or sets the mesh normals as an array of floats. + /// + [Inspector(DisplayInInspector = false)] + public float[] Normals { get; set; } = []; + + /// + /// Gets or sets the mesh normals as a list of objects. + /// + public List Normals2 { get; set; } = []; + + /// + /// Gets or sets the mesh texture UV coordinates as an array of floats. + /// + [Inspector(DisplayInInspector = false)] + public float[] TextureCoordinates { get; set; } = []; + + /// + /// Gets or sets the mesh texture UV coordinates as a list of objects. + /// + public List TextureCoordinates2 { get; set; } = []; + + /// + /// Gets or sets the indices of the mesh as an array of unsigned integers. + /// + [Inspector(DisplayInInspector = false)] + public uint[] Indices { get; set; } = []; + + /// + /// Gets or sets the groups of faces in the mesh. + /// + public List Groups { get; set; } = []; + + /// + /// Gets or sets the materials used by the mesh. + /// + public List Materials { get; set; } = []; + + /// + /// Gets or sets the textures used by the mesh. + /// + public IReadOnlyList Textures { get; set; } + + /// + /// Gets or sets the legacy textures used by the mesh. + /// + public IReadOnlyList Textures_Old { get; set; } + + /// + /// Gets or sets the Vertex Array Object (VAO) for the mesh. + /// + public VertexArrayObject VAO { get; set; } + + /// + /// Gets or sets the Vertex Buffer Object (VBO) for the mesh. + /// + public BufferObject VBO { get; set; } + + /// + /// Gets or sets the Element Buffer Object (EBO) for the mesh. + /// + public BufferObject EBO { get; set; } + + /// + /// Gets the OpenGL context associated with the mesh. + /// + public GL GL { get; } + + /// + /// Initializes a new instance of the class with the specified OpenGL context, vertices, indices, and textures. + /// + /// The OpenGL context. + /// The vertices of the mesh. + /// The indices of the mesh. + /// The textures of the mesh. + public Mesh(GL gl, float[] vertices, uint[] indices, List textures) + { + GL = gl; + Vertices = vertices; + Indices = indices; + Textures = textures; + SetupMesh(); + } + + /// + /// Initializes a new instance of the class with the specified OpenGL context, vertices, indices, and legacy textures. + /// + /// The OpenGL context. + /// The vertices of the mesh. + /// The indices of the mesh. + /// The legacy textures of the mesh. + public Mesh(GL gl, float[] vertices, uint[] indices, List textures) + { + GL = gl; + Vertices = vertices; + Indices = indices; + Textures_Old = textures; + SetupMesh(); + } + + /// + /// Initializes a new instance of the class with the specified OpenGL context. + /// + /// The OpenGL context. + public Mesh(GL gl) + { + GL = gl; + 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). + /// + public void SetupMesh() + { + EBO = new BufferObject(GL, Indices, BufferTargetARB.ElementArrayBuffer); + VBO = new BufferObject(GL, Vertices, BufferTargetARB.ArrayBuffer); + VAO = new VertexArrayObject(GL, VBO, EBO); + + VAO.VertexAttributePointer(0, 3, VertexAttribPointerType.Float, 8, 0); + VAO.VertexAttributePointer(1, 3, VertexAttribPointerType.Float, 8, 3); + VAO.VertexAttributePointer(2, 2, VertexAttribPointerType.Float, 8, 6); + + // VAO.VertexAttributePointer(0, VertexData.VerticesSize, VertexAttribPointerType.Float, VertexData.Stride, VertexData.VerticesOffset); + // VAO.VertexAttributePointer(1, VertexData.NormalsSize, VertexAttribPointerType.Float, VertexData.Stride, VertexData.NormalsOffset); + // VAO.VertexAttributePointer(2, VertexData.TexCoordsSize, VertexAttribPointerType.Float, VertexData.Stride, VertexData.TexCoordsOffset); + } + + /// + /// Binds the mesh Vertex Array Object (VAO) to the current OpenGL context. + /// + public void Bind() + => VAO.Bind(); + + /// + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + return; + + if (disposing) + { + VAO.Dispose(); + VBO.Dispose(); + EBO.Dispose(); + } + + disposedValue = true; + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs new file mode 100644 index 0000000..e4dfdb5 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs @@ -0,0 +1,27 @@ +namespace ObjLoader.Loader.Data.Elements +{ + public class Face + { + public readonly List _vertices = []; + + public void AddVertex(FaceVertex vertex) => _vertices.Add(vertex); + + public FaceVertex this[int i] => _vertices[i]; + + public int Count => _vertices.Count; + } + + public struct FaceVertex + { + public FaceVertex(int vertexIndex, int textureIndex, int normalIndex) : this() + { + VertexIndex = vertexIndex; + TextureIndex = textureIndex; + NormalIndex = normalIndex; + } + + public int VertexIndex { get; set; } + public int TextureIndex { get; set; } + public int NormalIndex { get; set; } + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Group.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Group.cs new file mode 100644 index 0000000..1cd52c9 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Group.cs @@ -0,0 +1,22 @@ +using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties; + +namespace ObjLoader.Loader.Data.Elements +{ + public class Group : IFaceGroup + { + private readonly List _faces = []; + + public Group(string name) + { + Name = name; + } + + public string Name { get; private set; } + public Material Material { get; set; } + + public IList Faces => _faces; + + public void AddFace(Face face) => _faces.Add(face); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Normal.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Normal.cs new file mode 100644 index 0000000..5e40c14 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Normal.cs @@ -0,0 +1,16 @@ +namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +{ + public struct Normal + { + public Normal(float x, float y, float z) : this() + { + X = x; + Y = y; + Z = z; + } + + public float X { get; private set; } + public float Y { get; private set; } + public float Z { get; private set; } + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/TextureCoordinate.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/TextureCoordinate.cs new file mode 100644 index 0000000..5bd29ce --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/TextureCoordinate.cs @@ -0,0 +1,14 @@ +namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +{ + public struct TextureCoordinate + { + public TextureCoordinate(float x, float y) : this() + { + X = x; + Y = y; + } + + public float X { get; private set; } + public float Y { get; private set; } + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Vertex.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Vertex.cs new file mode 100644 index 0000000..5eb1fda --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Vertex.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; + +namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +{ + public struct Vertex + { + public Vector3 Position; + public Vector3 Normal; + public Vector3 Tangent; + public Vector2 TexCoords; + public Vector3 Bitangent; + + // TODO: #65 Skeletal mesh + public const int MAX_BONE_INFLUENCE = 4; + public int[] BoneIds; + public float[] Weights; + } +} diff --git a/Core/Entities/Properties/Meshes/MeshExtensions.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshExtensions.cs similarity index 100% rename from Core/Entities/Properties/Meshes/MeshExtensions.cs rename to Engine/SharpEngine.Core.Components/Properties/Meshes/MeshExtensions.cs diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/Model.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/Model.cs new file mode 100644 index 0000000..513cf4e --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/Model.cs @@ -0,0 +1,104 @@ +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using Silk.NET.Assimp; +using Silk.NET.OpenGL; +using System.Numerics; +using System.Linq; +using Mesh = SharpEngine.Core.Entities.Properties.Meshes.Mesh; +using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; + +namespace Tutorial +{ + public class Model : IDisposable + { + public readonly string Path; + public List Meshes { get; set; } = new List(); + + private readonly GL _gl; + + public Model(GL gl, string path) + { + _gl = gl; + Path = path; + } + + public Model(GL gl, string path, List meshes) + { + _gl = gl; + Path = path; + Meshes = meshes; + + foreach (var mesh in Meshes) + ProcessMesh(mesh); + } + + public void Dispose() + { + foreach (var mesh in Meshes) + { + mesh.Dispose(); + } + + GC.SuppressFinalize(this); + } + + public Mesh ProcessMesh(Mesh mesh) + { + // data to fill + for (int i = 0; i < mesh.Vertices2.Count; i++) + { + var vertex = mesh.Vertices2[i]; + vertex.BoneIds = new int[Vertex.MAX_BONE_INFLUENCE]; + vertex.Weights = new float[Vertex.MAX_BONE_INFLUENCE]; + } + + List indices = new List(); + foreach (var group in mesh.Groups) + { + foreach (var face in group.Faces) + { + indices.AddRange(face._vertices.Select(v => (uint)v.VertexIndex)); + } + } + + List textures = new List(); + foreach (var material in mesh.Materials) + { + if (material.DiffuseMap != null) + textures.Add(material.DiffuseMap); + + if (material.UseSpecularMap && material.SpecularMap != null) + textures.Add(material.SpecularMap); + } + + // return a mesh object updated with the extracted mesh data + mesh.Vertices = BuildVertices(mesh.Vertices2); + mesh.Indices = BuildIndices(indices); + mesh.Textures = textures; + + return mesh; + } + + private float[] BuildVertices(List vertexCollection) + { + var vertices = new List(); + + foreach (var vertex in vertexCollection) + { + vertices.Add(vertex.Position.X); + vertices.Add(vertex.Position.Y); + vertices.Add(vertex.Position.Z); + + // vertices.Add(vertex.Normal.X); + // vertices.Add(vertex.Normal.Y); + + vertices.Add(vertex.TexCoords.X); + vertices.Add(vertex.TexCoords.Y); + } + + return vertices.ToArray(); + } + + private uint[] BuildIndices(List indices) + => indices.ToArray(); + } +} diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/Model_Old.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/Model_Old.cs new file mode 100644 index 0000000..0f477aa --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/Model_Old.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.Entities.Properties.Meshes; +using Silk.NET.Assimp; +using Silk.NET.OpenGL; +using System.Numerics; +using AssimpMesh = Silk.NET.Assimp.Mesh; +using Mesh = SharpEngine.Core.Entities.Properties.Meshes.Mesh; +using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; + +namespace Tutorial +{ + [Obsolete("This should be removed and the ObjLoader should be used instead.")] + public class Model_Old : IDisposable + { + public Model_Old(GL gl, string path) + { + _assimp = Assimp.GetApi(); + _gl = gl; + + if (!string.IsNullOrWhiteSpace(path)) + LoadModel(path); + } + + public Model_Old(GL gl, Mesh mesh) + { + var result = new Mesh(gl, mesh.GetVertices(), mesh.Indices, mesh.Textures.ToList()); + Meshes.Add(result); + } + + private readonly GL _gl; + private Assimp _assimp; + private List _texturesLoaded = []; + public string Directory { get; protected set; } = string.Empty; + public List Meshes { get; protected set; } = []; + + private unsafe void LoadModel(string path) + { + var scene = _assimp.ImportFile(path, (uint)PostProcessSteps.Triangulate); + + if (scene == null || scene->MFlags == Assimp.SceneFlagsIncomplete || scene->MRootNode == null) + { + string error = _assimp.GetErrorStringS(); + throw new Exception(error); + } + + Directory = path; + + ProcessNode(scene->MRootNode, scene); + } + + private unsafe void ProcessNode(Node* node, Scene* scene) + { + for (int i = 0; i < node->MNumMeshes; i++) + { + var mesh = scene->MMeshes[node->MMeshes[i]]; + Meshes.Add(ProcessMesh(mesh, scene)); + + } + + for (int i = 0; i < node->MNumChildren; i++) + { + ProcessNode(node->MChildren[i], scene); + } + } + + private unsafe Mesh ProcessMesh(AssimpMesh* mesh, Scene* scene) + { + // data to fill + List vertices = []; + List indices = []; + List textures = []; + + // walk through each of the mesh's vertices + for (uint i = 0; i < mesh->MNumVertices; i++) + { + var vertex = new Vertex + { + BoneIds = new int[Vertex.MAX_BONE_INFLUENCE], + Weights = new float[Vertex.MAX_BONE_INFLUENCE], + + Position = mesh->MVertices[i] + }; + + // normals + if (mesh->MNormals != null) + vertex.Normal = mesh->MNormals[i]; + // tangent + if (mesh->MTangents != null) + vertex.Tangent = mesh->MTangents[i]; + // bitangent + if (mesh->MBitangents != null) + vertex.Bitangent = mesh->MBitangents[i]; + + // texture coordinates + if (mesh->MTextureCoords[0] != null) // does the mesh contain texture coordinates? + { + // a vertex can contain up to 8 different texture coordinates. We thus make the assumption that we won't + // use models where a vertex can have multiple texture coordinates so we always take the first set (0). + var texcoord3 = mesh->MTextureCoords[0][i]; + vertex.TexCoords = new Vector2(texcoord3.X, texcoord3.Y); + } + + vertices.Add(vertex); + } + + // now wak through each of the mesh's faces (a face is a mesh its triangle) and retrieve the corresponding vertex indices. + for (uint i = 0; i < mesh->MNumFaces; i++) + { + var face = mesh->MFaces[i]; + // retrieve all indices of the face and store them in the indices vector + for (uint j = 0; j < face.MNumIndices; j++) + indices.Add(face.MIndices[j]); + } + + // process materials + var material = scene->MMaterials[mesh->MMaterialIndex]; + // we assume a convention for sampler names in the shaders. Each diffuse texture should be named + // as 'texture_diffuseN' where N is a sequential number ranging from 1 to MAX_SAMPLER_NUMBER. + // Same applies to other texture as the following list summarizes: + // diffuse: texture_diffuseN + // specular: texture_specularN + // normal: texture_normalN + + // 1. diffuse maps + var diffuseMaps = LoadMaterialTextures(material, TextureType.Diffuse); + if (diffuseMaps.Any()) + textures.AddRange(diffuseMaps); + + // 2. specular maps + var specularMaps = LoadMaterialTextures(material, TextureType.Specular); + if (specularMaps.Any()) + textures.AddRange(specularMaps); + + // 3. normal maps + var normalMaps = LoadMaterialTextures(material, TextureType.Height); + if (normalMaps.Any()) + textures.AddRange(normalMaps); + + // 4. height maps + var heightMaps = LoadMaterialTextures(material, TextureType.Ambient); + if (heightMaps.Any()) + textures.AddRange(heightMaps); + + // return a mesh object created from the extracted mesh data + var result = new Mesh(_gl, BuildVertices(vertices), BuildIndices(indices), textures); + return result; + } + + private unsafe List LoadMaterialTextures(Material* mat, TextureType type) + { + uint textureCount = _assimp.GetMaterialTextureCount(mat, type); + List textures = []; + for (uint i = 0; i < textureCount; i++) + { + AssimpString path; + _assimp.GetMaterialTexture(mat, type, i, &path, null, null, null, null, null, null); + bool skip = false; + for (int j = 0; j < _texturesLoaded.Count; j++) + { + if (_texturesLoaded[j].Path == path) + { + textures.Add(_texturesLoaded[j]); + skip = true; + break; + } + } + if (!skip) + { + var texture = new Texture(_gl, Directory, type); + textures.Add(texture); + _texturesLoaded.Add(texture); + } + } + return textures; + } + + private static float[] BuildVertices(List vertexCollection) + { + var vertices = new List(); + + foreach (var vertex in vertexCollection) + { + vertices.Add(vertex.Position.X); + vertices.Add(vertex.Position.Y); + vertices.Add(vertex.Position.Z); + + vertices.Add(vertex.Normal.X); + vertices.Add(vertex.Normal.Y); + vertices.Add(vertex.Normal.Z); + + vertices.Add(vertex.TexCoords.X); + vertices.Add(vertex.TexCoords.Y); + } + + return vertices.ToArray(); + } + + private static uint[] BuildIndices(List indices) => indices.ToArray(); + + public void Dispose() + { + foreach (var mesh in Meshes) + { + mesh.Dispose(); + } + + _texturesLoaded = []; + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/ObjMeshData.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/ObjMeshData.cs new file mode 100644 index 0000000..e1e45de --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/ObjMeshData.cs @@ -0,0 +1,22 @@ +namespace SharpEngine.Core.Entities.Properties.Meshes; + +/// +/// Contains the different types of data the .obj file contains. +/// +public static class ObjMeshData +{ + /// Represents a line of vertex data. + public const string VERTEX = "v"; + + /// Represents a line of vertex normal data. + public const string VERTEX_NORMAL = "vn"; + + /// Represents a line of uv coordinate data. + public const string VERTEX_TEXTURE = "vt"; + + /// Represents a line of vertex indicie data. + public const string FACE = "f"; + + /// Represents that a new object begins. + public const string OBJECT = "o"; +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/VertexArrayObject.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/VertexArrayObject.cs new file mode 100644 index 0000000..4ba4ad0 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/VertexArrayObject.cs @@ -0,0 +1,79 @@ +using Silk.NET.OpenGL; +using System.Runtime.InteropServices; + +namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +{ + /// + /// Represents a vertex array object for managing vertex and index data in OpenGL. + /// + /// Specifies the type of vertex data used in the vertex array. + /// Specifies the type of index data used for indexing vertices in the vertex array. + public class VertexArrayObject : IDisposable + where TVertexType : unmanaged + where TIndexType : unmanaged + { + private readonly uint _handle; + private readonly GL _gl; + private bool disposedValue; + + /// + /// Initializes an instance of . + /// + /// Represents the OpenGL context used for generating and managing the vertex array. + /// Represents the buffer object containing vertex data that will be bound to the vertex array. + /// Represents the buffer object containing index data that will be bound to the vertex array. + public VertexArrayObject(GL gl, BufferObject vbo, BufferObject ebo) + { + _gl = gl; + + _handle = _gl.GenVertexArray(); + Bind(); + vbo.Bind(); + ebo.Bind(); + } + + /// + /// Configures the vertex attribute pointer for rendering. + /// + /// Specifies the index of the vertex attribute to be configured. + /// Indicates the number of components per vertex attribute. + /// Defines the data type of each component in the vertex attribute. + /// Determines the size of each vertex in bytes. + /// Sets the offset in bytes from the start of the vertex data. + public void VertexAttributePointer(uint index, int count, VertexAttribPointerType type, uint vertexSize, int offSet) + { + var size = Marshal.SizeOf(); + var stride = (uint)(vertexSize * size); + var pointer = (nint)offSet * size; + + _gl.VertexAttribPointer(index, count, type, false, stride, pointer); + _gl.EnableVertexAttribArray(index); + } + + /// + /// Binds the vertex array object (VAO) to the current OpenGL context. + /// + public void Bind() + => _gl.BindVertexArray(_handle); + + /// + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + return; + + if (disposing) + _gl.DeleteVertexArray(_handle); + + 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/Core/Entities/Properties/VertexData.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/VertexData.cs similarity index 78% rename from Core/Entities/Properties/VertexData.cs rename to Engine/SharpEngine.Core.Components/Properties/Meshes/VertexData.cs index c25e5d4..328d38d 100644 --- a/Core/Entities/Properties/VertexData.cs +++ b/Engine/SharpEngine.Core.Components/Properties/Meshes/VertexData.cs @@ -1,11 +1,11 @@ -namespace SharpEngine.Core.Entities.Properties; +namespace SharpEngine.Core.Entities.Properties.Meshes; /// /// Represents the data of a vertex. /// -public struct VertexData +public class VertexData { - /// The stride or lenght of the vertex data object. + /// The stride or length of the vertex data object. public const int Stride = 8 * sizeof(float); /// The size of the vertices in the vertex data object. @@ -25,9 +25,4 @@ public struct VertexData /// The byte offset to the texture coordinates in the vertex data object. public const int TexCoordsOffset = (VerticesSize + NormalsSize) * sizeof(float); - - /// - /// Initializes a new instance of . - /// - public VertexData() { } } diff --git a/Core/Entities/Properties/Quaternion.cs b/Engine/SharpEngine.Core.Components/Properties/Quaternion.cs similarity index 100% rename from Core/Entities/Properties/Quaternion.cs rename to Engine/SharpEngine.Core.Components/Properties/Quaternion.cs diff --git a/Engine/SharpEngine.Core.Components/Properties/Shaders/Shader.cs b/Engine/SharpEngine.Core.Components/Properties/Shaders/Shader.cs new file mode 100644 index 0000000..13aea94 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Shaders/Shader.cs @@ -0,0 +1,83 @@ +using SharpEngine.Shared; +using Silk.NET.OpenGL; +using System.Diagnostics.CodeAnalysis; + +namespace SharpEngine.Core.Shaders; + +/// +/// Represents a shader program. +/// +public partial class Shader : 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; } + + /// Gets or sets the path to the vertex shader file. + public string VertPath { get; set; } + + /// Gets or sets the path to the fragment shader file. + public string FragPath { get; set; } + + private Dictionary _uniformLocations = []; + private bool disposedValue; + private readonly GL _gl; + + /// + /// Initializes a new instance of . + /// + /// + /// Shaders are written in GLSL, which is a language very similar to C in its semantics. + /// The GLSL source is compiled *at runtime*, so it can optimize itself for the graphics card it's currently being used on. + /// + /// The OpenGL instance where this shader is used. + /// 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) + { + Name = name; + _gl = gl; + + // 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. + // The fragment shader is responsible for then converting the vertices to "fragments", which represent all the data OpenGL needs to draw a pixel. + // The fragment shader is what we'll be using the most here. + + if (!vertPath.EndsWith(".vert")) + Debug.Log.Warning("Vertex shaders should have the file extension '.vert' for easier manageability."); + + if (!fragPath.EndsWith(".frag")) + Debug.Log.Warning("Fragment shaders should have the file extension '.frag' for easier manageability."); + + VertPath = vertPath; + FragPath = fragPath; + } + + /// + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + return; + + if (disposing) + { + _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/Core/Shaders/ShaderAttributes.cs b/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderAttributes.cs similarity index 83% rename from Core/Shaders/ShaderAttributes.cs rename to Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderAttributes.cs index f099087..29c14ac 100644 --- a/Core/Shaders/ShaderAttributes.cs +++ b/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderAttributes.cs @@ -16,5 +16,9 @@ public static class ShaderAttributes public const int AttributeLocationNotFound = -1; /// Represents the model matrix attribute. - public const string Model = "model"; + public const string Model = "uModel"; + + public const string View = "uView"; + + public const string Projection = "uProjection"; } diff --git a/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs b/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs new file mode 100644 index 0000000..830a2aa --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs @@ -0,0 +1,261 @@ +using SharpEngine.Shared; +using Silk.NET.OpenGL; +using System.Numerics; +using System.Text.RegularExpressions; + +namespace SharpEngine.Core.Shaders; + +/// +/// Represents a shader program in the engine. +/// +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)) + { + Debug.Log.Error("Unable to load vertex shader."); + return this; + } + + if (!LoadShader(ShaderType.FragmentShader, FragPath, out uint fragmentShader)) + { + Debug.Log.Error("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) + { + Debug.Log.Information("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)) + { + Debug.Log.Information("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)) + { + Debug.Log.Information("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); + Debug.Log.Error("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); + Debug.Log.Error("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) + { + Debug.Log.Warning("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. + + // Setting a uniform is almost always the exact same, so I'll explain it here once, instead of in every method: + // 1. Bind the program you want to set the uniform on + // 2. Get a handle to the location of the uniform with GL.GetUniformLocation. + // 3. Use the appropriate GL.Uniform* function to set the uniform. + + private bool TrySetUniform(string uniformName, T data, Action setter) + { + if (!_uniformLocations.TryGetValue(uniformName, out int location)) + { + Debug.Log.Information("Uniform '{UniformName}' not found in shader '{ShaderName}'.", uniformName, Name); + 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); + setter(location, data); + + return true; + } + + /// + /// Set a uniform int on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetInt(string name, int data) + => TrySetUniform(name, data, _gl.Uniform1); + + /// + /// Set a uniform float on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetFloat(string name, float data) + => TrySetUniform(name, data, _gl.Uniform1); + + /// + /// Set a uniform Matrix4 on this shader + /// + /// The name of the uniform. + /// The data to set. + /// Determines whether or not the matrix should be transposed. Defaults to . + /// + /// + /// The matrix is transposed before being sent to the shader unless is set to . + /// + /// + public bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true) + => TrySetUniform(name, data, (uniform, d) => _gl.UniformMatrix4(uniform, transpose, d.ToSpan())); + + /// + /// Set a uniform Vector2 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetVector2(string name, Vector2 data) + => TrySetUniform(name, data, _gl.Uniform2); + + /// + /// Set a uniform Vector3 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetVector3(string name, Vector3 data) + => TrySetUniform(name, data, _gl.Uniform3); + + /// + /// Set a uniform Vector3 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetVector4(string name, Vector4 data) + => TrySetUniform(name, data, _gl.Uniform4); +} diff --git a/Engine/SharpEngine.Core.Components/Properties/Textures/Texture.cs b/Engine/SharpEngine.Core.Components/Properties/Textures/Texture.cs new file mode 100644 index 0000000..d271f1e --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Textures/Texture.cs @@ -0,0 +1,31 @@ +using Silk.NET.Assimp; +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Components.Properties.Textures; + +/// +/// Represents a texture program. +/// +public partial class Texture : IDisposable +{ + /// The OpenGL handle for the texture. + public readonly uint Handle; + public readonly TextureType Type; + public readonly string Path; + + private readonly GL _gl; + + /// + /// Initializes a new instance of . + /// + public Texture(GL gl, string path, TextureType type = TextureType.Diffuse) + { + _gl = gl; + Handle = _gl.GenTexture(); + + Path = path; + Type = type; + + Initialize(); + } +} diff --git a/Engine/SharpEngine.Core.Components/Properties/Textures/TextureExtensions.cs b/Engine/SharpEngine.Core.Components/Properties/Textures/TextureExtensions.cs new file mode 100644 index 0000000..a060f8c --- /dev/null +++ b/Engine/SharpEngine.Core.Components/Properties/Textures/TextureExtensions.cs @@ -0,0 +1,63 @@ +using StbImageSharp; +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Components.Properties.Textures; + +public partial class Texture +{ + public void Initialize() + { + // Bind the handle + Use(); + + // OpenGL has its texture origin in the lower left corner instead of the top left corner, + // so we tell StbImageSharp to flip the image when loading. + StbImage.stbi_set_flip_vertically_on_load(1); + + // Here we open a stream to the file and pass it to StbImageSharp to load. + using (Stream stream = File.OpenRead(Path)) + { + var image = ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha); + + // Generate a texture + _gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, (uint)image.Width, (uint)image.Height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, image.Data.AsSpan()); + } + + // Set texture parameters + SetParameters(); + + // Generate mipmaps + _gl.GenerateMipmap(GLEnum.Texture2D); + } + + public void SetParameters() + { + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)GLEnum.ClampToEdge); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)GLEnum.ClampToEdge); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)GLEnum.LinearMipmapLinear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)GLEnum.Linear); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBaseLevel, 0); + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, 8); + _gl.GenerateMipmap(TextureTarget.Texture2D); + } + + // Multiple textures can be bound, if your shader needs more than just one. + // If you want to do that, use GL.ActiveTexture to set which slot GL.BindTexture binds to. + // The OpenGL standard requires that there be at least 16, but there can be more depending on your graphics card. + + /// + /// Activate the texture. + /// + /// The bound texture location. + public void Use(TextureUnit unit = TextureUnit.Texture0) + { + _gl.ActiveTexture(unit); + _gl.BindTexture(TextureTarget.Texture2D, Handle); + } + + public void Dispose() + { + _gl.DeleteTexture(Handle); + GC.SuppressFinalize(this); + } +} diff --git a/Core/Entities/Properties/Transform.cs b/Engine/SharpEngine.Core.Components/Properties/Transform.cs similarity index 58% rename from Core/Entities/Properties/Transform.cs rename to Engine/SharpEngine.Core.Components/Properties/Transform.cs index 50b8bea..3f2f075 100644 --- a/Core/Entities/Properties/Transform.cs +++ b/Engine/SharpEngine.Core.Components/Properties/Transform.cs @@ -1,12 +1,22 @@ -using System.Numerics; +using SharpEngine.Core.Numerics; +using Matrix4x4 = System.Numerics.Matrix4x4; namespace SharpEngine.Core.Entities.Properties; /// /// Represents a game object transformation in 3D space. /// -public class Transform +public class Transform : ITransform { + /// + /// Initializes a new instance of . + /// + public Transform() { } + public Transform(Vector3 position) + { + Position = position; + } + /// /// Gets or sets the position of the game object. /// @@ -25,7 +35,7 @@ public class Transform /// /// Gets the transformation of the game object as a model matrix. /// - public Matrix4x4 ModelMatrix => Matrix4x4.CreateScale(Scale) * + public Matrix4x4 ModelMatrix => Matrix4x4.CreateScale(new System.Numerics.Vector3(Scale.X, Scale.Y, Scale.Z)) * Matrix4x4.CreateFromAxisAngle(Rotation.Axis, Math.DegreesToRadians(Rotation.Angle)) * - Matrix4x4.CreateTranslation(Position); + Matrix4x4.CreateTranslation(new System.Numerics.Vector3(Position.X, Position.Y, Position.Z)); } diff --git a/Core/Entities/Properties/Transform2D.cs b/Engine/SharpEngine.Core.Components/Properties/Transform2D.cs similarity index 59% rename from Core/Entities/Properties/Transform2D.cs rename to Engine/SharpEngine.Core.Components/Properties/Transform2D.cs index cbcbbee..8a44d56 100644 --- a/Core/Entities/Properties/Transform2D.cs +++ b/Engine/SharpEngine.Core.Components/Properties/Transform2D.cs @@ -1,16 +1,17 @@ using System.Numerics; +using Vector2 = SharpEngine.Core.Numerics.Vector2; namespace SharpEngine.Core.Entities.Properties; /// /// Represents a game object transformation in 2D space. /// -public class Transform2D +public class Transform2D : ITransform { /// /// Gets or sets the position of the game object. /// - public Vector2 Position { get; set; } + public Vector2 Position { get; set; } = new(0, 0); /// /// Gets or sets the scale of the game object. @@ -20,12 +21,14 @@ public class Transform2D /// /// Gets or sets the rotation of the game object. /// - public float Rotation { get; set; } + public Quaternion Rotation { get; set; } = new() { Angle = 0 }; /// /// Gets the transformation of the game object as a model matrix. /// public Matrix4x4 ModelMatrix => Matrix4x4.CreateScale(new Vector3(Scale.X, Scale.Y, 0)) * - Matrix4x4.CreateRotationZ(Rotation) * - Matrix4x4.CreateTranslation(new Vector3(Position.X, Position.Y, 0)); + // Matrix4x4.CreateRotationZ(Math.DegreesToRadians(Rotation.Angle)) * + Matrix4x4.CreateRotationZ(0) * + // Matrix4x4.CreateTranslation(new Vector3(Position.X, Position.Y, 0)); + Matrix4x4.CreateTranslation(new Vector3(0, 0, 0)); } \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj b/Engine/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj new file mode 100644 index 0000000..bec75b6 --- /dev/null +++ b/Engine/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + + + + $(GenerateNuGetPackages) + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/Engine/SharpEngine.Core.Numerics/Float.cs b/Engine/SharpEngine.Core.Numerics/Float.cs new file mode 100644 index 0000000..333f856 --- /dev/null +++ b/Engine/SharpEngine.Core.Numerics/Float.cs @@ -0,0 +1,9 @@ +namespace SharpEngine.Core.Numerics; + +public static class Float +{ + /// + /// Represents a constant value for tolerance set to 1e-6. Used for precision in calculations. + /// + public const float Tolerance = 1e-6f; +} diff --git a/Engine/SharpEngine.Core.Numerics/HashCodeHelper.cs b/Engine/SharpEngine.Core.Numerics/HashCodeHelper.cs new file mode 100644 index 0000000..601d499 --- /dev/null +++ b/Engine/SharpEngine.Core.Numerics/HashCodeHelper.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/HashCodeHelper.cs + +namespace SharpEngine.Core.Numerics +{ + internal static class HashCodeHelper + { + /// + /// Combines two hash codes, useful for combining hash codes of individual vector elements + /// + internal static int CombineHashCodes(int h1, int h2) + => (((h1 << 5) + h1) ^ h2); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Numerics/IVector.cs b/Engine/SharpEngine.Core.Numerics/IVector.cs new file mode 100644 index 0000000..a620e8d --- /dev/null +++ b/Engine/SharpEngine.Core.Numerics/IVector.cs @@ -0,0 +1,15 @@ +namespace SharpEngine.Core.Numerics; + +/// +/// An abstraction of the different vector types. +/// +public interface IVector +{ + /// + float X { get; set; } + + /// + float Y { get; set; } + + public const float Epsilon = 1e-5f; +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Numerics/JitIntrinsicAttribute.cs b/Engine/SharpEngine.Core.Numerics/JitIntrinsicAttribute.cs new file mode 100644 index 0000000..0f6846a --- /dev/null +++ b/Engine/SharpEngine.Core.Numerics/JitIntrinsicAttribute.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/JITIntrinsicAttribute.cs + +namespace SharpEngine.Core.Numerics +{ + /// + /// An attribute that can be attached to JIT Intrinsic methods/properties + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property)] + internal class JitIntrinsicAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Numerics/SharpEngine.Core.Numerics.csproj b/Engine/SharpEngine.Core.Numerics/SharpEngine.Core.Numerics.csproj new file mode 100644 index 0000000..3082977 --- /dev/null +++ b/Engine/SharpEngine.Core.Numerics/SharpEngine.Core.Numerics.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + $(GenerateNuGetPackages) + + + + diff --git a/Engine/SharpEngine.Core.Numerics/Vector2.cs b/Engine/SharpEngine.Core.Numerics/Vector2.cs new file mode 100644 index 0000000..0e94630 --- /dev/null +++ b/Engine/SharpEngine.Core.Numerics/Vector2.cs @@ -0,0 +1,419 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/Vector2.cs + +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using Matrix3x2 = System.Numerics.Matrix3x2; +using Matrix4x4 = System.Numerics.Matrix4x4; +using Quaternion = System.Numerics.Quaternion; +using Vector = System.Numerics.Vector; + +namespace SharpEngine.Core.Numerics +{ + /// + /// A structure encapsulating two single precision floating point values and provides hardware accelerated methods. + /// + public partial struct Vector2 : IEquatable, IFormattable + { + #region Public Static Properties + + /// Returns the vector (0,0). + public static Vector2 Zero => new(); + + /// Returns the vector (1,1). + public static Vector2 One => new(1.0f, 1.0f); + + /// Returns the vector (1,0). + public static Vector2 UnitX => new(1.0f, 0.0f); + + /// Returns the vector (0,1). + public static Vector2 UnitY => new(0.0f, 1.0f); + + #endregion Public Static Properties + + #region Public instance methods + + /// + /// Returns the hash code for this instance. + /// + /// The hash code. + public override readonly int GetHashCode() + => HashCodeHelper.CombineHashCodes(X.GetHashCode(), Y.GetHashCode()); + + /// + /// Returns a boolean indicating whether the given Object is equal to this Vector2 instance. + /// + /// The Object to compare against. + /// True if the Object is equal to this Vector2; False otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj is not Vector2 vec2) + return false; + + return Equals(vec2); + } + + /// + /// Returns a String representing this Vector2 instance. + /// + /// The string representation. + public override readonly string ToString() => ToString("G", CultureInfo.CurrentCulture); + + /// + /// Returns a String representing this Vector2 instance, using the specified format to format individual elements. + /// + /// The format of individual elements. + /// The string representation. + public readonly string ToString(string format) => ToString(format, CultureInfo.CurrentCulture); + + /// + /// Returns a String representing this Vector2 instance, using the specified format to format individual elements + /// and the given IFormatProvider. + /// + /// The format of individual elements. + /// The format provider to use when formatting elements. + /// The string representation. + public readonly string ToString(string? format, IFormatProvider? formatProvider) + { + var sb = new StringBuilder(); + string separator = NumberFormatInfo.GetInstance(formatProvider).NumberGroupSeparator; + sb.Append('<'); + sb.Append(X.ToString(format, formatProvider)); + sb.Append(separator); + sb.Append(' '); + sb.Append(Y.ToString(format, formatProvider)); + sb.Append('>'); + return sb.ToString(); + } + + /// + /// Returns the length of the vector. + /// + /// The vector's length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float Length() + { + if (Vector.IsHardwareAccelerated) + { + float ls = Vector2.Dot(this, this); + return (float)MathF.Sqrt(ls); + } + else + { + float ls = (X * X) + (Y * Y); + return MathF.Sqrt(ls); + } + } + + /// + /// Returns the length of the vector squared. This operation is cheaper than Length(). + /// + /// The vector's length squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float LengthSquared() + { + if (Vector.IsHardwareAccelerated) + return Vector2.Dot(this, this); + + else + return (X * X) + (Y * Y); + } + #endregion Public Instance Methods + + #region Public Static Methods + /// + /// Returns the Euclidean distance between the two given points. + /// + /// The first point. + /// The second point. + /// The distance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Distance(Vector2 value1, Vector2 value2) + { + if (Vector.IsHardwareAccelerated) + { + var difference = value1 - value2; + float ls = Vector2.Dot(difference, difference); + return (float)System.MathF.Sqrt(ls); + } + else + { + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + + float ls = (dx * dx) + (dy * dy); + + return MathF.Sqrt(ls); + } + } + + /// + /// Returns the Euclidean distance squared between the two given points. + /// + /// The first point. + /// The second point. + /// The distance squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DistanceSquared(Vector2 value1, Vector2 value2) + { + if (Vector.IsHardwareAccelerated) + { + var difference = value1 - value2; + return Vector2.Dot(difference, difference); + } + else + { + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + + return (dx * dx) + (dy * dy); + } + } + + /// + /// Returns a vector with the same direction as the given vector, but with a length of 1. + /// + /// The vector to normalize. + /// The normalized vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Normalize(Vector2 value) + { + if (Vector.IsHardwareAccelerated) + { + float length = value.Length(); + return value / length; + } + else + { + float ls = (value.X * value.X) + (value.Y * value.Y); + float invNorm = 1.0f / MathF.Sqrt(ls); + + return new Vector2( + value.X * invNorm, + value.Y * invNorm); + } + } + + /// + /// Returns the reflection of a vector off a surface that has the specified normal. + /// + /// The source vector. + /// The normal of the surface being reflected off. + /// The reflected vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Reflect(Vector2 vector, Vector2 normal) + { + if (Vector.IsHardwareAccelerated) + { + float dot = Vector2.Dot(vector, normal); + return vector - (2 * dot * normal); + } + else + { + float dot = (vector.X * normal.X) + (vector.Y * normal.Y); + + return new Vector2( + vector.X - (2.0f * dot * normal.X), + vector.Y - (2.0f * dot * normal.Y)); + } + } + + /// + /// Restricts a vector between a min and max value. + /// + /// The source vector. + /// The minimum value. + /// The maximum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Clamp(Vector2 value1, Vector2 min, Vector2 max) + { + // This compare order is very important!!! + // We must follow HLSL behavior in the case user specified min value is bigger than max value. + float x = value1.X; + x = (x > max.X) ? max.X : x; + x = (x < min.X) ? min.X : x; + + float y = value1.Y; + y = (y > max.Y) ? max.Y : y; + y = (y < min.Y) ? min.Y : y; + + return new Vector2(x, y); + } + + /// + /// Linearly interpolates between two vectors based on the given weighting. + /// + /// The first source vector. + /// The second source vector. + /// Value between 0 and 1 indicating the weight of the second source vector. + /// The interpolated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Lerp(Vector2 value1, Vector2 value2, float amount) + { + return new Vector2( + value1.X + ((value2.X - value1.X) * amount), + value1.Y + ((value2.Y - value1.Y) * amount)); + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 position, Matrix3x2 matrix) + { + return new Vector2( + (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M31, + (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M32); + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 position, Matrix4x4 matrix) + { + return new Vector2( + (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M41, + (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M42); + } + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 TransformNormal(Vector2 normal, Matrix3x2 matrix) + { + return new Vector2( + (normal.X * matrix.M11) + (normal.Y * matrix.M21), + (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + } + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 TransformNormal(Vector2 normal, Matrix4x4 matrix) + { + return new Vector2( + (normal.X * matrix.M11) + (normal.Y * matrix.M21), + (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + } + + /// + /// Transforms a vector by the given Quaternion rotation value. + /// + /// The source vector to be rotated. + /// The rotation to apply. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 value, Quaternion rotation) + { + float x2 = rotation.X + rotation.X; + float y2 = rotation.Y + rotation.Y; + float z2 = rotation.Z + rotation.Z; + + float wz2 = rotation.W * z2; + float xx2 = rotation.X * x2; + float xy2 = rotation.X * y2; + float yy2 = rotation.Y * y2; + float zz2 = rotation.Z * z2; + + return new Vector2( + (value.X * (1.0f - yy2 - zz2)) + (value.Y * (xy2 - wz2)), + (value.X * (xy2 + wz2)) + (value.Y * (1.0f - xx2 - zz2))); + } + #endregion Public Static Methods + + #region Public operator methods + // all the below methods should be inlined as they are + // implemented over JIT intrinsics + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Add(Vector2 left, Vector2 right) => left + right; + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Subtract(Vector2 left, Vector2 right) => left - right; + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(Vector2 left, Vector2 right) => left * right; + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(Vector2 left, float right) => left * right; + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(float left, Vector2 right) => left * right; + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Divide(Vector2 left, Vector2 right) => left / right; + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Divide(Vector2 left, float divisor) => left / divisor; + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Negate(Vector2 value) => -value; + + #endregion Public operator methods + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Numerics/Vector2_Intrinsics.cs b/Engine/SharpEngine.Core.Numerics/Vector2_Intrinsics.cs new file mode 100644 index 0000000..093518f --- /dev/null +++ b/Engine/SharpEngine.Core.Numerics/Vector2_Intrinsics.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; + +namespace SharpEngine.Core.Numerics +{ + // This file contains the definitions for all of the JIT intrinsic methods and properties that are recognized by the current x64 JIT compiler. + // The implementation defined here is used in any circumstance where the JIT fails to recognize these members as intrinsic. + // The JIT recognizes these methods and properties by name and signature: if either is changed, the JIT will no longer recognize the member. + // Some methods declared here are not strictly intrinsic, but delegate to an intrinsic method. For example, only one overload of CopyTo() + + public partial struct Vector2 : IVector + { + /// + public float X { get; set; } + + /// + public float Y { get; set; } + + #region Constructors + + /// + /// Constructs a vector whose elements are all the single specified value. + /// + /// The element to fill the vector with. + [JitIntrinsic] + public Vector2(float value) : this(value, value) { } + + /// + /// Constructs a vector with the given individual elements. + /// + /// The X component. + /// The Y component. + [JitIntrinsic] + public Vector2(float x, float y) + { + X = x; + Y = y; + } + + #endregion Constructors + + #region Public Instance Methods + /// + /// Copies the contents of the vector into the given array. + /// + /// The destination array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void CopyTo(float[] array) => CopyTo(array, 0); + + /// + /// Copies the contents of the vector into the given array, starting from the given index. + /// + /// If array is null. + /// If array is multidimensional. + /// If index is greater than end of the array or index is less than zero. + /// If number of elements in source vector is greater than those available in destination array + /// or if there are not enough elements to copy. + public readonly void CopyTo(float[] array, int index) + { + // Match the JIT's exception type here. For perf, a NullReference is thrown instead of an ArgumentNull. + ArgumentNullException.ThrowIfNull(array); + + if (index < 0 || index >= array.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + if ((array.Length - index) < 2) + throw new ArgumentException("The given index is outside the supported range.", nameof(index)); + + array[index] = X; + array[index + 1] = Y; + } + + /// + /// Returns a boolean indicating whether the given Vector2 is equal to this Vector2 instance. + /// + /// The Vector2 to compare this instance to. + /// True if the other Vector2 is approximately equal to this instance; False otherwise. + [JitIntrinsic] + public readonly bool Equals(Vector2 other) + => MathF.Abs(X - other.X) < Float.Tolerance && MathF.Abs(Y - other.Y) < Float.Tolerance; + + #endregion Public Instance Methods + + #region Public Static Methods + + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Dot(Vector2 value1, Vector2 value2) + => (value1.X * value2.X) + (value1.Y * value2.Y); + + /// + /// Returns a vector whose elements are the minimum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The minimized vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Min(Vector2 value1, Vector2 value2) + { + var x = (value1.X < value2.X) ? value1.X : value2.X; + var y = (value1.Y < value2.Y) ? value1.Y : value2.Y; + + return new Vector2(x, y); + } + + /// + /// Returns a vector whose elements are the maximum of each of the pairs of elements in the two source vectors + /// + /// The first source vector + /// The second source vector + /// The maximized vector + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Max(Vector2 value1, Vector2 value2) + { + var x = (value1.X > value2.X) ? value1.X : value2.X; + var y = (value1.Y > value2.Y) ? value1.Y : value2.Y; + + return new Vector2(x, y); + } + + /// + /// Returns a vector whose elements are the absolute values of each of the source vector's elements. + /// + /// The source vector. + /// The absolute value vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Abs(Vector2 value) + => new(MathF.Abs(value.X), MathF.Abs(value.Y)); + + /// + /// Returns a vector whose elements are the square root of each of the source vector's elements. + /// + /// The source vector. + /// The square root vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 SquareRoot(Vector2 value) + => new(MathF.Sqrt(value.X), MathF.Sqrt(value.Y)); + + #endregion Public Static Methods + + #region Public Static Operators + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator +(Vector2 left, Vector2 right) => new(left.X + right.X, left.Y + right.Y); + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator -(Vector2 left, Vector2 right) => new(left.X - right.X, left.Y - right.Y); + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator *(Vector2 left, Vector2 right) => new(left.X * right.X, left.Y * right.Y); + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator *(float left, Vector2 right) => new Vector2(left, left) * right; + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator *(Vector2 left, float right) => left * new Vector2(right, right); + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator /(Vector2 left, Vector2 right) => new(left.X / right.X, left.Y / right.Y); + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator /(Vector2 value1, float value2) + { + float invDiv = 1.0f / value2; + + return new Vector2(value1.X * invDiv, value1.Y * invDiv); + } + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator -(Vector2 value) => Zero - value; + + /// + /// Returns a boolean indicating whether the two given vectors are equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are equal; False otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector2 left, Vector2 right) => left.Equals(right); + + /// + /// Returns a boolean indicating whether the two given vectors are not equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are not equal; False if they are equal. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector2 left, Vector2 right) => !(left == right); + + /// + /// Converts the SharpEngine representation to a System.Numerics representation. + /// + /// The value to convert + public static explicit operator System.Numerics.Vector2(Vector2 value) + => new(value.X, value.Y); + + /// + /// Converts the System.Numerics representation to a SharpEngine representation. + /// + /// The value to convert + public static explicit operator Vector2(System.Numerics.Vector2 value) + => new(value.X, value.Y); + + #endregion Public Static Operators + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Numerics/Vector3.cs b/Engine/SharpEngine.Core.Numerics/Vector3.cs new file mode 100644 index 0000000..da6f282 --- /dev/null +++ b/Engine/SharpEngine.Core.Numerics/Vector3.cs @@ -0,0 +1,427 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/Vector3.cs + +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using Matrix4x4 = System.Numerics.Matrix4x4; +using Quaternion = System.Numerics.Quaternion; +using Vector = System.Numerics.Vector; + +namespace SharpEngine.Core.Numerics +{ + /// + /// A structure encapsulating three single precision floating point values and provides hardware accelerated methods. + /// + public partial struct Vector3 : IEquatable, IFormattable + { + #region Public Static Properties + + /// Returns the vector (0,0,0). + public static Vector3 Zero => new(); + + /// Returns the vector (1,1,1). + public static Vector3 One => new(1.0f, 1.0f, 1.0f); + + /// Returns the vector (1,0,0). + public static Vector3 UnitX => new(1.0f, 0.0f, 0.0f); + + /// Returns the vector (0,1,0). + public static Vector3 UnitY => new(0.0f, 1.0f, 0.0f); + + /// Returns the vector (0,0,1). + public static Vector3 UnitZ => new(0.0f, 0.0f, 1.0f); + + #endregion Public Static Properties + + #region Public Instance Methods + + /// + /// Returns the hash code for this instance. + /// + /// The hash code. + public override readonly int GetHashCode() + { + int hash = X.GetHashCode(); + hash = HashCodeHelper.CombineHashCodes(hash, Y.GetHashCode()); + hash = HashCodeHelper.CombineHashCodes(hash, Z.GetHashCode()); + return hash; + } + + /// + /// Returns a boolean indicating whether the given Object is equal to this Vector3 instance. + /// + /// The Object to compare against. + /// True if the Object is equal to this Vector3; False otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly bool Equals(object? obj) + { + if (obj is not Vector3 vec3) + return false; + + return Equals(vec3); + } + + /// + /// Returns a String representing this Vector3 instance. + /// + /// The string representation. + public override readonly string ToString() => ToString("G", CultureInfo.CurrentCulture); + + /// + /// Returns a String representing this Vector3 instance, using the specified format to format individual elements. + /// + /// The format of individual elements. + /// The string representation. + public readonly string ToString(string format) => ToString(format, CultureInfo.CurrentCulture); + + /// + /// Returns a String representing this Vector3 instance, using the specified format to format individual elements + /// and the given IFormatProvider. + /// + /// The format of individual elements. + /// The format provider to use when formatting elements. + /// The string representation. + public readonly string ToString(string? format, IFormatProvider? formatProvider) + { + var sb = new StringBuilder(); + string separator = NumberFormatInfo.GetInstance(formatProvider).NumberGroupSeparator; + sb.Append('<'); + sb.Append(X.ToString(format, formatProvider)); + sb.Append(separator); + sb.Append(' '); + sb.Append(Y.ToString(format, formatProvider)); + sb.Append(separator); + sb.Append(' '); + sb.Append(Z.ToString(format, formatProvider)); + sb.Append('>'); + return sb.ToString(); + } + + /// + /// Returns the length of the vector. + /// + /// The vector's length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float Length() + { + if (Vector.IsHardwareAccelerated) + { + float ls = Vector3.Dot(this, this); + return System.MathF.Sqrt(ls); + } + else + { + float ls = (X * X) + (Y * Y) + (Z * Z); + return System.MathF.Sqrt(ls); + } + } + + /// + /// Returns the length of the vector squared. This operation is cheaper than Length(). + /// + /// The vector's length squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float LengthSquared() + { + if (Vector.IsHardwareAccelerated) + return Vector3.Dot(this, this); + else + return (X * X) + (Y * Y) + (Z * Z); + } + + #endregion Public Instance Methods + + #region Public Static Methods + + /// + /// Returns the Euclidean distance between the two given points. + /// + /// The first point. + /// The second point. + /// The distance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Distance(Vector3 value1, Vector3 value2) + { + if (Vector.IsHardwareAccelerated) + { + var difference = value1 - value2; + float ls = Vector3.Dot(difference, difference); + return System.MathF.Sqrt(ls); + } + else + { + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + float dz = value1.Z - value2.Z; + + float ls = (dx * dx) + (dy * dy) + (dz * dz); + + return System.MathF.Sqrt(ls); + } + } + + /// + /// Returns the Euclidean distance squared between the two given points. + /// + /// The first point. + /// The second point. + /// The distance squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DistanceSquared(Vector3 value1, Vector3 value2) + { + if (Vector.IsHardwareAccelerated) + { + var difference = value1 - value2; + return Vector3.Dot(difference, difference); + } + + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + float dz = value1.Z - value2.Z; + + return (dx * dx) + (dy * dy) + (dz * dz); + } + + /// + /// Returns a vector with the same direction as the given vector, but with a length of 1. + /// + /// The vector to normalize. + /// The normalized vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Normalize(Vector3 value) + { + if (Vector.IsHardwareAccelerated) + { + float length = value.Length(); + return value / length; + } + else + { + float ls = (value.X * value.X) + (value.Y * value.Y) + (value.Z * value.Z); + float length = (float)System.Math.Sqrt(ls); + return new Vector3(value.X / length, value.Y / length, value.Z / length); + } + } + + /// + /// Computes the cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The cross product. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Cross(Vector3 vector1, Vector3 vector2) + => new((vector1.Y * vector2.Z) - (vector1.Z * vector2.Y), + (vector1.Z * vector2.X) - (vector1.X * vector2.Z), + (vector1.X * vector2.Y) - (vector1.Y * vector2.X)); + + /// + /// Returns the reflection of a vector off a surface that has the specified normal. + /// + /// The source vector. + /// The normal of the surface being reflected off. + /// The reflected vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Reflect(Vector3 vector, Vector3 normal) + { + if (Vector.IsHardwareAccelerated) + { + float dot = Vector3.Dot(vector, normal); + var temp = normal * dot * 2f; + return vector - temp; + } + else + { + float dot = (vector.X * normal.X) + (vector.Y * normal.Y) + (vector.Z * normal.Z); + float tempX = normal.X * dot * 2f; + float tempY = normal.Y * dot * 2f; + float tempZ = normal.Z * dot * 2f; + return new Vector3(vector.X - tempX, vector.Y - tempY, vector.Z - tempZ); + } + } + + /// + /// Restricts a vector between a min and max value. + /// + /// The source vector. + /// The minimum value. + /// The maximum value. + /// The restricted vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Clamp(Vector3 value1, Vector3 min, Vector3 max) + { + // This compare order is very important!!! + // We must follow HLSL behavior in the case user specified min value is bigger than max value. + + float x = value1.X; + x = (x > max.X) ? max.X : x; + x = (x < min.X) ? min.X : x; + + float y = value1.Y; + y = (y > max.Y) ? max.Y : y; + y = (y < min.Y) ? min.Y : y; + + float z = value1.Z; + z = (z > max.Z) ? max.Z : z; + z = (z < min.Z) ? min.Z : z; + + return new Vector3(x, y, z); + } + + /// + /// Linearly interpolates between two vectors based on the given weighting. + /// + /// The first source vector. + /// The second source vector. + /// Value between 0 and 1 indicating the weight of the second source vector. + /// The interpolated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Lerp(Vector3 value1, Vector3 value2, float amount) + { + if (Vector.IsHardwareAccelerated) + { + var firstInfluence = value1 * (1f - amount); + var secondInfluence = value2 * amount; + return firstInfluence + secondInfluence; + } + + return new Vector3(value1.X + ((value2.X - value1.X) * amount), + value1.Y + ((value2.Y - value1.Y) * amount), + value1.Z + ((value2.Z - value1.Z) * amount)); + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Transform(Vector3 position, Matrix4x4 matrix) + => new((position.X * matrix.M11) + (position.Y * matrix.M21) + (position.Z * matrix.M31) + matrix.M41, + (position.X * matrix.M12) + (position.Y * matrix.M22) + (position.Z * matrix.M32) + matrix.M42, + (position.X * matrix.M13) + (position.Y * matrix.M23) + (position.Z * matrix.M33) + matrix.M43); + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 TransformNormal(Vector3 normal, Matrix4x4 matrix) + => new((normal.X * matrix.M11) + (normal.Y * matrix.M21) + (normal.Z * matrix.M31), + (normal.X * matrix.M12) + (normal.Y * matrix.M22) + (normal.Z * matrix.M32), + (normal.X * matrix.M13) + (normal.Y * matrix.M23) + (normal.Z * matrix.M33)); + + /// + /// Transforms a vector by the given Quaternion rotation value. + /// + /// The source vector to be rotated. + /// The rotation to apply. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Transform(Vector3 value, Quaternion rotation) + { + float x2 = rotation.X + rotation.X; + float y2 = rotation.Y + rotation.Y; + float z2 = rotation.Z + rotation.Z; + + float wx2 = rotation.W * x2; + float wy2 = rotation.W * y2; + float wz2 = rotation.W * z2; + float xx2 = rotation.X * x2; + float xy2 = rotation.X * y2; + float xz2 = rotation.X * z2; + float yy2 = rotation.Y * y2; + float yz2 = rotation.Y * z2; + float zz2 = rotation.Z * z2; + + return new Vector3( + (value.X * (1.0f - yy2 - zz2)) + (value.Y * (xy2 - wz2)) + (value.Z * (xz2 + wy2)), + (value.X * (xy2 + wz2)) + (value.Y * (1.0f - xx2 - zz2)) + (value.Z * (yz2 - wx2)), + (value.X * (xz2 - wy2)) + (value.Y * (yz2 + wx2)) + (value.Z * (1.0f - xx2 - yy2))); + } + #endregion Public Static Methods + + #region Public operator methods + + // All these methods should be inlined as they are implemented + // over JIT intrinsics + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Add(Vector3 left, Vector3 right) => left + right; + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Subtract(Vector3 left, Vector3 right) => left - right; + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Multiply(Vector3 left, Vector3 right) => left * right; + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Multiply(Vector3 left, float right) => left * right; + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Multiply(float left, Vector3 right) => left * right; + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Divide(Vector3 left, Vector3 right) => left / right; + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Divide(Vector3 left, float divisor) => left / divisor; + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Negate(Vector3 value) => -value; + + #endregion Public operator methods + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs b/Engine/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs new file mode 100644 index 0000000..e747bab --- /dev/null +++ b/Engine/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; + +namespace SharpEngine.Core.Numerics +{ + // This file contains the definitions for all of the JIT intrinsic methods and properties that are recognized by the current x64 JIT compiler. + // The implementation defined here is used in any circumstance where the JIT fails to recognize these members as intrinsic. + // The JIT recognizes these methods and properties by name and signature: if either is changed, the JIT will no longer recognize the member. + // Some methods declared here are not strictly intrinsic, but delegate to an intrinsic method. For example, only one overload of CopyTo() + // is actually recognized by the JIT, but both are here for simplicity. + + public partial struct Vector3 : IVector + { + /// + public float X { get; set; } + + /// + public float Y { get; set; } + + /// + public float Z { get; set; } + + #region Constructors + + /// + /// Constructs a vector whose elements are all the single specified value. + /// + /// The element to fill the vector with. + [JitIntrinsic] + public Vector3(float value) : this(value, value, value) { } + + /// + /// Constructs a Vector3 from the given Vector2 and a third value. + /// + /// The Vector to extract X and Y components from. + /// The Z component. + public Vector3(Vector2 value, float z) : this(value.X, value.Y, z) { } + + /// + /// Constructs a vector with the given individual elements. + /// + /// The X component. + /// The Y component. + /// The Z component. + [JitIntrinsic] + public Vector3(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + #endregion Constructors + + #region Public Instance Methods + + /// + /// Copies the contents of the vector into the given array. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void CopyTo(float[] array) => CopyTo(array, 0); + + /// + /// Copies the contents of the vector into the given array, starting from index. + /// + /// If array is null. + /// If array is multidimensional. + /// If index is greater than end of the array or index is less than zero. + /// If number of elements in source vector is greater than those available in destination array. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void CopyTo(float[] array, int index) + { + // Match the JIT's exception type here. For perf, a NullReference is thrown instead of an ArgumentNull. + ArgumentNullException.ThrowIfNull(array); + + if (index < 0 || index >= array.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + if ((array.Length - index) < 3) + throw new ArgumentException("", nameof(index)); + + array[index] = X; + array[index + 1] = Y; + array[index + 2] = Z; + } + + /// + /// Returns a boolean indicating whether the given Vector3 is equal to this Vector3 instance. + /// + /// The Vector3 to compare this instance to. + /// True if the other Vector3 is approximately equal to this instance; False otherwise. + [JitIntrinsic] + public readonly bool Equals(Vector3 other) + => MathF.Abs(X - other.X) < Float.Tolerance && + MathF.Abs(Y - other.Y) < Float.Tolerance && + MathF.Abs(Z - other.Z) < Float.Tolerance; + + #endregion Public Instance Methods + + #region Public Static Methods + + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Dot(Vector3 vector1, Vector3 vector2) + => (vector1.X * vector2.X) + + (vector1.Y * vector2.Y) + + (vector1.Z * vector2.Z); + + /// + /// Returns a vector whose elements are the minimum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The minimized vector. + [JitIntrinsic] + public static Vector3 Min(Vector3 value1, Vector3 value2) + => new((value1.X < value2.X) ? value1.X : value2.X, + (value1.Y < value2.Y) ? value1.Y : value2.Y, + (value1.Z < value2.Z) ? value1.Z : value2.Z); + + /// + /// Returns a vector whose elements are the maximum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The maximized vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Max(Vector3 value1, Vector3 value2) + => new((value1.X > value2.X) ? value1.X : value2.X, + (value1.Y > value2.Y) ? value1.Y : value2.Y, + (value1.Z > value2.Z) ? value1.Z : value2.Z); + + /// + /// Returns a vector whose elements are the absolute values of each of the source vector's elements. + /// + /// The source vector. + /// The absolute value vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Abs(Vector3 value) + => new(MathF.Abs(value.X), + MathF.Abs(value.Y), + MathF.Abs(value.Z)); + + /// + /// Returns a vector whose elements are the square root of each of the source vector's elements. + /// + /// The source vector. + /// The square root vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 SquareRoot(Vector3 value) + => new(MathF.Sqrt(value.X), + MathF.Sqrt(value.Y), + MathF.Sqrt(value.Z)); + + #endregion Public Static Methods + + #region Public Static Operators + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator +(Vector3 left, Vector3 right) + => new(left.X + right.X, + left.Y + right.Y, + left.Z + right.Z); + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator -(Vector3 left, Vector3 right) + => new(left.X - right.X, + left.Y - right.Y, + left.Z - right.Z); + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(Vector3 left, Vector3 right) + => new(left.X * right.X, + left.Y * right.Y, + left.Z * right.Z); + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(Vector3 left, float right) => left * new Vector3(right); + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(float left, Vector3 right) => new Vector3(left) * right; + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator /(Vector3 left, Vector3 right) + => new(left.X / right.X, + left.Y / right.Y, + left.Z / right.Z); + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator /(Vector3 value1, float value2) + { + float invDiv = 1.0f / value2; + + return new Vector3( + value1.X * invDiv, + value1.Y * invDiv, + value1.Z * invDiv); + } + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator -(Vector3 value) => Zero - value; + + /// + /// Returns a boolean indicating whether the two given vectors are equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are equal; False otherwise. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector3 left, Vector3 right) + => Math.Abs(left.X - right.X) < Float.Tolerance && + Math.Abs(left.Y - right.Y) < Float.Tolerance && + Math.Abs(left.Z - right.Z) < Float.Tolerance; + + /// + /// Returns a boolean indicating whether the two given vectors are not equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are not equal; False if they are equal. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector3 left, Vector3 right) + => !(left == right); + + /// + /// Converts the SharpEngine representation to a System.Numerics representation. + /// + /// The value to convert + public static explicit operator System.Numerics.Vector3(Vector3 value) + => new(value.X, value.Y, value.Z); + + /// + /// Converts the System.Numerics representation to a SharpEngine representation. + /// + /// The value to convert + public static explicit operator Vector3(System.Numerics.Vector3 value) + => new(value.X, value.Y, value.Z); + + #endregion Public Static Operators + } +} \ No newline at end of file diff --git a/Core/Interfaces/IGame.cs b/Engine/SharpEngine.Core/Abstractions/IGame.cs similarity index 100% rename from Core/Interfaces/IGame.cs rename to Engine/SharpEngine.Core/Abstractions/IGame.cs diff --git a/Engine/SharpEngine.Core/Abstractions/IRenderable.cs b/Engine/SharpEngine.Core/Abstractions/IRenderable.cs new file mode 100644 index 0000000..e248e60 --- /dev/null +++ b/Engine/SharpEngine.Core/Abstractions/IRenderable.cs @@ -0,0 +1,14 @@ +using SharpEngine.Core.Entities.Properties.Meshes; + +namespace SharpEngine.Core.Interfaces; + +// TODO: #55 Make an abstract class and move Render method here? + +/// +/// Contains definitions for rendering objects. +/// +public interface IRenderable +{ + + +} diff --git a/Core/Interfaces/ISettings.cs b/Engine/SharpEngine.Core/Abstractions/ISettings.cs similarity index 69% rename from Core/Interfaces/ISettings.cs rename to Engine/SharpEngine.Core/Abstractions/ISettings.cs index e2ee475..23888d2 100644 --- a/Core/Interfaces/ISettings.cs +++ b/Engine/SharpEngine.Core/Abstractions/ISettings.cs @@ -1,4 +1,5 @@ using SharpEngine.Core.Renderers; +using Silk.NET.Input; using Silk.NET.Windowing; namespace SharpEngine.Core.Interfaces; @@ -20,6 +21,12 @@ public interface ISettings /// Gets or sets which renderer should be enabled for the current window. public RenderFlags RendererFlags { get; set; } + + /// Gets or sets the primary mouse button. + public MouseButton PrimaryButton { get; set; } + + /// Gets or sets the secondary mouse button. + public MouseButton SecondaryButton { get; set; } } /// @@ -33,4 +40,10 @@ public class DefaultSettings : ISettings /// public RenderFlags RendererFlags { get; set; } = RenderFlags.All; + + /// + public MouseButton PrimaryButton { get; set; } = MouseButton.Left; + + /// + public MouseButton SecondaryButton { get; set; } = MouseButton.Right; } diff --git a/Engine/SharpEngine.Core/Audio/Audio.cs b/Engine/SharpEngine.Core/Audio/Audio.cs new file mode 100644 index 0000000..7d9d171 --- /dev/null +++ b/Engine/SharpEngine.Core/Audio/Audio.cs @@ -0,0 +1,28 @@ +using System; +using System.IO; + +namespace SharpEngine.Core.Audio; + +/// +/// Handles Audio related operations. +/// +public static class Audio +{ + /// + /// Plays an audio file if it is in the WAV format. + /// + /// Specifies the location of the audio file to be played. + /// Thrown when the file format is not supported, such as when it is not a WAV file. + public static void Play(string filePath) + { + switch (Path.GetExtension(filePath)) + { + case ".wav": + new WavPlayer().Play(filePath); + break; + + default: + throw new NotSupportedException("Unsupported file format."); + } + } +} diff --git a/Engine/SharpEngine.Core/Audio/AudioBuffer.cs b/Engine/SharpEngine.Core/Audio/AudioBuffer.cs new file mode 100644 index 0000000..506f426 --- /dev/null +++ b/Engine/SharpEngine.Core/Audio/AudioBuffer.cs @@ -0,0 +1,43 @@ +using Silk.NET.OpenAL; +using System; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents an audio buffer with a unique identifier. +/// +public class AudioBuffer +{ + /// Gets the identifier to the audio buffer. + public uint BufferId => _buffer; + + private readonly uint _buffer; + private readonly AL _al; + + /// + /// Initializes a new instance of . + /// + /// The OpenAL API context. + public AudioBuffer(AL al) + { + _al = al; + _buffer = _al.GenBuffer(); + } + + /// + /// Loads the given into the buffer. + /// + /// The data to be loaded into the buffer. + /// Details about the data. + public void LoadData(ReadOnlySpan data, WavData wavData) + { + unsafe + { + fixed (byte* pData = data) + { + _al.BufferData(_buffer, wavData.Format, pData, data.Length, wavData.SampleRate); + } + } + } + +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Audio/AudioDevice.cs b/Engine/SharpEngine.Core/Audio/AudioDevice.cs new file mode 100644 index 0000000..aa0560d --- /dev/null +++ b/Engine/SharpEngine.Core/Audio/AudioDevice.cs @@ -0,0 +1,46 @@ +using Silk.NET.OpenAL; +using System; + +namespace SharpEngine.Core.Audio; + +/// +/// Manages an audio device and its context for audio operations. +/// +public unsafe class AudioDevice : IDisposable +{ + /// Gets the OpenAL context. + public AL Al { get; } + + private readonly ALContext _alc; + private readonly Device* _device; + private readonly Context* _context; + + /// + /// Initializes a new instance of . + /// + /// Thrown when unable to create the audio device. + public AudioDevice() + { + _alc = ALContext.GetApi(); + Al = AL.GetApi(); + _device = _alc.OpenDevice(""); + + if (_device == null) + throw new InvalidOperationException("Could not create device."); + + _context = _alc.CreateContext(_device, null); + _alc.MakeContextCurrent(_context); + Al.GetError(); + } + + /// + public void Dispose() + { + _alc.DestroyContext(_context); + _alc.CloseDevice(_device); + Al.Dispose(); + _alc.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/Engine/SharpEngine.Core/Audio/AudioFileExtensions.cs b/Engine/SharpEngine.Core/Audio/AudioFileExtensions.cs new file mode 100644 index 0000000..93160e2 --- /dev/null +++ b/Engine/SharpEngine.Core/Audio/AudioFileExtensions.cs @@ -0,0 +1,16 @@ +namespace SharpEngine.Core.Audio; + +/// +/// Contains all the supported audio file extensions. +/// +public static class AudioFileExtensions +{ + /// The file extension of the Waveform audio file. + public const string WavExtension = ".wav"; + + /// The file extension of the MPEG-1 Audio Layer III or MPEG-2 Audio Layer III (MP3) file. + public const string Mp3Extension = ".mp3"; + + /// The file extension of the MPEG-4 (MP4) file. + public const string Mp4Extension = ".mp4"; +} diff --git a/Engine/SharpEngine.Core/Audio/AudioPlayerBase.cs b/Engine/SharpEngine.Core/Audio/AudioPlayerBase.cs new file mode 100644 index 0000000..5778672 --- /dev/null +++ b/Engine/SharpEngine.Core/Audio/AudioPlayerBase.cs @@ -0,0 +1,67 @@ +using SharpEngine.Core.Scenes; + +using System; +using System.IO; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents a base class for all audio players. +/// +public abstract class AudioPlayerBase : SceneNode +{ + /// Gets the extension accepted by the audio player. + protected abstract string FileExtension { get; } + + /// Gets or sets the properties for the player. + public AudioProperties AudioProperties { get; set; } + + /// Contains the audio data of the media file. + protected readonly WavData Data; + + /// Represents a container for the . + protected readonly AudioBuffer AudioBuffer; + + /// The source of the audio. + protected readonly AudioSource AudioSource; + + /// + /// Initializes a new instance of . + /// + protected AudioPlayerBase() + { + Data = new WavData(); + + var audioDevice = new AudioDevice(); + AudioBuffer = new AudioBuffer(audioDevice.Al); + AudioSource = new AudioSource(audioDevice.Al); + + AudioProperties = new AudioProperties(audioDevice.Al, AudioSource); + } + + /// + /// Plays a media file specified by the given path. + /// + /// The location of the media file to be played. + public abstract void Play(string filePath); + + /// + /// Validates the specified file by checking its extension and existence. + /// + /// The path to the file that needs to be validated for its type and existence. + /// Thrown when the file does not have the allowed extension. + /// Thrown when the specified file does not exist in the file system. + protected virtual void ValidateFile(string filePath) + { + if (Path.GetExtension(filePath) != FileExtension) + throw new ArgumentException($"Given file is not a {FileExtension} file", nameof(filePath)); + + if (!File.Exists(filePath)) + throw new FileNotFoundException("Given file does not exist", filePath); + } + + /// + /// Stops the audio playback of the associated AudioSource. + /// + public virtual void Stop() => AudioSource.Stop(); +} diff --git a/Engine/SharpEngine.Core/Audio/AudioProperties.cs b/Engine/SharpEngine.Core/Audio/AudioProperties.cs new file mode 100644 index 0000000..8a57383 --- /dev/null +++ b/Engine/SharpEngine.Core/Audio/AudioProperties.cs @@ -0,0 +1,36 @@ +using Silk.NET.OpenAL; + +namespace SharpEngine.Core.Audio; + +/// +/// Contains properties used to control how the audio should be played. +/// +public class AudioProperties +{ + private readonly AL _al; + private readonly uint _audioSourceId; + + /// + /// Initializes a new instance of . + /// + /// The context where the audio should be played. + /// The source at which the audio should be played. + public AudioProperties(AL al, AudioSource audioSource) + { + _al = al; + _audioSourceId = audioSource.Get(); + } + + /// Gets or sets whether the audio should be looping. + public bool IsLooping + { + get => _isLooping; + set + { + _isLooping = value; + _al.SetSourceProperty(_audioSourceId, SourceBoolean.Looping, value); + } + } + + private bool _isLooping; +} diff --git a/Engine/SharpEngine.Core/Audio/AudioSource.cs b/Engine/SharpEngine.Core/Audio/AudioSource.cs new file mode 100644 index 0000000..9e59179 --- /dev/null +++ b/Engine/SharpEngine.Core/Audio/AudioSource.cs @@ -0,0 +1,47 @@ +using Silk.NET.OpenAL; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents a source for audio +/// +public class AudioSource +{ + private readonly uint _source; + private readonly AL _al; + + /// + /// Initializes a new instance of . + /// + /// An accessor to the OpenAL context. + public AudioSource(AL al) + { + _al = al; + _source = _al.GenSource(); + } + + /// + /// Gets the source identifier. + /// + /// The identifier to the source. + public uint Get() => _source; + + /// + /// Plays the given audio from the . + /// + /// A buffer containing the audio data. + public void Play(AudioBuffer buffer) + { + _al.SetSourceProperty(_source, SourceInteger.Buffer, buffer.BufferId); + _al.SourcePlay(_source); + } + + /// + /// Stops playing the audio. + /// + public void Stop() + { + _al.SourceStop(_source); + _al.DeleteSource(_source); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Audio/WavData.cs b/Engine/SharpEngine.Core/Audio/WavData.cs new file mode 100644 index 0000000..80ac937 --- /dev/null +++ b/Engine/SharpEngine.Core/Audio/WavData.cs @@ -0,0 +1,22 @@ +using Silk.NET.OpenAL; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents audio data in WAV format. +/// +public class WavData +{ + /// Gets or sets the number of channels used in the data. + public short NumChannels { get; set; } + + /// Gets or sets the sample rate of the data. + public int SampleRate { get; set; } + + /// Gets or sets the number of bits per sample. + public short BitsPerSample { get; set; } + + /// Gets or sets the format of the buffer. + /// Defines how data is organized in the buffer. + public BufferFormat Format { get; set; } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Audio/WavPlayer.cs b/Engine/SharpEngine.Core/Audio/WavPlayer.cs new file mode 100644 index 0000000..a686bea --- /dev/null +++ b/Engine/SharpEngine.Core/Audio/WavPlayer.cs @@ -0,0 +1,116 @@ +using SharpEngine.Shared; +using Silk.NET.OpenAL; + +using System; +using System.Buffers.Binary; +using System.IO; + +namespace SharpEngine.Core.Audio; + +/// +/// Represents a player for .wav files. +/// +public class WavPlayer : AudioPlayerBase +{ + /// + protected override string FileExtension => ".wav"; + + private static class WavConstants + { + public static ReadOnlySpan RiffHeader => "RIFF"u8; + public static ReadOnlySpan WaveHeader => "WAVE"u8; + public static ReadOnlySpan FmtHeader => "fmt "u8; + public static ReadOnlySpan DataHeader => "data"u8; + + public const int ChunkSizeLength = 4; + public const int HeaderLength = 4; + public const int FormatChunkSkipLength = 6; + public const int AudioFormatLength = 2; + public const int NumChannelsLength = 2; + public const int SampleRateLength = 4; + public const int BitsPerSampleLength = 2; + } + + /// + public override void Play(string filePath) + { + ValidateFile(filePath); + + ReadOnlySpan file = File.ReadAllBytes(filePath); + int index = 0; + + if (!CheckHeader(file, ref index, WavConstants.RiffHeader)) + { + Debug.Log.Warning("Given file is not in RIFF format"); + return; + } + + index += WavConstants.ChunkSizeLength; // Skip chunk size + + if (!CheckHeader(file, ref index, WavConstants.WaveHeader)) + { + Debug.Log.Warning("Given file is not in WAVE format"); + return; + } + + while (index + WavConstants.HeaderLength <= file.Length) + { + var identifier = file.Slice(index, WavConstants.HeaderLength); + index += WavConstants.HeaderLength; + + var size = BinaryPrimitives.ReadInt32LittleEndian(file.Slice(index, WavConstants.ChunkSizeLength)); + index += WavConstants.ChunkSizeLength; + + var data = file.Slice(index, size); + switch (identifier) + { + case var id when id.SequenceEqual(WavConstants.FmtHeader): + ParseFormat(data, Data); + break; + + case var id when id.SequenceEqual(WavConstants.DataHeader): + AudioBuffer.LoadData(data, Data); + break; + } + + index += size; + } + + AudioSource.Play(AudioBuffer); + } + + private static bool CheckHeader(ReadOnlySpan file, ref int index, ReadOnlySpan header) + { + if (!file.Slice(index, header.Length).SequenceEqual(header)) + return false; + + index += header.Length; + return true; + } + + private static void ParseFormat(ReadOnlySpan formatChunk, WavData wavData) + { + int index = 0; + var audioFormat = BinaryPrimitives.ReadInt16LittleEndian(formatChunk.Slice(index, WavConstants.AudioFormatLength)); + index += WavConstants.AudioFormatLength; + + if (audioFormat != 1) + throw new InvalidOperationException($"Unknown Audio Format with ID {audioFormat}"); + + wavData.NumChannels = BinaryPrimitives.ReadInt16LittleEndian(formatChunk.Slice(index, WavConstants.NumChannelsLength)); + index += WavConstants.NumChannelsLength; + + wavData.SampleRate = BinaryPrimitives.ReadInt32LittleEndian(formatChunk.Slice(index, WavConstants.SampleRateLength)); + index += WavConstants.SampleRateLength; + + index += WavConstants.FormatChunkSkipLength; // Skip byteRate and blockAlign + wavData.BitsPerSample = BinaryPrimitives.ReadInt16LittleEndian(formatChunk.Slice(index, WavConstants.BitsPerSampleLength)); + + wavData.Format = wavData.NumChannels switch + { + 1 => wavData.BitsPerSample == 8 ? BufferFormat.Mono8 : BufferFormat.Mono16, + 2 => wavData.BitsPerSample == 8 ? BufferFormat.Stereo8 : BufferFormat.Stereo16, + _ => throw new InvalidOperationException($"Can't play audio with {wavData.NumChannels} channels") + }; + } +} diff --git a/Engine/SharpEngine.Core/Entities/BezierCurve.cs b/Engine/SharpEngine.Core/Entities/BezierCurve.cs new file mode 100644 index 0000000..cd2547c --- /dev/null +++ b/Engine/SharpEngine.Core/Entities/BezierCurve.cs @@ -0,0 +1,65 @@ +using Silk.NET.Maths; +using System; +using System.Collections.Generic; + +namespace SharpEngine.Core.Entities; + +/// +/// Represents a cubic Bezier curve. +/// +public class BezierCurve +{ + private Vector3D P0, P1, P2, P3; + + /// Gets the start point of the curve. + public Vector3D Start => P0; + + /// Gets the end point of the curve. + + public Vector3D End => P3; + + /// + /// Initializes a new instance of . + /// + /// The point where the curve should be created. + public BezierCurve(Vector3D initializationPosition) + { + const float defaultWidth = 1; + + P0 = initializationPosition; + P1 = new Vector3D(initializationPosition.X + defaultWidth, initializationPosition.Y + defaultWidth, initializationPosition.Z); + P2 = new Vector3D(initializationPosition.X + defaultWidth, initializationPosition.Y - defaultWidth, initializationPosition.Z); + P3 = new Vector3D(initializationPosition.X + defaultWidth, initializationPosition.Y, initializationPosition.Z); + } + + /// + /// Gets the points of the curve. + /// + /// The distance between each point, defines the roughness of the curve. + /// The points for the curve with the given . + public List> GetPoints(uint resolution) + { + var points = new List>(); + for (var i = 0; i <= resolution; i++) + { + var t = i / (float)resolution; + points.Add(GetPointAt(t)); + } + + return points; + } + + private Vector3D GetPointAt(float t) + { + var x = GetCoordinateAt(t, P0.X, P1.X, P2.X, P3.X); + var y = GetCoordinateAt(t, P0.Y, P1.Y, P2.Y, P3.Y); + var z = GetCoordinateAt(t, P0.Z, P1.Z, P2.Z, P3.Z); + return new(x, y, z); + } + + private static float GetCoordinateAt(float t, float p0, float p1, float p2, float p3) + { + float u = 1 - t; + return MathF.Pow(u, 3) * p0 + 3 * MathF.Pow(u, 2) * t * p1 + 3 * u * MathF.Pow(t, 2) * p2 + MathF.Pow(t, 3) * p3; + } +} diff --git a/Engine/SharpEngine.Core/Entities/GameObject.cs b/Engine/SharpEngine.Core/Entities/GameObject.cs new file mode 100644 index 0000000..160896a --- /dev/null +++ b/Engine/SharpEngine.Core/Entities/GameObject.cs @@ -0,0 +1,114 @@ +using SharpEngine.Core.Attributes; +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Views; +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 Silk.NET.OpenGL; +using Tutorial; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Entities; + +/// +/// Represents a game object in the scene. +/// +public class GameObject : EmptyNode, IRenderable +{ + /// + /// Initializes a new instance of the . + /// + public GameObject() : base(string.Empty) + { + BoundingBox = BoundingBox.CalculateBoundingBox(Transform); + Shader = ShaderService.Instance.LoadShader(_Resources.Default.VertexShader, _Resources.Default.FragmentShader, "lighting"); + } + + public GameObject(Model_Old model) : base(string.Empty) + { + Model = model; + BoundingBox = BoundingBox.CalculateBoundingBox(Transform); + Shader = ShaderService.Instance.LoadShader(_Resources.Default.VertexShader, _Resources.Default.FragmentShader, "lighting"); + } + + /// + /// Initializes a new instance of the with specified textures and shaders. + /// + /// The file path of the diffuse map texture. + /// The file path of the specular map texture. + /// The file path of the vertex shader. + /// The file path of the fragment shader. + public GameObject(Shader shader, Model_Old model) : base(string.Empty) + { + Model = model; + BoundingBox = BoundingBox.CalculateBoundingBox(Transform); + Shader = shader; + } + + public Shader Shader { get; set; } + + /// + /// Gets or sets the mesh of the game object. + /// + public Model_Old Model { get; set; } + + /// + /// Gets or sets the transform of the game object. + /// + public override Transform Transform + { + get => _transform; + set + { + _transform = value; + BoundingBox = BoundingBox.CalculateBoundingBox(_transform); + } + } + + private Transform _transform = new(); + + /// + /// Gets the bounding box of the game object. + /// + [Inspector(DisplayInInspector = false)] + public BoundingBox BoundingBox { get; set; } + + protected virtual void SetShaderUniforms(CameraView camera) + { + Shader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); + + // TODO: One of these could be calculated once and "reloaded" if it changes. + Shader.SetMatrix4(ShaderAttributes.View, camera.GetViewMatrix(), true); + Shader.SetMatrix4(ShaderAttributes.Projection, camera.GetProjectionMatrix(), true); + } + + /// + public override Task Render(CameraView camera, Window window) + { + // TODO: This needs to removed later once fixed. + if (Model is null || Model.Meshes is null) + return Task.CompletedTask; + + foreach (var mesh in Model.Meshes) + { + mesh.Bind(); + + foreach (var texture in mesh.Textures) + texture.Use(); + + Shader.Use(); + SetShaderUniforms(camera); + + foreach (var material in mesh.Materials) + material.SetUniformValues(Shader); + + Window.GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Vertices.Length); + } + + return Task.CompletedTask; + } +} diff --git a/Core/Entities/Lights/DirectionalLight.cs b/Engine/SharpEngine.Core/Entities/Lights/DirectionalLight.cs similarity index 58% rename from Core/Entities/Lights/DirectionalLight.cs rename to Engine/SharpEngine.Core/Entities/Lights/DirectionalLight.cs index 4fcb63b..c7c0701 100644 --- a/Core/Entities/Lights/DirectionalLight.cs +++ b/Engine/SharpEngine.Core/Entities/Lights/DirectionalLight.cs @@ -1,6 +1,9 @@ -using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core._Resources; +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Extensions; using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; using System.Numerics; using System.Threading.Tasks; @@ -24,16 +27,16 @@ public DirectionalLight() Diffuse = new Vector3(0.4f, 0.4f, 0.4f); Specular = new Vector3(0.5f, 0.5f, 0.5f); - Material.Shader = ShaderService.Instance.LoadShader(PathExtensions.GetAssemblyPath("Shaders/shader.vert"), PathExtensions.GetAssemblyPath("Shaders/lighting.frag"), "lighting"); + Shader = ShaderService.Instance.LoadShader(Default.VertexShader, Default.FragmentShader, "lighting"); } /// - public override Task Render() + public Task Render(CameraView camera, Window window) { - Material.Shader.SetVector3("dirLight.direction", Direction); - Material.Shader.SetVector3("dirLight.ambient", Ambient); - Material.Shader.SetVector3("dirLight.diffuse", Diffuse); - Material.Shader.SetVector3("dirLight.specular", Specular); + Shader.SetVector3("dirLight.direction", Direction); + Shader.SetVector3("dirLight.ambient", Ambient); + Shader.SetVector3("dirLight.diffuse", Diffuse); + Shader.SetVector3("dirLight.specular", Specular); return Task.CompletedTask; } diff --git a/Core/Entities/Lights/Light.cs b/Engine/SharpEngine.Core/Entities/Lights/Light.cs similarity index 100% rename from Core/Entities/Lights/Light.cs rename to Engine/SharpEngine.Core/Entities/Lights/Light.cs diff --git a/Core/Entities/Lights/PointLight.cs b/Engine/SharpEngine.Core/Entities/Lights/PointLight.cs similarity index 54% rename from Core/Entities/Lights/PointLight.cs rename to Engine/SharpEngine.Core/Entities/Lights/PointLight.cs index 858dbf8..0522f97 100644 --- a/Core/Entities/Lights/PointLight.cs +++ b/Engine/SharpEngine.Core/Entities/Lights/PointLight.cs @@ -1,5 +1,8 @@ -using SharpEngine.Core.Extensions; +using SharpEngine.Core._Resources; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Extensions; using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; using System.Numerics; using System.Threading.Tasks; @@ -18,14 +21,13 @@ public class PointLight : Light public PointLight(Vector3 position, int index) { Transform.Scale = new(0.2f, 0.2f, 0.2f); - Transform.Position = position; + Transform.Position = (Numerics.Vector3)position; Constant = 1.0f; Linear = 0.09f; Quadratic = 0.032f; _index = index; - Material.Shader = ShaderService.Instance.LoadShader(PathExtensions.GetAssemblyPath("Shaders/shader.vert"), PathExtensions.GetAssemblyPath("Shaders/lighting.frag"), "lighting"); LampShader = new LampShader(); } @@ -49,21 +51,23 @@ public PointLight(Vector3 position, int index) /// public float Quadratic { get; set; } - /// - public override Task Render() + protected override void SetShaderUniforms(CameraView camera) { - Material.Shader.SetVector3($"pointLights[{_index}].position", Transform.Position); - Material.Shader.SetVector3($"pointLights[{_index}].ambient", Ambient); - Material.Shader.SetVector3($"pointLights[{_index}].diffuse", Diffuse); - Material.Shader.SetVector3($"pointLights[{_index}].specular", Specular); - Material.Shader.SetFloat($"pointLights[{_index}].constant", Constant); - Material.Shader.SetFloat($"pointLights[{_index}].linear", Linear); - Material.Shader.SetFloat($"pointLights[{_index}].quadratic", Quadratic); + Shader.SetVector3($"pointLights[{_index}].position", (Vector3)Transform.Position); + Shader.SetVector3($"pointLights[{_index}].ambient", Ambient); + Shader.SetVector3($"pointLights[{_index}].diffuse", Diffuse); + Shader.SetVector3($"pointLights[{_index}].specular", Specular); + Shader.SetFloat($"pointLights[{_index}].constant", Constant); + Shader.SetFloat($"pointLights[{_index}].linear", Linear); + Shader.SetFloat($"pointLights[{_index}].quadratic", Quadratic); - var lampMatrix = Matrix4x4.CreateScale(Transform.Scale); - lampMatrix *= Matrix4x4.CreateTranslation(Transform.Position); + base.SetShaderUniforms(camera); + } - LampShader.Shader?.SetMatrix4(ShaderAttributes.Model, lampMatrix); + /// + public Task Render(CameraView camera, Window window) + { + SetShaderUniforms(camera); return Task.CompletedTask; } diff --git a/Core/Entities/Lights/SpotLight.cs b/Engine/SharpEngine.Core/Entities/Lights/SpotLight.cs similarity index 57% rename from Core/Entities/Lights/SpotLight.cs rename to Engine/SharpEngine.Core/Entities/Lights/SpotLight.cs index 502952d..59ad0fe 100644 --- a/Core/Entities/Lights/SpotLight.cs +++ b/Engine/SharpEngine.Core/Entities/Lights/SpotLight.cs @@ -1,5 +1,8 @@ -using SharpEngine.Core.Extensions; +using SharpEngine.Core._Resources; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Extensions; using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; using System.Numerics; using System.Threading.Tasks; @@ -11,7 +14,7 @@ namespace SharpEngine.Core.Entities.Lights; public class SpotLight : Light { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of . /// public SpotLight() { @@ -22,7 +25,7 @@ public SpotLight() Linear = 0.09f; Quadratic = 0.032f; - Material.Shader = ShaderService.Instance.LoadShader(PathExtensions.GetAssemblyPath("Shaders/shader.vert"), PathExtensions.GetAssemblyPath("Shaders/lighting.frag"), "lighting"); + Shader = ShaderService.Instance.LoadShader(Default.VertexShader, Default.FragmentShader, "lighting"); } /// @@ -55,20 +58,27 @@ public SpotLight() /// public float Quadratic { get; set; } - /// - public override Task Render() + protected override void SetShaderUniforms(CameraView camera) { - Material.Shader.SetVector3("spotLight.position", Transform.Position); - Material.Shader.SetVector3("spotLight.direction", Direction); - Material.Shader.SetVector3("spotLight.ambient", Ambient); - Material.Shader.SetVector3("spotLight.diffuse", Diffuse); - Material.Shader.SetVector3("spotLight.specular", Specular); - Material.Shader.SetFloat("spotLight.constant", Constant); - Material.Shader.SetFloat("spotLight.linear", Linear); - Material.Shader.SetFloat("spotLight.quadratic", Quadratic); - Material.Shader.SetFloat("spotLight.cutOff", CutOff); - Material.Shader.SetFloat("spotLight.outerCutOff", OuterCutOff); + Shader.SetVector3("spotLight.position", (Vector3)Transform.Position); + Shader.SetVector3("spotLight.direction", Direction); + Shader.SetVector3("spotLight.ambient", Ambient); + Shader.SetVector3("spotLight.diffuse", Diffuse); + Shader.SetVector3("spotLight.specular", Specular); + Shader.SetFloat("spotLight.constant", Constant); + Shader.SetFloat("spotLight.linear", Linear); + Shader.SetFloat("spotLight.quadratic", Quadratic); + Shader.SetFloat("spotLight.cutOff", CutOff); + Shader.SetFloat("spotLight.outerCutOff", OuterCutOff); + + base.SetShaderUniforms(camera); + } + /// + public override Task Render(CameraView camera, Window window) + { + SetShaderUniforms(camera); + return Task.CompletedTask; } } diff --git a/Core/Entities/Properties/Meshes/MeshService.cs b/Engine/SharpEngine.Core/Entities/MeshService.cs similarity index 69% rename from Core/Entities/Properties/Meshes/MeshService.cs rename to Engine/SharpEngine.Core/Entities/MeshService.cs index 67e5300..99a60a2 100644 --- a/Core/Entities/Properties/Meshes/MeshService.cs +++ b/Engine/SharpEngine.Core/Entities/MeshService.cs @@ -1,5 +1,4 @@ -using Silk.NET.OpenGL; -using System.Collections.Generic; +using System.Collections.Generic; namespace SharpEngine.Core.Entities.Properties.Meshes; @@ -26,15 +25,7 @@ public Mesh LoadMesh(string identifier, Mesh mesh) if (Meshes.TryGetValue(identifier, out var cachedMesh)) return cachedMesh; - var meshData = mesh.GetVertices(); - - var vertexBufferObject = Window.GL.GenBuffer(); - Window.GL.BindBuffer(GLEnum.ArrayBuffer, vertexBufferObject); - Window.GL.BufferData(GLEnum.ArrayBuffer, (uint)meshData.Length * sizeof(float), meshData, GLEnum.StaticDraw); - Meshes.Add(identifier, mesh); return mesh; } - - // TODO: #3 & #2 Load mesh from file } \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs b/Engine/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs new file mode 100644 index 0000000..b0dded0 --- /dev/null +++ b/Engine/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs @@ -0,0 +1,24 @@ +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Scenes; +using System; +using System.Numerics; + +namespace SharpEngine.Core.Entities.UI.Layouts; + +/// +/// Represents a layout that dynamically distributes items in a flex layout. +/// +/// The type of items in the collection. +/// The amount of dimensions in the content area. +public class FlexLayout : LayoutBase where T : SceneNode, new() where TVector : IVector, new() +{ + /// Gets or sets the gap between each object. + public int Gap { get; set; } + + /// Gets or sets the dimensions of the content area. + public required IVector ContentAreaSize { get; set; } + + /// + /// + public override T[][] GetValues() => throw new NotImplementedException(); +} diff --git a/Engine/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs b/Engine/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs new file mode 100644 index 0000000..452cc3e --- /dev/null +++ b/Engine/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs @@ -0,0 +1,88 @@ +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Scenes; +using System; + +namespace SharpEngine.Core.Entities.UI.Layouts; + +/// +/// Represents a grid layout. +/// +/// The type of items that can be stored and retrieved within the grid. +public class GridLayout : LayoutBase where TItem : SceneNode, new() +{ + /// Gets or sets the amount of rows in the grid. + public uint Rows { get; set; } = 2; + + /// Gets or sets the amount of columns in the grid. + public uint Columns { get; set; } = 3; + + /// + /// Retrieves the item at [, ]. + /// + /// The row where the items should be retrieved. + /// The column where the item should be retrieved. + /// The item at [, ]. + public TItem this[uint row, uint column] + { + get + { + uint index = GetIndex(row, column); + return Items[(int)index]; + } + set + { + uint index = GetIndex(row, column); + Items[(int)index] = value; + } + } + + /// + public override void AddItem(TItem item) + { + base.AddItem(item); + + var offset = -100; // TODO: #26 Fix this, this is just a temporary solution. + + if (Items.Count % Columns != 0) + offset = offset + (Items.Count * 30); + + // item.Transform.Position.X = offset; + } + + /// + /// Calculates the index in the flat list based on row and column. + /// + /// The row where the items should be retrieved. + /// The column where the item should be retrieved. + /// The index of the item at [, ]. + /// + private uint GetIndex(uint row, uint column) + { + if (row < 0 || column < 0 || column >= Columns) + throw new ArgumentOutOfRangeException(nameof(row), "Invalid row or column index."); + + var index = row * Columns + column; + if (index >= Items.Count) + throw new ArgumentOutOfRangeException(nameof(row), "Index exceeds the number of items."); + + return index; + } + + /// + public override TItem[][] GetValues() + { + var values = new TItem[Rows][]; + for (uint i = 0; i < Rows; i++) + { + values[i] = new TItem[Columns]; + for (uint j = 0; j < Columns; j++) + { + var item = this[i, j]; + values[i][j] = item; + } + } + + return values; + } +} diff --git a/Engine/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs b/Engine/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs new file mode 100644 index 0000000..bb65554 --- /dev/null +++ b/Engine/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs @@ -0,0 +1,44 @@ +using SharpEngine.Core.Scenes; +using System.Collections.Generic; + +namespace SharpEngine.Core.Entities.UI.Layouts; + +/// +/// Represents the base class for all layout types. +/// +/// +public abstract class LayoutBase : SceneNode where TItem : SceneNode, new() +{ + /// Represents the items in the layout. + public List Items { get; set; } = []; + + /// + /// Adds a new item to the layout. + /// + /// The items to be added. + /// The object itself. + public override LayoutBase AddChild(params SceneNode[] nodes) + { + base.AddChild(nodes); + + foreach (var node in nodes) + AddItem((TItem)node); + + return this; + } + + /// + /// Adds and item to the container. + /// + /// The item to be added. + public virtual void AddItem(TItem item) + { + Items.Add(item); + } + + /// + /// Gets the items in the container. + /// + /// The items as a 2D array representation. + public abstract TItem[][] GetValues(); +} diff --git a/Engine/SharpEngine.Core/Entities/UI/UIElement.cs b/Engine/SharpEngine.Core/Entities/UI/UIElement.cs new file mode 100644 index 0000000..4230410 --- /dev/null +++ b/Engine/SharpEngine.Core/Entities/UI/UIElement.cs @@ -0,0 +1,108 @@ +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; + +using Silk.NET.OpenGL; +using System.Numerics; +using System.Threading.Tasks; +using Vector2 = SharpEngine.Core.Numerics.Vector2; + +namespace SharpEngine.Core.Entities.UI; + +/// +/// Represents a User Interface entity. +/// +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); + + Initialize(); + } + + private readonly UIShader _uiShader = new(); + + /// Gets or sets the width of the ui element. + public float Width { get; set; } = 10; + + /// Gets or sets the height of the ui element. + public float Height { get; set; } = 10; + + /// Gets or sets the mesh of the UI element. + public Mesh Mesh { get; set; } + + /// + public uint VAO { get; set; } + + /// + public void Initialize() + { + VAO = Window.GL.GenVertexArray(); + Bind(); + + InitializeBuffers(Mesh); + + _uiShader.Shader?.Use(); + _uiShader.SetAttributes(); + } + + /// + public void Bind() + { + Window.GL.BindVertexArray(VAO); + } + + /// + public void InitializeBuffers(Mesh mesh, bool useMeshVertices = false) + { + var vertexBufferObject = Window.GL.GenBuffer(); + Window.GL.BindBuffer(GLEnum.ArrayBuffer, vertexBufferObject); + Window.GL.BufferData(GLEnum.ArrayBuffer, mesh.GetVertices(), GLEnum.StaticDraw); + + var elementBufferObject = Window.GL.GenBuffer(); + Window.GL.BindBuffer(GLEnum.ElementArrayBuffer, elementBufferObject); + Window.GL.BufferData(GLEnum.ElementArrayBuffer, mesh.Indices, GLEnum.StaticDraw); + } + + Matrix4x4 OrthoMatrix = Matrix4x4.CreateOrthographicOffCenter(-1, 1, -1, 1, -1, 1); + + /// + /// Render the UI element. + /// + public override Task Render(CameraView camera, Window window) + { + _uiShader.Shader.Use(); + Bind(); + + // 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.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); + _uiShader.Shader.SetMatrix4("orthoMatrix", OrthoMatrix); // Pass the orthographic matrix to the shader + + Window.GL.DrawElements(PrimitiveType.Triangles, (uint)Mesh.Indices.Length, DrawElementsType.UnsignedInt, []); + + return Task.CompletedTask; + } +} diff --git a/Core/Entities/Views/CameraView.cs b/Engine/SharpEngine.Core/Entities/Views/CameraView.cs similarity index 91% rename from Core/Entities/Views/CameraView.cs rename to Engine/SharpEngine.Core/Entities/Views/CameraView.cs index f3eb785..d98a3ff 100644 --- a/Core/Entities/Views/CameraView.cs +++ b/Engine/SharpEngine.Core/Entities/Views/CameraView.cs @@ -1,3 +1,4 @@ +using SharpEngine.Core.Components.Properties; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Shaders; using System.Numerics; @@ -24,20 +25,6 @@ public CameraView(Vector3 position, IViewSettings settings) : base(settings) AspectRatio = settings.WindowOptions.Size.X / (float)settings.WindowOptions.Size.Y; } - // Those vectors are directions pointing outwards from the camera to define how it rotated. - private Vector3 _front = -Vector3.UnitZ; - private Vector3 _up = Vector3.UnitY; - private Vector3 _right = Vector3.UnitX; - - /// Gets the front vector of the camera. - public Vector3 Front => _front; - - /// Gets the up vector of the camera. - public Vector3 Up => _up; - - /// Gets the right vector of the camera. - public Vector3 Right => _right; - // Rotation around the X axis (radians) private float _pitch; @@ -147,7 +134,7 @@ public override void UpdateMousePosition(Vector2 mousePosition) lastPos = new Vector2(mousePosition.X, mousePosition.Y); firstMove = false; } - else // TODO: Split so that only the camera view will rotate + else // TODO: #48 Split so that only the camera view will rotate { var deltaX = mousePosition.X - lastPos.X; var deltaY = mousePosition.Y - lastPos.Y; @@ -222,8 +209,8 @@ public Plane[] GetFrustumPlanes() /// The shader to set the uniforms on. public override void SetShaderUniforms(Shader shader) { - shader.SetMatrix4("view", GetViewMatrix()); - shader.SetMatrix4("projection", GetProjectionMatrix()); + shader.SetMatrix4(ShaderAttributes.View, GetViewMatrix()); + shader.SetMatrix4(ShaderAttributes.Projection, GetProjectionMatrix()); shader.SetVector3("viewPos", Position); } } \ No newline at end of file diff --git a/Core/Entities/Views/Settings/DefaultViewSettings.cs b/Engine/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs similarity index 77% rename from Core/Entities/Views/Settings/DefaultViewSettings.cs rename to Engine/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs index 5a777e1..cef7852 100644 --- a/Core/Entities/Views/Settings/DefaultViewSettings.cs +++ b/Engine/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs @@ -1,5 +1,5 @@ using SharpEngine.Core.Renderers; - +using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.Windowing; @@ -19,7 +19,7 @@ public DefaultViewSettings() } /// - /// Convenience wrapper around creating a new WindowProperties with sensible defaults. + /// Convenience wrapper around creating a new WindowProperties with sensible defaults. /// public static ViewSettings Default { get; private set; } @@ -41,4 +41,10 @@ public DefaultViewSettings() Title = "SharpEngine", Size = new Vector2D(1280, 720), }; + + /// + public MouseButton PrimaryButton { get; set; } = MouseButton.Left; + + /// + public MouseButton SecondaryButton { get; set; } = MouseButton.Right; } diff --git a/Core/Entities/Views/Settings/IViewSettings.cs b/Engine/SharpEngine.Core/Entities/Views/Settings/IViewSettings.cs similarity index 100% rename from Core/Entities/Views/Settings/IViewSettings.cs rename to Engine/SharpEngine.Core/Entities/Views/Settings/IViewSettings.cs diff --git a/Core/Entities/Views/Settings/ViewSettings.cs b/Engine/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs similarity index 82% rename from Core/Entities/Views/Settings/ViewSettings.cs rename to Engine/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs index a28ffe3..09c65cb 100644 --- a/Core/Entities/Views/Settings/ViewSettings.cs +++ b/Engine/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs @@ -1,6 +1,5 @@ -using SharpEngine.Core.Interfaces; -using SharpEngine.Core.Renderers; -using Silk.NET.Maths; +using SharpEngine.Core.Renderers; +using Silk.NET.Input; using Silk.NET.Windowing; namespace SharpEngine.Core.Entities.Views.Settings; @@ -47,4 +46,10 @@ public ViewSettings() /// public WindowOptions WindowOptions { get; set; } + + /// + public MouseButton PrimaryButton { get; set; } = MouseButton.Left; + + /// + public MouseButton SecondaryButton { get; set; } = MouseButton.Right; } diff --git a/Core/Entities/Views/View.cs b/Engine/SharpEngine.Core/Entities/Views/View.cs similarity index 55% rename from Core/Entities/Views/View.cs rename to Engine/SharpEngine.Core/Entities/Views/View.cs index a6f5bf6..02aecba 100644 --- a/Core/Entities/Views/View.cs +++ b/Engine/SharpEngine.Core/Entities/Views/View.cs @@ -1,5 +1,9 @@ -using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; +using System; +using System.Collections.Generic; using System.Numerics; namespace SharpEngine.Core.Entities.Views; @@ -18,15 +22,28 @@ public View(IViewSettings settings) Settings = settings; } + protected Vector3 _front = -Vector3.UnitZ; + protected Vector3 _up = Vector3.UnitY; + protected Vector3 _right = Vector3.UnitX; + + /// Gets the front vector of the camera. + public Vector3 Front => _front; + + /// Gets the up vector of the camera. + public Vector3 Up => _up; + + /// Gets the right vector of the camera. + public Vector3 Right => _right; + + /// Gets or sets the position of the camera. + public Vector3 Position { get; set; } + /// Gets or sets the settings for the view. public IViewSettings Settings { get; set; } /// Gets or sets the aspect ratio of the viewport, used for the projection matrix. public float AspectRatio { get; set; } - /// Gets or sets the position of the camera. - public Vector3 Position { get; set; } - private protected bool firstMove; private protected Vector2 lastPos; @@ -65,4 +82,18 @@ public virtual void UpdateMousePosition(Vector2 mousePosition) firstMove = false; } } + + /// + /// Checks whether a game object is in view of the camera. + /// + /// + /// Uses a step based ray marching algorithm to check if any object is in view of the camera. + /// + /// The scene where the check should be applied to. + /// The types that are allowed to be hit. + /// The first object that the ray intersected. + /// The position where the object was hit. + /// if any object was hit; otherwise, . + public bool IsInView(Scene scene, out GameObject? intersectingObject, out Vector3 hitPosition, params Type[] allowedTypes) + => new Ray(Position, Front).IsGameObjectInView(scene, out intersectingObject, out hitPosition, allowedTypes); } \ No newline at end of file diff --git a/Core/Enums/MouseWheelScrollDirection.cs b/Engine/SharpEngine.Core/Enums/MouseWheelScrollDirection.cs similarity index 100% rename from Core/Enums/MouseWheelScrollDirection.cs rename to Engine/SharpEngine.Core/Enums/MouseWheelScrollDirection.cs diff --git a/Core/Extensions/CollectionExtensions.cs b/Engine/SharpEngine.Core/Extensions/CollectionExtensions.cs similarity index 97% rename from Core/Extensions/CollectionExtensions.cs rename to Engine/SharpEngine.Core/Extensions/CollectionExtensions.cs index aab5eb9..fa8686d 100644 --- a/Core/Extensions/CollectionExtensions.cs +++ b/Engine/SharpEngine.Core/Extensions/CollectionExtensions.cs @@ -13,7 +13,7 @@ public static class CollectionExtensions /// /// The type of the item. /// The item which is being checked if it's in the collection. - /// The collection where the item's existance is being checked. + /// The collection where the item's existence is being checked. /// if the item is in the collection; otherwise, . public static bool IsAnyOf(this T item, params T[] items) => items.Contains(item); diff --git a/Core/Extensions/PathExtensions.cs b/Engine/SharpEngine.Core/Extensions/PathExtensions.cs similarity index 100% rename from Core/Extensions/PathExtensions.cs rename to Engine/SharpEngine.Core/Extensions/PathExtensions.cs diff --git a/Engine/SharpEngine.Core/Primitives/Cube.cs b/Engine/SharpEngine.Core/Primitives/Cube.cs new file mode 100644 index 0000000..16be872 --- /dev/null +++ b/Engine/SharpEngine.Core/Primitives/Cube.cs @@ -0,0 +1,203 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Textures; +using SharpEngine.Core.Windowing; +using Tutorial; + +namespace SharpEngine.Core.Primitives; + +/// +/// Used to create a primitive cube object. +/// +public static class Cube +{ + static Cube() + { + if (_loaded) + return; + + var defaultTexture = TextureService.Instance.LoadTexture(Default.DebugTexture); + + var mesh = new Mesh(Window.GL) + { + Vertices = [.. Vertices], + Normals = [.. Normals], + TextureCoordinates = [.. TextureCoordinates], + Indices = [.. Indices], + Textures = [defaultTexture], + // Materials = [MaterialService.Instance.LoadMaterial(Default.DebugMaterial)], + Materials = [new(defaultTexture)] + }; + + Mesh = MeshService.Instance.LoadMesh(nameof(Cube), mesh); + Model = new(Window.GL, Mesh); + + _loaded = true; + } + + private static bool _loaded; + + public static Model_Old Model; + + /// The cube mesh. + public static readonly Mesh Mesh; + + public static float[] Vertices = + [ + -0.5f, -0.5f, -0.5f, + 0.5f, -0.5f, -0.5f, + 0.5f, 0.5f, -0.5f, + 0.5f, 0.5f, -0.5f, + -0.5f, 0.5f, -0.5f, + -0.5f, -0.5f, -0.5f, + + -0.5f, -0.5f, 0.5f, + 0.5f, -0.5f, 0.5f, + 0.5f, 0.5f, 0.5f, + 0.5f, 0.5f, 0.5f, + -0.5f, 0.5f, 0.5f, + -0.5f, -0.5f, 0.5f, + + -0.5f, 0.5f, 0.5f, + -0.5f, 0.5f, -0.5f, + -0.5f, -0.5f, -0.5f, + -0.5f, -0.5f, -0.5f, + -0.5f, -0.5f, 0.5f, + -0.5f, 0.5f, 0.5f, + + 0.5f, 0.5f, 0.5f, + 0.5f, 0.5f, -0.5f, + 0.5f, -0.5f, -0.5f, + 0.5f, -0.5f, -0.5f, + 0.5f, -0.5f, 0.5f, + 0.5f, 0.5f, 0.5f, + + -0.5f, -0.5f, -0.5f, + 0.5f, -0.5f, -0.5f, + 0.5f, -0.5f, 0.5f, + 0.5f, -0.5f, 0.5f, + -0.5f, -0.5f, 0.5f, + -0.5f, -0.5f, -0.5f, + + -0.5f, 0.5f, -0.5f, + 0.5f, 0.5f, -0.5f, + 0.5f, 0.5f, 0.5f, + 0.5f, 0.5f, 0.5f, + -0.5f, 0.5f, 0.5f, + -0.5f, 0.5f, -0.5f, + ]; + public static float[] 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, + 0.0f, 0.0f, -1.0f, + 0.0f, 0.0f, -1.0f, + + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + 0.0f, 0.0f, 1.0f, + + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + -1.0f, 0.0f, 0.0f, + + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + 1.0f, 0.0f, 0.0f, + + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + 0.0f, -1.0f, 0.0f, + + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + ]; + public static float[] TextureCoordinates = + [ + 0.0f, 0.0f, + 1.0f, 0.0f, + 1.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 1.0f, + 0.0f, 0.0f, + + 0.0f, 0.0f, + 1.0f, 0.0f, + 1.0f, 1.0f, + 1.0f, 1.0f, + 0.0f, 1.0f, + 0.0f, 0.0f, + + 1.0f, 0.0f, + 1.0f, 1.0f, + 0.0f, 1.0f, + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f, + + 1.0f, 0.0f, + 1.0f, 1.0f, + 0.0f, 1.0f, + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f, + + 0.0f, 1.0f, + 1.0f, 1.0f, + 1.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 0.0f, + 0.0f, 1.0f, + + 0.0f, 1.0f, + 1.0f, 1.0f, + 1.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 0.0f, + 0.0f, 1.0f + ]; + public static uint[] Indices = + [ + // Front face + 0, 1, 2, + 2, 3, 0, + + // Back face + 4, 5, 6, + 6, 7, 4, + + // Left face + 4, 0, 3, + 3, 7, 4, + + // Right face + 1, 5, 6, + 6, 2, 1, + + // Top face + 3, 2, 6, + 6, 7, 3, + + // Bottom face + 4, 5, 1, + 1, 0, 4 + ]; +} diff --git a/Core/Primitives/Plane.cs b/Engine/SharpEngine.Core/Primitives/Plane.cs similarity index 52% rename from Core/Primitives/Plane.cs rename to Engine/SharpEngine.Core/Primitives/Plane.cs index 1468423..ba046a5 100644 --- a/Core/Primitives/Plane.cs +++ b/Engine/SharpEngine.Core/Primitives/Plane.cs @@ -1,4 +1,5 @@ using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Windowing; namespace SharpEngine.Core.Primitives; @@ -8,28 +9,33 @@ namespace SharpEngine.Core.Primitives; public static class Plane { /// The plane mesh. - public static Mesh Mesh { get; } = new() + public static Mesh Mesh { get; } = new(Window.GL) { Vertices = [ - 0.5f, 0.5f, 0.0f, // top right - 0.5f, -0.5f, 0.0f, // bottom right - -0.5f, -0.5f, 0.0f, // bottom left - -0.5f, 0.5f, 0.0f, // top left + 1f, 1f, 0.0f, // top right + 1f, -1f, 0.0f, // bottom right + -1f, -1f, 0.0f, // bottom left + -1f, 1f, 0.0f, // top left ], Normals = - [ - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - ], - TextureCoordinates = [ 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 ] }; } diff --git a/Core/Primitives/PrimitiveFactory.cs b/Engine/SharpEngine.Core/Primitives/PrimitiveFactory.cs similarity index 63% rename from Core/Primitives/PrimitiveFactory.cs rename to Engine/SharpEngine.Core/Primitives/PrimitiveFactory.cs index 38ec50e..633580d 100644 --- a/Core/Primitives/PrimitiveFactory.cs +++ b/Engine/SharpEngine.Core/Primitives/PrimitiveFactory.cs @@ -1,8 +1,11 @@ using SharpEngine.Core.Entities; using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Extensions; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Shaders; using System; +using System.Collections.Generic; using System.Numerics; +using Tutorial; namespace SharpEngine.Core.Primitives; @@ -11,17 +14,13 @@ namespace SharpEngine.Core.Primitives; /// public static class PrimitiveFactory { - private static string DebugTexture => PathExtensions.GetAssemblyPath("Textures/DefaultTextures/debug.JPG"); - private static string DefaultVertexShader => PathExtensions.GetAssemblyPath("Shaders/shader.vert"); - private static string DefaultFragmentShader => PathExtensions.GetAssemblyPath("Shaders/lighting.frag"); - /// /// Creates a new by the given primitive type. /// /// The type of primitive to create. /// Where the game object should be placed. public static GameObject Create(PrimitiveType primitiveType, Vector3 position) - => Create(primitiveType, position, DebugTexture); + => Create(primitiveType, position, _Resources.Default.DebugTexture); /// /// The type of primitive to be created. @@ -33,15 +32,21 @@ public static GameObject Create(PrimitiveType primitiveType, Vector3 position) /// A new game object. /// Thrown when the specified primitive type does not exist. public static GameObject Create(PrimitiveType primitiveType, Vector3 position, string diffuseMapFile, string? specularMapFile = null, string? vertShaderFile = null, string? fragShaderFile = null) - => new(diffuseMapFile, specularMapFile ?? DebugTexture, vertShaderFile ?? DefaultVertexShader, fragShaderFile ?? DefaultFragmentShader) + { + // var material = diffuseMapFile, specularMapFile, + + Model_Old model = primitiveType switch + { + PrimitiveType.Cube => Cube.Model, + // PrimitiveType.Plane => [Plane.Mesh], + _ => throw new InvalidOperationException($"A primitive of type {primitiveType} does not exist.") + }; + + var shader = ShaderService.Instance.LoadShader(vertShaderFile ?? _Resources.Default.VertexShader, fragShaderFile ?? _Resources.Default.FragmentShader, "lighting"); + return new GameObject(shader, model) { - Transform = new Transform { Position = position }, - Mesh = primitiveType switch - { - PrimitiveType.Cube => [Cube.Mesh], - PrimitiveType.Plane => [Plane.Mesh], - _ => throw new InvalidOperationException($"A primitive of type {primitiveType} does not exist.") - } + Transform = new Transform((Numerics.Vector3)position), }; + } } diff --git a/Core/Primitives/PrimitiveType.cs b/Engine/SharpEngine.Core/Primitives/PrimitiveType.cs similarity index 100% rename from Core/Primitives/PrimitiveType.cs rename to Engine/SharpEngine.Core/Primitives/PrimitiveType.cs diff --git a/Core/Ray.cs b/Engine/SharpEngine.Core/Ray.cs similarity index 89% rename from Core/Ray.cs rename to Engine/SharpEngine.Core/Ray.cs index 68f1dee..942346b 100644 --- a/Core/Ray.cs +++ b/Engine/SharpEngine.Core/Ray.cs @@ -13,14 +13,10 @@ namespace SharpEngine.Core; /// public class Ray { - /// - /// Gets or sets the ray origin. - /// + /// Gets or sets the ray origin. public Vector3 Origin { get; set; } - /// - /// Gets or sets the ray direction. - /// + /// Gets or sets the ray direction. public Vector3 Direction { get; set; } /// @@ -35,7 +31,7 @@ public Ray(Vector3 origin, Vector3 direction) } /// - /// Checks if the ray intersects with any objects in the scene. + /// Checks if the ray intersects with any objects in the scene. /// /// The scene to check for intersections. /// The intersecting object; if the ray did not intersect an object. @@ -47,9 +43,9 @@ public bool IsGameObjectInView(Scene scene, out GameObject? intersectingObject, const float maxDistance = 100.0f; // Maximum distance to check for intersections const float stepSize = 0.1f; // Step size for ray marching - var sceneNodes = scene.GetAllGameObjects(); + var sceneNodes = scene.GetObjectsOfType(); - if (allowedTypes.Any()) + if (allowedTypes.Length != 0) sceneNodes = [.. sceneNodes.Where(go => allowedTypes.Any(type => type.IsInstanceOfType(go)))]; for (float t = 0; t < maxDistance; t += stepSize) @@ -70,7 +66,7 @@ public bool IsGameObjectInView(Scene scene, out GameObject? intersectingObject, } /// - /// Determines if a point is inside a given object. + /// Determines if a point is inside a given object. /// /// The point to check. /// The object to check against. @@ -86,7 +82,7 @@ public static bool IsPointInsideObject(Vector3 point, GameObject obj) } /// - /// Gets the normal of the closest face of an object to a given point. + /// Gets the normal of the closest face of an object to a given point. /// /// The point to check. /// The object to check against. diff --git a/Core/Renderers/RenderFlags.cs b/Engine/SharpEngine.Core/Renderers/RenderFlags.cs similarity index 63% rename from Core/Renderers/RenderFlags.cs rename to Engine/SharpEngine.Core/Renderers/RenderFlags.cs index 68d5137..005999b 100644 --- a/Core/Renderers/RenderFlags.cs +++ b/Engine/SharpEngine.Core/Renderers/RenderFlags.cs @@ -14,13 +14,12 @@ public enum RenderFlags /// All the vertices will for the mesh will be rendered. Renderer3D = 1, - /// - /// The front face of the mesh will be rendered. - /// + /// The front face of the mesh will be rendered. UIRenderer = 2, - /// - /// Enables all renderers. - /// - All = 3 + /// Text components will be rendered. + Text = 4, + + /// Enables all renderers. + All = 7, } diff --git a/Core/Renderers/Renderer.cs b/Engine/SharpEngine.Core/Renderers/Renderer.cs similarity index 72% rename from Core/Renderers/Renderer.cs rename to Engine/SharpEngine.Core/Renderers/Renderer.cs index 9532509..91f4417 100644 --- a/Core/Renderers/Renderer.cs +++ b/Engine/SharpEngine.Core/Renderers/Renderer.cs @@ -4,7 +4,8 @@ using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; - +using SharpEngine.Core.Windowing; +using SharpEngine.Shared; using Silk.NET.OpenGL; using System; @@ -23,9 +24,14 @@ public class Renderer : RendererBase private readonly CameraView _camera; private readonly Scene _scene; + private readonly Window _window; + + // TODO: #7 Property for specific type of objects + // No heavy iteration reads for filtering, + // instead use a notification system from the scene that an item has been removed / added? // Read only once, load into OpenGL buffer once. - // TODO: Multiple meshes + // TODO: #5 Multiple meshes /// public override RenderFlags RenderFlag => RenderFlags.Renderer3D; @@ -34,13 +40,16 @@ public class Renderer : RendererBase /// Initializes a new instance of . /// /// The game the renderer is being used for. + /// The window executing the renderer. /// The settings for the renderer. /// The game scene to be rendered. - public Renderer(CameraView camera, ISettings settings, Scene scene) : base(settings) + public Renderer(CameraView camera, Window window, ISettings settings, Scene scene) : base(settings) { _camera = camera; _scene = scene; + _window = window; + // TODO: #5 These should be refactored out. The minimum build shouldn't need to use these. _lightingShader = new LightingShader(); _lampShader = new LampShader(); } @@ -48,10 +57,18 @@ public Renderer(CameraView camera, ISettings settings, Scene scene) : base(setti /// public override Task Render() { + // TODO: Make toggling these shaders into a hotkey. + // return Task.CompletedTask; + try { Window.GL.Enable(EnableCap.DepthTest); + // Enable image transparency. + // TODO: #62 Needs testing. + Window.GL.Enable(EnableCap.Blend); + Window.GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + _camera.SetShaderUniforms(_lightingShader.Shader!); Window.GL.BindVertexArray(_lightingShader.Vao); @@ -64,7 +81,7 @@ public override Task Render() } catch (Exception ex) { - Console.WriteLine(ex); + Debug.Log.Error(ex, "{Message}", ex.Message); return Task.FromException(ex); } } @@ -74,13 +91,14 @@ private Task RenderGameObject(SceneNode node) if (node is not GameObject gameObject) return Task.CompletedTask; - // TODO: Fix culling for blocks that are partially in view + // TODO: #7 Fix culling for blocks that are partially in view // Perform frustum culling if (!IsInViewFrustum(gameObject.BoundingBox, _camera)) return Task.CompletedTask; - // TODO: Skip blocks that are behind others relative to the camera - return gameObject.Render(); + // TODO: #7 Skip blocks that are behind others relative to the camera + return gameObject.Render(_camera, _window); + // gameObject.Render(_camera, _window); } private static bool IsInViewFrustum(BoundingBox boundingBox, CameraView camera) diff --git a/Core/Renderers/RendererBase.cs b/Engine/SharpEngine.Core/Renderers/RendererBase.cs similarity index 97% rename from Core/Renderers/RendererBase.cs rename to Engine/SharpEngine.Core/Renderers/RendererBase.cs index c36bd03..1491fab 100644 --- a/Core/Renderers/RendererBase.cs +++ b/Engine/SharpEngine.Core/Renderers/RendererBase.cs @@ -1,3 +1,4 @@ +using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; using System; using System.Threading.Tasks; diff --git a/Engine/SharpEngine.Core/Renderers/TextRenderer.cs b/Engine/SharpEngine.Core/Renderers/TextRenderer.cs new file mode 100644 index 0000000..5787fc5 --- /dev/null +++ b/Engine/SharpEngine.Core/Renderers/TextRenderer.cs @@ -0,0 +1,22 @@ +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Windowing; + +using System.Threading.Tasks; + +namespace SharpEngine.Core.Renderers; +internal class TextRenderer : RendererBase +{ + public TextRenderer(CameraView camera, Window window, ISettings settings, Scene scene) : base(settings) + { + } + + public override RenderFlags RenderFlag => RenderFlags.Text; + + /// + public override Task Render() + { + return Task.CompletedTask; + } +} diff --git a/Core/Renderers/UIRenderer.cs b/Engine/SharpEngine.Core/Renderers/UIRenderer.cs similarity index 51% rename from Core/Renderers/UIRenderer.cs rename to Engine/SharpEngine.Core/Renderers/UIRenderer.cs index 11e2165..54ee5da 100644 --- a/Core/Renderers/UIRenderer.cs +++ b/Engine/SharpEngine.Core/Renderers/UIRenderer.cs @@ -1,8 +1,10 @@ -using SharpEngine.Core.Entities; +using SharpEngine.Core.Entities.UI; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; +using SharpEngine.Shared; using Silk.NET.OpenGL; using System; @@ -16,7 +18,8 @@ namespace SharpEngine.Core.Renderers; public class UIRenderer : RendererBase { private readonly Scene _scene; - private readonly UIShader _uiShader; + private readonly CameraView _camera; + private readonly Window _window; /// public override RenderFlags RenderFlag => RenderFlags.UIRenderer; @@ -24,51 +27,34 @@ public class UIRenderer : RendererBase /// /// Initializes a new instance of . /// - public UIRenderer(CameraView _, ISettings settings, Scene scene) : base(settings) + public UIRenderer(CameraView camera, Window window, ISettings settings, Scene scene) : base(settings) { _scene = scene; - _uiShader = new UIShader(); - } - - /// - public override void Initialize() - { - if (_uiShader.Shader is null) - { - Console.WriteLine("The UI shader is null. This is broken mostly when the exe is started outside the solution. This needs to be fixed later."); - return; - } - - _uiShader.Shader.Use(); - - //_scene.UIElements.Add(new UIElement("uiElement")); - - _scene.Iterate(_scene.UIElements, elem => elem.Initialize()); - - InitializeVertexArrays(); - } - - private void InitializeVertexArrays() - { - _uiShader.SetAttributes(); + _camera = camera; + _window = window; } /// public override Task Render() { + // TODO: Make toggling these shaders into a hotkey. + // return Task.CompletedTask; + try { - Window.GL.Disable(EnableCap.DepthTest); + Window.GL.Enable(EnableCap.DepthTest); Window.GL.DepthFunc(DepthFunction.Less); - _uiShader.Shader.Use(); + // Disable face culling to render both sides of the quad + Window.GL.Disable(EnableCap.CullFace); + + var uiElementRenderTasks = _scene.IterateAsync(_scene.UIElements, elem => elem.Render(_camera, _window)); - var uiElementRenderTasks = _scene.IterateAsync(_scene.UIElements, elem => elem.Render()); return Task.WhenAll(uiElementRenderTasks); } catch (Exception ex) { - Console.WriteLine(ex); + Debug.Log.Error(ex, "{Message}", ex.Message); return Task.FromException(ex); } } diff --git a/Core/Scenes/Scene.cs b/Engine/SharpEngine.Core/Scenes/Scene.cs similarity index 87% rename from Core/Scenes/Scene.cs rename to Engine/SharpEngine.Core/Scenes/Scene.cs index 0370af4..e53d524 100644 --- a/Core/Scenes/Scene.cs +++ b/Engine/SharpEngine.Core/Scenes/Scene.cs @@ -1,5 +1,7 @@ using SharpEngine.Core.Entities; - +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.UI; +using SharpEngine.Shared; using System; using System.Collections.Generic; using System.Linq; @@ -41,13 +43,13 @@ public Scene(string sceneFile) /// /// Gets or sets the root node of the scene. /// - public SceneNode Root { get; set; } = new SceneNode("Root"); + public SceneNode Root { get; set; } = new EmptyNode("Root"); /// Gets or sets the nodes in the scene. private List Nodes { get; set; } = []; /// Gets or sets the UI elements in the scene. - public List UIElements { get; private set; } = []; + public List UIElements { get; private set; } = []; /// Gets or sets the active element in the scene. /// Editor only. @@ -59,7 +61,7 @@ public Scene(string sceneFile) /// The name of the new empty node. public void AddNode(string name) { - var node = new SceneNode(name); + var node = new EmptyNode(name); Nodes.Add(node); Root.Children.Add(node); } @@ -104,26 +106,27 @@ public virtual void RemoveNode(SceneNode node) } /// - /// Gets all game objects in the scene. + /// Gets all the objects in the scene of type . /// + /// The type of the objects to be retrieved. /// All the game objects in the current scene. - public List GetAllGameObjects() + public List GetObjectsOfType(SceneNode? root = null) { - // TODO: Can we make this async? - var gameObjects = new List(); - static void FindGameObjects(SceneNode node, List gameObjects) + // TODO: #94 Can we make this async? + var result = new List(); + static void FindsObjects(SceneNode node, List result) { - if (node is GameObject gameObject) - gameObjects.Add(gameObject); + if (node is T obj) + result.Add(obj); foreach (var child in node.Children) - FindGameObjects(child, gameObjects); + FindsObjects(child, result); } - foreach (var node in Root.Children) - FindGameObjects(node, gameObjects); + foreach (var node in root?.Children ?? Root.Children) + FindsObjects(node, result); - return gameObjects; + return result; } /// @@ -180,7 +183,7 @@ public IEnumerable IterateAsync(List elements, FuncThe scene from the given file. Loads an empty scene if unable to load the scene. public static Scene LoadScene(string sceneFile) { - Console.WriteLine(sceneFile); + Debug.Log.Debug(sceneFile); var loadedScene = JsonSerializer.Deserialize(sceneFile); diff --git a/Engine/SharpEngine.Core/Scenes/SceneNode.cs b/Engine/SharpEngine.Core/Scenes/SceneNode.cs new file mode 100644 index 0000000..13b3526 --- /dev/null +++ b/Engine/SharpEngine.Core/Scenes/SceneNode.cs @@ -0,0 +1,110 @@ +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Windowing; + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Scenes; + +/// +/// Represents a empty node in a scene. +/// +/// Specifies the type used for transformations. +/// Defines vector used by the transform. +public class EmptyNode : SceneNode where TTransform : ITransform, new() where TVector : IVector, new() +{ + /// + /// Initializes a new instance of . + /// + /// + public EmptyNode(string name) : base(name) { } + + /// + /// Gets or sets the transform of the node. + /// + public virtual TTransform Transform { get; set; } = new(); +} + +/// +/// Represents a node in the scene. +/// +public abstract class SceneNode +{ + /// Gets a new empty node. + public static SceneNode Empty => new EmptyNode("Empty Node"); + + /// + /// Gets or sets the name of the node. + /// + public string Name { get; set; } = "New Object"; + + /// + /// Gets or sets the children of the node. + /// + public List Children { get; set; } = []; + + /// + /// Initializes a new empty . + /// + public SceneNode() { } + + /// + /// Initializes a new instance of the with the specified . + /// + /// The name of the new empty node. + public SceneNode(string name) + { + Name = name; + } + + /// + /// Adds an empty child node to this node by the given . + /// + /// The name of the empty node to be added. + /// The created node. + public virtual SceneNode AddChild(string name) + => AddChild(name); + + /// + /// Adds an empty child node to this node by the given . + /// + /// The name of the empty node to be added. + /// The created node. + public virtual SceneNode AddChild(string name) where TTransform : ITransform, new() where TVector : IVector, new() + { + var node = new EmptyNode(name) as SceneNode; + Children.Add(node!); + + return node!; + } + + /// + /// Adds a child node to this node. + /// + /// The nodes to be added. + /// The current node. + public virtual SceneNode AddChild(params SceneNode[] nodes) + { + foreach (var node in nodes) + Children.Add(node); + + return this; + } + + /// + /// Removes a child node from this node. + /// + /// The node to be removed. + public void RemoveChild(SceneNode node) + { + Children.Remove(node); + } + + /// + /// Renders the current object to the screen. + /// + /// A representing an asynchronous operation. + public virtual Task Render(CameraView camera, Window window) => Task.CompletedTask; +} diff --git a/Engine/SharpEngine.Core/Shaders/LampShader.cs b/Engine/SharpEngine.Core/Shaders/LampShader.cs new file mode 100644 index 0000000..898d08b --- /dev/null +++ b/Engine/SharpEngine.Core/Shaders/LampShader.cs @@ -0,0 +1,39 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Windowing; +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +internal class LampShader : ShaderBase +{ + /// + /// Initializes a new instance of . + /// + public LampShader() + { + Shader = ShaderService.Instance.LoadShader(Default.VertexShader, Default.LightShader, "lamp").Initialize(); + + Vao = Window.GL.GenVertexArray(); + Window.GL.BindVertexArray(Vao); + + SetAttributes(); + } + + /// + public override bool SetAttributes() + { + if (!base.SetAttributes()) + return false; + + if (!Shader!.TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) + return false; + + var positionLocationUint = (uint)positionLocation; + Window.GL.EnableVertexAttribArray(positionLocationUint); + Window.GL.VertexAttribPointer(positionLocationUint, 3, VertexAttribPointerType.Float, false, VertexData.Stride, 0); + + return true; + } +} diff --git a/Engine/SharpEngine.Core/Shaders/LightingShader.cs b/Engine/SharpEngine.Core/Shaders/LightingShader.cs new file mode 100644 index 0000000..0893499 --- /dev/null +++ b/Engine/SharpEngine.Core/Shaders/LightingShader.cs @@ -0,0 +1,53 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Windowing; +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +internal class LightingShader : ShaderBase +{ + /// + /// Initializes a new instance of . + /// + public LightingShader() + { + Shader = ShaderService.Instance.LoadShader(Default.VertexShader, Default.FragmentShader, "lighting").Initialize(); + + Vao = Window.GL.GenVertexArray(); + Window.GL.BindVertexArray(Vao); + + SetAttributes(); + } + + /// + public override bool SetAttributes() + { + if (!base.SetAttributes()) + return false; + + /*if (!Shader!.TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) + return false; + + var positionLocationUint = (uint)positionLocation; + Window.GL.EnableVertexAttribArray(positionLocationUint); + Window.GL.VertexAttribPointer(positionLocationUint, VertexData.VerticesSize, VertexAttribPointerType.Float, false, VertexData.Stride, 0); + + if (!Shader!.TryGetAttribLocation(ShaderAttributes.Normal, out int normalLocation)) + return false; + + var normalLocationUint = (uint)normalLocation; + Window.GL.EnableVertexAttribArray(normalLocationUint); + Window.GL.VertexAttribPointer(normalLocationUint, VertexData.NormalsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.NormalsOffset); + + if (!Shader!.TryGetAttribLocation(ShaderAttributes.TexCoords, out int texCoordLocation)) + return false; + + var texCoordLocationUint = (uint)texCoordLocation; + Window.GL.EnableVertexAttribArray(texCoordLocationUint); + Window.GL.VertexAttribPointer(texCoordLocationUint, VertexData.TexCoordsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.TexCoordsOffset); + */ + return true; + } +} diff --git a/Core/Shaders/ShaderBase.cs b/Engine/SharpEngine.Core/Shaders/ShaderBase.cs similarity index 80% rename from Core/Shaders/ShaderBase.cs rename to Engine/SharpEngine.Core/Shaders/ShaderBase.cs index 2eac3ae..e7fd389 100644 --- a/Core/Shaders/ShaderBase.cs +++ b/Engine/SharpEngine.Core/Shaders/ShaderBase.cs @@ -1,4 +1,6 @@ -using System; +using SharpEngine.Core.Components.Properties; +using SharpEngine.Shared; +using System; namespace SharpEngine.Core.Shaders; @@ -23,7 +25,7 @@ public virtual bool SetAttributes() { if (Shader is null) { - Console.WriteLine("Unable to set shader attributes, shader not found."); + Debug.Log.Error("Unable to set shader attributes, shader not found."); return false; } diff --git a/Core/Shaders/ShaderService.cs b/Engine/SharpEngine.Core/Shaders/ShaderService.cs similarity index 86% rename from Core/Shaders/ShaderService.cs rename to Engine/SharpEngine.Core/Shaders/ShaderService.cs index db685b3..ee8f516 100644 --- a/Core/Shaders/ShaderService.cs +++ b/Engine/SharpEngine.Core/Shaders/ShaderService.cs @@ -1,7 +1,9 @@ -using System; -using System.IO; +using System.IO; using System.Collections.Generic; +using SharpEngine.Core.Windowing; +using SharpEngine.Shared; + namespace SharpEngine.Core.Shaders; /// @@ -14,8 +16,7 @@ public class ShaderService /// public static ShaderService Instance { get; } = new ShaderService(); - // TODO: Cache data is never set - private readonly Dictionary _shaderCache = new(); + private readonly Dictionary _shaderCache = []; /// /// Gets or sets whether there are shaders to load. @@ -34,7 +35,7 @@ private ShaderService() { } public List GetAll() { HasShadersToLoad = false; - return new(_shaderCache.Values); + return [.. _shaderCache.Values]; } /// @@ -71,18 +72,18 @@ public Shader LoadShader(string vertPath, string fragPath, string name) if (!File.Exists(vertPath)) { - Console.WriteLine($"Vertex shader file not found: {vertPath}"); + Debug.Log.Information("Vertex shader file not found: {VertPath}", vertPath); throw new FileNotFoundException($"Vertex shader file not found: {vertPath}"); } if (!File.Exists(fragPath)) { - Console.WriteLine($"Fragment shader file not found: {fragPath}"); + Debug.Log.Information("Fragment shader file not found: {FragPath}", fragPath); throw new FileNotFoundException($"Fragment shader file not found: {fragPath}"); } // Create a new shader instance and add it to the cache - var shader = new Shader(vertPath, fragPath, name); + var shader = new Shader(Window.GL, vertPath, fragPath, name).Initialize(); _shaderCache[name] = shader; HasShadersToLoad = true; diff --git a/Engine/SharpEngine.Core/Shaders/UIShader.cs b/Engine/SharpEngine.Core/Shaders/UIShader.cs new file mode 100644 index 0000000..37caf51 --- /dev/null +++ b/Engine/SharpEngine.Core/Shaders/UIShader.cs @@ -0,0 +1,48 @@ +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Windowing; +using SharpEngine.Core._Resources; + +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +internal class UIShader : ShaderBase +{ + /// + /// Initializes a new instance of . + /// + public UIShader() + { + Shader = new Shader(Window.GL, Default.UIVertexShader, Default.UIFragmentShader, nameof(UIShader)).Initialize(); + } + + /// + public override bool SetAttributes() + { + if (!base.SetAttributes()) + return false; + + if (!Shader!.TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) + return false; + + var positionLocationUint = (uint)positionLocation; + Window.GL.EnableVertexAttribArray(positionLocationUint); + Window.GL.VertexAttribPointer(positionLocationUint, VertexData.VerticesSize, VertexAttribPointerType.Float, false, VertexData.Stride, 0); + + if (!Shader!.TryGetAttribLocation(ShaderAttributes.Normal, out int normalLocation)) + return false; + + var normalLocationUint = (uint)normalLocation; + Window.GL.EnableVertexAttribArray(normalLocationUint); + Window.GL.VertexAttribPointer(normalLocationUint, VertexData.NormalsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.NormalsOffset); + + if (!Shader!.TryGetAttribLocation(ShaderAttributes.TexCoords, out int texCoordLocation)) + return false; + + var texCoordLocationUint = (uint)texCoordLocation; + Window.GL.EnableVertexAttribArray(texCoordLocationUint); + Window.GL.VertexAttribPointer(texCoordLocationUint, VertexData.TexCoordsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.TexCoordsOffset); + + return true; + } +} diff --git a/Engine/SharpEngine.Core/SharpEngine.Core.csproj b/Engine/SharpEngine.Core/SharpEngine.Core.csproj new file mode 100644 index 0000000..4a179d4 --- /dev/null +++ b/Engine/SharpEngine.Core/SharpEngine.Core.csproj @@ -0,0 +1,62 @@ + + + + Everything needed to build games using a purely C# game engine. + + AnyCPU;x64;x86 + $(GenerateNuGetPackages) + https://github.com/Av3boy/SharpEngine + game-engine; + True + + + $(AssemblyName).NET + + + true + + README.md + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + + + + + + True + \ + + + + \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Textures/TextureService.cs b/Engine/SharpEngine.Core/Textures/TextureService.cs new file mode 100644 index 0000000..4c40568 --- /dev/null +++ b/Engine/SharpEngine.Core/Textures/TextureService.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Collections.Generic; + +using SharpEngine.Core.Windowing; +using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; +using TextureType = Silk.NET.Assimp.TextureType; + +namespace SharpEngine.Core.Textures; + +/// +/// Represents a service for loading textures. +/// +public class TextureService +{ + /// + /// Gets the singleton instance of . + /// + public static TextureService Instance { get; } = new TextureService(); + private readonly Dictionary _textureCache = []; + + // Private constructor to prevent instantiation + private TextureService() { } + + /// + /// Loads a texture from the specified path. + /// + /// the full path to the texture. + /// The loaded texture program. + public Texture LoadTexture(string path, TextureType textureType = TextureType.Diffuse) + { + if (string.IsNullOrWhiteSpace(path)) + throw new FileNotFoundException("No texture file provided."); + + if (!File.Exists(path)) + throw new FileNotFoundException($"Texture file not found from path '{path}'."); + + // Check if the texture is already in the cache + if (_textureCache.TryGetValue(path, out var cachedTexture)) + return cachedTexture; + + // Generate handle + var texture = new Texture(Window.GL, path, textureType); + + // Add it to the cache + _textureCache[path] = texture; + + return texture; + } +} diff --git a/Engine/SharpEngine.Core/Windowing/Frame.cs b/Engine/SharpEngine.Core/Windowing/Frame.cs new file mode 100644 index 0000000..9b43fbf --- /dev/null +++ b/Engine/SharpEngine.Core/Windowing/Frame.cs @@ -0,0 +1,22 @@ +namespace SharpEngine.Core.Windowing; + +/// +/// Represents a frame with a specific duration in time. +/// +public class Frame +{ + /// + /// Initializes a new instance of . + /// + /// Represents the duration of the frame in seconds. + public Frame(double frameTime) + { + FrameTime = frameTime; + } + + /// Gets or sets the time taken to handle the previous frame. + public double FrameTime { get; set; } + + /// Gets the frame rate based on the frame time. + public float FrameRate => (float)(1 / FrameTime); +} diff --git a/Core/SilkWindow.cs b/Engine/SharpEngine.Core/Windowing/SilkWindow.cs similarity index 64% rename from Core/SilkWindow.cs rename to Engine/SharpEngine.Core/Windowing/SilkWindow.cs index 23989da..4301fa0 100644 --- a/Core/SilkWindow.cs +++ b/Engine/SharpEngine.Core/Windowing/SilkWindow.cs @@ -1,45 +1,69 @@ -using SharpEngine.Core.Enums; using Silk.NET.Core; using Silk.NET.Core.Contexts; using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.Windowing; +using MouseButton = Silk.NET.Input.MouseButton; + using StbImageSharp; + using System; using System.IO; using System.Numerics; -using MouseButton = Silk.NET.Input.MouseButton; -namespace SharpEngine.Core; +namespace SharpEngine.Core.Windowing; /// -/// Represents a class abstraction for the interface. +/// Represents an abstraction for the interface. /// +/// +/// For window initialization, see . +/// public abstract class SilkWindow : IWindow { /// - public virtual IWindowHost? Parent { get; } + public virtual IWindowHost? Parent => CurrentWindow.Parent; /// - public IMonitor? Monitor { get; set; } + public IMonitor? Monitor + { + get => CurrentWindow.Monitor; + set => CurrentWindow.Monitor = value; + } /// - public bool IsClosing { get; set; } + public bool IsClosing + { + get => CurrentWindow.IsClosing; + set => CurrentWindow.IsClosing = value; + } /// Gets or sets the input context for the window. public IInputContext? Input { get; protected set; } /// - public virtual Rectangle BorderSize { get; } + public virtual Rectangle BorderSize => CurrentWindow.BorderSize; /// - public bool IsVisible { get; set; } + public bool IsVisible + { + get => CurrentWindow.IsVisible; + set => CurrentWindow.IsVisible = value; + } /// - public Vector2D Position { get; set; } + public Vector2D Position + { + get => CurrentWindow.Position; + set => CurrentWindow.Position = value; + } /// - public Vector2D Size { get; set; } + public Vector2D Size + { + get => CurrentWindow.Size; + set => CurrentWindow.Size = value; + } /// public string Title @@ -49,16 +73,28 @@ public string Title } /// - public WindowState WindowState { get; set; } + public WindowState WindowState + { + get => CurrentWindow.WindowState; + set => CurrentWindow.WindowState = value; + } /// - public WindowBorder WindowBorder { get; set; } + public WindowBorder WindowBorder + { + get => CurrentWindow.WindowBorder; + set => CurrentWindow.WindowBorder = value; + } /// - public bool TransparentFramebuffer { get; } + public bool TransparentFramebuffer => CurrentWindow.TransparentFramebuffer; /// - public bool TopMost { get; set; } + public bool TopMost + { + get => CurrentWindow.TopMost; + set => CurrentWindow.TopMost = value; + } /// public IGLContext? SharedContext => CurrentWindow.SharedContext; @@ -79,25 +115,51 @@ public string Title public bool IsInitialized => CurrentWindow.IsInitialized; /// - public bool ShouldSwapAutomatically { get; set; } + public bool ShouldSwapAutomatically + { + get => CurrentWindow.ShouldSwapAutomatically; + set => CurrentWindow.ShouldSwapAutomatically = value; + } /// - public bool IsEventDriven { get; set; } - + public bool IsEventDriven + { + get => CurrentWindow.ShouldSwapAutomatically; + set => CurrentWindow.ShouldSwapAutomatically = value; + } + /// - public bool IsContextControlDisabled { get; set; } + public bool IsContextControlDisabled + { + get => CurrentWindow.ShouldSwapAutomatically; + set => CurrentWindow.ShouldSwapAutomatically = value; + } /// - public double FramesPerSecond { get; set; } - + public double FramesPerSecond + { + get => CurrentWindow.FramesPerSecond; + set => CurrentWindow.FramesPerSecond = value; + } + /// - public double UpdatesPerSecond { get; set; } + public double UpdatesPerSecond + { + get => CurrentWindow.UpdatesPerSecond; + set => CurrentWindow.UpdatesPerSecond = value; + } /// - public GraphicsAPI API { get; } + public GraphicsAPI API => CurrentWindow.API; /// - public bool VSync { get; set; } + public bool VSync + { + get => CurrentWindow.ShouldSwapAutomatically; + set => CurrentWindow.ShouldSwapAutomatically = value; + } + + public bool IsFocused { get; } /// public VideoMode VideoMode => CurrentWindow.VideoMode; @@ -154,6 +216,9 @@ public string Title /// public event Action? Render; + /// Raised after the scene has been rendered. + public event Action? OnAfterRender; + /// An event executed when the window has loaded. public event Action? OnLoaded; @@ -173,12 +238,25 @@ public IWindow CurrentWindow } /// + // [Obsolete("Use the 'Create' implementation instead.")] public IWindow CreateWindow(WindowOptions opts) { + if (CurrentWindow is not null) + throw new InvalidOperationException("Window already created for this instance."); + CurrentWindow = Silk.NET.Windowing.Window.Create(opts); return CurrentWindow; } + // public static T Create(WindowOptions opts) where T : SilkWindow, new() + // { + // var instance = new T(); + // var window = instance.CreateWindow(opts); + // + // instance.CurrentWindow = window; + // return instance; + // } + /// protected virtual void Dispose(bool disposing) { } @@ -205,16 +283,16 @@ public void Dispose() public virtual void Initialize() => CurrentWindow.Initialize(); /// - public virtual object Invoke(Delegate d, params object[] args) => new(); + public virtual object Invoke(Delegate d, params object[] args) => CurrentWindow.Invoke(d, args); /// - public virtual Vector2D PointToClient(Vector2D point) => throw new NotImplementedException(); + public virtual Vector2D PointToClient(Vector2D point) => CurrentWindow.PointToClient(point); /// - public virtual Vector2D PointToFramebuffer(Vector2D point) => throw new NotImplementedException(); + public virtual Vector2D PointToFramebuffer(Vector2D point) => CurrentWindow.PointToFramebuffer(point); /// - public virtual Vector2D PointToScreen(Vector2D point) => throw new NotImplementedException(); + public virtual Vector2D PointToScreen(Vector2D point) => CurrentWindow.PointToScreen(point); /// public virtual void Reset() => CurrentWindow.Reset(); @@ -259,16 +337,15 @@ public virtual void OnLoad() /// /// Handles operations needed to be executed after the renderers are finished. /// - /// Time since the last frame. - protected virtual void AfterRender(double deltaTime) { } + /// Contains information about the previous frame. + protected virtual void AfterRender(Frame frame) => OnAfterRender?.Invoke(frame); /// /// Handles operations needed to be executed before the renderers are executed. /// - /// Time since the last frame. - protected virtual void PreRender(double deltaTime) { } + /// Contains information about the previous frame. + protected virtual void PreRender(Frame frame) { } - // TODO: Scene unsaved changes warning. /// /// Handles closing the application. /// diff --git a/Core/Window.cs b/Engine/SharpEngine.Core/Windowing/Window.cs similarity index 73% rename from Core/Window.cs rename to Engine/SharpEngine.Core/Windowing/Window.cs index 29e9936..787d9e3 100644 --- a/Core/Window.cs +++ b/Engine/SharpEngine.Core/Windowing/Window.cs @@ -1,44 +1,49 @@ -using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Enums; using SharpEngine.Core.Extensions; -using SharpEngine.Core.Interfaces; using SharpEngine.Core.Renderers; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; -using Silk.NET.Core; -using Silk.NET.GLFW; + using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.OpenGL; using Silk.NET.OpenGL.Extensions.ImGui; using Silk.NET.Windowing; -using StbImageSharp; +using MouseButton = Silk.NET.Input.MouseButton; + using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Numerics; -using System.Runtime.InteropServices; using System.Threading.Tasks; -using MouseButton = Silk.NET.Input.MouseButton; using Shader = SharpEngine.Core.Shaders.Shader; +using Silk.NET.GLFW; +using SharpEngine.Shared; -namespace SharpEngine.Core; +namespace SharpEngine.Core.Windowing; /// /// Represents the game window. /// public class Window : SilkWindow { - private readonly ISettings _settings; - private readonly CameraView _camera; - private bool _initialized; private IEnumerable _renderers = []; private ImGuiController? _imGuiController; + /// + /// Gets or sets the view for the window. + /// + public readonly CameraView Camera; + + /// + /// Gets the settings for the current window. + /// + public IViewSettings Settings; + /// The event executed when mouse events are executed. public event Action? OnHandleMouse; @@ -62,14 +67,14 @@ public class Window : SilkWindow /// The OpenGL context. public static GL GL; - // TODO: Use this method. + // TODO: #93 Use this method. /// /// Gets the current OpenGL context. /// /// The OpenGL context for this window. public static GL GetGL() => GL; private static void SetGL(GL gl) => GL = gl; - + /// /// Initializes a new instance of . /// @@ -79,10 +84,9 @@ public class Window : SilkWindow public Window(CameraView camera, Scene scene, IViewSettings settings) { Scene = scene; - _settings = settings; - _camera = camera; - - Initialize(settings.WindowOptions); + Settings = settings; + Camera = camera; + InitializeWindow(); } /// @@ -93,17 +97,19 @@ public Window(CameraView camera, Scene scene, IViewSettings settings) public Window(Scene scene, IViewSettings settings) { Scene = scene; - _settings = settings; - _camera = new(Vector3.One, new DefaultViewSettings()); + Settings = settings; + Camera = new(Vector3.One, settings); - Initialize(settings.WindowOptions); + InitializeWindow(); } - private void Initialize(WindowOptions options) + private bool _windowInitialized; + + private void InitializeWindow() { - CurrentWindow = CreateWindow(options); - CurrentWindow.Update += OnUpdateFrame; - CurrentWindow.Render += RenderFrame; + CurrentWindow = CreateWindow(Settings.WindowOptions); + CurrentWindow.Update += deltaTime => OnUpdateFrame(new Frame(deltaTime)); + CurrentWindow.Render += deltaTime => RenderFrame(new Frame(deltaTime)); CurrentWindow.Resize += OnResize; CurrentWindow.Load += OnLoad; CurrentWindow.Closing += OnClosing; @@ -112,6 +118,9 @@ private void Initialize(WindowOptions options) /// public override void Run(Action onFrame) { + if (!_windowInitialized) + throw new InvalidOperationException("Window not been initialized for this instance. Try calling 'window.Initialize()' first."); + try { base.Run(onFrame); @@ -119,7 +128,7 @@ public override void Run(Action onFrame) } catch (Exception ex) { - Debug.LogInformation("Error running window: " + ex.Message, ex); + Debug.Log.Error(ex, "Error running window: {Message}", ex.Message); } } @@ -140,11 +149,8 @@ public override void OnLoad() GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f); - // TODO: Set cursor shape - // CursorShape = CursorShape.Hand; - // Load all meshes from the mesh cache - MeshService.Instance.LoadMesh("cube", Primitives.Cube.Mesh); + // MeshService.Instance.LoadMesh("cube", Primitives.Cube.Mesh); // Using reflection, find all renderers that implement the RendererBase. var rendererTypes = AppDomain.CurrentDomain.GetAssemblies() @@ -154,8 +160,8 @@ public override void OnLoad() foreach (var type in rendererTypes) { // Make sure the renderer has the correct constructor parameters! - // TODO: The static reference to the context will not work when multiple windows are implemented, since the context will be different. - var requiredArguments = new object[] { _camera, _settings, Scene }; + // TODO: #75 The static reference to the context will not work when multiple windows are implemented, since the context will be different. + var requiredArguments = new object[] { Camera, this, Settings, Scene }; var renderer = (RendererBase)Activator.CreateInstance(type, requiredArguments)!; _renderers = _renderers.Append(renderer); @@ -170,7 +176,7 @@ public override void OnLoad() } catch (Exception ex) { - Debug.LogInformation("Error loading window: " + ex.Message, ex); + Debug.Log.Information(ex, "Error loading window: {Message}", ex.Message); } base.OnLoad(); @@ -179,8 +185,8 @@ public override void OnLoad() /// /// Renders the current view and all specified renderers. /// - /// The time since the last frame. - protected void RenderFrame(double deltaTime) + /// Contains information about the previous frame. + protected void RenderFrame(Frame frame) { while (!_initialized) { @@ -189,32 +195,29 @@ protected void RenderFrame(double deltaTime) try { - PreRender(deltaTime); + PreRender(frame); - _imGuiController?.Update((float)deltaTime); + _imGuiController?.Update((float)frame.FrameTime); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - ToggleWireFrame(_settings.UseWireFrame); + ToggleWireFrame(Settings.UseWireFrame); UseShaders(); - var renderTasks = _renderers.Where(renderer => _settings.RendererFlags.HasFlag(renderer.RenderFlag)) + var renderTasks = _renderers.Where(renderer => Settings.RendererFlags.HasFlag(renderer.RenderFlag)) .Select(renderer => renderer.Render()) .ToList(); Task.WaitAll([.. renderTasks]); - // TODO: This call causes flickering in the new framework. Investigate why. - // _window.SwapBuffers(); - - AfterRender(deltaTime); + AfterRender(frame); _imGuiController?.Render(); } catch (Exception ex) { - Debug.LogInformation(ex.Message); + Debug.Log.Information(ex.Message); } } @@ -236,25 +239,21 @@ private void UseShaders() } /// - protected void OnUpdateFrame(double deltaTime) + protected void OnUpdateFrame(Frame frame) { while (!_initialized) { // Wait for the window to be initialized. } - // TODO: GLFW_ISFOCUSED - // if (!_window.IsFocused) - // return; + if (Settings.PrintFrameRate) + Debug.Log.Information($"FPS: {frame.FrameRate}"); - if (_settings.PrintFrameRate) - Console.WriteLine($"FPS: {1f / deltaTime}"); - - // TODO: Handle multiple mice? + // TODO: #21 Handle multiple mice? var mouse = Input?.Mice[0]; if (mouse is not null) { - _camera.UpdateMousePosition(mouse.Position); + Camera.UpdateMousePosition(mouse.Position); OnHandleMouse?.Invoke(mouse); } @@ -264,11 +263,11 @@ protected void OnUpdateFrame(double deltaTime) if (keyboard.IsKeyPressed(Key.Escape)) CurrentWindow.Close(); - OnHandleKeyboard?.Invoke(keyboard, deltaTime); + OnHandleKeyboard?.Invoke(keyboard, frame.FrameTime); } if (Input is not null) - OnUpdate?.Invoke(deltaTime, Input); + OnUpdate?.Invoke(frame.FrameTime, Input); } // TODO: #21 Input system @@ -276,7 +275,7 @@ private void AssignInputEvents() { if (Input is null) { - Debug.LogInformation("Input is null. No input events will be assigned."); + Debug.Log.Information("Input is null. No input events will be assigned."); return; } @@ -288,6 +287,9 @@ private void AssignInputEvents() mouse.Scroll += OnMouseWheel; mouse.Click += OnMouseClick; mouse.MouseDown += OnMouseDown; + + if (IsFocused) + mouse.Cursor.CursorMode = CursorMode.Raw; } } @@ -309,14 +311,16 @@ protected void OnMouseWheel(IMouse mouse, ScrollWheel sw) }; HandleMouseWheel?.Invoke(direction, sw); - _camera.Fov -= sw.Y; + Camera.Fov -= sw.Y; } /// protected void OnResize(Vector2D size) { GL.Viewport(size); - _camera.AspectRatio = size.X / size.Y; + + if (size != Vector2D.Zero) + Camera.AspectRatio = (float)size.X / size.Y; } /// @@ -329,7 +333,7 @@ protected override void OnMouseDown(IMouse mouse, MouseButton button) /// The contents of the new scene. protected void SetScene(Scene scene) { - // TODO: Do we need to clear anything from e.g. the GPU when we change change the scene? + // TODO: #92 Do we need to clear anything from e.g. the GPU when we change change the scene? Scene = scene; } } diff --git a/Engine/SharpEngine.Core/_Resources/Default.cs b/Engine/SharpEngine.Core/_Resources/Default.cs new file mode 100644 index 0000000..ef3e138 --- /dev/null +++ b/Engine/SharpEngine.Core/_Resources/Default.cs @@ -0,0 +1,23 @@ +using SharpEngine.Core.Extensions; + +namespace SharpEngine.Core._Resources; + +/// +/// Contains default resources used by the engine. +/// +public static class Default +{ + /// Gets the path to the debug texture. + public static string DebugTexture => PathExtensions.GetAssemblyPath("_Resources\\Textures\\debug.JPG"); + + /// Gets the path to the default vertex shader. + public static string VertexShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\shader.vert"); + + /// Gets the path to the lighting fragment shader. + public static string FragmentShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\lighting.frag"); + + public static string LightShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\shader.frag"); + + public static string UIVertexShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\uiShader.vert"); + public static string UIFragmentShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\uiShader.frag"); +} diff --git a/Core/Shaders/Light/DirectionalLight.glsl b/Engine/SharpEngine.Core/_Resources/Shaders/Light/DirectionalLight.glsl similarity index 100% rename from Core/Shaders/Light/DirectionalLight.glsl rename to Engine/SharpEngine.Core/_Resources/Shaders/Light/DirectionalLight.glsl diff --git a/Core/Shaders/Light/PointLight.glsl b/Engine/SharpEngine.Core/_Resources/Shaders/Light/PointLight.glsl similarity index 100% rename from Core/Shaders/Light/PointLight.glsl rename to Engine/SharpEngine.Core/_Resources/Shaders/Light/PointLight.glsl diff --git a/Core/Shaders/Light/SpotLight.glsl b/Engine/SharpEngine.Core/_Resources/Shaders/Light/SpotLight.glsl similarity index 100% rename from Core/Shaders/Light/SpotLight.glsl rename to Engine/SharpEngine.Core/_Resources/Shaders/Light/SpotLight.glsl diff --git a/Core/Shaders/Material.glsl b/Engine/SharpEngine.Core/_Resources/Shaders/Material.glsl similarity index 100% rename from Core/Shaders/Material.glsl rename to Engine/SharpEngine.Core/_Resources/Shaders/Material.glsl diff --git a/Core/Shaders/lighting.frag b/Engine/SharpEngine.Core/_Resources/Shaders/lighting.frag similarity index 84% rename from Core/Shaders/lighting.frag rename to Engine/SharpEngine.Core/_Resources/Shaders/lighting.frag index 8e2354d..b725244 100644 --- a/Core/Shaders/lighting.frag +++ b/Engine/SharpEngine.Core/_Resources/Shaders/lighting.frag @@ -1,3 +1,5 @@ +// Represents a basic forward rendered lighting shader. + #version 330 core #include "Material.glsl" @@ -23,15 +25,18 @@ uniform SpotLight spotLight; void main() { + vec3 norm = normalize(Normal); vec3 viewDir = normalize(viewPos - FragPos); - + vec3 result = CalcDirLight(dirLight, norm, viewDir); - + for(int i = 0; i < NR_POINT_LIGHTS; i++) result += CalcPointLight(pointLights[i], norm, FragPos, viewDir); - + result += CalcSpotLight(spotLight, norm, FragPos, viewDir); - + FragColor = vec4(result, 1.0); + + // FragColor = vec4(1.0); // set all 4 vector values to 1.0 } \ No newline at end of file diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/shader.frag b/Engine/SharpEngine.Core/_Resources/Shaders/shader.frag new file mode 100644 index 0000000..c35339c --- /dev/null +++ b/Engine/SharpEngine.Core/_Resources/Shaders/shader.frag @@ -0,0 +1,14 @@ +#version 330 core +in vec2 fUv; + + +uniform sampler2D uTexture0; + +out vec4 FragColor; + +void main() +{ + // FragColor = vec4(1.0); // set all 4 vector values to 1.0 + FragColor = texture(uTexture0, fUv); + +} \ No newline at end of file diff --git a/Core/Shaders/shader.vert b/Engine/SharpEngine.Core/_Resources/Shaders/shader.vert similarity index 50% rename from Core/Shaders/shader.vert rename to Engine/SharpEngine.Core/_Resources/Shaders/shader.vert index af12da3..b2f7b4a 100644 --- a/Core/Shaders/shader.vert +++ b/Engine/SharpEngine.Core/_Resources/Shaders/shader.vert @@ -3,9 +3,9 @@ layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aNormal; layout (location = 2) in vec2 aTexCoords; -uniform mat4 model; -uniform mat4 view; -uniform mat4 projection; +uniform mat4 uModel; +uniform mat4 uView; +uniform mat4 uProjection; out vec3 Normal; out vec3 FragPos; @@ -13,8 +13,8 @@ out vec2 TexCoords; void main() { - gl_Position = vec4(aPos, 1.0) * model * view * projection; - FragPos = vec3(vec4(aPos, 1.0) * model); - Normal = aNormal * mat3(transpose(inverse(model))); + gl_Position = vec4(aPos, 1.0) * uModel * uView * uProjection; + FragPos = vec3(vec4(aPos, 1.0) * uModel); + Normal = aNormal * mat3(transpose(inverse(uModel))); TexCoords = aTexCoords; } \ No newline at end of file diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/shader2.vert b/Engine/SharpEngine.Core/_Resources/Shaders/shader2.vert new file mode 100644 index 0000000..fc46c04 --- /dev/null +++ b/Engine/SharpEngine.Core/_Resources/Shaders/shader2.vert @@ -0,0 +1,16 @@ +#version 330 core +layout (location = 0) in vec3 vPos; +layout (location = 1) in vec2 vUv; + +uniform mat4 uModel; +uniform mat4 uView; +uniform mat4 uProjection; + +out vec2 fUv; + +void main() +{ + //Multiplying our uniform with the vertex position, the multiplication order here does matter. + gl_Position = uProjection * uView * uModel * vec4(vPos, 1.0); + fUv = vUv; +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.frag b/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.frag new file mode 100644 index 0000000..1cc0a9e --- /dev/null +++ b/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.frag @@ -0,0 +1,10 @@ +#version 330 core + +uniform sampler2D texture1; +out vec4 FragColor; +in vec2 TexCoords; + +void main() +{ + FragColor = texture(texture1, TexCoords); +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.vert b/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.vert new file mode 100644 index 0000000..7787de5 --- /dev/null +++ b/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.vert @@ -0,0 +1,58 @@ +#version 330 core + +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aNormal; +layout (location = 2) in vec2 aTexCoords; + +uniform float width; +uniform float height; + +uniform vec2 position; +uniform float rotation; +uniform vec2 screenSize; +uniform mat4 orthoMatrix; + +uniform mat4 uModel; + +out vec3 Normal; +out vec3 FragPos; +out vec2 TexCoords; + +void main() +{ + float reciprScaleOnscreen = 0.005f; + + // Calculate the aspect ratio + float aspectRatio = screenSize.x / screenSize.y; + + // Create the rotation matrix for the z-axis + mat4 rotationMatrix = mat4( + cos(rotation), -sin(rotation), 0.0, 0.0, + sin(rotation), cos(rotation), 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); + + // Scale the vertex position using width and height + vec3 scaledPos = vec3( + aPos.x * width * reciprScaleOnscreen, + aPos.y * height * reciprScaleOnscreen, + aPos.z + ); + + // Calculate the model-view-projection matrix using the orthographic projection matrix + mat4 mvp = orthoMatrix * rotationMatrix * uModel; + + // Apply the MVP matrix to the scaled vertex position with aspect ratio + gl_Position = mvp * vec4( + (scaledPos.x + (position.x * reciprScaleOnscreen)) * aspectRatio, + scaledPos.y + (position.y * reciprScaleOnscreen), + scaledPos.z, + 1 + ); + + // Pass through other attributes + Normal = mat3(uModel) * aNormal; // Transform the normal + FragPos = vec3(uModel * vec4(aPos, 1.0)); // Transform the fragment position + TexCoords = aTexCoords; +} \ No newline at end of file diff --git a/Core/Textures/DefaultTextures/debug.jpg b/Engine/SharpEngine.Core/_Resources/Textures/debug.jpg similarity index 100% rename from Core/Textures/DefaultTextures/debug.jpg rename to Engine/SharpEngine.Core/_Resources/Textures/debug.jpg diff --git a/Resources/icon.png b/Engine/SharpEngine.Core/_Resources/icon.png similarity index 100% rename from Resources/icon.png rename to Engine/SharpEngine.Core/_Resources/icon.png diff --git a/Engine/SharpEngine.Rest/Clients/AssetStoreClient.cs b/Engine/SharpEngine.Rest/Clients/AssetStoreClient.cs new file mode 100644 index 0000000..e209d12 --- /dev/null +++ b/Engine/SharpEngine.Rest/Clients/AssetStoreClient.cs @@ -0,0 +1,12 @@ +using SharpEngine.Rest.Urls; +using SharpEngine.Shared.Dto.AssetStore; + +namespace SharpEngine.Rest.Clients; + +public class AssetStoreClient : RestClient, IAssetStoreClient +{ + public AssetStoreClient(HttpClient httpClient) : base(httpClient) { } + + public async Task GetAssetAsync(Guid assetId) + => await GetAsync(AssetStore.GetById(assetId)); +} diff --git a/Engine/SharpEngine.Rest/Clients/IAssetStoreClient.cs b/Engine/SharpEngine.Rest/Clients/IAssetStoreClient.cs new file mode 100644 index 0000000..b99147a --- /dev/null +++ b/Engine/SharpEngine.Rest/Clients/IAssetStoreClient.cs @@ -0,0 +1,8 @@ +using SharpEngine.Shared.Dto.AssetStore; + +namespace SharpEngine.Rest.Clients; + +public interface IAssetStoreClient +{ + Task GetAssetAsync(Guid assetId); +} diff --git a/Engine/SharpEngine.Rest/Clients/IPortalClient.cs b/Engine/SharpEngine.Rest/Clients/IPortalClient.cs new file mode 100644 index 0000000..55de6a5 --- /dev/null +++ b/Engine/SharpEngine.Rest/Clients/IPortalClient.cs @@ -0,0 +1,5 @@ +namespace SharpEngine.Rest.Clients; + +internal interface IPortalClient +{ +} \ No newline at end of file diff --git a/Engine/SharpEngine.Rest/Clients/PortalClient.cs b/Engine/SharpEngine.Rest/Clients/PortalClient.cs new file mode 100644 index 0000000..f8213f6 --- /dev/null +++ b/Engine/SharpEngine.Rest/Clients/PortalClient.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpEngine.Rest.Clients; + +internal class PortalClient : RestClient, IPortalClient +{ + public PortalClient(HttpClient httpClient) : base(httpClient) { } +} diff --git a/Engine/SharpEngine.Rest/Clients/RestClient.cs b/Engine/SharpEngine.Rest/Clients/RestClient.cs new file mode 100644 index 0000000..3e96916 --- /dev/null +++ b/Engine/SharpEngine.Rest/Clients/RestClient.cs @@ -0,0 +1,28 @@ +using SharpEngine.Rest.Urls; +using System.Diagnostics; +using System.Text.Json; + +namespace SharpEngine.Rest.Clients; + +public abstract class RestClient +{ + private readonly HttpClient _httpClient; + public RestClient(HttpClient httpClient) + { + _httpClient = httpClient; + _httpClient.BaseAddress = new Uri(AssetStore.BaseUrl); + } + + public async Task GetAsync(string url) + { + var response = await _httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + + var result = JsonSerializer.Deserialize(json); + Debug.Assert(result is not null, "Deserialized result is null."); + + return result; + } +} diff --git a/Engine/SharpEngine.Rest/SharpEngine.Rest.csproj b/Engine/SharpEngine.Rest/SharpEngine.Rest.csproj new file mode 100644 index 0000000..d6ab7f9 --- /dev/null +++ b/Engine/SharpEngine.Rest/SharpEngine.Rest.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Engine/SharpEngine.Rest/Urls/AssetStore.cs b/Engine/SharpEngine.Rest/Urls/AssetStore.cs new file mode 100644 index 0000000..e369a96 --- /dev/null +++ b/Engine/SharpEngine.Rest/Urls/AssetStore.cs @@ -0,0 +1,7 @@ +namespace SharpEngine.Rest.Urls; + +public static class AssetStore +{ + public const string BaseUrl = "https://assetstore.sharpengine.com/"; + public static string GetById(Guid assetId) => $"assets/{assetId}"; +} diff --git a/Engine/SharpEngine.Rest/Urls/Portal.cs b/Engine/SharpEngine.Rest/Urls/Portal.cs new file mode 100644 index 0000000..749348f --- /dev/null +++ b/Engine/SharpEngine.Rest/Urls/Portal.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Rest.Urls; + +public static class Portal +{ + public const string BaseUrl = "https://portal.sharpengine.com/"; +} diff --git a/Engine/SharpEngine.Shared/AssetType.cs b/Engine/SharpEngine.Shared/AssetType.cs new file mode 100644 index 0000000..e460210 --- /dev/null +++ b/Engine/SharpEngine.Shared/AssetType.cs @@ -0,0 +1,8 @@ +namespace SharpEngine.Shared; + +public enum AssetType +{ + Shader, + Model, + Texture, +} diff --git a/Engine/SharpEngine.Shared/Attributes/FilterAttribute.cs b/Engine/SharpEngine.Shared/Attributes/FilterAttribute.cs new file mode 100644 index 0000000..f37af87 --- /dev/null +++ b/Engine/SharpEngine.Shared/Attributes/FilterAttribute.cs @@ -0,0 +1,20 @@ +namespace SharpEngine.Shared.Attributes; + +/// +/// Represents a attribute that indicates if a property is filterable. +/// +[AttributeUsage(AttributeTargets.Property)] +public class FilterAttribute : Attribute +{ + /// Gets or sets whether the property is filterable in collections. + public bool IsFilterable { get; set; } + + /// + /// Initializes a new instance of . + /// + /// Determines whether the target property is filterable. + public FilterAttribute(bool isFilterable = true) + { + IsFilterable = isFilterable; + } +} diff --git a/Core/Attributes/InspectorAttribute.cs b/Engine/SharpEngine.Shared/Attributes/InspectorAttribute.cs similarity index 100% rename from Core/Attributes/InspectorAttribute.cs rename to Engine/SharpEngine.Shared/Attributes/InspectorAttribute.cs diff --git a/Engine/SharpEngine.Shared/Debug.cs b/Engine/SharpEngine.Shared/Debug.cs new file mode 100644 index 0000000..07fa37d --- /dev/null +++ b/Engine/SharpEngine.Shared/Debug.cs @@ -0,0 +1,32 @@ +using Serilog; +using Serilog.Events; +using System; + +namespace SharpEngine.Shared; + +/// +/// Contains methods for debugging the application. +/// +public static class Debug +{ + public static ILogger Log; + + private static LogEventLevel _logLevel = LogEventLevel.Information; + + static Debug() + { + SetLogger(); + } + + public static void SetLogger() + => Log = new LoggerConfiguration() + .MinimumLevel.Is(_logLevel) + .WriteTo.Console() + .CreateLogger(); + + public static void SetLogLevel(LogEventLevel logLevel) + { + _logLevel = logLevel; + SetLogger(); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Shared/Dto/AssetStore/AssetDto.cs b/Engine/SharpEngine.Shared/Dto/AssetStore/AssetDto.cs new file mode 100644 index 0000000..1acf076 --- /dev/null +++ b/Engine/SharpEngine.Shared/Dto/AssetStore/AssetDto.cs @@ -0,0 +1,33 @@ +using SharpEngine.Shared.Dto.Primitives; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace SharpEngine.Shared.Dto.AssetStore; + +public class AssetDto +{ + public AssetId Id { get; init; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public decimal Price { get; set; } + public DateTime CreatedAt { get; init; } + public DateTime LastUpdatedAt { get; init; } + public IReadOnlyList Tags { get; set; } = []; + public IReadOnlyList Comments { get; set; } = []; + public UserId AuthorId { get; init; } + public bool Tombstoned { get; set; } + public Version Version { get; set; } + + + // (e.g., model, texture, sound, etc.) + // public AssetType + + public required string BlobUri { get; set; } + public required string FileSize { get; set; } + + public required string Checksum { get; set; } +} diff --git a/Engine/SharpEngine.Shared/Dto/AssetStore/CommentDto.cs b/Engine/SharpEngine.Shared/Dto/AssetStore/CommentDto.cs new file mode 100644 index 0000000..053abeb --- /dev/null +++ b/Engine/SharpEngine.Shared/Dto/AssetStore/CommentDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpEngine.Shared.Dto.AssetStore; + +public class CommentDto +{ +} diff --git a/Engine/SharpEngine.Shared/Dto/Primitives/AssetId.cs b/Engine/SharpEngine.Shared/Dto/Primitives/AssetId.cs new file mode 100644 index 0000000..eada9db --- /dev/null +++ b/Engine/SharpEngine.Shared/Dto/Primitives/AssetId.cs @@ -0,0 +1,8 @@ +namespace SharpEngine.Shared.Dto.Primitives; + +public readonly record struct AssetId(Guid Value) +{ + public static AssetId New() => new(Guid.NewGuid()); + public static implicit operator Guid(AssetId id) => id.Value; + public static implicit operator AssetId(Guid value) => new(value); +} diff --git a/Engine/SharpEngine.Shared/Dto/Primitives/CommentId.cs b/Engine/SharpEngine.Shared/Dto/Primitives/CommentId.cs new file mode 100644 index 0000000..1ec8ef4 --- /dev/null +++ b/Engine/SharpEngine.Shared/Dto/Primitives/CommentId.cs @@ -0,0 +1,8 @@ +namespace SharpEngine.Shared.Dto.Primitives; + +public readonly record struct CommentId(Guid Value) +{ + public static CommentId New() => new(Guid.NewGuid()); + public static implicit operator Guid(CommentId id) => id.Value; + public static implicit operator CommentId(Guid value) => new(value); +} diff --git a/Engine/SharpEngine.Shared/Dto/Primitives/UserId.cs b/Engine/SharpEngine.Shared/Dto/Primitives/UserId.cs new file mode 100644 index 0000000..d0fb0c2 --- /dev/null +++ b/Engine/SharpEngine.Shared/Dto/Primitives/UserId.cs @@ -0,0 +1,8 @@ +namespace SharpEngine.Shared.Dto.Primitives; + +public readonly record struct UserId(Guid Value) +{ + public static UserId New() => new(Guid.NewGuid()); + public static implicit operator Guid(UserId id) => id.Value; + public static implicit operator UserId(Guid value) => new(value); +} diff --git a/SharpEngine.Shared/Project.cs b/Engine/SharpEngine.Shared/Dto/Project.cs similarity index 85% rename from SharpEngine.Shared/Project.cs rename to Engine/SharpEngine.Shared/Dto/Project.cs index 5cac973..cf6b427 100644 --- a/SharpEngine.Shared/Project.cs +++ b/Engine/SharpEngine.Shared/Dto/Project.cs @@ -1,8 +1,10 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Text.Json; using System.Text.Json.Serialization; +using SharpEngine.Shared.Attributes; -namespace Launcher.UI; +namespace SharpEngine.Shared.Dto; /// /// Represents a SharpEngine project. @@ -30,6 +32,8 @@ public class Project /// Gets or sets when the project was last modified. /// [JsonIgnore] + [DisplayName("Last Modified")] + [Filter(false)] public DateTime LastModified { get; set; } = DateTime.Now; /// diff --git a/Engine/SharpEngine.Shared/Dto/UserProfile.cs b/Engine/SharpEngine.Shared/Dto/UserProfile.cs new file mode 100644 index 0000000..b7a2f8b --- /dev/null +++ b/Engine/SharpEngine.Shared/Dto/UserProfile.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpEngine.Shared.Dto; + +public class UserProfile +{ + public string Username { get; set; } = default!; + public string Email { get; set; } = default!; + public string Password + { + get => _password; + set => SetPassword(value); + } + + private string _password; + + public List LinkedSocialMedia { get; set; } = []; + + public List Projects { get; set; } = []; + + public List Achievements { get; set; } = []; + + private void SetPassword(string pwd) + { + // TODO: #60 Figure out a good way to handle encryption + _password = pwd; + } +} + +public class SocialMedia +{ + public string Name { get; set; } + public string Url { get; set; } +} + +public class Achievement +{ + public string Name { get; set; } + public string Description { get; set; } + public DateTime? DateAchieved { get; set; } + + public bool IsAchieved => DateAchieved.HasValue; +} diff --git a/Engine/SharpEngine.Shared/Enums/FilterMode.cs b/Engine/SharpEngine.Shared/Enums/FilterMode.cs new file mode 100644 index 0000000..71d1da1 --- /dev/null +++ b/Engine/SharpEngine.Shared/Enums/FilterMode.cs @@ -0,0 +1,16 @@ +namespace SharpEngine.Shared.Enums; + +/// +/// Contains the possible ways to filter a collection. +/// +public enum FilterMode +{ + /// No filter applied. + None, + + /// Filter in ascending order. + Ascending, + + /// Filter in descending order. + Descending +} diff --git a/SharpEngine.Shared/EnvironmentVariables.cs b/Engine/SharpEngine.Shared/EnvironmentVariables.cs similarity index 100% rename from SharpEngine.Shared/EnvironmentVariables.cs rename to Engine/SharpEngine.Shared/EnvironmentVariables.cs diff --git a/Engine/SharpEngine.Shared/Extensions/OrderedEnumerableExtensions.cs b/Engine/SharpEngine.Shared/Extensions/OrderedEnumerableExtensions.cs new file mode 100644 index 0000000..d6f93d6 --- /dev/null +++ b/Engine/SharpEngine.Shared/Extensions/OrderedEnumerableExtensions.cs @@ -0,0 +1,39 @@ +using Launcher.UI; +using SharpEngine.Shared.Dto; +using SharpEngine.Shared.Enums; +using SharpEngine.Shared.Extensions; + +/// +/// Handles ordering collections. +/// +public static class OrderedEnumerableExtensions +{ + /// + /// Filters and orders a collection of projects based on specified criteria. + /// + /// The collection of projects to be filtered and ordered. + /// The names of properties and their corresponding filter modes. + /// A filtered collection of projects based on the applied filters. + public static IEnumerable FilterBy(this IEnumerable toFilter, Dictionary filters) + { + var stringComparer = new NaturalStringComparer(); + IOrderedEnumerable? ordered = null; + + foreach (var filter in filters) + { + string keySelector(Project p) => p.GetPropertyValue(filter.Key)?.ToString() ?? string.Empty; + + if (ordered == null) + ordered = filter.Value == FilterMode.Ascending + ? toFilter.OrderBy(keySelector, stringComparer) + : toFilter.OrderByDescending(keySelector, stringComparer); + + else + ordered = filter.Value == FilterMode.Ascending + ? ordered.ThenBy(keySelector, stringComparer) + : ordered.ThenByDescending(keySelector, stringComparer); + } + + return ordered ?? toFilter; + } +} diff --git a/SharpEngine.Shared/ProcessExtensions.cs b/Engine/SharpEngine.Shared/Extensions/ProcessExtensions.cs similarity index 97% rename from SharpEngine.Shared/ProcessExtensions.cs rename to Engine/SharpEngine.Shared/Extensions/ProcessExtensions.cs index 00cb0bc..f8d2a36 100644 --- a/SharpEngine.Shared/ProcessExtensions.cs +++ b/Engine/SharpEngine.Shared/Extensions/ProcessExtensions.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; -namespace SharpEngine.Shared; +namespace SharpEngine.Shared.Extensions; /// /// Contains extensions for handling processes. @@ -68,7 +68,7 @@ void SendMessage(string? msg) } }); - Thread thread = new Thread(start); + var thread = new Thread(start); thread.Start(); SendMessage("Editor launched."); diff --git a/Engine/SharpEngine.Shared/Extensions/ReflectionExtensions.cs b/Engine/SharpEngine.Shared/Extensions/ReflectionExtensions.cs new file mode 100644 index 0000000..a663bf7 --- /dev/null +++ b/Engine/SharpEngine.Shared/Extensions/ReflectionExtensions.cs @@ -0,0 +1,23 @@ +namespace SharpEngine.Shared.Extensions; + +/// +/// Contains extensions for reflection. +/// +public static class ReflectionExtensions +{ + /// + /// Gets the value of a property from the given . + /// + /// The type of the object. + /// The object whose property value is being retrieved. + /// The name of the property whose values is being retrieved. + /// The value of the property, if not found. + public static object? GetPropertyValue(this T obj, string propertyName) + { + var property = typeof(T).GetProperty(propertyName); + if (property == null) + return null; + + return property.GetValue(obj); + } +} diff --git a/Engine/SharpEngine.Shared/Extensions/StringExtensions.cs b/Engine/SharpEngine.Shared/Extensions/StringExtensions.cs new file mode 100644 index 0000000..f9d5e36 --- /dev/null +++ b/Engine/SharpEngine.Shared/Extensions/StringExtensions.cs @@ -0,0 +1,28 @@ +using System; +using System.Globalization; + +namespace ObjLoader.Loader.Common +{ + public static class StringExtensions + { + public static float ParseInvariantFloat(this string floatString) + { + return float.Parse(floatString, CultureInfo.InvariantCulture.NumberFormat); + } + + public static int ParseInvariantInt(this string intString) + { + return int.Parse(intString, CultureInfo.InvariantCulture.NumberFormat); + } + + public static bool EqualsOrdinalIgnoreCase(this string str, string s) + { + return str.Equals(s, StringComparison.OrdinalIgnoreCase); + } + + public static bool IsNullOrEmpty(this string str) + { + return string.IsNullOrEmpty(str); + } + } +} \ No newline at end of file diff --git a/Core/Math.cs b/Engine/SharpEngine.Shared/Math.cs similarity index 92% rename from Core/Math.cs rename to Engine/SharpEngine.Shared/Math.cs index 5006ec2..7b74cad 100644 --- a/Core/Math.cs +++ b/Engine/SharpEngine.Shared/Math.cs @@ -27,7 +27,7 @@ public static ReadOnlySpan ToSpan(this Matrix4x4 matrix) /// /// Converts a value in to radians. /// - /// The degress to be converted + /// The degrees to be converted. /// The given in radians. public static double DegreesToRadians(double degrees) => System.Math.PI / 180 * degrees; @@ -40,7 +40,7 @@ public static float DegreesToRadians(float degrees) /// Converts a value in to degrees. /// /// The radians to convert. - /// The given as degress. + /// The given as degrees. public static float RadiansToDegrees(float radians) => radians * (180 / MathF.PI); } diff --git a/Engine/SharpEngine.Shared/NaturalStringComparer.cs b/Engine/SharpEngine.Shared/NaturalStringComparer.cs new file mode 100644 index 0000000..50b10b7 --- /dev/null +++ b/Engine/SharpEngine.Shared/NaturalStringComparer.cs @@ -0,0 +1,73 @@ +using System.Globalization; + +namespace Launcher.UI; + +/// +/// Checks if two strings are equal in a natural way. +/// +public class NaturalStringComparer : IComparer +{ + /// + public int Compare(string? s1, string? s2) + { + if (s1 == s2) + return 0; + if (s1 == null) + return -1; + if (s2 == null) + return 1; + + int i = 0, j = 0; + int len1 = s1.Length, len2 = s2.Length; + + while (i < len1 && j < len2) + { + int result = CompareCharacters(s1, s2, ref i, ref j); + if (result != 0) + return result; + } + + // If one string has remaining characters, it is greater + return (len1 - i).CompareTo(len2 - j); + } + + private static int CompareCharacters(string s1, string s2, ref int i, ref int j) + { + char c1 = s1[i]; + char c2 = s2[j]; + + // If both characters are digits, compare as numbers + if (char.IsDigit(c1) && char.IsDigit(c2)) + { + int start1 = i; + while (i < s1.Length && char.IsDigit(s1[i])) + i++; + + int start2 = j; + while (j < s2.Length && char.IsDigit(s2[j])) + j++; + + string numStr1 = s1.Substring(start1, i - start1); + string numStr2 = s2.Substring(start2, j - start2); + + // Parse numbers ignoring leading zeros + int num1 = int.Parse(numStr1, CultureInfo.InvariantCulture); + int num2 = int.Parse(numStr2, CultureInfo.InvariantCulture); + + if (num1 != num2) + return num1.CompareTo(num2); + + return 0; + } + + // Compare characters case-insensitively + int result = char.ToUpperInvariant(c1).CompareTo(char.ToUpperInvariant(c2)); + if (result != 0) + return result; + + i++; + j++; + + return 0; + } +} diff --git a/Engine/SharpEngine.Shared/SharpEngine.Shared.csproj b/Engine/SharpEngine.Shared/SharpEngine.Shared.csproj new file mode 100644 index 0000000..e0034eb --- /dev/null +++ b/Engine/SharpEngine.Shared/SharpEngine.Shared.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Engine/SharpEngine.sln b/Engine/SharpEngine.sln new file mode 100644 index 0000000..c51bee3 --- /dev/null +++ b/Engine/SharpEngine.sln @@ -0,0 +1,175 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.35906.104 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Editor", "Editor\SharpEngine.Editor.csproj", "{F72A74DC-54F9-0C76-2D40-32D2524683F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Launcher", "Launcher\Launcher.csproj", "{3751B661-BE0E-9CD7-CA51-361A34450372}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Shared", "SharpEngine.Shared\SharpEngine.Shared.csproj", "{F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Minecraft", "Examples\MinecraftClone\Minecraft.csproj", "{E8A75C3B-1D82-8B30-C854-AD35FB123CDC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Minimal", "Examples\Minimal\Minimal.csproj", "{1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{089100B1-113F-4E66-888A-E83F3999EAFD}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props + README.md = README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipleWindows", "Examples\MultipleWindows\MultipleWindows.csproj", "{0AF16D15-5BD2-4FED-9C6A-227DE6A56C5E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" + ProjectSection(SolutionItems) = preProject + Test.Directory.Build.props = Test.Directory.Build.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Core.Tests", "Tests\SharpEngine.Core.Tests\SharpEngine.Core.Tests.csproj", "{60B90F0B-14B8-4FA5-8482-B09EDEC46BF8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Shared.Tests", "Tests\SharpEngine.Shared.Tests\SharpEngine.Shared.Tests.csproj", "{385CCB8D-CA17-4B58-8DA6-D3E9210CE49B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Editor.Tests", "Tests\SharpEngine.Editor.Tests\SharpEngine.Editor.Tests.csproj", "{D374B8A7-51F6-4688-BD99-74EB0E159079}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Core.Numerics", "SharpEngine.Core.Numerics\SharpEngine.Core.Numerics.csproj", "{C5A54BE1-00E6-43F5-B0DB-41C6C1F47DDF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Core.Components", "SharpEngine.Core.Components\SharpEngine.Core.Components.csproj", "{94482B3A-4230-45C3-A899-689DA882942E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Core", "SharpEngine.Core\SharpEngine.Core.csproj", "{52043A27-E9FF-3216-A026-E241F706D49B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Core.ObjLoader", "ObjLoader\SharpEngine.Core.ObjLoader.csproj", "{9D23A8B2-C143-B170-10B8-069D9FA119FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObjLoader.Tests", "Tests\ObjLoader.Test\ObjLoader.Tests.csproj", "{1807AD4A-C310-4BD8-9542-07D3D3EAE617}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tutorial 4.1 - Model Loading", "Examples\Tutorial 4.1 - Model Loading\Tutorial 4.1 - Model Loading.csproj", "{17243B7C-E72B-6C26-FCFC-3D194F29B0AF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Rest", "SharpEngine.Rest\SharpEngine.Rest.csproj", "{FE7C7460-6599-4BF8-A756-0C04315D3144}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + LauncherNonWindows|Any CPU = LauncherNonWindows|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F72A74DC-54F9-0C76-2D40-32D2524683F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F72A74DC-54F9-0C76-2D40-32D2524683F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F72A74DC-54F9-0C76-2D40-32D2524683F8}.LauncherNonWindows|Any CPU.ActiveCfg = Debug|Any CPU + {F72A74DC-54F9-0C76-2D40-32D2524683F8}.LauncherNonWindows|Any CPU.Build.0 = Debug|Any CPU + {F72A74DC-54F9-0C76-2D40-32D2524683F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F72A74DC-54F9-0C76-2D40-32D2524683F8}.Release|Any CPU.Build.0 = Release|Any CPU + {3751B661-BE0E-9CD7-CA51-361A34450372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3751B661-BE0E-9CD7-CA51-361A34450372}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3751B661-BE0E-9CD7-CA51-361A34450372}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {3751B661-BE0E-9CD7-CA51-361A34450372}.LauncherNonWindows|Any CPU.ActiveCfg = Debug|Any CPU + {3751B661-BE0E-9CD7-CA51-361A34450372}.LauncherNonWindows|Any CPU.Build.0 = Debug|Any CPU + {3751B661-BE0E-9CD7-CA51-361A34450372}.LauncherNonWindows|Any CPU.Deploy.0 = Debug|Any CPU + {3751B661-BE0E-9CD7-CA51-361A34450372}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3751B661-BE0E-9CD7-CA51-361A34450372}.Release|Any CPU.Build.0 = Release|Any CPU + {3751B661-BE0E-9CD7-CA51-361A34450372}.Release|Any CPU.Deploy.0 = Release|Any CPU + {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.Release|Any CPU.Build.0 = Release|Any CPU + {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.LauncherNonWindows|Any CPU.ActiveCfg = Debug|Any CPU + {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.LauncherNonWindows|Any CPU.Build.0 = Debug|Any CPU + {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.Release|Any CPU.Build.0 = Release|Any CPU + {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.LauncherNonWindows|Any CPU.ActiveCfg = Debug|Any CPU + {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.LauncherNonWindows|Any CPU.Build.0 = Debug|Any CPU + {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.Release|Any CPU.Build.0 = Release|Any CPU + {0AF16D15-5BD2-4FED-9C6A-227DE6A56C5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0AF16D15-5BD2-4FED-9C6A-227DE6A56C5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0AF16D15-5BD2-4FED-9C6A-227DE6A56C5E}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {0AF16D15-5BD2-4FED-9C6A-227DE6A56C5E}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {0AF16D15-5BD2-4FED-9C6A-227DE6A56C5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0AF16D15-5BD2-4FED-9C6A-227DE6A56C5E}.Release|Any CPU.Build.0 = Release|Any CPU + {60B90F0B-14B8-4FA5-8482-B09EDEC46BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60B90F0B-14B8-4FA5-8482-B09EDEC46BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60B90F0B-14B8-4FA5-8482-B09EDEC46BF8}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {60B90F0B-14B8-4FA5-8482-B09EDEC46BF8}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {60B90F0B-14B8-4FA5-8482-B09EDEC46BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60B90F0B-14B8-4FA5-8482-B09EDEC46BF8}.Release|Any CPU.Build.0 = Release|Any CPU + {385CCB8D-CA17-4B58-8DA6-D3E9210CE49B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {385CCB8D-CA17-4B58-8DA6-D3E9210CE49B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {385CCB8D-CA17-4B58-8DA6-D3E9210CE49B}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {385CCB8D-CA17-4B58-8DA6-D3E9210CE49B}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {385CCB8D-CA17-4B58-8DA6-D3E9210CE49B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {385CCB8D-CA17-4B58-8DA6-D3E9210CE49B}.Release|Any CPU.Build.0 = Release|Any CPU + {D374B8A7-51F6-4688-BD99-74EB0E159079}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D374B8A7-51F6-4688-BD99-74EB0E159079}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D374B8A7-51F6-4688-BD99-74EB0E159079}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {D374B8A7-51F6-4688-BD99-74EB0E159079}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {D374B8A7-51F6-4688-BD99-74EB0E159079}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D374B8A7-51F6-4688-BD99-74EB0E159079}.Release|Any CPU.Build.0 = Release|Any CPU + {C5A54BE1-00E6-43F5-B0DB-41C6C1F47DDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5A54BE1-00E6-43F5-B0DB-41C6C1F47DDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5A54BE1-00E6-43F5-B0DB-41C6C1F47DDF}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {C5A54BE1-00E6-43F5-B0DB-41C6C1F47DDF}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {C5A54BE1-00E6-43F5-B0DB-41C6C1F47DDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5A54BE1-00E6-43F5-B0DB-41C6C1F47DDF}.Release|Any CPU.Build.0 = Release|Any CPU + {94482B3A-4230-45C3-A899-689DA882942E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94482B3A-4230-45C3-A899-689DA882942E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94482B3A-4230-45C3-A899-689DA882942E}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {94482B3A-4230-45C3-A899-689DA882942E}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {94482B3A-4230-45C3-A899-689DA882942E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94482B3A-4230-45C3-A899-689DA882942E}.Release|Any CPU.Build.0 = Release|Any CPU + {52043A27-E9FF-3216-A026-E241F706D49B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52043A27-E9FF-3216-A026-E241F706D49B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52043A27-E9FF-3216-A026-E241F706D49B}.LauncherNonWindows|Any CPU.ActiveCfg = LauncherNonWindows|Any CPU + {52043A27-E9FF-3216-A026-E241F706D49B}.LauncherNonWindows|Any CPU.Build.0 = LauncherNonWindows|Any CPU + {52043A27-E9FF-3216-A026-E241F706D49B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52043A27-E9FF-3216-A026-E241F706D49B}.Release|Any CPU.Build.0 = Release|Any CPU + {9D23A8B2-C143-B170-10B8-069D9FA119FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D23A8B2-C143-B170-10B8-069D9FA119FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D23A8B2-C143-B170-10B8-069D9FA119FD}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {9D23A8B2-C143-B170-10B8-069D9FA119FD}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {9D23A8B2-C143-B170-10B8-069D9FA119FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D23A8B2-C143-B170-10B8-069D9FA119FD}.Release|Any CPU.Build.0 = Release|Any CPU + {1807AD4A-C310-4BD8-9542-07D3D3EAE617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1807AD4A-C310-4BD8-9542-07D3D3EAE617}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1807AD4A-C310-4BD8-9542-07D3D3EAE617}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {1807AD4A-C310-4BD8-9542-07D3D3EAE617}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {1807AD4A-C310-4BD8-9542-07D3D3EAE617}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1807AD4A-C310-4BD8-9542-07D3D3EAE617}.Release|Any CPU.Build.0 = Release|Any CPU + {17243B7C-E72B-6C26-FCFC-3D194F29B0AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17243B7C-E72B-6C26-FCFC-3D194F29B0AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17243B7C-E72B-6C26-FCFC-3D194F29B0AF}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {17243B7C-E72B-6C26-FCFC-3D194F29B0AF}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {17243B7C-E72B-6C26-FCFC-3D194F29B0AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17243B7C-E72B-6C26-FCFC-3D194F29B0AF}.Release|Any CPU.Build.0 = Release|Any CPU + {FE7C7460-6599-4BF8-A756-0C04315D3144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE7C7460-6599-4BF8-A756-0C04315D3144}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE7C7460-6599-4BF8-A756-0C04315D3144}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU + {FE7C7460-6599-4BF8-A756-0C04315D3144}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU + {FE7C7460-6599-4BF8-A756-0C04315D3144}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE7C7460-6599-4BF8-A756-0C04315D3144}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E8A75C3B-1D82-8B30-C854-AD35FB123CDC} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} + {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} + {0AF16D15-5BD2-4FED-9C6A-227DE6A56C5E} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} + {60B90F0B-14B8-4FA5-8482-B09EDEC46BF8} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {385CCB8D-CA17-4B58-8DA6-D3E9210CE49B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {D374B8A7-51F6-4688-BD99-74EB0E159079} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {1807AD4A-C310-4BD8-9542-07D3D3EAE617} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {17243B7C-E72B-6C26-FCFC-3D194F29B0AF} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AA03EC4B-4E7B-4B73-BEA4-60C539332773} + EndGlobalSection +EndGlobal diff --git a/SharpEngine.slnx b/Engine/SharpEngine.slnx similarity index 100% rename from SharpEngine.slnx rename to Engine/SharpEngine.slnx diff --git a/Engine/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs b/Engine/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs new file mode 100644 index 0000000..39c13c4 --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs @@ -0,0 +1,148 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using NUnit.Framework; +using ObjLoader.Loader.Data; +using System.Linq; +using FluentAssertions; +using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Components.Properties; +using ObjLoader.Loaders.MaterialLoader; + +namespace ObjLoader.Test.Loaders +{ + [TestFixture] + public class MaterialLibraryLoaderTests + { + private MaterialLibrarySpy _materialLibrarySpy; + private Material _firstMaterial; + private Material _secondMaterial; + private const float Epsilon = 0.000001f; + + private MaterialLibraryLoader _materialLibraryLoader; + + [SetUp] + public void SetUp() + { + _materialLibrarySpy = new MaterialLibrarySpy(); + // _materialLibraryLoader = new MaterialLibraryLoader(_materialLibrarySpy); + } + + [Test] + public void Adds_correct_materials() + { + LoadMaterial(); + + _materialLibrarySpy.Materials.Count().Should().Be(2); + _firstMaterial.Name.Should().BeEquivalentTo("wire_134110008"); + _secondMaterial.Name.Should().BeEquivalentTo("second_material"); + } + + [Test] + public void Sets_correct_ambient_color() + { + LoadMaterial(); + + _firstMaterial.AmbientColor.X.Should().BeApproximately(1, Epsilon); + _firstMaterial.AmbientColor.Y.Should().BeApproximately(2, Epsilon); + _firstMaterial.AmbientColor.Z.Should().BeApproximately(3, Epsilon); + } + + [Test] + public void Sets_correct_diffuse_color() + { + LoadMaterial(); + + _firstMaterial.DiffuseColor.X.Should().BeApproximately(0.5255f, Epsilon); + _firstMaterial.DiffuseColor.Y.Should().BeApproximately(0.4314f, Epsilon); + _firstMaterial.DiffuseColor.Z.Should().BeApproximately(0.0314f, Epsilon); + } + + [Test] + public void Sets_correct_specular() + { + LoadMaterial(); + + _firstMaterial.SpecularColor.X.Should().BeApproximately(0.3500f, Epsilon); + _firstMaterial.SpecularColor.Y.Should().BeApproximately(0.3600f, Epsilon); + _firstMaterial.SpecularColor.Z.Should().BeApproximately(0.3700f, Epsilon); + + _firstMaterial.SpecularCoefficient.Should().BeApproximately(32, Epsilon); + } + + [Test] + public void Sets_correct_transparency() + { + LoadMaterial(); + + _firstMaterial.Transparency.Should().BeApproximately(0.5f, Epsilon); + } + + [Test] + public void Sets_correct_illumination_model() + { + LoadMaterial(); + + _firstMaterial.IlluminationModel.Should().Be(2); + } + + [Test] + 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.SpecularHighlightTextureMap.Should().BeEquivalentTo("lenna_spec.tga"); + _firstMaterial.AlphaTextureMap.Should().BeEquivalentTo("lenna_alpha.tga"); + _firstMaterial.BumpMap.Should().BeEquivalentTo("lenna_bump.tga"); + _firstMaterial.DisplacementMap.Should().BeEquivalentTo("lenna_disp.tga"); + _firstMaterial.StencilDecalMap.Should().BeEquivalentTo("lenna_stencil.tga"); + } + + private void LoadMaterial() + { + var data = Encoding.ASCII.GetBytes(MaterialLibrary); + var materialStream = new MemoryStream(data); + + _materialLibraryLoader.Load(materialStream); + _firstMaterial = _materialLibrarySpy.Materials.First(); + _secondMaterial = _materialLibrarySpy.Materials.ElementAt(1); + } + + private const string MaterialLibrary = + "newmtl wire_134110008\r\n" + +"\tNs\t32\r\n" + +@"Tr 0.5 +illum 2 +Ka 1.0000 2.0000 3.0000 +Kd 0.5255 0.4314 0.0314 +Ks 0.3500 0.3600 0.3700 +map_Ka lenna1.tga +map_Kd lenna2.tga +map_Ks lenna3.tga +map_Ns lenna_spec.tga +map_d lenna_alpha.tga +map_bump lenna_bump.tga +disp lenna_disp.tga +decal lenna_stencil.tga +newmtl second_material"; + + private class MaterialLibrarySpy : IMaterialLibrary + { + public List Materials { get; private set; } + + public MaterialLibrarySpy() + { + Materials = new List(); + } + + public void Push(Material material) + { + Materials.Add(material); + } + } + } +} \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs b/Engine/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs new file mode 100644 index 0000000..8e4190a --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs @@ -0,0 +1,190 @@ +using System.IO; +using System.Text; +using System.Linq; +using NUnit.Framework; +using ObjLoader.Loader.Loaders; +using ObjLoader.Loader.TypeParsers; +using FluentAssertions; +using ObjLoader.Loaders.MaterialLoader; +using SharpEngine.Core.Entities.Properties.Meshes; + +namespace ObjLoader.Test.Loaders +{ + [TestFixture] + public class ObjLoaderTests + { + private class MaterialStreamProviderSpy : IMaterialStreamProvider + { + public string RequestedMaterialFilePath { get; private set; } + public Stream StreamToReturn { get; set; } + + public Stream Open(string materialFilePath) + { + RequestedMaterialFilePath = materialFilePath; + return StreamToReturn; + } + } + + private ObjLoader.Loaders.ObjLoader.ObjLoader _loader; + + private Mesh _loadResult; + private DataStore _textureDataStore; + private FaceParser _faceParser; + private GroupParser _groupParser; + private NormalParser _normalParser; + private TextureParser _textureParser; + private VertexParser _vertexParser; + private MaterialLibraryLoader _materialLibraryLoader; + private MaterialLibraryParser _materialLibraryParser; + private UseMaterialParser _useMaterialParser; + private MaterialLibraryLoaderFacade _materialLibraryLoaderFacade; + private MaterialStreamProviderSpy _materialStreamProviderSpy; + + [SetUp] + public void SetUp() + { + _textureDataStore = new DataStore(); + + _faceParser = new FaceParser(_textureDataStore); + _groupParser = new GroupParser(_textureDataStore); + _normalParser = new NormalParser(_textureDataStore); + _textureParser = new TextureParser(_textureDataStore); + _vertexParser = new VertexParser(_textureDataStore); + _materialStreamProviderSpy = new MaterialStreamProviderSpy(); + _materialStreamProviderSpy.StreamToReturn = CreateMemoryStreamFromString(MaterialLibraryString); + + _materialLibraryLoader = new MaterialLibraryLoader("", _textureDataStore); + _materialLibraryLoaderFacade = new MaterialLibraryLoaderFacade(_materialLibraryLoader); + // _materialLibraryParser = new MaterialLibraryParser(_materialLibraryLoaderFacade); + _useMaterialParser = new UseMaterialParser(_textureDataStore); + + // _loader = new Loader.Loaders.ObjLoader(_textureDataStore, _faceParser, _groupParser, _normalParser, _textureParser, _vertexParser, _materialLibraryParser, _useMaterialParser); + } + + [Test] + public void Loads_object_and_material_correctly() + { + Load(); + + _loadResult.Vertices.Should().HaveCount(8); + _loadResult.TextureCoordinates2.Should().HaveCount(14); + _loadResult.Normals.Should().HaveCount(8); + _loadResult.Materials.Should().HaveCount(1); + + _materialStreamProviderSpy.RequestedMaterialFilePath.Should().BeEquivalentTo("cube.mtl"); + + _loadResult.Groups.Should().HaveCount(1); + + var group = _loadResult.Groups.First(); + group.Faces.Should().HaveCount(12); + group.Material.Name.Should().BeEquivalentTo("cube_material"); + } + + [Test] + public void Loads_object_correctly_when_material_is_not_found() + { + _materialStreamProviderSpy.StreamToReturn = null; + var materialLibraryLoaderFacade = new MaterialLibraryLoaderFacade(_materialLibraryLoader); + // var materialLibraryParser = new MaterialLibraryParser(materialLibraryLoaderFacade); + + // _loader = new Loader.Loaders.ObjLoader(_textureDataStore, _faceParser, _groupParser, _normalParser, _textureParser, _vertexParser, materialLibraryParser, _useMaterialParser); + + Load(); + + _loadResult.Vertices.Should().HaveCount(8); + _loadResult.TextureCoordinates2.Should().HaveCount(14); + _loadResult.Normals.Should().HaveCount(8); + _loadResult.Materials.Should().HaveCount(0); + + _loadResult.Groups.Should().HaveCount(1); + + var group = _loadResult.Groups.First(); + group.Faces.Should().HaveCount(12); + group.Material.Should().BeNull(); + } + + private void Load() + { + var objectStream = CreateMemoryStreamFromString(ObjectFileString); + + // _loadResult = _loader.Load(objectStream); + } + + private Stream CreateMemoryStreamFromString(string str) + { + var data = Encoding.ASCII.GetBytes(str); + return new MemoryStream(data); + } + + private const string ObjectFileString = +"#Comment\r\n" + +"mtllib\tcube.mtl\r\n" + +@"v 1.000000 -1.000000 -1.000000 +v 1.000000 -1.000000 1.000000 +v -1.000000 -1.000000 1.000000 +v -1.000000 -1.000000 -1.000000 +v 1.000000 1.000000 -1.000000 +v 0.999999 1.000000 1.000001 +v -1.000000 1.000000 1.000000 +v -1.000000 1.000000 -1.000000 +vt 0.748573 0.750412 +vt 0.749279 0.501284 +vt 0.999110 0.501077 +vt 0.999455 0.750380 +vt 0.250471 0.500702 +vt 0.249682 0.749677 +vt 0.001085 0.750380 +vt 0.001517 0.499994 +vt 0.499422 0.500239 +vt 0.500149 0.750166 +vt 0.748355 0.998230 +vt 0.500193 0.998728 +vt 0.498993 0.250415 +vt 0.748953 0.250920 + + +vn 0.000000 0.000000 -1.000000 +vn -1.000000 -0.000000 -0.000000 +vn -0.000000 -0.000000 1.000000 +vn -0.000001 0.000000 1.000000 +vn 1.000000 -0.000000 0.000000 +vn 1.000000 0.000000 0.000001 +vn 0.000000 1.000000 -0.000000 +vn -0.000000 -1.000000 0.000000 +usemtl cube_material +s off +f 5/1/1 1/2/1 4/3/1 +f 5/1/1 4/3/1 8/4/1 +f 3/5/2 7/6/2 8/7/2 +f 3/5/2 8/7/2 4/8/2 +f 2/9/3 6/10/3 3/5/3 +f 6/10/4 7/6/4 3/5/4 +f 1/2/5 5/1/5 2/9/5 +f 5/1/6 6/10/6 2/9/6 +f 5/1/7 8/11/7 6/10/7 +f 8/11/7 7/12/7 6/10/7 +f 1/2/8 2/9/8 3/13/8 +f 1/2/8 3/13/8 4/14/8"; + + private const string MaterialLibraryString = +@"newmtl cube_material +Ns 32 +Tr 0.5 +illum 2 +Ka 1.0000 2.0000 3.0000 +Kd 0.5255 0.4314 0.0314 +Ks 0.3500 0.3600 0.3700 +map_Ka lenna1.tga +map_Kd lenna2.tga +map_Ks lenna3.tga +map_Ns lenna_spec.tga +map_d lenna_alpha.tga +map_bump lenna_bump.tga +disp lenna_disp.tga +decal lenna_stencil.tga"; + } + + internal interface IMaterialStreamProvider + { + } +} \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj b/Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj new file mode 100644 index 0000000..7725988 --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs b/Engine/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs new file mode 100644 index 0000000..64ba447 --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs @@ -0,0 +1,147 @@ +using FluentAssertions; +using NUnit.Framework; +using ObjLoader.Loader.Data; +using ObjLoader.Loader.Data.Elements; +using ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; + +namespace ObjLoader.Test.TypeParsers +{ + [TestFixture] + public class FaceParserTests + { + private FaceGroupSpy _faceGroupSpy; + private FaceParser _faceParser; + + [SetUp] + public void SetUp() + { + _faceGroupSpy = new FaceGroupSpy(); + + _faceParser = new FaceParser(_faceGroupSpy); + } + + [Test] + public void CanParse_returns_true_on_face_line() + { + const string groupKeyword = "f"; + + bool canParse = _faceParser.CanParse(groupKeyword); + canParse.Should().BeTrue(); + } + + [Test] + public void CanParse_returns_false_on_non_normal_line() + { + const string invalidKeyword = "vt"; + + bool canParse = _faceParser.CanParse(invalidKeyword); + canParse.Should().BeFalse(); + } + + [Test] + public void Parses_normal_line_correctly_1() + { + const string faceLine = "1 2 3"; + _faceParser.Parse(faceLine); + + var parsedFace = _faceGroupSpy.ParsedFace; + + parsedFace[0].VertexIndex.Should().Be(1); + parsedFace[0].TextureIndex.Should().Be(0); + parsedFace[0].NormalIndex.Should().Be(0); + + parsedFace[1].VertexIndex.Should().Be(2); + parsedFace[1].TextureIndex.Should().Be(0); + parsedFace[1].NormalIndex.Should().Be(0); + + parsedFace[2].VertexIndex.Should().Be(3); + parsedFace[2].TextureIndex.Should().Be(0); + parsedFace[2].NormalIndex.Should().Be(0); + } + + [Test] + public void Parses_normal_line_correctly_2() + { + const string faceLine = "3/1 4/2 5/3"; + _faceParser.Parse(faceLine); + + var parsedFace = _faceGroupSpy.ParsedFace; + + parsedFace.Count.Should().Be(3); + + parsedFace[0].VertexIndex.Should().Be(3); + parsedFace[0].TextureIndex.Should().Be(1); + parsedFace[0].NormalIndex.Should().Be(0); + + parsedFace[1].VertexIndex.Should().Be(4); + parsedFace[1].TextureIndex.Should().Be(2); + parsedFace[1].NormalIndex.Should().Be(0); + + parsedFace[2].VertexIndex.Should().Be(5); + parsedFace[2].TextureIndex.Should().Be(3); + parsedFace[2].NormalIndex.Should().Be(0); + } + + [Test] + public void Parses_normal_line_correctly_3() + { + const string faceLine = "6/4/1 3/5/3 7/6/5"; + _faceParser.Parse(faceLine); + + var parsedFace = _faceGroupSpy.ParsedFace; + + parsedFace.Count.Should().Be(3); + + parsedFace[0].VertexIndex.Should().Be(6); + parsedFace[0].TextureIndex.Should().Be(4); + parsedFace[0].NormalIndex.Should().Be(1); + + parsedFace[1].VertexIndex.Should().Be(3); + parsedFace[1].TextureIndex.Should().Be(5); + parsedFace[1].NormalIndex.Should().Be(3); + + parsedFace[2].VertexIndex.Should().Be(7); + parsedFace[2].TextureIndex.Should().Be(6); + parsedFace[2].NormalIndex.Should().Be(5); + } + + [Test] + public void Parses_normal_line_correctly_4() + { + const string faceLine = "6//1 3//3 7//5"; + _faceParser.Parse(faceLine); + + var parsedFace = _faceGroupSpy.ParsedFace; + + parsedFace.Count.Should().Be(3); + + parsedFace[0].VertexIndex.Should().Be(6); + parsedFace[0].TextureIndex.Should().Be(0); + parsedFace[0].NormalIndex.Should().Be(1); + + parsedFace[1].VertexIndex.Should().Be(3); + parsedFace[1].TextureIndex.Should().Be(0); + parsedFace[1].NormalIndex.Should().Be(3); + + parsedFace[2].VertexIndex.Should().Be(7); + parsedFace[2].TextureIndex.Should().Be(0); + parsedFace[2].NormalIndex.Should().Be(5); + } + } + + public class FaceGroupSpy : IFaceGroup + { + public Face ParsedFace { get; private set; } + + public Face GetFace(int i) + { + throw new System.NotImplementedException(); + } + + public void AddFace(Face face) + { + ParsedFace = face; + } + } +} \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs b/Engine/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs new file mode 100644 index 0000000..97ea846 --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs @@ -0,0 +1,61 @@ +using FluentAssertions; +using NUnit.Framework; +using ObjLoader.Loader.Data; +using ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; + +namespace ObjLoader.Test.TypeParsers +{ + [TestFixture] + public class GroupParserTests + { + private GroupDataStoreMock _groupDataStoreMock; + private GroupParser _groupParser; + + [SetUp] + public void SetUp() + { + _groupDataStoreMock = new GroupDataStoreMock(); + + // _groupParser = new GroupParser(_groupDataStoreMock); + } + + [Test] + public void CanParse_returns_true_on_normal_line() + { + const string groupKeyword = "g"; + + bool canParse = _groupParser.CanParse(groupKeyword); + canParse.Should().BeTrue(); + } + + [Test] + public void CanParse_returns_false_on_non_normal_line() + { + const string invalidKeyword = "vt"; + + bool canParse = _groupParser.CanParse(invalidKeyword); + canParse.Should().BeFalse(); + } + + [Test] + public void Parses_normal_line_correctly() + { + const string normalLine = "test group"; + _groupParser.Parse(normalLine); + + var parsedGroupName = _groupDataStoreMock.ParsedGroupName; + parsedGroupName.Should().Be("test group"); + } + } + + class GroupDataStoreMock : IGroupDataStore + { + public string ParsedGroupName { get; set; } + + public void PushGroup(string groupName) + { + ParsedGroupName = groupName; + } + } +} \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs b/Engine/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs new file mode 100644 index 0000000..91089f1 --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs @@ -0,0 +1,58 @@ +using FluentAssertions; +using NUnit.Framework; +using ObjLoader.Loader.TypeParsers; +using ObjLoader.Loaders.MaterialLoader; + +namespace ObjLoader.Test.TypeParsers +{ + [TestFixture] + public class MaterialLibraryParserTests + { + private MaterialLibraryLoaderFacadeSpy _materialLibraryLoaderFacadeSpy; + private MaterialLibraryParser _parser; + + [SetUp] + public void SetUp() + { + _materialLibraryLoaderFacadeSpy = new MaterialLibraryLoaderFacadeSpy(); + // _parser = new MaterialLibraryParser(_materialLibraryLoaderFacadeSpy); + } + + [Test] + public void CanParse_returns_true_on_mtlib_line() + { + const string groupKeyword = "mtllib"; + + bool canParse = _parser.CanParse(groupKeyword); + canParse.Should().BeTrue(); + } + + [Test] + public void CanParse_returns_false_on_non_mtlib_line() + { + const string invalidKeyword = "vt"; + + bool canParse = _parser.CanParse(invalidKeyword); + canParse.Should().BeFalse(); + } + + [Test] + public void Parses_mtlib_line_correctly() + { + const string faceLine = "cube.mtl"; + _parser.Parse(faceLine); + + _materialLibraryLoaderFacadeSpy.RequestedLoadFileName.Should().BeEquivalentTo("cube.mtl"); + } + + private class MaterialLibraryLoaderFacadeSpy : IMaterialLibraryLoaderFacade + { + public string RequestedLoadFileName { get; set; } + + public void Load(string materialFileName) + { + RequestedLoadFileName = materialFileName; + } + } + } +} \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs b/Engine/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs new file mode 100644 index 0000000..f9e03c7 --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs @@ -0,0 +1,68 @@ +using FluentAssertions; +using NUnit.Framework; +using ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace ObjLoader.Test.TypeParsers +{ + [TestFixture] + public class NormalParserTests + { + private NormalParser _normalParser; + private NormalDataStoreMock _normalDataStoreMock; + + [SetUp] + public void SetUp() + { + _normalDataStoreMock = new NormalDataStoreMock(); + + // _normalParser = new NormalParser(_normalDataStoreMock); + } + + [Test] + public void CanParse_returns_true_on_normal_line() + { + const string normalLine = "vn"; + + bool canParse = _normalParser.CanParse(normalLine); + canParse.Should().BeTrue(); + } + + [Test] + public void CanParse_returns_false_on_non_normal_line() + { + const string invalidNormal = "vt"; + + bool canParse = _normalParser.CanParse(invalidNormal); + canParse.Should().BeFalse(); + } + + [Test] + public void Parses_normal_line_correctly() + { + const string normalLine = "1.000000 2.000000 -1.000000"; + _normalParser.Parse(normalLine); + + var parsedNormal = _normalDataStoreMock.ParsedNormal; + parsedNormal.X.Should().BeApproximately(1, 0.000001f); + parsedNormal.Y.Should().BeApproximately(2, 0.000001f); + parsedNormal.Z.Should().BeApproximately(-1, 0.000001f); + } + } + + class NormalDataStoreMock : INormalDataStore + { + public Normal ParsedNormal { get; set; } + + public Normal GetNormal(int i) + { + throw new System.NotImplementedException(); + } + + public void AddNormal(Normal normal) + { + ParsedNormal = normal; + } + } +} \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs b/Engine/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs new file mode 100644 index 0000000..539308b --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs @@ -0,0 +1,67 @@ +using FluentAssertions; +using NUnit.Framework; +using ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace ObjLoader.Test.TypeParsers +{ + [TestFixture] + public class TextureParserTests + { + private TextureDataStoreMock _textureDataStoreMock; + private TextureParser _textureParser; + + [SetUp] + public void SetUp() + { + _textureDataStoreMock = new TextureDataStoreMock(); + + // _textureParser = new TextureParser(_textureDataStoreMock); + } + + [Test] + public void CanParse_returns_true_on_normal_line() + { + const string textureKeyword = "vt"; + + bool canParse = _textureParser.CanParse(textureKeyword); + canParse.Should().BeTrue(); + } + + [Test] + public void CanParse_returns_false_on_non_normal_line() + { + const string invalidKeyword = "vn"; + + bool canParse = _textureParser.CanParse(invalidKeyword); + canParse.Should().BeFalse(); + } + + [Test] + public void Parses_normal_line_correctly() + { + const string textureLine = "0.500 -1.352"; + _textureParser.Parse(textureLine); + + var parsedNormal = _textureDataStoreMock.ParsedTexture; + parsedNormal.X.Should().BeApproximately(0.5f, 0.000001f); + parsedNormal.Y.Should().BeApproximately(-1.352f, 0.000001f); + } + + class TextureDataStoreMock : ITextureDataStore + { + public TextureCoordinate ParsedTexture { get; private set; } + + public TextureCoordinate GetTexture(int i) + { + throw new System.NotImplementedException(); + } + + public void AddTexture(TextureCoordinate texture) + { + ParsedTexture = texture; + } + } + } +} \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs b/Engine/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs new file mode 100644 index 0000000..863fac8 --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs @@ -0,0 +1,57 @@ +using FluentAssertions; +using NUnit.Framework; +using ObjLoader.Loader.TypeParsers; + +namespace ObjLoader.Test.TypeParsers +{ + [TestFixture] + public class UseMaterialParserTests + { + private ElementGroupSpy _elementGroupSpy; + private UseMaterialParser _parser; + + [SetUp] + public void SetUp() + { + _elementGroupSpy = new ElementGroupSpy(); + // _parser = new UseMaterialParser(_elementGroupSpy); + } + + [Test] + public void CanParse_returns_true_on_usemtl_line() + { + const string groupKeyword = "usemtl"; + + bool canParse = _parser.CanParse(groupKeyword); + canParse.Should().BeTrue(); + } + + [Test] + public void CanParse_returns_false_on_non_usemtl_line() + { + const string invalidKeyword = "vt"; + + bool canParse = _parser.CanParse(invalidKeyword); + canParse.Should().BeFalse(); + } + + [Test] + public void Parses_usemtl_line_correctly_1() + { + const string useMtlLine = "materialName"; + _parser.Parse(useMtlLine); + + _elementGroupSpy.MaterialName.Should().BeEquivalentTo("materialName"); + } + + private class ElementGroupSpy // : IElementGroup + { + public string MaterialName { get; private set; } + + public void SetMaterial(string materialName) + { + MaterialName = materialName; + } + } + } +} \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs b/Engine/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs new file mode 100644 index 0000000..ba27316 --- /dev/null +++ b/Engine/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs @@ -0,0 +1,69 @@ +using FluentAssertions; +using NUnit.Framework; +using ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace ObjLoader.Test.TypeParsers +{ + [TestFixture] + public class VertexParserTests + { + private VertexDataStoreMock _vertexDataStoreMock; + private VertexParser _vertexParser; + + [SetUp] + public void SetUp() + { + _vertexDataStoreMock = new VertexDataStoreMock(); + + // _vertexParser = new VertexParser(_vertexDataStoreMock); + } + + [Test] + public void CanParse_returns_false_on_non_vertex_line() + { + const string invalidVertex = "vt"; + + bool canParse = _vertexParser.CanParse(invalidVertex); + canParse.Should().BeFalse(); + } + + [Test] + public void CanParse_returns_true_on_vertex_line() + { + const string vertexLine = "v"; + + bool canParse = _vertexParser.CanParse(vertexLine); + canParse.Should().BeTrue(); + } + + [Test] + public void Parses_vertex_line_correctly() + { + const string vertexLine = "0.123 0.234 0.345"; + _vertexParser.Parse(vertexLine); + + var parsedNormal = _vertexDataStoreMock.ParsedVertex.Position; + parsedNormal.X.Should().BeApproximately(0.123f, 0.000001f); + parsedNormal.Y.Should().BeApproximately(0.234f, 0.000001f); + parsedNormal.Z.Should().BeApproximately(0.345f, 0.000001f); + } + + class VertexDataStoreMock : IVertexDataStore + { + public Vertex ParsedVertex { get; set; } + + public Vertex GetVertex(int i) + { + throw new System.NotImplementedException(); + } + + public void AddVertex(Vertex vertex) + { + ParsedVertex = vertex; + } + } + + } +} \ No newline at end of file diff --git a/Engine/Tests/SharpEngine.Core.Tests/Class1.cs b/Engine/Tests/SharpEngine.Core.Tests/Class1.cs new file mode 100644 index 0000000..ff34b98 --- /dev/null +++ b/Engine/Tests/SharpEngine.Core.Tests/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Core.Tests; + +public class Class1 +{ + +} diff --git a/SharpEngine.Shared/SharpEngine.Shared.csproj b/Engine/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj similarity index 100% rename from SharpEngine.Shared/SharpEngine.Shared.csproj rename to Engine/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj diff --git a/Engine/Tests/SharpEngine.Editor.Tests/Class1.cs b/Engine/Tests/SharpEngine.Editor.Tests/Class1.cs new file mode 100644 index 0000000..2ae551d --- /dev/null +++ b/Engine/Tests/SharpEngine.Editor.Tests/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Editor.Tests; + +public class Class1 +{ + +} diff --git a/Engine/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj b/Engine/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/Engine/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Engine/Tests/SharpEngine.Shared.Net8/Class1.cs b/Engine/Tests/SharpEngine.Shared.Net8/Class1.cs new file mode 100644 index 0000000..e8f6eff --- /dev/null +++ b/Engine/Tests/SharpEngine.Shared.Net8/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Shared.Net8; + +public class Class1 +{ + +} diff --git a/Engine/Tests/SharpEngine.Shared.Net8/SharpEngine.Shared.Net8.csproj b/Engine/Tests/SharpEngine.Shared.Net8/SharpEngine.Shared.Net8.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/Engine/Tests/SharpEngine.Shared.Net8/SharpEngine.Shared.Net8.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Engine/Tests/SharpEngine.Shared.Tests/Class1.cs b/Engine/Tests/SharpEngine.Shared.Tests/Class1.cs new file mode 100644 index 0000000..f2af683 --- /dev/null +++ b/Engine/Tests/SharpEngine.Shared.Tests/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Shared.Tests; + +public class Class1 +{ + +} diff --git a/Engine/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj b/Engine/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj new file mode 100644 index 0000000..fa71b7a --- /dev/null +++ b/Engine/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Engine/Tests/Test.Directory.Build.props b/Engine/Tests/Test.Directory.Build.props new file mode 100644 index 0000000..a52359c --- /dev/null +++ b/Engine/Tests/Test.Directory.Build.props @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/CODE_OF_CONDUCT.md b/Engine/docs/CODE_OF_CONDUCT.md similarity index 100% rename from docs/CODE_OF_CONDUCT.md rename to Engine/docs/CODE_OF_CONDUCT.md diff --git a/docs/CONTRIBUTING.md b/Engine/docs/CONTRIBUTING.md similarity index 100% rename from docs/CONTRIBUTING.md rename to Engine/docs/CONTRIBUTING.md diff --git a/docs/SECURITY.md b/Engine/docs/SECURITY.md similarity index 100% rename from docs/SECURITY.md rename to Engine/docs/SECURITY.md diff --git a/docs/materials_and_inspiration.md b/Engine/docs/materials_and_inspiration.md similarity index 100% rename from docs/materials_and_inspiration.md rename to Engine/docs/materials_and_inspiration.md diff --git a/docs/pull_request_template.md b/Engine/docs/pull_request_template.md similarity index 100% rename from docs/pull_request_template.md rename to Engine/docs/pull_request_template.md diff --git a/Launcher/UI/Components/Dialogs/Notification.razor b/Launcher/UI/Components/Dialogs/Notification.razor deleted file mode 100644 index 8a40e57..0000000 --- a/Launcher/UI/Components/Dialogs/Notification.razor +++ /dev/null @@ -1,29 +0,0 @@ -@using Launcher.Services -@inherits DialogBase - -@inject INotificationService NotificationService - -
-
- @foreach (var notification in NotificationService.Notifications) - { - - } -
-
- -@code { - /// Gets or sets the type of the notification. - [Parameter] - public NotificationType Type { get; set; } = NotificationType.Warning; - - /// - protected override void OnInitialized() - { - NotificationService.SetInvokeAsync(InvokeAsync); - NotificationService.SetOnNotificationsChanged(StateHasChanged); - } -} diff --git a/Launcher/UI/Components/DiscardIcon.razor.css b/Launcher/UI/Components/DiscardIcon.razor.css deleted file mode 100644 index 301f29c..0000000 --- a/Launcher/UI/Components/DiscardIcon.razor.css +++ /dev/null @@ -1,6 +0,0 @@ -span { - cursor: pointer; -} -span:hover { - filter: brightness(0.8); -} diff --git a/Launcher/UI/Components/DiscardIcon.razor.min.css b/Launcher/UI/Components/DiscardIcon.razor.min.css deleted file mode 100644 index 6048a35..0000000 --- a/Launcher/UI/Components/DiscardIcon.razor.min.css +++ /dev/null @@ -1 +0,0 @@ -span{cursor:pointer;}span:hover{filter:brightness(.8);} \ No newline at end of file diff --git a/Launcher/UI/Components/DiscardIcon.razor.scss b/Launcher/UI/Components/DiscardIcon.razor.scss deleted file mode 100644 index f5cb62a..0000000 --- a/Launcher/UI/Components/DiscardIcon.razor.scss +++ /dev/null @@ -1,7 +0,0 @@ -span { - cursor: pointer; - - &:hover { - filter: brightness(0.8); - } -} diff --git a/Launcher/UI/Components/Grid.razor b/Launcher/UI/Components/Grid.razor deleted file mode 100644 index a65a5ef..0000000 --- a/Launcher/UI/Components/Grid.razor +++ /dev/null @@ -1,35 +0,0 @@ -@typeparam TItem - - - - - - - - - - - @foreach (var item in Items) - { - - - @Row(item) - - - } - -
NameLast ModifiedActions
- -@code { - /// - /// Gets or sets the items to be rendered onto the grid. - /// - [Parameter] - public List Items { get; set; } - - /// - /// Gets or sets a template by which each row should be rendered. - /// - [Parameter] - public RenderFragment Row { get; set; } -} \ No newline at end of file diff --git a/Launcher/UI/Components/Layout.razor b/Launcher/UI/Components/Layout.razor deleted file mode 100644 index 21adf40..0000000 --- a/Launcher/UI/Components/Layout.razor +++ /dev/null @@ -1,5 +0,0 @@ -@inherits LayoutComponentBase - -
-@Body -
\ No newline at end of file diff --git a/Launcher/UI/Home.razor b/Launcher/UI/Home.razor deleted file mode 100644 index 35fb873..0000000 --- a/Launcher/UI/Home.razor +++ /dev/null @@ -1,31 +0,0 @@ -@page "/" - -@using Launcher.UI.Components -@using Launcher.UI.Components.Dialogs -@using System.Diagnostics - - - - - -
-

Projects

- -
- - -
-
- -
- - - @context.Name - @context.LastModified - - - - - - -
\ No newline at end of file diff --git a/Launcher/UI/_Imports.razor b/Launcher/UI/_Imports.razor deleted file mode 100644 index 464f868..0000000 --- a/Launcher/UI/_Imports.razor +++ /dev/null @@ -1,12 +0,0 @@ -@using System.Net.Http -@using System.Net.Http.Json - -@using Microsoft.AspNetCore.Components.Forms -@using Microsoft.AspNetCore.Components.Routing -@using Microsoft.AspNetCore.Components.Web -@using Microsoft.AspNetCore.Components.Web.Virtualization -@using Microsoft.JSInterop - -@using Launcher -@using Launcher.UI -@using Launcher.Services \ No newline at end of file diff --git a/Launcher/compilerconfig.json b/Launcher/compilerconfig.json deleted file mode 100644 index 9fe060d..0000000 --- a/Launcher/compilerconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - "outputFile": "UI/Home.css", - "inputFile": "UI/Home.scss" - }, - { - "outputFile": "UI/Home.razor.css", - "inputFile": "UI/Home.razor.scss" - }, - { - "outputFile": "UI/Components/DiscardIcon.razor.css", - "inputFile": "UI/Components/DiscardIcon.razor.scss" - }, - { - "outputFile": "wwwroot/css/additional.razor.css", - "inputFile": "wwwroot/css/additional.razor.scss" - }, - { - "outputFile": "wwwroot/css/additional.css", - "inputFile": "wwwroot/css/additional.scss" - }, - { - "outputFile": "UI/Components/Dialogs/ConfirmDeleteDialog.razor.css", - "inputFile": "UI/Components/Dialogs/ConfirmDeleteDialog.razor.scss" - } -] \ No newline at end of file diff --git a/Portal/Portal.Backend/Controllers/AchievementController.cs b/Portal/Portal.Backend/Controllers/AchievementController.cs new file mode 100644 index 0000000..40157d1 --- /dev/null +++ b/Portal/Portal.Backend/Controllers/AchievementController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using Portal.Database.Models; + +namespace Database.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AchievementController : ControllerBase + { + private readonly ILogger _logger; + + public AchievementController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + public UserAchievements GetUserAchievements() + { + return new(); + } + } +} diff --git a/Portal/Portal.Backend/Controllers/UserController.cs b/Portal/Portal.Backend/Controllers/UserController.cs new file mode 100644 index 0000000..0e0be3f --- /dev/null +++ b/Portal/Portal.Backend/Controllers/UserController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Mvc; +using Portal.Database.Models; + +namespace Database.Controllers +{ + [ApiController] + [Route("[controller]")] + public class UserController : ControllerBase + { + private readonly ILogger _logger; + + public UserController(ILogger logger) + { + _logger = logger; + } + + [HttpGet] + public User Get() + { + return new(); + } + } +} diff --git a/Portal/Portal.Backend/Database.http b/Portal/Portal.Backend/Database.http new file mode 100644 index 0000000..7a874e2 --- /dev/null +++ b/Portal/Portal.Backend/Database.http @@ -0,0 +1,6 @@ +@Database_HostAddress = http://localhost:5088 + +GET {{Database_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Portal/Portal.Backend/Portal.Backend.csproj b/Portal/Portal.Backend/Portal.Backend.csproj new file mode 100644 index 0000000..cde64ec --- /dev/null +++ b/Portal/Portal.Backend/Portal.Backend.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + diff --git a/Portal/Portal.Backend/Program.cs b/Portal/Portal.Backend/Program.cs new file mode 100644 index 0000000..666a9c5 --- /dev/null +++ b/Portal/Portal.Backend/Program.cs @@ -0,0 +1,23 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Portal/Portal.Backend/Properties/launchSettings.json b/Portal/Portal.Backend/Properties/launchSettings.json new file mode 100644 index 0000000..20fd58f --- /dev/null +++ b/Portal/Portal.Backend/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5088", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7063;http://localhost:5088", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Portal/Portal.Backend/Services/AchievementService.cs b/Portal/Portal.Backend/Services/AchievementService.cs new file mode 100644 index 0000000..37a7309 --- /dev/null +++ b/Portal/Portal.Backend/Services/AchievementService.cs @@ -0,0 +1,5 @@ +namespace Portal.Backend.Services; + +public class AchievementService +{ +} diff --git a/Portal/Portal.Backend/appsettings.Development.json b/Portal/Portal.Backend/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Portal/Portal.Backend/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Portal/Portal.Backend/appsettings.json b/Portal/Portal.Backend/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/Portal/Portal.Backend/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Portal/Portal.Database/DatabaseContext.cs b/Portal/Portal.Database/DatabaseContext.cs new file mode 100644 index 0000000..75bbecd --- /dev/null +++ b/Portal/Portal.Database/DatabaseContext.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using Portal.Database.Models; + +public class BloggingContext : DbContext +{ + public DbSet User { get; set; } + public DbSet Achievements { get; set; } + public DbSet UserAchievements { get; set; } + + public BloggingContext() + { + } + + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + + } +} \ No newline at end of file diff --git a/Portal/Portal.Database/Models/Achievement.cs b/Portal/Portal.Database/Models/Achievement.cs new file mode 100644 index 0000000..b9aa35c --- /dev/null +++ b/Portal/Portal.Database/Models/Achievement.cs @@ -0,0 +1,9 @@ +namespace Portal.Database.Models; + +public class Achievement +{ + public Guid Id { get; set; } + public string Title { get; set; } + public string Description { get; set; } + public string IconUrl { get; set; } +} diff --git a/Portal/Portal.Database/Models/User.cs b/Portal/Portal.Database/Models/User.cs new file mode 100644 index 0000000..58e7876 --- /dev/null +++ b/Portal/Portal.Database/Models/User.cs @@ -0,0 +1,10 @@ +namespace Portal.Database.Models; + +public class User +{ + public Guid Id { get; set; } + public string Username { get; set; } + public string Email { get; set; } + public string PasswordHash { get; set; } + public IReadOnlyList Achievements { get; set; } = []; +} diff --git a/Portal/Portal.Database/Models/UserAchievements.cs b/Portal/Portal.Database/Models/UserAchievements.cs new file mode 100644 index 0000000..160f939 --- /dev/null +++ b/Portal/Portal.Database/Models/UserAchievements.cs @@ -0,0 +1,9 @@ +namespace Portal.Database.Models; + +public class UserAchievements +{ + public User User { get; set; } + public Achievement Achievement { get; set; } + public DateTime UnlockedAt { get; set; } + public int Progress { get; set; } +} diff --git a/Portal/Portal.Database/Portal.Database.csproj b/Portal/Portal.Database/Portal.Database.csproj new file mode 100644 index 0000000..1fbe411 --- /dev/null +++ b/Portal/Portal.Database/Portal.Database.csproj @@ -0,0 +1,17 @@ + + + + net10.0 + enable + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Portal/Portal.Dto/Class1.cs b/Portal/Portal.Dto/Class1.cs new file mode 100644 index 0000000..b3f360e --- /dev/null +++ b/Portal/Portal.Dto/Class1.cs @@ -0,0 +1,6 @@ +namespace Portal.Dto; + +public class Class1 +{ + +} diff --git a/Portal/Portal.Dto/Portal.Dto.csproj b/Portal/Portal.Dto/Portal.Dto.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/Portal/Portal.Dto/Portal.Dto.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/Portal/Portal.slnx b/Portal/Portal.slnx new file mode 100644 index 0000000..48b5f58 --- /dev/null +++ b/Portal/Portal.slnx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/SharpEngine.sln b/SharpEngine.sln deleted file mode 100644 index 71f591f..0000000 --- a/SharpEngine.sln +++ /dev/null @@ -1,81 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.14.35906.104 d17.14 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Core", "Core\SharpEngine.Core.csproj", "{9A1984BA-6D84-374A-8BF0-A26A6493FEA9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Editor", "Editor\SharpEngine.Editor.csproj", "{F72A74DC-54F9-0C76-2D40-32D2524683F8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Launcher", "Launcher\Launcher.csproj", "{3751B661-BE0E-9CD7-CA51-361A34450372}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpEngine.Shared", "SharpEngine.Shared\SharpEngine.Shared.csproj", "{F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Minecraft", "Examples\MinecraftClone\Minecraft.csproj", "{E8A75C3B-1D82-8B30-C854-AD35FB123CDC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Minimal", "Examples\Minimal\Minimal.csproj", "{1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{089100B1-113F-4E66-888A-E83F3999EAFD}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - Directory.Build.props = Directory.Build.props - README.md = README.md - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - LauncherNonWindows|Any CPU = LauncherNonWindows|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9A1984BA-6D84-374A-8BF0-A26A6493FEA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9A1984BA-6D84-374A-8BF0-A26A6493FEA9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9A1984BA-6D84-374A-8BF0-A26A6493FEA9}.LauncherNonWindows|Any CPU.ActiveCfg = LauncherNonWindows|Any CPU - {9A1984BA-6D84-374A-8BF0-A26A6493FEA9}.LauncherNonWindows|Any CPU.Build.0 = LauncherNonWindows|Any CPU - {9A1984BA-6D84-374A-8BF0-A26A6493FEA9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9A1984BA-6D84-374A-8BF0-A26A6493FEA9}.Release|Any CPU.Build.0 = Release|Any CPU - {F72A74DC-54F9-0C76-2D40-32D2524683F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F72A74DC-54F9-0C76-2D40-32D2524683F8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F72A74DC-54F9-0C76-2D40-32D2524683F8}.LauncherNonWindows|Any CPU.ActiveCfg = Debug|Any CPU - {F72A74DC-54F9-0C76-2D40-32D2524683F8}.LauncherNonWindows|Any CPU.Build.0 = Debug|Any CPU - {F72A74DC-54F9-0C76-2D40-32D2524683F8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F72A74DC-54F9-0C76-2D40-32D2524683F8}.Release|Any CPU.Build.0 = Release|Any CPU - {3751B661-BE0E-9CD7-CA51-361A34450372}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3751B661-BE0E-9CD7-CA51-361A34450372}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3751B661-BE0E-9CD7-CA51-361A34450372}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {3751B661-BE0E-9CD7-CA51-361A34450372}.LauncherNonWindows|Any CPU.ActiveCfg = Debug|Any CPU - {3751B661-BE0E-9CD7-CA51-361A34450372}.LauncherNonWindows|Any CPU.Build.0 = Debug|Any CPU - {3751B661-BE0E-9CD7-CA51-361A34450372}.LauncherNonWindows|Any CPU.Deploy.0 = Debug|Any CPU - {3751B661-BE0E-9CD7-CA51-361A34450372}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3751B661-BE0E-9CD7-CA51-361A34450372}.Release|Any CPU.Build.0 = Release|Any CPU - {3751B661-BE0E-9CD7-CA51-361A34450372}.Release|Any CPU.Deploy.0 = Release|Any CPU - {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.LauncherNonWindows|Any CPU.ActiveCfg = Release|Any CPU - {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.LauncherNonWindows|Any CPU.Build.0 = Release|Any CPU - {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F0BEECF6-BA81-40E2-AA60-FC5B1539FC65}.Release|Any CPU.Build.0 = Release|Any CPU - {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.LauncherNonWindows|Any CPU.ActiveCfg = Debug|Any CPU - {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.LauncherNonWindows|Any CPU.Build.0 = Debug|Any CPU - {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E8A75C3B-1D82-8B30-C854-AD35FB123CDC}.Release|Any CPU.Build.0 = Release|Any CPU - {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.LauncherNonWindows|Any CPU.ActiveCfg = Debug|Any CPU - {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.LauncherNonWindows|Any CPU.Build.0 = Debug|Any CPU - {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {E8A75C3B-1D82-8B30-C854-AD35FB123CDC} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} - {1BEC06D8-094D-8BC2-288E-E2C4C13F3D2E} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} - EndGlobalSection -EndGlobal