diff --git a/src/EngineSharp.Core/EngineSharp.Core.sln b/src/EngineSharp.Core/EngineSharp.Core.sln index 40d2949..6968ebe 100644 --- a/src/EngineSharp.Core/EngineSharp.Core.sln +++ b/src/EngineSharp.Core/EngineSharp.Core.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineSharp.Core", "EngineS EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineSharp", "..\EngineSharp\EngineSharp.csproj", "{1D984FEE-1A61-4E35-9D00-0264D92A31F4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineSharp.Extensions", "..\EngineSharp.Extensions\EngineSharp.Extensions.csproj", "{61306157-AA01-4899-8F80-50D6E76FEBB0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {1D984FEE-1A61-4E35-9D00-0264D92A31F4}.Debug|Any CPU.Build.0 = Debug|Any CPU {1D984FEE-1A61-4E35-9D00-0264D92A31F4}.Release|Any CPU.ActiveCfg = Release|Any CPU {1D984FEE-1A61-4E35-9D00-0264D92A31F4}.Release|Any CPU.Build.0 = Release|Any CPU + {61306157-AA01-4899-8F80-50D6E76FEBB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61306157-AA01-4899-8F80-50D6E76FEBB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61306157-AA01-4899-8F80-50D6E76FEBB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61306157-AA01-4899-8F80-50D6E76FEBB0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/EngineSharp.Core/EngineSharp.Core/EngineSharp.Core.csproj b/src/EngineSharp.Core/EngineSharp.Core/EngineSharp.Core.csproj index e3c652e..afac08e 100644 --- a/src/EngineSharp.Core/EngineSharp.Core/EngineSharp.Core.csproj +++ b/src/EngineSharp.Core/EngineSharp.Core/EngineSharp.Core.csproj @@ -1,7 +1,7 @@  - net9.0 + net10.0 enable enable true diff --git a/src/EngineSharp.Core/EngineSharp.Core/Rendering/Mesh.cs b/src/EngineSharp.Core/EngineSharp.Core/Rendering/Mesh.cs index 0e624e1..3a6935f 100644 --- a/src/EngineSharp.Core/EngineSharp.Core/Rendering/Mesh.cs +++ b/src/EngineSharp.Core/EngineSharp.Core/Rendering/Mesh.cs @@ -4,9 +4,9 @@ namespace EngineSharp.Core.Rendering; public class Mesh { - public Vector3D[] Vertices { get; set; } - public Vector3D[] Normals { get; set; } - public uint[] Indices { get; set; } + public Vector3D[] Vertices { get; set; } = []; + public Vector3D[] Normals { get; set; } = []; + public uint[] Indices { get; set; } = []; // TODO: Add uv textures but make them nullable; MeshRenderer should then only send uv data to shader if specified diff --git a/src/EngineSharp.Core/EngineSharp.Core/Rendering/MeshRenderer.cs b/src/EngineSharp.Core/EngineSharp.Core/Rendering/MeshRenderer.cs index 180caf3..ab1f974 100644 --- a/src/EngineSharp.Core/EngineSharp.Core/Rendering/MeshRenderer.cs +++ b/src/EngineSharp.Core/EngineSharp.Core/Rendering/MeshRenderer.cs @@ -5,7 +5,7 @@ namespace EngineSharp.Core.Rendering; public class MeshRenderer : RenderComponent { - public Mesh Mesh { get; set; } + public required Mesh Mesh { get; set; } private uint vao, vbo, ebo; diff --git a/src/EngineSharp.Extensions/EngineSharp.Extensions.csproj b/src/EngineSharp.Extensions/EngineSharp.Extensions.csproj new file mode 100644 index 0000000..7bb8277 --- /dev/null +++ b/src/EngineSharp.Extensions/EngineSharp.Extensions.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/src/EngineSharp.Extensions/VectorExtensions.cs b/src/EngineSharp.Extensions/VectorExtensions.cs new file mode 100644 index 0000000..46021a4 --- /dev/null +++ b/src/EngineSharp.Extensions/VectorExtensions.cs @@ -0,0 +1,55 @@ +using Silk.NET.Maths; + +namespace EngineSharp.Extensions; + +public static class VectorExtensions +{ + extension(Vector3D) + where T : unmanaged, IFormattable, IEquatable, IComparable + { + public static Vector3D Slerp(Vector3D start, Vector3D end, float percent) + { + // the cosine of the angle between 2 vectors. + var dot = Vector3D.Dot(start, end); + + // Clamp it to be in the range of Acos() + // This may be unnecessary, but floating point precision can be a fickle mistress. + Math.Clamp(dot, -1.0f, 1.0f); + + // Acos(dot) returns the angle between start and end, + // And multiplying that by percent returns the angle between start and the final result. + var theta = (float)Math.Acos(dot) * percent; + var relativeVec = end - start * dot; + relativeVec = Vector3D.Normalize(relativeVec); + + // Orthonormal basis + // The final result. + return ((start * (float)Math.Cos(theta)) + (relativeVec * (float)Math.Sin(theta))); + } + } + + extension(Vector3D vector) + // where T : unmanaged, IFormattable, IEquatable, IComparable + { + public Vector3D Slerp(Vector3D end, float percent) + { + // the cosine of the angle between 2 vectors. + var dot = Vector3D.Dot(vector, end); + + // Clamp it to be in the range of Acos() + // This may be unnecessary, but floating point precision can be a fickle mistress. + // Math.Clamp(dot, -1.0f, 1.0f); + + + // Acos(dot) returns the angle between start and end, + // And multiplying that by percent returns the angle between start and the final result. + var theta = Math.Acos(dot) * percent; + var relativeVec = end - vector * dot; + relativeVec = Vector3D.Normalize(relativeVec); + + // Orthonormal basis + // The final result. + return ((vector * (float)Math.Cos(theta)) + (relativeVec * (float)Math.Sin(theta))); + } + } +} \ No newline at end of file diff --git a/src/EngineSharp/EngineSharp.csproj b/src/EngineSharp/EngineSharp.csproj index 440c32e..edb13ba 100644 --- a/src/EngineSharp/EngineSharp.csproj +++ b/src/EngineSharp/EngineSharp.csproj @@ -2,16 +2,18 @@ Exe - net9.0 + net10.0 enable enable + + diff --git a/src/EngineSharp/IcoSphere.cs b/src/EngineSharp/IcoSphere.cs new file mode 100644 index 0000000..b720e7f --- /dev/null +++ b/src/EngineSharp/IcoSphere.cs @@ -0,0 +1,182 @@ +using EngineSharp.Core.Rendering; +using EngineSharp.Extensions; +using Silk.NET.Maths; + +namespace Engine_silk.NET; + +public class IcoSphere +{ + private readonly IcoSphereGenerator _generator = new(); + + public required int Resolution + { + get; + set + { + if (value < 2) + { + throw new ArgumentOutOfRangeException(nameof(value), value, + "Resolution must be greater than 1"); + } + + field = value; + } + } + + public Mesh CreateSphere() + { + var mesh = new Mesh(); + _generator.Generate(Resolution); + + mesh.Vertices = _generator.Vertices; + mesh.Indices = Array.ConvertAll(_generator.Triangles, x => (uint)x); + mesh.CalculateNormals(); + + return mesh; + } +} + +// Thank you Sebastian Lague +public class IcoSphereGenerator +{ + + // Output: + public Vector3D[] Vertices => vertices?.Items ?? []; + public int[] Triangles => triangles?.Items ?? []; + + // Internal: + FixedSizeList>? vertices; + FixedSizeList? triangles; + int numDivisions; + int numVertsPerFace; + + // Indices of the vertex pairs that make up each of the initial 12 edges + static readonly int[] VertexPairs = [0, 1, 0, 2, 0, 3, 0, 4, 1, 2, 2, 3, 3, 4, 4, 1, 5, 1, 5, 2, 5, 3, 5, 4]; + // Indices of the edge triplets that make up the initial 8 faces + static readonly int[] EdgeTriplets = [0, 1, 4, 1, 2, 5, 2, 3, 6, 3, 0, 7, 8, 9, 4, 9, 10, 5, 10, 11, 6, 11, 8, 7]; + // The six initial vertices up left back right forward down + static readonly Vector3D[] BaseVertices = [Vector3D.UnitY, -Vector3D.UnitX, -Vector3D.UnitZ, Vector3D.UnitX, Vector3D.UnitZ, -Vector3D.UnitY]; + + public void Generate (int resolution) { + numDivisions = Math.Max (0, resolution); + numVertsPerFace = ((numDivisions + 3) * (numDivisions + 3) - (numDivisions + 3)) / 2; + int numVerts = numVertsPerFace * 8 - (numDivisions + 2) * 12 + 6; + int numTrisPerFace = (numDivisions + 1) * (numDivisions + 1); + + vertices = new FixedSizeList> (numVerts); + triangles = new FixedSizeList (numTrisPerFace * 8 * 3); + + vertices.AddRange (BaseVertices); + + // Create 12 edges, with n vertices added along them (n = numDivisions) + var edges = new Edge[12]; + for (var i = 0; i < VertexPairs.Length; i += 2) { + var startVertex = vertices.Items[VertexPairs[i]]; + var endVertex = vertices.Items[VertexPairs[i + 1]]; + + var edgeVertexIndices = new int[numDivisions + 2]; + edgeVertexIndices[0] = VertexPairs[i]; + + // Add vertices along edge + for (var divisionIndex = 0; divisionIndex < numDivisions; divisionIndex++) { + var t = (divisionIndex + 1f) / (numDivisions + 1f); + edgeVertexIndices[divisionIndex + 1] = vertices.NextIndex; + vertices.Add (startVertex.Slerp(endVertex, t)); + } + edgeVertexIndices[numDivisions + 1] = VertexPairs[i + 1]; + var edgeIndex = i / 2; + edges[edgeIndex] = new Edge (edgeVertexIndices); + } + + // Create faces + for (var i = 0; i < EdgeTriplets.Length; i += 3) { + var faceIndex = i / 3; + var reverse = faceIndex >= 4; + CreateFace (edges[EdgeTriplets[i]], edges[EdgeTriplets[i + 1]], edges[EdgeTriplets[i + 2]], reverse); + } + } + + private void CreateFace (Edge sideA, Edge sideB, Edge bottom, bool reverse) + { + if (vertices is null) throw new ArgumentException("Vertices cannot be null", nameof(vertices)); + if (triangles is null) throw new ArgumentException("triangles cannot be null", nameof(triangles)); + + var numPointsInEdge = sideA.VertexIndices.Length; + var vertexMap = new FixedSizeList (numVertsPerFace); + vertexMap.Add (sideA.VertexIndices[0]); // top of triangle + + for (var i = 1; i < numPointsInEdge - 1; i++) { + // Side A vertex + vertexMap.Add (sideA.VertexIndices[i]); + + // Add vertices between sideA and sideB + var sideAVertex = vertices.Items[sideA.VertexIndices[i]]; + var sideBVertex = vertices.Items[sideB.VertexIndices[i]]; + var numInnerPoints = i - 1; + for (var j = 0; j < numInnerPoints; j++) { + var t = (j + 1f) / (numInnerPoints + 1f); + vertexMap.Add (vertices.NextIndex); + vertices.Add (sideAVertex.Slerp(sideBVertex, t)); + } + + // Side B vertex + vertexMap.Add (sideB.VertexIndices[i]); + } + + // Add bottom edge vertices + for (var i = 0; i < numPointsInEdge; i++) { + vertexMap.Add (bottom.VertexIndices[i]); + } + + // Triangulate + var numRows = numDivisions + 1; + for (var row = 0; row < numRows; row++) { + // vertices down left edge follow quadratic sequence: 0, 1, 3, 6, 10, 15... + // the nth term can be calculated with: (n^2 - n)/2 + var topVertex = ((row + 1) * (row + 1) - row - 1) / 2; + var bottomVertex = ((row + 2) * (row + 2) - row - 2) / 2; + + var numTrianglesInRow = 1 + 2 * row; + for (var column = 0; column < numTrianglesInRow; column++) { + int v0, v1, v2; + + v0 = topVertex; + if (column % 2 == 0) { + v1 = bottomVertex + 1; + v2 = bottomVertex; + topVertex++; + bottomVertex++; + } else { + v1 = bottomVertex; + v2 = topVertex - 1; + } + + triangles.Add (vertexMap.Items[v0]); + triangles.Add (vertexMap.Items[(reverse) ? v2 : v1]); + triangles.Add (vertexMap.Items[(reverse) ? v1 : v2]); + } + } + + } + + // Convenience classes: + + private record Edge(int[] VertexIndices); + + private class FixedSizeList(int size) + { + public readonly T[] Items = new T[size]; + public int NextIndex; + + public void Add (T item) { + Items[NextIndex] = item; + NextIndex++; + } + + public void AddRange (IEnumerable items) { + foreach (var item in items) { + Add (item); + } + } + } +} \ No newline at end of file diff --git a/src/EngineSharp/Program.cs b/src/EngineSharp/Program.cs index af71db6..a7001b5 100644 --- a/src/EngineSharp/Program.cs +++ b/src/EngineSharp/Program.cs @@ -1,4 +1,6 @@ -using EngineSharp.Core; +using Engine_silk.NET; +using EngineSharp.Core; +using EngineSharp.Core.Rendering; using Silk.NET.Maths; using Silk.NET.Windowing; using GraphicsAPI = EngineSharp.Core.GraphicsAPI; @@ -16,6 +18,21 @@ static class Program }; var engine = EngineFactory.Create(GraphicsAPI.OpenGL, options); + var mainScene = engine.CreateScene(); + var cube = mainScene.CreateEntity("cube"); + var sphereGenerator = new IcoSphere + { + Resolution = 10 + }; + var cubeMeshRenderer = new MeshRenderer + { + Mesh = sphereGenerator.CreateSphere(), + }; + + cube.AddComponent(cubeMeshRenderer); + + // TODO: ensure that model matrix etc. will be set correctly on rendering, so that the icosphere can actually be rendered + engine.Start(); } } \ No newline at end of file