improved architecture; made first steps to compiling shaders on engine startup and not JIT

This commit is contained in:
2025-12-14 12:28:45 +01:00
parent 739d2e8daf
commit bde20fbf01
23 changed files with 280 additions and 100 deletions

View File

@ -1,9 +1,7 @@
namespace EngineSharp.Core.ECS;
// TODO: A LogicComponent would have a list of Components. A LogicComponent is basically what will be stored in the scene graph and all components of all LogicComponents will have their Update methods etc. be called
public abstract class LogicComponent : IComponent
public abstract class LogicComponent : IComponent // Kinda like Unity's MonoBehaviour
{
public abstract void Start();
public abstract void Initialise();
public abstract void OnUpdate(double deltaTime);
}

View File

@ -1,8 +1,9 @@
using Silk.NET.OpenGL;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
namespace EngineSharp.Core.ECS;
public abstract class RenderComponent : IComponent
{
internal abstract void Render(GL gl);
internal abstract void Render(GL gl, Matrix4X4<float> projectionMatrix, Matrix4X4<float> viewMatrix, Matrix4X4<float> modelMatrix);
}

View File

@ -1,11 +1,13 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
namespace EngineSharp.Core.ECS;
public class Scene
{
// TODO: Maybe instead of a List, use a HashSet instead. Maybe implement a equality comparer to ensure, only one element per type can be present
// TODO: Maybe instead of a List, use a HashSet. Implement a equality comparer to ensure, only one element per type can be present
private readonly Dictionary<long, List<LogicComponent>> _logicComponents = new();
private readonly Dictionary<long, List<DataComponent>> _dataComponents = new();
private readonly Dictionary<long, List<RenderComponent>> _renderComponents = new();
@ -20,11 +22,11 @@ public class Scene
};
}
internal void StartComponents()
internal void InitialiseComponents()
{
foreach (var components in _logicComponents.Values)
{
components.ForEach(c => c.Start());
components.ForEach(c => c.Initialise());
}
}
@ -36,11 +38,14 @@ public class Scene
}
}
internal void RenderComponents(GL gl)
internal void RenderComponents(GL gl, Matrix4X4<float> projectionMatrix, Matrix4X4<float> viewMatrix)
{
foreach (var components in _renderComponents.Values)
{
components.ForEach(c => c.Render(gl));
// TODO: retrieve the transform component of the entity to generate the model matrix
// TODO: make a record which holds the matrices and maybe more, so the parameter list doesn't explode
var modelMatrix = Matrix4X4<float>.Identity;
components.ForEach(c => c.Render(gl, projectionMatrix, viewMatrix, modelMatrix));
}
}

View File

@ -1,4 +1,6 @@
namespace EngineSharp.Core;
using System;
namespace EngineSharp.Core;
public static class NumberExtensions
{

View File

@ -1,4 +1,5 @@
using Silk.NET.Maths;
using System;
using Silk.NET.Maths;
namespace EngineSharp.Core;

View File

@ -1,4 +1,6 @@
using EngineSharp.Core.ECS;
using System;
using EngineSharp.Core.ECS;
using EngineSharp.Core.Rendering;
using Silk.NET.Windowing;
namespace EngineSharp.Core;
@ -9,6 +11,8 @@ public interface IEngine
void Stop();
Scene CreateScene();
Shader CreateShader(string vertexShaderPath, string fragmentShaderPath);
}
public enum GraphicsAPI

View File

@ -1,4 +1,6 @@
using Silk.NET.Input;
using System.Collections.Generic;
using System.Linq;
using Silk.NET.Input;
namespace EngineSharp.Core;

View File

@ -1,17 +1,19 @@
using System.Drawing;
using EngineSharp.Core.ECS;
using EngineSharp.Core.Rendering;
using Silk.NET.Input;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
using Shader = EngineSharp.Core.Rendering.Shader;
namespace EngineSharp.Core;
internal class OpenGLEngine : IEngine
internal class OpenGLEngine : Engine
{
private readonly IWindow _window;
private readonly List<Scene> _scenes;
private GL _gl = null!; // because between constructing the engine and OnLoad being called nothing should be able to call these fields. Therefore, we do this to get rid of warnings
private GL _gl = null!; // because between constructing the engine and OnLoad being called nothing should call these fields. Therefore, we do "!" to get rid of warnings
private Input _input = null!;
private PerspectiveCamera _camera = null!;
private Scene? _currentScene;
@ -24,30 +26,52 @@ internal class OpenGLEngine : IEngine
_window.Render += OnRender;
_window.Resize += OnResize;
_scenes = new();
_scenes = [];
}
public void Start()
public override void Start()
{
_window.Run();
}
public void Stop()
public override void Stop()
{
_window.Close();
}
public Scene CreateScene()
public override Scene CreateScene()
{
var scene = new Scene();
_scenes.Add(scene);
return scene;
}
public override Shader CreateShader(string vertexShaderPath, string fragmentShaderPath)
{
if (_currentScene is null)
{
throw new InvalidOperationException("Cannot create a shader if there is no scene set as the current scene.");
}
var shader = new Shader(vertexShaderPath, fragmentShaderPath);
RegisterShader(_currentScene, shader);
return shader;
}
protected override void InitialiseShaders(Scene scene)
{
if (_shaders.TryGetValue(scene, out var shaders))
{
shaders.ForEach(s => s.Initialize(_gl));
}
}
public void SetCurrentScene(Scene scene)
{
_currentScene = scene;
_currentScene.StartComponents();
_currentScene.InitialiseComponents();
InitialiseShaders(scene);
}
private void OnLoad()
@ -68,10 +92,7 @@ internal class OpenGLEngine : IEngine
_window.Close();
}
if (_currentScene is not null)
{
_currentScene.UpdateComponents(deltaTime);
}
_currentScene?.UpdateComponents(deltaTime);
}
private void OnRender(double deltaTime)
@ -79,15 +100,13 @@ internal class OpenGLEngine : IEngine
var projectionMatrix = _camera.ProjectionMatrix;
var viewMatrix = _camera.ViewMatrix;
// TODO: Here render all meshes etc.
// MeshRenderer contains a mesh; Mesh contains a material; Material contains a shader and the values for all uniforms of the shader (apart from the matrices; I am not sure how to best handle the model matrix with this approach)
_currentScene?.RenderComponents(_gl);
_currentScene?.RenderComponents(_gl, projectionMatrix, viewMatrix);
}
private void OnResize(Vector2D<int> newDimensions)
{
// when using an entity component system the camera should probably read the aspect ratio from a central location or should listen to the "Resize" event rather than the engine updating the aspect on the camera
// when using an entity component system the camera should probably read the aspect ratio from a central location or should listen to the "Resize" event rather than the engine updating the aspect on the camera.
// Especially if more than one camera should be able to exist simultaneously
_camera.UpdateAspect(newDimensions);
}
}

View File

@ -1,4 +1,5 @@
using Silk.NET.Maths;
using System;
using Silk.NET.Maths;
namespace EngineSharp.Core;

View File

@ -0,0 +1,28 @@
using EngineSharp.Core.ECS;
namespace EngineSharp.Core.Rendering;
internal abstract class Engine : IEngine
{
protected readonly Dictionary<Scene, List<Shader>> _shaders = [];
public abstract void Start();
public abstract void Stop();
public abstract Scene CreateScene();
public abstract Shader CreateShader(string vertexShaderPath, string fragmentShaderPath);
protected void RegisterShader(Scene scene, Shader shader)
{
if (_shaders.TryGetValue(scene, out var shaders))
{
shaders.Add(shader);
}
else
{
_shaders.Add(scene, [shader]);
}
}
protected abstract void InitialiseShaders(Scene scene);
}

View File

@ -1,4 +1,5 @@
using Silk.NET.OpenGL;
using System;
using Silk.NET.OpenGL;
namespace EngineSharp.Core.Rendering.Exceptions;

View File

@ -1,4 +1,6 @@
namespace EngineSharp.Core.Rendering.Exceptions;
using System;
namespace EngineSharp.Core.Rendering.Exceptions;
public class ShaderLinkException : Exception
{

View File

@ -1,4 +1,5 @@
using Silk.NET.Maths;
using Silk.NET.OpenGL;
namespace EngineSharp.Core.Rendering;
@ -6,44 +7,44 @@ public class Material
{
public required Shader Shader { get; init; }
internal void UseShader() => Shader.Use();
internal void UseShader() => Shader.Use();
#region Set uniforms
// All of this just relays to the shader. While it is a lot of "unnecessary" code it ensures that everything unsafe of OpenGL/Vulkan specific is abstracted away
public void SetInt(string name, int value)
{
UseShader();
Shader.Use();
Shader.SetInt(name, value);
}
public void SetFloat(string name, float value)
{
UseShader();
Shader.Use();
Shader.SetFloat(name, value);
}
public void SetVector(string name, Vector3D<double> value)
{
UseShader();
Shader.Use();
Shader.SetVector(name, value);
}
public void SetVector(string name, Vector3D<float> value)
{
UseShader();
Shader.Use();
Shader.SetVector(name, value);
}
public void SetMatrix(string name, Matrix4X4<double> matrix)
{
UseShader();
Shader.Use();
Shader.SetMatrix(name, matrix);
}
public void SetMatrix(string name, Matrix4X4<float> matrix)
{
UseShader();
Shader.Use();
Shader.SetMatrix(name, matrix);
}

View File

@ -1,4 +1,5 @@
using Silk.NET.Maths;
using Silk.NET.OpenGL;
namespace EngineSharp.Core.Rendering;
@ -44,5 +45,13 @@ public class Mesh
}
// This is probably the location where the model, view, projection matrices will be set!
internal void PrepareForRendering() => Material.UseShader();
internal void PrepareForRendering(Matrix4X4<float> projectionMatrix, Matrix4X4<float> viewMatrix, Matrix4X4<float> modelMatrix)
{
Material.UseShader();
// We would also need the inverse (see comment in any of the shaders) and probably other stuff. This other stuff might be automatically added before compiling the shader. kinda like an include for ALL shaders
Material.SetMatrix("projectionMatrix", projectionMatrix);
Material.SetMatrix("viewMatrix", viewMatrix);
Material.SetMatrix("modelMatrix", modelMatrix);
}
}

View File

@ -1,4 +1,6 @@
using EngineSharp.Core.ECS;
using System;
using EngineSharp.Core.ECS;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
namespace EngineSharp.Core.Rendering;
@ -9,14 +11,14 @@ public class MeshRenderer : RenderComponent
private uint vao, vbo, ebo;
internal override void Render(GL gl)
internal override void Render(GL gl, Matrix4X4<float> projectionMatrix, Matrix4X4<float> viewMatrix, Matrix4X4<float> modelMatrix)
{
GenerateRenderableMesh(gl);
GenerateRenderableMesh(gl, projectionMatrix, viewMatrix, modelMatrix);
gl.BindVertexArray(vao);
gl.DrawElements(PrimitiveType.Triangles, (uint)Mesh.Indices.Length, DrawElementsType.UnsignedInt, 0);
}
private void GenerateRenderableMesh(GL gl)
private void GenerateRenderableMesh(GL gl, Matrix4X4<float> projectionMatrix, Matrix4X4<float> viewMatrix, Matrix4X4<float> modelMatrix)
{
if(vao == 0) { vao = gl.CreateVertexArray(); }
gl.BindVertexArray(vao);
@ -24,7 +26,7 @@ public class MeshRenderer : RenderComponent
if(vbo == 0) { vbo = gl.GenBuffer(); }
if(ebo == 0) { ebo = gl.GenBuffer(); }
Mesh.PrepareForRendering();
Mesh.PrepareForRendering(projectionMatrix, viewMatrix, modelMatrix);
var meshData = new float[Mesh.Vertices.Length * 3 + Mesh.Indices.Length * 3];
for (int i = 0, insert = 0; i < Mesh.Vertices.Length; i++, insert += 6)
{

View File

@ -1,4 +1,5 @@
using EngineSharp.Core.Rendering.Exceptions;
using System.IO;
using EngineSharp.Core.Rendering.Exceptions;
using Silk.NET.Maths;
using Silk.NET.OpenGL;
@ -9,55 +10,102 @@ namespace EngineSharp.Core.Rendering;
// I don't know if Vulkan also uses an ID as a uint but I think so. If so, this change would decouple the shader from OpenGL (but the question is, do I want to decouple the shader? Maybe not, as this could then couple the material to OpenGL)
public class Shader
{
private readonly GL _gl; // not as a variable. Add it as a parameter to the methods probably (in the future it should be possible to compile the shaders beforehand. if context is passed as parameter, all shaders are JIT compiled which isn't great
private readonly uint _shaderProgramId;
private readonly string _vertexCode, _fragmentCode;
private GL? _gl; // not as a variable. In the future it should be possible to compile the shaders beforehand. Right now, all shaders are JIT compiled which isn't great
private uint? _shaderProgramId;
public Shader(GL openGLContext, string pathToVertexShader, string pathToFragmentShader)
// TODO: Make this public. The paths are then used as an index into a dictionary. This dictionary stores the shaderProgramId (maybe only use one path as an index, as the other shader should have the same name, just with a different file ending)
// With this, the shaders can be compiled once the engine is started (shaders must be in folder "assets/shaders"). With this I don't have to worry how I can create the shader class and make it compile the shader etc.
// Maybe the dict needs to also store the GL context so the shader can be used ans values can be set on it, but we will see
internal Shader(string pathToVertexShader, string pathToFragmentShader)
{
_gl = openGLContext;
var vertexCode = File.ReadAllText(pathToVertexShader);
var fragmentCode = File.ReadAllText(pathToFragmentShader);
var vertexShader = CompileShader(vertexCode, ShaderType.VertexShader);
var fragmentShader = CompileShader(fragmentCode, ShaderType.FragmentShader);
_shaderProgramId = CreateProgram(vertexShader, fragmentShader);
_vertexCode = File.ReadAllText(pathToVertexShader);
_fragmentCode = File.ReadAllText(pathToFragmentShader);
}
internal void Use() => _gl.UseProgram(_shaderProgramId);
#region Set uniforms
internal void SetInt(string name, int value)
/// <summary>
/// Initialises the shader, by compiling it and making it ready to be used in the application. <br/>
/// Only initialises the first time it is called. <b>Is a NoOp every subsequent call.</b>
/// </summary>
/// <param name="glContext"></param>
internal void Initialize(GL glContext)
{
_gl.Uniform1(_gl.GetUniformLocation(_shaderProgramId, name), value);
if (_gl is not null || _shaderProgramId is not null)
{
return;
}
_gl = glContext;
var vertexShader = CompileShader(_vertexCode, ShaderType.VertexShader);
var fragmentShader = CompileShader(_fragmentCode, ShaderType.FragmentShader);
_shaderProgramId = CreateProgram(vertexShader, fragmentShader);
}
internal void SetFloat(string name, float value)
/// <summary>
/// Can only be called after <see cref="Initialize"/> has been called.
/// </summary>
internal void Use()
{
_gl.Uniform1(_gl.GetUniformLocation(_shaderProgramId, name), value);
}
internal void SetVector(string name, Vector3D<double> value)
{
_gl.Uniform3(_gl.GetUniformLocation(_shaderProgramId, name), value.X, value.Y, value.Z);
}
internal void SetVector(string name, Vector3D<float> value)
{
_gl.Uniform3(_gl.GetUniformLocation(_shaderProgramId, name), value.X, value.Y, value.Z);
if (_shaderProgramId is null || _gl is null)
{
throw new InvalidOperationException("This shader has not been initialized and can therefore not be used.");
}
_gl.UseProgram(_shaderProgramId.Value);
}
#region Set uniforms
/// <summary>
/// Call <see cref="Use"/> before calling this method, otherwise the value might be set for the wrong shader.
/// </summary>
internal void SetInt(string name, int value)
{
_gl!.Uniform1(_gl.GetUniformLocation(_shaderProgramId!.Value, name), value);
}
/// <summary>
/// Call <see cref="Use"/> before calling this method, otherwise the value might be set for the wrong shader.
/// </summary>
internal void SetFloat(string name, float value)
{
_gl!.Uniform1(_gl.GetUniformLocation(_shaderProgramId!.Value, name), value);
}
/// <summary>
/// Call <see cref="Use"/> before calling this method, otherwise the value might be set for the wrong shader.
/// </summary>
internal void SetVector(string name, Vector3D<double> value)
{
_gl!.Uniform3(_gl.GetUniformLocation(_shaderProgramId!.Value, name), value.X, value.Y, value.Z);
}
/// <summary>
/// Call <see cref="Use"/> before calling this method, otherwise the value might be set for the wrong shader.
/// </summary>
internal void SetVector(string name, Vector3D<float> value)
{
_gl!.Uniform3(_gl.GetUniformLocation(_shaderProgramId!.Value, name), value.X, value.Y, value.Z);
}
/// <summary>
/// Call <see cref="Use"/> before calling this method, otherwise the value might be set for the wrong shader.
/// </summary>
internal void SetMatrix(string name, Matrix4X4<double> matrix)
{
unsafe
{
_gl.UniformMatrix4(_gl.GetUniformLocation(_shaderProgramId, name), 1, false, (double*) &matrix);
_gl!.UniformMatrix4(_gl.GetUniformLocation(_shaderProgramId!.Value, name), 1, false, (double*) &matrix);
}
}
/// <summary>
/// Call <see cref="Use"/> before calling this method, otherwise the value might be set for the wrong shader.
/// </summary>
internal void SetMatrix(string name, Matrix4X4<float> matrix)
{
unsafe
{
_gl.UniformMatrix4(_gl.GetUniformLocation(_shaderProgramId, name), 1, false, (float*) &matrix);
_gl!.UniformMatrix4(_gl.GetUniformLocation(_shaderProgramId!.Value, name), 1, false, (float*) &matrix);
}
}
#endregion

View File

@ -1,4 +1,5 @@
using Silk.NET.Maths;
using System;
using Silk.NET.Maths;
namespace EngineSharp.Extensions;

View File

@ -17,4 +17,8 @@
<PackageReference Include="Silk.NET.Windowing" Version="2.22.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="assets\" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,6 @@
using EngineSharp.Core.Rendering;
using System;
using System.Collections.Generic;
using EngineSharp.Core.Rendering;
using EngineSharp.Extensions;
using Silk.NET.Maths;

31
src/EngineSharp/Planet.cs Normal file
View File

@ -0,0 +1,31 @@
using Engine_silk.NET;
using EngineSharp.Core.Rendering;
namespace EngineSharp;
public class Planet : Core.ECS.LogicComponent
{
public MeshRenderer PlanetMesh;
public override void Initialise()
{
// var material = new Material
// {
// Shader = shader,
// };
// var sphereGenerator = new IcoSphere
// {
// Resolution = 10,
// Material = material,
// };
// PlanetMesh = new MeshRenderer
// {
// Mesh = sphereGenerator.CreateSphere(),
// };
}
public override void OnUpdate(double deltaTime)
{
// Nothing to do at the moment
}
}

View File

@ -7,9 +7,9 @@ using GraphicsAPI = EngineSharp.Core.GraphicsAPI;
namespace EngineSharp;
static class Program
internal static class Program
{
static void Main(string[] args)
private static void Main(string[] args)
{
var options = WindowOptions.Default with
{
@ -19,26 +19,11 @@ static class Program
var engine = EngineFactory.Create(GraphicsAPI.OpenGL, options);
var mainScene = engine.CreateScene();
var cube = mainScene.CreateEntity("cube");
var planet = mainScene.CreateEntity("planet");
var shader = new Shader(null, "", ""); // make a factory in IEngine so I don't have to pass graphics library specific values
var material = new Material
{
Shader = shader,
};
var sphereGenerator = new IcoSphere
{
Resolution = 10,
Material = material,
};
var cubeMeshRenderer = new MeshRenderer
{
Mesh = sphereGenerator.CreateSphere(),
};
var shader = engine.CreateShader("./assets/shaders/sphere.vert", "./assets/shaders/sphere.frag");
cube.AddComponent(cubeMeshRenderer);
// TODO: ensure that model matrix etc. will be set correctly on rendering, so that the icosphere can actually be rendered
planet.AddComponent(new Planet());
engine.Start();
}

View File

@ -0,0 +1,13 @@
#version 330 core
out vec4 FragColour;
in vec3 FragPos;
in vec3 Normal;
void main()
{
vec3 col = vec3(1.0, 0.5, 0.2) * FragPos;
FragColour = vec4(col, 1);
}

View File

@ -0,0 +1,20 @@
#version 330 core
layout (location = 0) in vec3 aPosition;
layout (location = 1) in vec3 normalVector;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
out vec3 FragPos;
out vec3 Normal;
void main()
{
vec4 pos = vec4(aPosition, 1.0);
// TODO: calculate the inverse of the model matrix beforehand since "inverse()" is very costly to calculate for every vertex
Normal = mat3(transpose(inverse(modelMatrix))) * normalVector;
FragPos = vec3(modelMatrix * pos);
gl_Position = projectionMatrix * viewMatrix * modelMatrix * pos;
}