From 29cb24b9d1ab0dd02f71427b795adf2d502c992e Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Mon, 5 Jan 2026 12:56:32 +0200 Subject: [PATCH 01/10] #123 Remove extra pipelines --- .github/workflows/README.md | 36 ---------- ...tic-web-apps-victorious-moss-06d128203.yml | 59 ----------------- .github/workflows/publish-asset-store.yml | 65 ------------------- .github/workflows/publish-docs.yml | 16 ----- .github/workflows/publish-launcher.yml | 16 ----- .github/workflows/publish-web.yml | 49 -------------- 6 files changed, 241 deletions(-) delete mode 100644 .github/workflows/README.md delete mode 100644 .github/workflows/azure-static-web-apps-victorious-moss-06d128203.yml delete mode 100644 .github/workflows/publish-asset-store.yml delete mode 100644 .github/workflows/publish-docs.yml delete mode 100644 .github/workflows/publish-launcher.yml delete mode 100644 .github/workflows/publish-web.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md deleted file mode 100644 index 8f5aa5e..0000000 --- a/.github/workflows/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# SharpEngine Pipelines - -## Pull Request pipelines - - - -## Deployment Pipelines - -SharpEngine consists of 5 different deployment pipelines: -- Publish Core -- Publish Asset Store -- Publish Web -- Publish Docs -- Publish Launcher - -All of these pipelines are responsible of building and exposing the respective components of the system. - -### Publish Core -The `publish-core.yml` creates a zip file of the engine and adds a tag to github and the zip to the releases section. That tag is also added for the commit that it was built off. If a tag for the commit this is being run-off is already found, we don't want to create another copy of the essentially the same version so we skip it. - -### Publish Asset Store -The `publish-asset-store.yml` is responsible of deploying the Asset Store web interface, API and Database into Azure. - -All Asset Store's resources can be found under the `rg-asset-store` resource group. -TODO: Enable from selected access ips - -### Publish Web -The `publish-web.yml` is responsible of (similarly to the Asset Store) deploying the SharpEngine web interface, API and Database into Azure. - -All SharpEngine Web's resources can be found under the `rg-sharpengine-web` resource group. - -### Publish Docs -The `publish-docs.yml` deploys the documentation files under the ´./docs´ in the root of the repository into the `docs.sharpengine.com` site. - -### Publish Launcher -The `` \ No newline at end of file diff --git a/.github/workflows/azure-static-web-apps-victorious-moss-06d128203.yml b/.github/workflows/azure-static-web-apps-victorious-moss-06d128203.yml deleted file mode 100644 index 3bc08ab..0000000 --- a/.github/workflows/azure-static-web-apps-victorious-moss-06d128203.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Azure Static Web Apps CI/CD - -on: - push: - branches: - - main - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main - -jobs: - build_and_deploy_job: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') - runs-on: ubuntu-latest - name: Build and Deploy Job - permissions: - id-token: write - contents: read - steps: - - uses: actions/checkout@v3 - with: - submodules: true - lfs: false - - name: Install OIDC Client from Core Package - run: npm install @actions/core@1.6.0 @actions/http-client - - name: Get Id Token - uses: actions/github-script@v6 - id: idtoken - with: - script: | - const coredemo = require('@actions/core') - return await coredemo.getIDToken() - result-encoding: string - - name: Build And Deploy - id: builddeploy - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_VICTORIOUS_MOSS_06D128203 }} - action: "upload" - ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### - # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "./Portal/sharpengine-web-ui" # App source code path - # api_location: "" # Api source code path - optional - output_location: "build" # Built app content directory - optional - github_id_token: ${{ steps.idtoken.outputs.result }} - app_build_command: "CI=false npm run build" - ###### End of Repository/Build Configurations ###### - - close_pull_request_job: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - steps: - - name: Close Pull Request - id: closepullrequest - uses: Azure/static-web-apps-deploy@v1 - with: - action: "close" diff --git a/.github/workflows/publish-asset-store.yml b/.github/workflows/publish-asset-store.yml deleted file mode 100644 index e1a0afc..0000000 --- a/.github/workflows/publish-asset-store.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Publish Asset Store - -on: - push: - branches: - - main - - feature/52-av-web - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main - - feature/52-av-web - -jobs: - build_and_deploy_job: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') - runs-on: ubuntu-latest - name: Build and Deploy Job - permissions: - id-token: write - contents: read - steps: - - uses: actions/checkout@v3 - with: - submodules: true - lfs: false - - - name: Install OIDC Client from Core Package - run: npm install @actions/core@1.6.0 @actions/http-client - - - name: Get Id Token - uses: actions/github-script@v6 - id: idtoken - with: - script: | - const coredemo = require('@actions/core') - return await coredemo.getIDToken() - result-encoding: string - - - name: Build And Deploy - id: builddeploy - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_SKY_0ACD4AD03 }} - action: "upload" - ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### - # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "/AssetStore/asset-store-ui" # App source code path - api_location: "" # Api source code path - optional - # output_location: "app" # Built app content directory - optional - github_id_token: ${{ steps.idtoken.outputs.result }} - app_build_command: "CI=false npm run build" - ###### End of Repository/Build Configurations ###### - - close_pull_request_job: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - steps: - - name: Close Pull Request - id: closepullrequest - uses: Azure/static-web-apps-deploy@v1 - with: - action: "close" - diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml deleted file mode 100644 index 7931f80..0000000 --- a/.github/workflows/publish-docs.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Publish Docs - -on: - push: - branches: - - test - pull_request: - branches: - - test - -jobs: - echo: - runs-on: ubuntu-latest - steps: - - name: Echo message - run: echo "Hello from the test branch 👋" diff --git a/.github/workflows/publish-launcher.yml b/.github/workflows/publish-launcher.yml deleted file mode 100644 index 685b861..0000000 --- a/.github/workflows/publish-launcher.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Publish Launcher - -on: - push: - branches: - - test - pull_request: - branches: - - test - -jobs: - echo: - runs-on: ubuntu-latest - steps: - - name: Echo message - run: echo "Hello from the test branch 👋" diff --git a/.github/workflows/publish-web.yml b/.github/workflows/publish-web.yml deleted file mode 100644 index 017a6b2..0000000 --- a/.github/workflows/publish-web.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Publish SharpEngine Web Portal - -on: - push: - branches: - # - main - - feature/52-av-web - pull_request: - types: [closed] - branches: - - main - # - feature/52-av-web - -jobs: - build_and_deploy_job: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') - runs-on: ubuntu-latest - name: Build and Deploy Job - steps: - - uses: actions/checkout@v3 - with: - submodules: true - lfs: false - - name: Build And Deploy - id: builddeploy - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_KIND_MEADOW_06167BF03 }} - repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) - action: "upload" - ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### - # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "./Portal/sharpengine-web-ui" # App source code path - # api_location: "" # Api source code path - optional - output_location: "build" # Built app content directory - optional - app_build_command: "CI=false npm run build" - ###### End of Repository/Build Configurations ###### - - close_pull_request_job: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - steps: - - name: Close Pull Request - id: closepullrequest - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_KIND_MEADOW_06167BF03 }} - action: "close" From 0c6e5d6ad9627cc13cb26f5a2ea50745062b91cd Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sat, 6 Jun 2026 00:11:41 +0300 Subject: [PATCH 02/10] #109 Engine Orchestration (#132) * #109 Engine orchestrator * #128 Cleanup project structure and introduce DI project * #128 Solution builds * #128 Move under core * #128 Add TODO comments * WIP on dev * #128 Use non static reference of GL for proper context usage * #11 Fix texturing * #128 Use MS ILogger * #128 Cleanup DI * #128 Fix startup issue * #128 Fix build errors after merge * #128 Cleanup * #128 Add missing xml docs and cleanup code * #128 Remove Obsolete entities * #128 Model refactoring and cleanup * #109 Add xml docs and cleanup warnings * #109 Add copilot instructions * #109 Add XML documentation and cleanup * #109 Fix windowing and DI * #109 Abstract obj loader --------- Co-authored-by: Antti Veikkolainen Co-authored-by: Antti Veikkolainen --- Engine/.editorconfig => .editorconfig | 0 .github/copilot-instructions.md | 17 + ATAM.md | 0 ...ctory.Build.props => Directory.Build.props | 2 +- .../Examples/MinecraftClone/Minecraft.csproj | 18 -- Engine/Examples/MinecraftClone/Program.cs | 36 --- Engine/ObjLoader/DataStore.cs | 52 --- Engine/ObjLoader/Loaders/LoaderBase.cs | 38 --- .../IMaterialLibraryLoaderFacade.cs | 7 - .../MaterialLoader/MaterialLibraryLoader.cs | 107 ------ .../MaterialLibraryLoaderFacade.cs | 34 -- Engine/ObjLoader/TypeParsers/GroupParser.cs | 21 -- Engine/ObjLoader/TypeParsers/ITypeParser.cs | 8 - .../TypeParsers/MaterialLibraryParser.cs | 22 -- Engine/ObjLoader/TypeParsers/NormalParser.cs | 32 -- Engine/ObjLoader/TypeParsers/TextureParser.cs | 31 -- .../ObjLoader/TypeParsers/TypeParserBase.cs | 14 - .../TypeParsers/UseMaterialParser.cs | 20 -- .../ObjLoader/DataStore/IFaceGroup.cs | 9 - .../ObjLoader/DataStore/IGroupDataStore.cs | 7 - .../ObjLoader/DataStore/IMaterialLibrary.cs | 9 - .../ObjLoader/DataStore/INormalDataStore.cs | 9 - .../ObjLoader/DataStore/ITextureDataStore.cs | 9 - .../ObjLoader/DataStore/IVertexDataStore.cs | 9 - .../Properties/Meshes/MeshData/Face.cs | 27 -- .../Properties/Meshes/MeshData/Group.cs | 22 -- .../Properties/Meshes/MeshData/Normal.cs | 16 - .../Meshes/MeshData/TextureCoordinate.cs | 14 - .../Properties/Meshes/MeshData/Vertex.cs | 21 -- .../Properties/Meshes/Model.cs | 104 ------ .../Properties/Meshes/Model_Old.cs | 214 ------------ .../Properties/Textures/Texture.cs | 31 -- .../SharpEngine.Core/Entities/GameObject.cs | 114 ------- .../SharpEngine.Core/Entities/MeshService.cs | 31 -- .../SharpEngine.Core/Entities/UI/UIElement.cs | 108 ------- Engine/SharpEngine.Core/Primitives/Cube.cs | 203 ------------ .../Renderers/TextRenderer.cs | 22 -- Engine/SharpEngine.Core/Shaders/LampShader.cs | 39 --- .../Shaders/LightingShader.cs | 53 --- .../SharpEngine.Core/Shaders/ShaderService.cs | 93 ------ Engine/SharpEngine.Core/Shaders/UIShader.cs | 48 --- .../Tests/SharpEngine.Shared.Net8/Class1.cs | 6 - .../SharpEngine.Shared.Net8.csproj | 9 - Engine/Tests/Test.Directory.Build.props | 6 - .../MinecraftClone/Block/BlockBase.cs | 0 .../MinecraftClone/Block/Dirt.cs | 0 .../MinecraftClone/Block/Stone.cs | 0 .../MinecraftClone/BlockFactory.cs | 0 .../MinecraftClone/Input.cs | 0 .../MinecraftClone/Inventory.cs | 0 .../MinecraftClone/Minecraft.cs | 61 ++-- Examples/MinecraftClone/Minecraft.csproj | 19 ++ .../MinecraftClone/Minecraft.sln | 0 Examples/MinecraftClone/Program.cs | 69 ++++ .../MinecraftClone/Resources/container2.png | Bin .../Resources/container2_specular.png | Bin .../MinecraftClone/Resources/grass.jpg | Bin .../Minimal/Minimal.csproj | 2 +- .../Examples => Examples}/Minimal/Minimal.sln | 0 .../Examples => Examples}/Minimal/Program.cs | 10 +- .../MultipleWindows/MultipleWindows.csproj | 8 +- .../MultipleWindows/Program.cs | 61 ++-- .../Tutorial 4.1 - Model Loading/Program.cs | 64 ++-- .../Tutorial 4.1 - Model Loading.csproj | 14 +- .../Tutorial 4.1 - Model Loading/shader2.vert | 0 .../Tutorial 4.1 - Model Loading/silk.png | Bin ObjLoader/DataStore.cs | 83 +++++ ObjLoader/IMaterialDataStore.cs | 14 + ObjLoader/Loaders/LoaderBase.cs | 50 +++ .../IMaterialLibraryLoaderFacade.cs | 14 + .../MaterialLoader/MaterialLibraryLoader.cs | 109 +++++++ .../MaterialLibraryLoaderFacade.cs | 43 +++ .../Loaders/ObjLoader/ObjLoader.cs | 31 +- .../Loaders/ObjLoader/ObjLoaderFactory.cs | 42 +-- .../SharpEngine.Core.ObjLoader.csproj | 8 +- .../TypeParsers/FaceParser.cs | 18 +- ObjLoader/TypeParsers/GroupParser.cs | 29 ++ ObjLoader/TypeParsers/ITypeParser.cs | 21 ++ .../TypeParsers/MaterialLibraryParser.cs | 28 ++ ObjLoader/TypeParsers/NormalParser.cs | 40 +++ ObjLoader/TypeParsers/TextureParser.cs | 39 +++ ObjLoader/TypeParsers/TypeParserBase.cs | 22 ++ ObjLoader/TypeParsers/UseMaterialParser.cs | 28 ++ .../TypeParsers/VertexParser.cs | 23 +- .../IComponent.cs | 4 + .../ObjLoader/DataStore/IDataStore.cs | 10 + .../ObjLoader/DataStore/IFaceGroup.cs | 19 ++ .../ObjLoader/DataStore/IGroupDataStore.cs | 14 + .../ObjLoader/DataStore/IMaterialLibrary.cs | 16 + .../ObjLoader/DataStore/INormalDataStore.cs | 16 + .../ObjLoader/DataStore/ITextureDataStore.cs | 16 + .../ObjLoader/DataStore/IVertexDataStore.cs | 16 + .../Properties/BoundingBox.cs | 0 .../Properties/ITransform.cs | 0 .../Properties/Material.cs | 68 ++-- .../Properties/Meshes/BufferObject.cs | 6 +- .../Properties/Meshes/Mesh.cs | 17 +- .../Properties/Meshes/MeshData/Face.cs | 59 ++++ .../Properties/Meshes/MeshData/Group.cs | 38 +++ .../Properties/Meshes/MeshData/Normal.cs | 36 +++ .../Meshes/MeshData/TextureCoordinate.cs | 36 +++ .../Properties/Meshes/MeshData/Vertex.cs | 56 ++++ .../Properties/Meshes/MeshExtensions.cs | 4 +- .../Properties/Meshes/Model.cs | 304 ++++++++++++++++++ .../Properties/Meshes/ObjMeshData.cs | 0 .../Properties/Meshes/VertexArrayObject.cs | 0 .../Properties/Meshes/VertexData.cs | 0 .../Properties/Quaternion.cs | 0 .../Properties/Shaders/Shader.cs | 21 +- .../Properties/Shaders/ShaderAttributes.cs | 2 + .../Properties/Shaders/ShaderExtensions.cs | 20 +- .../Properties/Textures/Texture.cs | 66 ++++ .../Properties/Textures/TextureExtensions.cs | 13 +- .../Properties/Textures/TextureType.cs | 27 ++ .../Properties/Transform.cs | 5 + .../Properties/Transform2D.cs | 0 .../SharpEngine.Core.Components.csproj | 18 +- .../Float.cs | 3 + .../HashCodeHelper.cs | 9 +- .../IVector.cs | 2 - .../JitIntrinsicAttribute.cs | 0 .../SharpEngine.Core.Numerics.csproj | 2 +- .../Vector2.cs | 0 .../Vector2_Intrinsics.cs | 0 .../Vector3.cs | 25 +- .../Vector3_Intrinsics.cs | 0 .../Abstractions/IGame.cs | 46 ++- .../Abstractions/IRenderable.cs | 4 +- .../Abstractions/ISettings.cs | 1 - .../Audio/Audio.cs | 0 .../Audio/AudioBuffer.cs | 0 .../Audio/AudioDevice.cs | 0 .../Audio/AudioFileExtensions.cs | 0 .../Audio/AudioPlayerBase.cs | 0 .../Audio/AudioProperties.cs | 0 .../Audio/AudioSource.cs | 6 +- .../Audio/WavData.cs | 0 .../Audio/WavPlayer.cs | 8 +- .../ObjLoader/DataStore/IVertexDataStore.cs | 0 SharpEngine.Core/DependencyInjection/App.cs | 100 ++++++ .../DependencyInjection/AppBuilder.cs | 68 ++++ .../ServiceCollectionExtensions.cs | 46 +++ SharpEngine.Core/Engine.cs | 80 +++++ SharpEngine.Core/EngineServiceManager.cs | 52 +++ .../Entities/BezierCurve.cs | 0 SharpEngine.Core/Entities/GameObject.cs | 249 ++++++++++++++ .../Entities/Lights/DirectionalLight.cs | 8 +- .../Entities/Lights/Light.cs | 0 .../Entities/Lights/PointLight.cs | 13 +- .../Entities/Lights/SpotLight.cs | 10 +- SharpEngine.Core/Entities/MeshService.cs | 40 +++ SharpEngine.Core/Entities/Particle.cs | 35 ++ .../Entities/UI/Layouts/FlexLayout.cs | 1 - .../Entities/UI/Layouts/GridLayout.cs | 4 +- .../Entities/UI/Layouts/LayoutBase.cs | 0 SharpEngine.Core/Entities/UI/UIElement.cs | 162 ++++++++++ .../Entities/Views/CameraView.cs | 11 +- .../Views/Settings/DefaultViewSettings.cs | 1 + .../Entities/Views/Settings/IViewSettings.cs | 0 .../Entities/Views/Settings/ViewSettings.cs | 0 .../Entities/Views/View.cs | 10 +- .../Enums/MouseWheelScrollDirection.cs | 0 .../Extensions/AssemblyExtensions.cs | 18 ++ .../Extensions/CollectionExtensions.cs | 0 .../Extensions/PathExtensions.cs | 0 SharpEngine.Core/Handlers/EngineHandler.cs | 161 ++++++++++ .../Handlers/EngineHandlerState.cs | 25 ++ SharpEngine.Core/Handlers/ParticleEmitter.cs | 111 +++++++ SharpEngine.Core/Handlers/WindowHandler.cs | 244 ++++++++++++++ SharpEngine.Core/Primitives/Cube.cs | 265 +++++++++++++++ .../Primitives/Plane.cs | 2 +- .../Primitives/PrimitiveFactory.cs | 19 +- .../Primitives/PrimitiveType.cs | 0 .../Ray.cs | 0 .../Renderers/ParticleRenderer.cs | 25 ++ .../Renderers/RenderFlags.cs | 0 .../Renderers/Renderer.cs | 58 ++-- .../Renderers/RendererBase.cs | 24 +- SharpEngine.Core/Renderers/TextRenderer.cs | 31 ++ .../Renderers/UIRenderer.cs | 36 ++- .../Scenes/Scene.cs | 9 +- .../Scenes/SceneNode.cs | 0 SharpEngine.Core/Shaders/LampShader.cs | 43 +++ SharpEngine.Core/Shaders/LightingShader.cs | 56 ++++ .../Shaders/ShaderBase.cs | 11 +- SharpEngine.Core/Shaders/ShaderService.cs | 134 ++++++++ SharpEngine.Core/Shaders/UIShader.cs | 50 +++ .../SharpEngine.Core.csproj | 20 +- .../Textures/TextureService.cs | 5 +- .../Windowing/Frame.cs | 0 SharpEngine.Core/Windowing/IWindowFactory.cs | 27 ++ .../Windowing/SilkWindow.cs | 11 + .../Windowing/Window.cs | 180 +++++++---- SharpEngine.Core/Windowing/WindowFactory.cs | 67 ++++ .../Windowing/WindowRegistration.cs | 22 ++ .../_Resources/Default.cs | 6 +- .../Shaders/Light/DirectionalLight.glsl | 0 .../_Resources/Shaders/Light/PointLight.glsl | 0 .../_Resources/Shaders/Light/SpotLight.glsl | 0 .../_Resources/Shaders/Material.glsl | 0 .../_Resources/Shaders/lighting.frag | 0 .../_Resources/Shaders/shader.frag | 0 .../_Resources/Shaders/shader.vert | 0 .../_Resources/Shaders/shader2.vert | 0 .../_Resources/Shaders/uiShader.frag | 0 .../_Resources/Shaders/uiShader.vert | 0 .../_Resources/Textures/debug.jpg | Bin .../_Resources/icon.png | Bin SharpEngine.Shared/Debug.cs | 79 +++++ Engine/SharpEngine.slnx => SharpEngine.slnx | 35 +- Tests/Directory.Build.props | 10 + .../Loaders/MaterialLibraryLoaderTests.cs | 30 +- .../ObjLoader.Test/Loaders/ObjLoaderTests.cs | 32 +- .../SharpEngine.Core.ObjLoader.Tests.csproj | 6 +- .../TypeParsers/FaceParserTests.cs | 10 +- .../TypeParsers/GroupParserTests.cs | 18 +- .../TypeParsers/MaterialLibraryParserTests.cs | 15 +- .../TypeParsers/NormalParserTests.cs | 17 +- .../TypeParsers/TextureParserTests.cs | 16 +- .../TypeParsers/UseMaterialParserTests.cs | 15 +- .../TypeParsers/VertexParserTests.cs | 18 +- .../SharpEngine.Core.Tests/Class1.cs | 0 .../SharpEngine.Core.Tests.csproj | 2 +- .../SharpEngine.Editor.Tests/Class1.cs | 0 .../SharpEngine.Editor.Tests.csproj | 2 +- .../SharpEngine.Shared.Tests/Class1.cs | 0 .../SharpEngine.Shared.Tests.csproj | 0 227 files changed, 4348 insertions(+), 2162 deletions(-) rename Engine/.editorconfig => .editorconfig (100%) create mode 100644 .github/copilot-instructions.md create mode 100644 ATAM.md rename Engine/Directory.Build.props => Directory.Build.props (94%) delete mode 100644 Engine/Examples/MinecraftClone/Minecraft.csproj delete mode 100644 Engine/Examples/MinecraftClone/Program.cs delete mode 100644 Engine/ObjLoader/DataStore.cs delete mode 100644 Engine/ObjLoader/Loaders/LoaderBase.cs delete mode 100644 Engine/ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs delete mode 100644 Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs delete mode 100644 Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs delete mode 100644 Engine/ObjLoader/TypeParsers/GroupParser.cs delete mode 100644 Engine/ObjLoader/TypeParsers/ITypeParser.cs delete mode 100644 Engine/ObjLoader/TypeParsers/MaterialLibraryParser.cs delete mode 100644 Engine/ObjLoader/TypeParsers/NormalParser.cs delete mode 100644 Engine/ObjLoader/TypeParsers/TextureParser.cs delete mode 100644 Engine/ObjLoader/TypeParsers/TypeParserBase.cs delete mode 100644 Engine/ObjLoader/TypeParsers/UseMaterialParser.cs delete mode 100644 Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IFaceGroup.cs delete mode 100644 Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IGroupDataStore.cs delete mode 100644 Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IMaterialLibrary.cs delete mode 100644 Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/INormalDataStore.cs delete mode 100644 Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/ITextureDataStore.cs delete mode 100644 Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IVertexDataStore.cs delete mode 100644 Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs delete mode 100644 Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Group.cs delete mode 100644 Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Normal.cs delete mode 100644 Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/TextureCoordinate.cs delete mode 100644 Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Vertex.cs delete mode 100644 Engine/SharpEngine.Core.Components/Properties/Meshes/Model.cs delete mode 100644 Engine/SharpEngine.Core.Components/Properties/Meshes/Model_Old.cs delete mode 100644 Engine/SharpEngine.Core.Components/Properties/Textures/Texture.cs delete mode 100644 Engine/SharpEngine.Core/Entities/GameObject.cs delete mode 100644 Engine/SharpEngine.Core/Entities/MeshService.cs delete mode 100644 Engine/SharpEngine.Core/Entities/UI/UIElement.cs delete mode 100644 Engine/SharpEngine.Core/Primitives/Cube.cs delete mode 100644 Engine/SharpEngine.Core/Renderers/TextRenderer.cs delete mode 100644 Engine/SharpEngine.Core/Shaders/LampShader.cs delete mode 100644 Engine/SharpEngine.Core/Shaders/LightingShader.cs delete mode 100644 Engine/SharpEngine.Core/Shaders/ShaderService.cs delete mode 100644 Engine/SharpEngine.Core/Shaders/UIShader.cs delete mode 100644 Engine/Tests/SharpEngine.Shared.Net8/Class1.cs delete mode 100644 Engine/Tests/SharpEngine.Shared.Net8/SharpEngine.Shared.Net8.csproj delete mode 100644 Engine/Tests/Test.Directory.Build.props rename {Engine/Examples => Examples}/MinecraftClone/Block/BlockBase.cs (100%) rename {Engine/Examples => Examples}/MinecraftClone/Block/Dirt.cs (100%) rename {Engine/Examples => Examples}/MinecraftClone/Block/Stone.cs (100%) rename {Engine/Examples => Examples}/MinecraftClone/BlockFactory.cs (100%) rename {Engine/Examples => Examples}/MinecraftClone/Input.cs (100%) rename {Engine/Examples => Examples}/MinecraftClone/Inventory.cs (100%) rename {Engine/Examples => Examples}/MinecraftClone/Minecraft.cs (85%) create mode 100644 Examples/MinecraftClone/Minecraft.csproj rename {Engine/Examples => Examples}/MinecraftClone/Minecraft.sln (100%) create mode 100644 Examples/MinecraftClone/Program.cs rename {Engine/Examples => Examples}/MinecraftClone/Resources/container2.png (100%) rename {Engine/Examples => Examples}/MinecraftClone/Resources/container2_specular.png (100%) rename {Engine/Examples => Examples}/MinecraftClone/Resources/grass.jpg (100%) rename {Engine/Examples => Examples}/Minimal/Minimal.csproj (82%) rename {Engine/Examples => Examples}/Minimal/Minimal.sln (100%) rename {Engine/Examples => Examples}/Minimal/Program.cs (61%) rename {Engine/Examples => Examples}/MultipleWindows/MultipleWindows.csproj (55%) rename {Engine/Examples => Examples}/MultipleWindows/Program.cs (68%) rename {Engine/Examples => Examples}/Tutorial 4.1 - Model Loading/Program.cs (78%) rename {Engine/Examples => Examples}/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj (56%) rename {Engine/Examples => Examples}/Tutorial 4.1 - Model Loading/shader2.vert (100%) rename {Engine/Examples => Examples}/Tutorial 4.1 - Model Loading/silk.png (100%) create mode 100644 ObjLoader/DataStore.cs create mode 100644 ObjLoader/IMaterialDataStore.cs create mode 100644 ObjLoader/Loaders/LoaderBase.cs create mode 100644 ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs create mode 100644 ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs create mode 100644 ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs rename {Engine/ObjLoader => ObjLoader}/Loaders/ObjLoader/ObjLoader.cs (54%) rename {Engine/ObjLoader => ObjLoader}/Loaders/ObjLoader/ObjLoaderFactory.cs (61%) rename {Engine/ObjLoader => ObjLoader}/SharpEngine.Core.ObjLoader.csproj (60%) rename {Engine/ObjLoader => ObjLoader}/TypeParsers/FaceParser.cs (73%) create mode 100644 ObjLoader/TypeParsers/GroupParser.cs create mode 100644 ObjLoader/TypeParsers/ITypeParser.cs create mode 100644 ObjLoader/TypeParsers/MaterialLibraryParser.cs create mode 100644 ObjLoader/TypeParsers/NormalParser.cs create mode 100644 ObjLoader/TypeParsers/TextureParser.cs create mode 100644 ObjLoader/TypeParsers/TypeParserBase.cs create mode 100644 ObjLoader/TypeParsers/UseMaterialParser.cs rename {Engine/ObjLoader => ObjLoader}/TypeParsers/VertexParser.cs (52%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/IComponent.cs (54%) create mode 100644 SharpEngine.Core.Components/ObjLoader/DataStore/IDataStore.cs create mode 100644 SharpEngine.Core.Components/ObjLoader/DataStore/IFaceGroup.cs create mode 100644 SharpEngine.Core.Components/ObjLoader/DataStore/IGroupDataStore.cs create mode 100644 SharpEngine.Core.Components/ObjLoader/DataStore/IMaterialLibrary.cs create mode 100644 SharpEngine.Core.Components/ObjLoader/DataStore/INormalDataStore.cs create mode 100644 SharpEngine.Core.Components/ObjLoader/DataStore/ITextureDataStore.cs create mode 100644 SharpEngine.Core.Components/ObjLoader/DataStore/IVertexDataStore.cs rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/BoundingBox.cs (100%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/ITransform.cs (100%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Material.cs (69%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Meshes/BufferObject.cs (83%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Meshes/Mesh.cs (92%) create mode 100644 SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs create mode 100644 SharpEngine.Core.Components/Properties/Meshes/MeshData/Group.cs create mode 100644 SharpEngine.Core.Components/Properties/Meshes/MeshData/Normal.cs create mode 100644 SharpEngine.Core.Components/Properties/Meshes/MeshData/TextureCoordinate.cs create mode 100644 SharpEngine.Core.Components/Properties/Meshes/MeshData/Vertex.cs rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Meshes/MeshExtensions.cs (93%) create mode 100644 SharpEngine.Core.Components/Properties/Meshes/Model.cs rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Meshes/ObjMeshData.cs (100%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Meshes/VertexArrayObject.cs (100%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Meshes/VertexData.cs (100%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Quaternion.cs (100%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Shaders/Shader.cs (81%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Shaders/ShaderAttributes.cs (85%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Shaders/ShaderExtensions.cs (91%) create mode 100644 SharpEngine.Core.Components/Properties/Textures/Texture.cs rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Textures/TextureExtensions.cs (84%) create mode 100644 SharpEngine.Core.Components/Properties/Textures/TextureType.cs rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Transform.cs (88%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/Properties/Transform2D.cs (100%) rename {Engine/SharpEngine.Core.Components => SharpEngine.Core.Components}/SharpEngine.Core.Components.csproj (51%) rename {Engine/SharpEngine.Core.Numerics => SharpEngine.Core.Numerics}/Float.cs (69%) rename {Engine/SharpEngine.Core.Numerics => SharpEngine.Core.Numerics}/HashCodeHelper.cs (51%) rename {Engine/SharpEngine.Core.Numerics => SharpEngine.Core.Numerics}/IVector.cs (88%) rename {Engine/SharpEngine.Core.Numerics => SharpEngine.Core.Numerics}/JitIntrinsicAttribute.cs (100%) rename {Engine/SharpEngine.Core.Numerics => SharpEngine.Core.Numerics}/SharpEngine.Core.Numerics.csproj (81%) rename {Engine/SharpEngine.Core.Numerics => SharpEngine.Core.Numerics}/Vector2.cs (100%) rename {Engine/SharpEngine.Core.Numerics => SharpEngine.Core.Numerics}/Vector2_Intrinsics.cs (100%) rename {Engine/SharpEngine.Core.Numerics => SharpEngine.Core.Numerics}/Vector3.cs (99%) rename {Engine/SharpEngine.Core.Numerics => SharpEngine.Core.Numerics}/Vector3_Intrinsics.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Abstractions/IGame.cs (63%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Abstractions/IRenderable.cs (66%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Abstractions/ISettings.cs (98%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Audio/Audio.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Audio/AudioBuffer.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Audio/AudioDevice.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Audio/AudioFileExtensions.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Audio/AudioPlayerBase.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Audio/AudioProperties.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Audio/AudioSource.cs (82%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Audio/WavData.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Audio/WavPlayer.cs (92%) create mode 100644 SharpEngine.Core/Components/ObjLoader/DataStore/IVertexDataStore.cs create mode 100644 SharpEngine.Core/DependencyInjection/App.cs create mode 100644 SharpEngine.Core/DependencyInjection/AppBuilder.cs create mode 100644 SharpEngine.Core/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 SharpEngine.Core/Engine.cs create mode 100644 SharpEngine.Core/EngineServiceManager.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/BezierCurve.cs (100%) create mode 100644 SharpEngine.Core/Entities/GameObject.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/Lights/DirectionalLight.cs (81%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/Lights/Light.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/Lights/PointLight.cs (83%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/Lights/SpotLight.cs (88%) create mode 100644 SharpEngine.Core/Entities/MeshService.cs create mode 100644 SharpEngine.Core/Entities/Particle.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/UI/Layouts/FlexLayout.cs (97%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/UI/Layouts/GridLayout.cs (96%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/UI/Layouts/LayoutBase.cs (100%) create mode 100644 SharpEngine.Core/Entities/UI/UIElement.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/Views/CameraView.cs (92%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/Views/Settings/DefaultViewSettings.cs (99%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/Views/Settings/IViewSettings.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/Views/Settings/ViewSettings.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Entities/Views/View.cs (93%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Enums/MouseWheelScrollDirection.cs (100%) create mode 100644 SharpEngine.Core/Extensions/AssemblyExtensions.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Extensions/CollectionExtensions.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Extensions/PathExtensions.cs (100%) create mode 100644 SharpEngine.Core/Handlers/EngineHandler.cs create mode 100644 SharpEngine.Core/Handlers/EngineHandlerState.cs create mode 100644 SharpEngine.Core/Handlers/ParticleEmitter.cs create mode 100644 SharpEngine.Core/Handlers/WindowHandler.cs create mode 100644 SharpEngine.Core/Primitives/Cube.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Primitives/Plane.cs (93%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Primitives/PrimitiveFactory.cs (77%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Primitives/PrimitiveType.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Ray.cs (100%) create mode 100644 SharpEngine.Core/Renderers/ParticleRenderer.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Renderers/RenderFlags.cs (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Renderers/Renderer.cs (69%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Renderers/RendererBase.cs (64%) create mode 100644 SharpEngine.Core/Renderers/TextRenderer.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Renderers/UIRenderer.cs (56%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Scenes/Scene.cs (97%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Scenes/SceneNode.cs (100%) create mode 100644 SharpEngine.Core/Shaders/LampShader.cs create mode 100644 SharpEngine.Core/Shaders/LightingShader.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Shaders/ShaderBase.cs (69%) create mode 100644 SharpEngine.Core/Shaders/ShaderService.cs create mode 100644 SharpEngine.Core/Shaders/UIShader.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/SharpEngine.Core.csproj (75%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Textures/TextureService.cs (88%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Windowing/Frame.cs (100%) create mode 100644 SharpEngine.Core/Windowing/IWindowFactory.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/Windowing/SilkWindow.cs (95%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/Windowing/Window.cs (57%) create mode 100644 SharpEngine.Core/Windowing/WindowFactory.cs create mode 100644 SharpEngine.Core/Windowing/WindowRegistration.cs rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Default.cs (77%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/Light/DirectionalLight.glsl (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/Light/PointLight.glsl (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/Light/SpotLight.glsl (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/Material.glsl (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/lighting.frag (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/shader.frag (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/shader.vert (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/shader2.vert (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/uiShader.frag (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Shaders/uiShader.vert (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/Textures/debug.jpg (100%) rename {Engine/SharpEngine.Core => SharpEngine.Core}/_Resources/icon.png (100%) create mode 100644 SharpEngine.Shared/Debug.cs rename Engine/SharpEngine.slnx => SharpEngine.slnx (74%) create mode 100644 Tests/Directory.Build.props rename {Engine/Tests => Tests}/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs (85%) rename {Engine/Tests => Tests}/ObjLoader.Test/Loaders/ObjLoaderTests.cs (82%) rename Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj => Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj (82%) rename {Engine/Tests => Tests}/ObjLoader.Test/TypeParsers/FaceParserTests.cs (94%) rename {Engine/Tests => Tests}/ObjLoader.Test/TypeParsers/GroupParserTests.cs (76%) rename {Engine/Tests => Tests}/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs (75%) rename {Engine/Tests => Tests}/ObjLoader.Test/TypeParsers/NormalParserTests.cs (80%) rename {Engine/Tests => Tests}/ObjLoader.Test/TypeParsers/TextureParserTests.cs (80%) rename {Engine/Tests => Tests}/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs (75%) rename {Engine/Tests => Tests}/ObjLoader.Test/TypeParsers/VertexParserTests.cs (80%) rename {Engine/Tests => Tests}/SharpEngine.Core.Tests/Class1.cs (100%) rename {Engine/Tests => Tests}/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj (71%) rename {Engine/Tests => Tests}/SharpEngine.Editor.Tests/Class1.cs (100%) rename {Engine/Tests => Tests}/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj (71%) rename {Engine/Tests => Tests}/SharpEngine.Shared.Tests/Class1.cs (100%) rename {Engine/Tests => Tests}/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj (100%) diff --git a/Engine/.editorconfig b/.editorconfig similarity index 100% rename from Engine/.editorconfig rename to .editorconfig diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..1318621 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,17 @@ +When adding new public members or editing existing ones, please ensure that you update the XML documentation comments for those members. +This helps maintain code readability and provides useful information for other developers who may use or maintain the code in the future. + +Additionally, if you are adding new features or making significant changes, consider updating any relevant unit tests to cover the new functionality and ensure that existing tests still pass. + +All changes need to be unit testable. Favor composition over inheritance when designing new classes or features, and ensure that your code adheres to SOLID principles for maintainability and scalability. +When referencing classes like System.IO, System.Net.Http or similar, try to use a abstraction layer or interface to allow for easier testing and flexibility in the future. +This will help decouple your code from specific implementations and make it easier to mock dependencies in unit tests. + +Give me a brief summary of the changes you are making, including the purpose and any relevant details, so that I can provide more specific guidance or suggestions if needed. + +If you see any areas of the codebase that could benefit from refactoring or improvement, please feel free to suggest those changes as well. +This could include improving code readability, reducing complexity, or enhancing performance. +All suggestions should be accompanied by a clear rationale and, if possible, examples of how the changes would improve the codebase. +Suggestions need to be manually approved, so do not implement those by yourself without prior discussion and approval. + +Finally, please make sure to run all existing tests and any new tests you create to verify that your changes do not introduce any regressions or issues in the codebase. \ No newline at end of file diff --git a/ATAM.md b/ATAM.md new file mode 100644 index 0000000..e69de29 diff --git a/Engine/Directory.Build.props b/Directory.Build.props similarity index 94% rename from Engine/Directory.Build.props rename to Directory.Build.props index c13bb19..368b137 100644 --- a/Engine/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ - net8.0 + net8.0 diff --git a/Engine/Examples/MinecraftClone/Minecraft.csproj b/Engine/Examples/MinecraftClone/Minecraft.csproj deleted file mode 100644 index d3d98c6..0000000 --- a/Engine/Examples/MinecraftClone/Minecraft.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - AnyCPU;x64;x86 - - - - - - - - - - - - - diff --git a/Engine/Examples/MinecraftClone/Program.cs b/Engine/Examples/MinecraftClone/Program.cs deleted file mode 100644 index 727a8f4..0000000 --- a/Engine/Examples/MinecraftClone/Program.cs +++ /dev/null @@ -1,36 +0,0 @@ -using SharpEngine.Core.Interfaces; -using SharpEngine.Core.Scenes; -using SharpEngine.Core.Windowing; - -namespace Minecraft; - -/// -/// Represents the entry point of the application. -/// -public static class Program -{ - private static void Main() - { - DefaultSettings gameSettings = new() - { - UseWireFrame = false - }; - - Scene scene = new Scene(); - Minecraft game = new Minecraft(scene, gameSettings); - - 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/Engine/ObjLoader/DataStore.cs b/Engine/ObjLoader/DataStore.cs deleted file mode 100644 index 5ee2b5c..0000000 --- a/Engine/ObjLoader/DataStore.cs +++ /dev/null @@ -1,52 +0,0 @@ -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 deleted file mode 100644 index 0de39a2..0000000 --- a/Engine/ObjLoader/Loaders/LoaderBase.cs +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 4417065..0000000 --- a/Engine/ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 90ebb1b..0000000 --- a/Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs +++ /dev/null @@ -1,107 +0,0 @@ -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 deleted file mode 100644 index c6f62af..0000000 --- a/Engine/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs +++ /dev/null @@ -1,34 +0,0 @@ -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/TypeParsers/GroupParser.cs b/Engine/ObjLoader/TypeParsers/GroupParser.cs deleted file mode 100644 index b8d1512..0000000 --- a/Engine/ObjLoader/TypeParsers/GroupParser.cs +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 9cb2d7e..0000000 --- a/Engine/ObjLoader/TypeParsers/ITypeParser.cs +++ /dev/null @@ -1,8 +0,0 @@ -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 deleted file mode 100644 index 7b7c2b4..0000000 --- a/Engine/ObjLoader/TypeParsers/MaterialLibraryParser.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 49af71a..0000000 --- a/Engine/ObjLoader/TypeParsers/NormalParser.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 0c596ba..0000000 --- a/Engine/ObjLoader/TypeParsers/TextureParser.cs +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 7880db8..0000000 --- a/Engine/ObjLoader/TypeParsers/TypeParserBase.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 72f33d8..0000000 --- a/Engine/ObjLoader/TypeParsers/UseMaterialParser.cs +++ /dev/null @@ -1,20 +0,0 @@ -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/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IFaceGroup.cs b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IFaceGroup.cs deleted file mode 100644 index e6c0922..0000000 --- a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IFaceGroup.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 3fe9d69..0000000 --- a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IGroupDataStore.cs +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 5652449..0000000 --- a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IMaterialLibrary.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 10ae87d..0000000 --- a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/INormalDataStore.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index a1591ff..0000000 --- a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/ITextureDataStore.cs +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index 6667f06..0000000 --- a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IVertexDataStore.cs +++ /dev/null @@ -1,9 +0,0 @@ -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/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs deleted file mode 100644 index e4dfdb5..0000000 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index 1cd52c9..0000000 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Group.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 5e40c14..0000000 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Normal.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 5bd29ce..0000000 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/TextureCoordinate.cs +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 5eb1fda..0000000 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshData/Vertex.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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/Engine/SharpEngine.Core.Components/Properties/Meshes/Model.cs b/Engine/SharpEngine.Core.Components/Properties/Meshes/Model.cs deleted file mode 100644 index 513cf4e..0000000 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/Model.cs +++ /dev/null @@ -1,104 +0,0 @@ -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 deleted file mode 100644 index 0f477aa..0000000 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/Model_Old.cs +++ /dev/null @@ -1,214 +0,0 @@ -// 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/Textures/Texture.cs b/Engine/SharpEngine.Core.Components/Properties/Textures/Texture.cs deleted file mode 100644 index d271f1e..0000000 --- a/Engine/SharpEngine.Core.Components/Properties/Textures/Texture.cs +++ /dev/null @@ -1,31 +0,0 @@ -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/Entities/GameObject.cs b/Engine/SharpEngine.Core/Entities/GameObject.cs deleted file mode 100644 index 160896a..0000000 --- a/Engine/SharpEngine.Core/Entities/GameObject.cs +++ /dev/null @@ -1,114 +0,0 @@ -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/Engine/SharpEngine.Core/Entities/MeshService.cs b/Engine/SharpEngine.Core/Entities/MeshService.cs deleted file mode 100644 index 99a60a2..0000000 --- a/Engine/SharpEngine.Core/Entities/MeshService.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; - -namespace SharpEngine.Core.Entities.Properties.Meshes; - -/// -/// Represents a service that loads meshes into the GPU. -/// -public class MeshService -{ - /// A global instance of the service. - public static readonly MeshService Instance = new(); - - private readonly Dictionary Meshes = []; - - private MeshService() { } - - /// - /// Loads a mesh into the mesh cache. - /// - /// Used to cache the loaded mesh. - /// The mesh that should be stored in to the GPU buffer. - /// The loaded mesh. - public Mesh LoadMesh(string identifier, Mesh mesh) - { - if (Meshes.TryGetValue(identifier, out var cachedMesh)) - return cachedMesh; - - Meshes.Add(identifier, mesh); - return mesh; - } -} \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Entities/UI/UIElement.cs b/Engine/SharpEngine.Core/Entities/UI/UIElement.cs deleted file mode 100644 index 4230410..0000000 --- a/Engine/SharpEngine.Core/Entities/UI/UIElement.cs +++ /dev/null @@ -1,108 +0,0 @@ -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/Engine/SharpEngine.Core/Primitives/Cube.cs b/Engine/SharpEngine.Core/Primitives/Cube.cs deleted file mode 100644 index 16be872..0000000 --- a/Engine/SharpEngine.Core/Primitives/Cube.cs +++ /dev/null @@ -1,203 +0,0 @@ -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/Engine/SharpEngine.Core/Renderers/TextRenderer.cs b/Engine/SharpEngine.Core/Renderers/TextRenderer.cs deleted file mode 100644 index 5787fc5..0000000 --- a/Engine/SharpEngine.Core/Renderers/TextRenderer.cs +++ /dev/null @@ -1,22 +0,0 @@ -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/Engine/SharpEngine.Core/Shaders/LampShader.cs b/Engine/SharpEngine.Core/Shaders/LampShader.cs deleted file mode 100644 index 898d08b..0000000 --- a/Engine/SharpEngine.Core/Shaders/LampShader.cs +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 0893499..0000000 --- a/Engine/SharpEngine.Core/Shaders/LightingShader.cs +++ /dev/null @@ -1,53 +0,0 @@ -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/Engine/SharpEngine.Core/Shaders/ShaderService.cs b/Engine/SharpEngine.Core/Shaders/ShaderService.cs deleted file mode 100644 index ee8f516..0000000 --- a/Engine/SharpEngine.Core/Shaders/ShaderService.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.IO; -using System.Collections.Generic; - -using SharpEngine.Core.Windowing; -using SharpEngine.Shared; - -namespace SharpEngine.Core.Shaders; - -/// -/// Contains all the shaders used in the game. -/// -public class ShaderService -{ - /// - /// Gets the singleton instance of the . - /// - public static ShaderService Instance { get; } = new ShaderService(); - - private readonly Dictionary _shaderCache = []; - - /// - /// Gets or sets whether there are shaders to load. - /// - public bool HasShadersToLoad { get; set; } = true; - - /// - /// Private constructor to prevent instantiation. - /// - private ShaderService() { } - - /// - /// Gets all the shaders in the cache. - /// - /// All the shaders found from the cache. - public List GetAll() - { - HasShadersToLoad = false; - return [.. _shaderCache.Values]; - } - - /// - /// Gets a shader by its name. - /// - /// The name of the shader to be found. - /// The found shader. - /// - /// Thrown if a shader by that is not found. - /// This exception is thrown to make sure there are no unexpected issues made by the developer. - /// - public Shader GetByName(string name) - { - if (_shaderCache.TryGetValue(name, out var cachedShader)) - return cachedShader; - - throw new KeyNotFoundException($"Shader with name {name} not found in cache."); - } - - /// - /// Loads a shader from the specified vertex and fragment paths.
- /// If the shader is loaded already, adds it to the cache. - ///
- /// The vertex shader full path. - /// The fragment shader full path. - /// A name identifier for the shader. - /// A shader with the given name. - /// Thrown when either the vertex or fragment shader is not found. - public Shader LoadShader(string vertPath, string fragPath, string name) - { - // Check if the shader is already in the cache - if (_shaderCache.TryGetValue(name, out var cachedShader)) - return cachedShader; - - if (!File.Exists(vertPath)) - { - Debug.Log.Information("Vertex shader file not found: {VertPath}", vertPath); - throw new FileNotFoundException($"Vertex shader file not found: {vertPath}"); - } - - if (!File.Exists(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(Window.GL, vertPath, fragPath, name).Initialize(); - _shaderCache[name] = shader; - - HasShadersToLoad = true; - - return shader; - } -} diff --git a/Engine/SharpEngine.Core/Shaders/UIShader.cs b/Engine/SharpEngine.Core/Shaders/UIShader.cs deleted file mode 100644 index 37caf51..0000000 --- a/Engine/SharpEngine.Core/Shaders/UIShader.cs +++ /dev/null @@ -1,48 +0,0 @@ -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/Tests/SharpEngine.Shared.Net8/Class1.cs b/Engine/Tests/SharpEngine.Shared.Net8/Class1.cs deleted file mode 100644 index e8f6eff..0000000 --- a/Engine/Tests/SharpEngine.Shared.Net8/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index fa71b7a..0000000 --- a/Engine/Tests/SharpEngine.Shared.Net8/SharpEngine.Shared.Net8.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net8.0 - enable - enable - - - diff --git a/Engine/Tests/Test.Directory.Build.props b/Engine/Tests/Test.Directory.Build.props deleted file mode 100644 index a52359c..0000000 --- a/Engine/Tests/Test.Directory.Build.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Engine/Examples/MinecraftClone/Block/BlockBase.cs b/Examples/MinecraftClone/Block/BlockBase.cs similarity index 100% rename from Engine/Examples/MinecraftClone/Block/BlockBase.cs rename to Examples/MinecraftClone/Block/BlockBase.cs diff --git a/Engine/Examples/MinecraftClone/Block/Dirt.cs b/Examples/MinecraftClone/Block/Dirt.cs similarity index 100% rename from Engine/Examples/MinecraftClone/Block/Dirt.cs rename to Examples/MinecraftClone/Block/Dirt.cs diff --git a/Engine/Examples/MinecraftClone/Block/Stone.cs b/Examples/MinecraftClone/Block/Stone.cs similarity index 100% rename from Engine/Examples/MinecraftClone/Block/Stone.cs rename to Examples/MinecraftClone/Block/Stone.cs diff --git a/Engine/Examples/MinecraftClone/BlockFactory.cs b/Examples/MinecraftClone/BlockFactory.cs similarity index 100% rename from Engine/Examples/MinecraftClone/BlockFactory.cs rename to Examples/MinecraftClone/BlockFactory.cs diff --git a/Engine/Examples/MinecraftClone/Input.cs b/Examples/MinecraftClone/Input.cs similarity index 100% rename from Engine/Examples/MinecraftClone/Input.cs rename to Examples/MinecraftClone/Input.cs diff --git a/Engine/Examples/MinecraftClone/Inventory.cs b/Examples/MinecraftClone/Inventory.cs similarity index 100% rename from Engine/Examples/MinecraftClone/Inventory.cs rename to Examples/MinecraftClone/Inventory.cs diff --git a/Engine/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs similarity index 85% rename from Engine/Examples/MinecraftClone/Minecraft.cs rename to Examples/MinecraftClone/Minecraft.cs index 92494da..1b347e2 100644 --- a/Engine/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -1,25 +1,24 @@ using ImGuiNET; + using Minecraft.Block; -using ObjLoader.Loaders.ObjLoader; +using Microsoft.Extensions.Logging; + 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.Windowing; -using SharpEngine.Shared; -using Silk.NET.Core.Native; +using SharpEngine.Core.ObjLoader.Loaders.ObjLoader; + using Silk.NET.Input; + using System; -using System.Collections.Generic; -using System.Linq; using System.Numerics; -using Tutorial; namespace Minecraft; @@ -31,6 +30,7 @@ namespace Minecraft; public class Minecraft : Game { private readonly Scene _scene; + private readonly ILogger _logger; private SceneNode _lightsNode; private SceneNode _blocksNode; @@ -43,15 +43,20 @@ public class Minecraft : Game /// /// Gets the main window. /// - public Window? Window { get; set; } + public static Window Window + { + get => field ?? throw new InvalidOperationException("The game window has not been assigned yet."); + set; + } /// /// Initializes a new instance of the . /// - public Minecraft(Scene scene, ISettings settings) + public Minecraft(Scene scene, ISettings settings, ILogger logger) { _scene = scene; CoreSettings = settings; + _logger = logger; _inventory = new Inventory(); @@ -76,21 +81,21 @@ public override void Initialize() _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); + var uiElem2 = new UIElement("uiElement"); + uiElem2.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(0.2f, 0.2f); + uiElem2.Transform.Position = new SharpEngine.Core.Numerics.Vector2(30, 0); - // gridLayout.AddChild(_uiElem, uiElem2); - // _scene.UIElements.Add(_uiElem); - // _scene.UIElements.Add(uiElem2); + gridLayout.AddChild(_uiElem, uiElem2); + _scene.UIElements.Add(_uiElem); + _scene.UIElements.Add(uiElem2); - //_scene.UIElements.Add(gridLayout); + _scene.UIElements.Add(gridLayout); InitializeWorld(); } catch (Exception ex) { - Debug.Log.Information(ex.Message, "{Message}", ex.Message); + _logger.LogInformation(ex, "{Message}", ex.Message); } } @@ -151,7 +156,7 @@ private void InitializeWorld() // 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 model = ObjLoaderFactory.Load(Window.GetGL(), @"C:\Users\antti\Documents\Untitled2.obj"); var go = new GameObject(model); var go2 = new GameObject(model) { @@ -220,14 +225,8 @@ private void GenerateChunk(int chunkSize, Vector3 chunkPos) } /// - public override void Update(double deltaTime, IInputContext input) - { - //UpdateUI(); - _input.HandleKeyboard(input.Keyboards[0], (float)deltaTime); - } - - private void UpdateUI() - => _uiElem.Transform.Rotation.Angle += 0.01f; + public override void Update(double deltaTime, IInputContext input) + => _input.HandleKeyboard(input.Keyboards[0], (float)deltaTime); // TODO: #21 Input system to let users change change key bindings? /// @@ -238,7 +237,7 @@ public override void HandleKeyboard(IKeyboard input, double deltaTime) if (input.IsKeyPressed(Key.Number0 + i)) { _inventory.SetSelectedSlot(i); - Debug.Log.Information("Selected slot: {I} ({Type})", i, _inventory.SelectedSlot.Items.Type); + _logger.LogInformation("Selected slot: {I} ({Type})", i, _inventory.SelectedSlot.Items.Type); } } @@ -273,7 +272,7 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) } else { - Debug.Log.Information("No more {Type}s.", _inventory.SelectedSlot.Items.Type); + _logger.LogInformation("No more {Type}s.", _inventory.SelectedSlot.Items.Type); } } @@ -284,7 +283,7 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) { // 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); + _logger.LogInformation("Block destroyed: {DestroyedBlockType}.", destroyedBlockType); _inventory.AddToolbarItem(destroyedBlockType); } } @@ -313,7 +312,7 @@ private void PlaceBlock() var newBlock = BlockFactory.CreateBlock(_inventory.SelectedSlot.Items.Type, newBlockPosition, $"Dirt ({_blocksNode.Children.Count})"); _blocksNode.AddChild(newBlock); - Debug.Log.Information("New block created: {Pos}, block in view location: {IntersectingPos}", newBlock.Transform.Position, intersectingObject!.Transform.Position); + _logger.LogInformation("New block created: {Pos}, block in view location: {IntersectingPos}", newBlock.Transform.Position, intersectingObject!.Transform.Position); } private static Vector3 GetNewBlockPosition(Vector3 hitPosition, GameObject intersectingObject) @@ -338,7 +337,7 @@ public override void HandleMouseWheel(MouseWheelScrollDirection direction, Scrol slotIndex = 0; _inventory.SetSelectedSlot(slotIndex); - Debug.Log.Information("Selected slot: {Index}", slotIndex); + _logger.LogInformation("Selected slot: {Index}", slotIndex); } } diff --git a/Examples/MinecraftClone/Minecraft.csproj b/Examples/MinecraftClone/Minecraft.csproj new file mode 100644 index 0000000..447f24c --- /dev/null +++ b/Examples/MinecraftClone/Minecraft.csproj @@ -0,0 +1,19 @@ + + + + Exe + $(DotNetTargetFramework) + AnyCPU;x64;x86 + + + + + + + + + + + + + diff --git a/Engine/Examples/MinecraftClone/Minecraft.sln b/Examples/MinecraftClone/Minecraft.sln similarity index 100% rename from Engine/Examples/MinecraftClone/Minecraft.sln rename to Examples/MinecraftClone/Minecraft.sln diff --git a/Examples/MinecraftClone/Program.cs b/Examples/MinecraftClone/Program.cs new file mode 100644 index 0000000..97df2f9 --- /dev/null +++ b/Examples/MinecraftClone/Program.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.DependencyInjection; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Renderers; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Windowing; + +namespace Minecraft; + +/// +/// Represents the entry point of the application. +/// +public static class Program +{ + private static void Main() + { + var builder = new AppBuilder() + .ConfigureServices(ConfigureServices); + + var app = builder.Build(); + app.Run(); + } + + private static void ConfigureServices(IServiceCollection services) + { + services.AddLogging(builder => builder.AddConsole()); + + services.AddSingleton(_ => new DefaultSettings + { + UseWireFrame = false + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService().Camera); + services.AddTransient(); + services.AddTransient(); + + services.AddWindow( + factory: serviceProvider => + { + var game = serviceProvider.GetRequiredService(); + var scene = serviceProvider.GetRequiredService(); + var windowLogger = serviceProvider.GetRequiredService>(); + var renderers = serviceProvider.GetServices(); + + return new Window(game.Camera, scene, game.Camera.Settings, windowLogger, renderers); + }, + configure: (serviceProvider, window) => + { + var game = serviceProvider.GetRequiredService(); + + 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 += game.OnAfterRender; + + Minecraft.Window = window; + }, + name: "minecraft", + isDefault: true); + } +} diff --git a/Engine/Examples/MinecraftClone/Resources/container2.png b/Examples/MinecraftClone/Resources/container2.png similarity index 100% rename from Engine/Examples/MinecraftClone/Resources/container2.png rename to Examples/MinecraftClone/Resources/container2.png diff --git a/Engine/Examples/MinecraftClone/Resources/container2_specular.png b/Examples/MinecraftClone/Resources/container2_specular.png similarity index 100% rename from Engine/Examples/MinecraftClone/Resources/container2_specular.png rename to Examples/MinecraftClone/Resources/container2_specular.png diff --git a/Engine/Examples/MinecraftClone/Resources/grass.jpg b/Examples/MinecraftClone/Resources/grass.jpg similarity index 100% rename from Engine/Examples/MinecraftClone/Resources/grass.jpg rename to Examples/MinecraftClone/Resources/grass.jpg diff --git a/Engine/Examples/Minimal/Minimal.csproj b/Examples/Minimal/Minimal.csproj similarity index 82% rename from Engine/Examples/Minimal/Minimal.csproj rename to Examples/Minimal/Minimal.csproj index 8bbdf5a..e786576 100644 --- a/Engine/Examples/Minimal/Minimal.csproj +++ b/Examples/Minimal/Minimal.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + $(DotNetTargetFramework) enable enable diff --git a/Engine/Examples/Minimal/Minimal.sln b/Examples/Minimal/Minimal.sln similarity index 100% rename from Engine/Examples/Minimal/Minimal.sln rename to Examples/Minimal/Minimal.sln diff --git a/Engine/Examples/Minimal/Program.cs b/Examples/Minimal/Program.cs similarity index 61% rename from Engine/Examples/Minimal/Program.cs rename to Examples/Minimal/Program.cs index 9751459..26310ec 100644 --- a/Engine/Examples/Minimal/Program.cs +++ b/Examples/Minimal/Program.cs @@ -1,5 +1,4 @@ using SharpEngine.Core.Interfaces; -using SharpEngine.Core.Scenes; using SharpEngine.Core.Windowing; namespace Minimal; @@ -14,10 +13,9 @@ public static class Program /// public static void Main(string[] _) { - var game = new Minimal(new DefaultSettings()); - var scene = new Scene(); + var game = new Minimal(); - using var window = new Window(game.Camera, scene, game.Camera.Settings); + using var window = new Window(game); window.Run(); } } @@ -30,9 +28,7 @@ public class Minimal : Game /// /// Initializes a new instance of /// - /// Provides configuration options for the instance. - public Minimal(ISettings settings) + public Minimal() { - CoreSettings = settings; } } diff --git a/Engine/Examples/MultipleWindows/MultipleWindows.csproj b/Examples/MultipleWindows/MultipleWindows.csproj similarity index 55% rename from Engine/Examples/MultipleWindows/MultipleWindows.csproj rename to Examples/MultipleWindows/MultipleWindows.csproj index e48ece3..1596f7d 100644 --- a/Engine/Examples/MultipleWindows/MultipleWindows.csproj +++ b/Examples/MultipleWindows/MultipleWindows.csproj @@ -2,18 +2,12 @@ Exe - net8.0 + $(DotNetTargetFramework) enable enable true - - - - - - diff --git a/Engine/Examples/MultipleWindows/Program.cs b/Examples/MultipleWindows/Program.cs similarity index 68% rename from Engine/Examples/MultipleWindows/Program.cs rename to Examples/MultipleWindows/Program.cs index 788b808..67751b3 100644 --- a/Engine/Examples/MultipleWindows/Program.cs +++ b/Examples/MultipleWindows/Program.cs @@ -1,10 +1,12 @@ -using Silk.NET.Input; +using Microsoft.Extensions.Logging; +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; + +using Window = SharpEngine.Core.Windowing.Window; namespace SharpEngine.Examples.MultipleWindows; @@ -16,6 +18,10 @@ namespace SharpEngine.Examples.MultipleWindows; /// public static partial class Program { + private static readonly ILoggerFactory _loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); + private static readonly ILogger _logger = _loggerFactory.CreateLogger(typeof(Program)); + private static readonly ILogger _windowLogger = _loggerFactory.CreateLogger(); + private static readonly List _windows = []; private static readonly List _inputContexts = []; private static readonly ConcurrentQueue _windowQueue = []; @@ -25,28 +31,23 @@ public static partial class Program /// The main entry point of the application. /// /// Arguments discarded. - public static void Main(string[] _) + public static async Task 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(); - } + Core.Handlers.WindowHandler windowHandler = new(); + windowHandler.Start(); + + Console.WriteLine("end"); + Console.ReadLine(); + + // StartWindowQueueTask(); + // + // while (!_cancellationTokenSource.IsCancellationRequested) + // { + // for (int i = 0; i < _windows.Count; i++) + // UpdateWindow(ref i); + // + // DequeueWindows(); + // } } private static void UpdateWindow(ref int i) @@ -88,11 +89,11 @@ private static void StartWindowQueueTask() while (!_cancellationTokenSource.IsCancellationRequested) { await Task.Delay(1000); - Debug.Log.Information("Running loop on background thread..."); + _logger.LogInformation("Running loop on background thread..."); } }); - private static SharpEngine.Core.Windowing.Window CreateWindow() + private static Window CreateWindow() { var options = new DefaultViewSettings() with { @@ -106,8 +107,8 @@ private static SharpEngine.Core.Windowing.Window CreateWindow() y: 400 + (50 * _windows.Count)) } }; - - var window = new SharpEngine.Core.Windowing.Window(new(), options); + + var window = new Window(new(), options, _windowLogger); window.Initialize(); return window; @@ -123,8 +124,6 @@ private static void EnqueueWindow() _windows.Add(window); } - private static void Mouse_Click(IMouse args1, Silk.NET.Input.MouseButton arg2, System.Numerics.Vector2 arg3) - { - _windowQueue.Enqueue(WindowOptions.Default); - } + private static void Mouse_Click(IMouse args1, MouseButton arg2, System.Numerics.Vector2 arg3) + => _windowQueue.Enqueue(WindowOptions.Default); } diff --git a/Engine/Examples/Tutorial 4.1 - Model Loading/Program.cs b/Examples/Tutorial 4.1 - Model Loading/Program.cs similarity index 78% rename from Engine/Examples/Tutorial 4.1 - Model Loading/Program.cs rename to Examples/Tutorial 4.1 - Model Loading/Program.cs index 1bffe63..742d903 100644 --- a/Engine/Examples/Tutorial 4.1 - Model Loading/Program.cs +++ b/Examples/Tutorial 4.1 - Model Loading/Program.cs @@ -1,6 +1,7 @@ -using ObjLoader.Loaders.ObjLoader; using SharpEngine.Core._Resources; using SharpEngine.Core.Extensions; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.ObjLoader.Loaders.ObjLoader; using Shader = SharpEngine.Core.Shaders.Shader; using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; @@ -15,16 +16,20 @@ namespace Tutorial { + /// + /// Represents the main entry point of the application, demonstrating model loading and rendering using Silk.NET and SharpEngine.Core. + /// 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 = []; + private static IWindow? window; + private static GL? Gl; + private static IKeyboard? primaryKeyboard; + + private static Texture? Texture; + private static Shader? Shader; + private static readonly List _models = []; //Setup the camera's location, directions, and movement speed private static Vector3 CameraPosition = new(0.0f, 0.0f, 3.0f); @@ -37,6 +42,7 @@ public static class Program //Used to track change in mouse movement to allow for moving of the Camera private static Vector2 LastMousePosition; + #endregion private static void Main(string[] _) @@ -59,11 +65,13 @@ private static void Main(string[] _) private static void OnLoad() { + if (window == null) + throw new NullReferenceException("Window is not initialized."); + IInputContext input = window.CreateInput(); primaryKeyboard = input.Keyboards[0]; - if (primaryKeyboard != null) - primaryKeyboard.KeyDown += KeyDown; + primaryKeyboard?.KeyDown += KeyDown; for (int i = 0; i < input.Mice.Count; i++) { @@ -79,12 +87,15 @@ private static void OnLoad() var model = ObjLoaderFactory.Load(Gl, "Untitled2.obj"); - Models.Add(model); - Models.Add(model); + _models.Add(model); + _models.Add(model); } private static void OnUpdate(double deltaTime) { + if (primaryKeyboard == null) + throw new NullReferenceException("Primary keyboard is not initialized."); + var moveSpeed = 2.5f * (float) deltaTime; //Move forwards @@ -106,6 +117,12 @@ private static void OnUpdate(double deltaTime) private static void OnRender(double deltaTime) { + if (Gl == null) + throw new NullReferenceException("OpenGL context is not initialized."); + + if (window == null) + throw new NullReferenceException("Window is not initialized."); + Gl.Enable(EnableCap.DepthTest); Gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); @@ -121,17 +138,26 @@ private static void OnRender(double deltaTime) //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++) + 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); + RenderModel(_models[i], modelMatrix, view, projection); } } - private static void RenderModel(Model_Old model, Matrix4x4 modelMatrix, Matrix4x4 view, Matrix4x4 projection) + private static void RenderModel(Model model, Matrix4x4 modelMatrix, Matrix4x4 view, Matrix4x4 projection) { + if (Texture == null) + throw new NullReferenceException("Texture is not initialized."); + + if (Shader == null) + throw new NullReferenceException("Shader is not initialized."); + + if (Gl == null) + throw new NullReferenceException("OpenGL context is not initialized."); + foreach (var mesh in model.Meshes) { mesh.Bind(); @@ -148,7 +174,7 @@ private static void RenderModel(Model_Old model, Matrix4x4 modelMatrix, Matrix4x } } - private static void OnFramebufferResize(Vector2D newSize) => Gl.Viewport(newSize); + private static void OnFramebufferResize(Vector2D newSize) => Gl!.Viewport(newSize); private static void OnMouseMove(IMouse mouse, Vector2 position) { @@ -183,18 +209,18 @@ private static void OnMouseWheel(IMouse mouse, ScrollWheel scrollWheel) private static void OnClose() { - foreach (var model in Models) + foreach (var model in _models) model.Dispose(); - Shader.Dispose(); - Texture.Dispose(); + Shader?.Dispose(); + Texture?.Dispose(); } private static void KeyDown(IKeyboard keyboard, Key key, int arg3) { if (key == Key.Escape) { - window.Close(); + window!.Close(); } } } diff --git a/Engine/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj b/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj similarity index 56% rename from Engine/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj rename to Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj index 079bf1a..bbd8dbb 100644 --- a/Engine/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj +++ b/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj @@ -2,22 +2,10 @@ Exe - net8.0 + $(DotNetTargetFramework) Tutorial - - - - - - - diff --git a/Engine/Examples/Tutorial 4.1 - Model Loading/shader2.vert b/Examples/Tutorial 4.1 - Model Loading/shader2.vert similarity index 100% rename from Engine/Examples/Tutorial 4.1 - Model Loading/shader2.vert rename to Examples/Tutorial 4.1 - Model Loading/shader2.vert diff --git a/Engine/Examples/Tutorial 4.1 - Model Loading/silk.png b/Examples/Tutorial 4.1 - Model Loading/silk.png similarity index 100% rename from Engine/Examples/Tutorial 4.1 - Model Loading/silk.png rename to Examples/Tutorial 4.1 - Model Loading/silk.png diff --git a/ObjLoader/DataStore.cs b/ObjLoader/DataStore.cs new file mode 100644 index 0000000..5833552 --- /dev/null +++ b/ObjLoader/DataStore.cs @@ -0,0 +1,83 @@ +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Shared.Extensions; + +using System.Collections.Generic; +using System.Linq; + +namespace SharpEngine.Core.ObjLoader +{ + /// + /// Stores parsed OBJ file data such as vertices, normals, texture coordinates, materials and groups. + /// This type is used by the OBJ loader type parsers to collect geometry and material information. + /// + /// + /// Stores parsed OBJ file data such as vertices, normals, texture coordinates, materials and groups. + /// This type is used by the OBJ loader type parsers to collect geometry and material information. + /// + public class DataStore : IGroupDataStore, IFaceGroup, ITextureDataStore, INormalDataStore, IVertexDataStore, IMaterialDataStore + { + private Group _currentGroup = null!; + + /// Gets the list of parsed vertices. + public List Vertices { get; } = []; + + /// Gets the list of parsed texture coordinates. + public List Textures { get; } = []; + + /// Gets the list of parsed normals. + public List Normals { get; } = []; + + /// Gets the collection of materials discovered in the file. + public List Materials { get; } = []; + + /// Gets the list of groups present in the file. + public List Groups { get; } = []; + + /// + /// Initializes a new instance of and creates a default group. + /// + public DataStore() + { + PushGroup("default"); + } + + /// + /// Adds a parsed face to the active group. + /// + /// The parsed face to add. + public void AddFace(Face face) + => _currentGroup.AddFace(face); + + /// + /// Creates and activates a new group with the given name. + /// + /// The name of the group to create. + public void PushGroup(string groupName) + { + _currentGroup = new Group(groupName); + Groups.Add(_currentGroup); + } + + /// + public void SetMaterial(string materialName) + { + // TOOD: This should probably throw or handle the case where the material is not found, but for now we'll just set it to null. + var material = Materials.SingleOrDefault(x => x.Name.EqualsOrdinalIgnoreCase(materialName)); + _currentGroup.Material = material; + } + + /// + public void AddTexture(TextureCoordinate texture) + => Textures.Add(texture); + + /// + public void AddNormal(Normal normal) + => Normals.Add(normal); + + /// + public void AddVertex(Vertex vertex) + => Vertices.Add(vertex); + } +} \ No newline at end of file diff --git a/ObjLoader/IMaterialDataStore.cs b/ObjLoader/IMaterialDataStore.cs new file mode 100644 index 0000000..38f3168 --- /dev/null +++ b/ObjLoader/IMaterialDataStore.cs @@ -0,0 +1,14 @@ +namespace SharpEngine.Core.ObjLoader; + +/// +/// Contains definitions for the material data store, which provides methods to manage material information during the OBJ file loading process. +/// +public interface IMaterialDataStore +{ + /// + /// Sets the material for the current group. + /// + /// The name of the material to set. + public void SetMaterial(string materialName); + +} \ No newline at end of file diff --git a/ObjLoader/Loaders/LoaderBase.cs b/ObjLoader/Loaders/LoaderBase.cs new file mode 100644 index 0000000..4ecbabc --- /dev/null +++ b/ObjLoader/Loaders/LoaderBase.cs @@ -0,0 +1,50 @@ +using System.IO; + +namespace SharpEngine.Core.ObjLoader.Loader.Loaders +{ + /// + /// Represents a base class for loading .obj and .mtl files, providing common line parsing functionality. + /// + public abstract class LoaderBase + { + /// + /// Parses the file at the specified path and processes each line via . + /// + /// + /// I/O exceptions (for example FileNotFoundException, UnauthorizedAccessException, IOException) are propagated to the caller. + /// + /// The path of the file to open and parse. + public void ParseFile(string path) + { + using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); + using var lineStreamReader = new StreamReader(fileStream); + + while (!lineStreamReader.EndOfStream) + { + var currentLine = lineStreamReader.ReadLine(); + + if (!string.IsNullOrWhiteSpace(currentLine)) + ParseLine(currentLine); + } + } + + private void ParseLine(string currentLine) + { + if (currentLine[0] == '#') + return; + + var fields = currentLine.Trim().Split(null, 2); + var keyword = fields[0].Trim(); + var data = fields[1].Trim(); + + ParseLine(keyword, data); + } + + /// + /// Parses a single line consisting of a keyword and its associated data. + /// + /// Keyword that identifies the line type or command. + /// Data associated with the keyword; may be empty or contain parameters or raw text to interpret. + protected abstract void ParseLine(string keyword, string data); + } +} \ No newline at end of file diff --git a/ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs b/ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs new file mode 100644 index 0000000..5d4cde3 --- /dev/null +++ b/ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs @@ -0,0 +1,14 @@ +namespace SharpEngine.Core.ObjLoader.Loaders.MaterialLoader +{ + /// + /// Provides a facade for loading material library files into the application's material system. + /// + public interface IMaterialLibraryLoaderFacade + { + /// + /// Loads the specified material file into the application's material system. + /// + /// The name of the material file to load. + void Load(string materialFileName); + } +} \ No newline at end of file diff --git a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs new file mode 100644 index 0000000..39aa7d3 --- /dev/null +++ b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs @@ -0,0 +1,109 @@ +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using SharpEngine.Shared.Extensions; + +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SharpEngine.Core.ObjLoader.Loaders.MaterialLoader +{ + // https://paulbourke.net/dataformats/mtl/ + + /// + /// Handles loading of material libraries (.mtl files) referenced by .obj files. + /// + /// + /// Parses material properties and texture maps, and stores them in the provided . + /// Also handles opening material library files based on the path of the .obj file. + /// + public class MaterialLibraryLoader : LoaderBase + { + private readonly DataStore _dataStore; + private readonly Dictionary> _parseActionDictionary = []; + private readonly List _unrecognizedLines = []; + + private Material _currentMaterial = new(string.Empty); + + // TODO: #2 See Model class. This support both materials and textures + + /// + /// Initializes a new instance of and registers parse actions for .mtl directives. + /// + /// + /// Registers parse actions for common .mtl directives (for example: newmtl, Ka, Kd, Ks, + /// Ns, d, Tr, illum, map_*, bump, disp, decal) to populate Material instances. + /// + /// DataStore used to register and store parsed materials and texture references. + public MaterialLibraryLoader(DataStore dataStore) + { + _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, out bool found); + + if (!found) + { + _unrecognizedLines.Add(keyword + " " + data); + return; + } + + parseAction!(data); + } + + private Action? GetKeywordAction(string keyword, out bool found) + { + found = _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); + } + } +} \ No newline at end of file diff --git a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs new file mode 100644 index 0000000..9ce3ee6 --- /dev/null +++ b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; +using System.IO; + +namespace SharpEngine.Core.ObjLoader.Loaders.MaterialLoader +{ + /// + /// A facade for the to provide a simplified interface for loading material libraries. + /// + public class MaterialLibraryLoaderFacade : IMaterialLibraryLoaderFacade + { + private readonly ILogger _logger; + private readonly MaterialLibraryLoader _loader; + private readonly string _path; + + /// + /// Initializes a new instance of . + /// + /// Provides the functionality to load material libraries. + /// Optional path where the material file should exist. + /// The logger to use for logging messages. + public MaterialLibraryLoaderFacade(MaterialLibraryLoader loader, string path, ILogger? logger = null) + { + _loader = loader; + _path = path; + + _logger = logger ?? LoggingExtensions.CreateLogger(); + } + + /// + public void Load(string materialFileName) + { + string materialFilePath = Path.Combine(Path.GetDirectoryName(_path)!, materialFileName); + if (!File.Exists(materialFilePath)) + { + _logger.LogWarning("Material file '{MaterialFileName}' doesn't exist.", materialFileName); + return; + } + + _loader.ParseFile(materialFilePath); + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/Loaders/ObjLoader/ObjLoader.cs b/ObjLoader/Loaders/ObjLoader/ObjLoader.cs similarity index 54% rename from Engine/ObjLoader/Loaders/ObjLoader/ObjLoader.cs rename to ObjLoader/Loaders/ObjLoader/ObjLoader.cs index 798bc3c..cfcd5f0 100644 --- a/Engine/ObjLoader/Loaders/ObjLoader/ObjLoader.cs +++ b/ObjLoader/Loaders/ObjLoader/ObjLoader.cs @@ -1,14 +1,14 @@ -using ObjLoader.Loader.Loaders; -using ObjLoader.TypeParsers; -using SharpEngine.Core.Entities.Properties.Meshes; -using Silk.NET.Core.Contexts; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using SharpEngine.Core.ObjLoader.TypeParsers; using Silk.NET.OpenGL; using System.Collections.Generic; -using System.IO; -using Tutorial; -namespace ObjLoader.Loaders.ObjLoader +namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader { + /// + /// Loads an OBJ model file and converts its parsed data into runtime Mesh instances. + /// public class ObjLoader : LoaderBase { private readonly string _path; @@ -16,12 +16,22 @@ public class ObjLoader : LoaderBase private readonly List _typeParsers = []; private readonly List _unrecognizedLines = []; + /// + /// Initializes a new instance of with the specified file path and data store. + /// + /// The path to the OBJ file. + /// The data store to populate during parsing. public ObjLoader(string path, DataStore dataStore) { _path = path; _dataStore = dataStore; } + /// + /// Adds the provided type parsers to the loader's internal list, enabling it to recognize and parse different line types in the OBJ file. + /// + /// The type parsers to add. + /// The current instance of . public ObjLoader SetupTypeParsers(params ITypeParser[] parsers) { foreach (var parser in parsers) @@ -43,10 +53,13 @@ protected override void ParseLine(string keyword, string data) _unrecognizedLines.Add(keyword + " " + data); } + /// + /// Loads the OBJ file and produces a list of objects suitable for rendering. + /// + /// The OpenGL context used to construct GPU resources. public List Load(GL gl) { - var fileStream = new FileStream(_path, FileMode.Open, FileAccess.Read); - StartLoad(fileStream); + ParseFile(_path); return [ diff --git a/Engine/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs b/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs similarity index 61% rename from Engine/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs rename to ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs index 408855c..5a7d90c 100644 --- a/Engine/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs +++ b/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs @@ -1,14 +1,15 @@ -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 +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; + +namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader { /// /// Handles loading 3D models. @@ -22,10 +23,10 @@ public static class ObjLoaderFactory /// 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. + /// 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) + public static Model Load(GL gl, string path) => Path.GetExtension(path) switch { FbxExtension => LoadFbx("", path), @@ -34,18 +35,20 @@ public static Model_Old Load(GL gl, string path) }; // TODO: #3 Load fbx mesh from file - private static Model_Old LoadFbx(string identifier, string meshFilePath) + private static Model LoadFbx(string identifier, string meshFilePath) { throw new NotImplementedException(); } - private static Model_Old LoadObj(GL gl, string path) + private static Model 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 meshes = LoadObjMeshes(gl, path); + return new Model(gl, path, meshes); + } - /*var dataStore = new DataStore(); + private static List LoadObjMeshes(GL gl, string path) + { + var dataStore = new DataStore(); var faceParser = new FaceParser(dataStore); var groupParser = new GroupParser(dataStore); @@ -53,13 +56,16 @@ private static Model_Old LoadObj(GL gl, string path) var textureParser = new TextureParser(dataStore); var vertexParser = new VertexParser(dataStore); - var materialLibraryLoader = new MaterialLibraryLoader(path, dataStore); - var materialLibraryParser = new MaterialLibraryParser(materialLibraryLoader); + var materialLibraryLoader = new MaterialLibraryLoader(dataStore); + + var materialLoader = new MaterialLibraryLoaderFacade(materialLibraryLoader, path); + var materialLibraryParser = new MaterialLibraryParser(materialLoader); var useMaterialParser = new UseMaterialParser(dataStore); - var loader = new ObjLoader(path, dataStore).SetupTypeParsers(faceParser, groupParser, normalParser, textureParser, vertexParser, materialLibraryParser, useMaterialParser); + 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/ObjLoader/SharpEngine.Core.ObjLoader.csproj similarity index 60% rename from Engine/ObjLoader/SharpEngine.Core.ObjLoader.csproj rename to ObjLoader/SharpEngine.Core.ObjLoader.csproj index 7cc24cf..7a52e96 100644 --- a/Engine/ObjLoader/SharpEngine.Core.ObjLoader.csproj +++ b/ObjLoader/SharpEngine.Core.ObjLoader.csproj @@ -1,16 +1,16 @@ - + - net8.0 + $(DotNetTargetFramework) - + - + \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/FaceParser.cs b/ObjLoader/TypeParsers/FaceParser.cs similarity index 73% rename from Engine/ObjLoader/TypeParsers/FaceParser.cs rename to ObjLoader/TypeParsers/FaceParser.cs index c7328f7..0df5e68 100644 --- a/Engine/ObjLoader/TypeParsers/FaceParser.cs +++ b/ObjLoader/TypeParsers/FaceParser.cs @@ -1,15 +1,23 @@ -using ObjLoader.Loader.Common; -using ObjLoader.Loader.Data.Elements; -using ObjLoader.TypeParsers; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.ObjLoader.TypeParsers; +using SharpEngine.Shared.Extensions; + using System; -namespace ObjLoader.Loader.TypeParsers +namespace SharpEngine.Core.ObjLoader.Loader.TypeParsers { + /// + /// Parses face definitions ("f") and populates the current group's faces. + /// public class FaceParser : TypeParserBase, ITypeParser { private readonly IFaceGroup _faceGroup; + /// + /// Initializes a new instance of the . + /// + /// public FaceParser(IFaceGroup faceGroup) { _faceGroup = faceGroup; diff --git a/ObjLoader/TypeParsers/GroupParser.cs b/ObjLoader/TypeParsers/GroupParser.cs new file mode 100644 index 0000000..e5dae69 --- /dev/null +++ b/ObjLoader/TypeParsers/GroupParser.cs @@ -0,0 +1,29 @@ +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.ObjLoader.TypeParsers; + +namespace SharpEngine.Core.ObjLoader.Loader.TypeParsers +{ + /// + /// Parses group definition lines ("g") and pushes a new group into the data store. + /// + public class GroupParser : TypeParserBase, ITypeParser + { + private readonly IGroupDataStore _dataStore; + + /// + /// Initializes a new instance of with the specified data store to populate during parsing. + /// + /// The data store to populate. + public GroupParser(IGroupDataStore 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/ObjLoader/TypeParsers/ITypeParser.cs b/ObjLoader/TypeParsers/ITypeParser.cs new file mode 100644 index 0000000..1b041ba --- /dev/null +++ b/ObjLoader/TypeParsers/ITypeParser.cs @@ -0,0 +1,21 @@ +namespace SharpEngine.Core.ObjLoader.TypeParsers +{ + /// + /// Defines a parser for handling different types of OBJ file lines. + /// + public interface ITypeParser + { + /// + /// Determines whether the specified keyword can be parsed. + /// + /// The keyword to check. + /// True if the keyword can be parsed; otherwise, false. + bool CanParse(string keyword); + + /// + /// Parses a single input line and updates the object's state accordingly. + /// + /// The input line to parse. + void Parse(string line); + } +} \ No newline at end of file diff --git a/ObjLoader/TypeParsers/MaterialLibraryParser.cs b/ObjLoader/TypeParsers/MaterialLibraryParser.cs new file mode 100644 index 0000000..f622cc8 --- /dev/null +++ b/ObjLoader/TypeParsers/MaterialLibraryParser.cs @@ -0,0 +1,28 @@ +using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; +using SharpEngine.Core.ObjLoader.TypeParsers; + +namespace SharpEngine.Core.ObjLoader.Loader.TypeParsers +{ + /// + /// Represents a parser for material library definitions in OBJ files, responsible for parsing lines that specify material libraries and delegating the loading of the material library to the provided loader facade. + /// + public class MaterialLibraryParser : TypeParserBase, ITypeParser + { + private readonly IMaterialLibraryLoaderFacade _libraryLoaderFacade; + + /// + /// Initializes a new instance of the . + /// + /// The material library loader facade. + public MaterialLibraryParser(IMaterialLibraryLoaderFacade facade) + { + _libraryLoaderFacade = facade; + } + + /// + protected override string Keyword => "mtllib"; + + /// + public override void Parse(string line) => _libraryLoaderFacade.Load(line); + } +} \ No newline at end of file diff --git a/ObjLoader/TypeParsers/NormalParser.cs b/ObjLoader/TypeParsers/NormalParser.cs new file mode 100644 index 0000000..af7974b --- /dev/null +++ b/ObjLoader/TypeParsers/NormalParser.cs @@ -0,0 +1,40 @@ +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.ObjLoader.TypeParsers; +using SharpEngine.Shared.Extensions; + +namespace SharpEngine.Core.ObjLoader.Loader.TypeParsers +{ + /// + /// Represents a parser for normal vector definitions in an OBJ file, responsible for parsing lines that define vertex normals and storing them in the data store. + /// + public class NormalParser : TypeParserBase, ITypeParser + { + private readonly INormalDataStore _dataStore; + + /// + /// Initializes a new instance of the . + /// + /// The data store to which the parsed normals will be added. + public NormalParser(INormalDataStore 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.AddNormal(normal); + } + } +} \ No newline at end of file diff --git a/ObjLoader/TypeParsers/TextureParser.cs b/ObjLoader/TypeParsers/TextureParser.cs new file mode 100644 index 0000000..43a2e8e --- /dev/null +++ b/ObjLoader/TypeParsers/TextureParser.cs @@ -0,0 +1,39 @@ +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.ObjLoader.TypeParsers; +using SharpEngine.Shared.Extensions; + +namespace SharpEngine.Core.ObjLoader.Loader.TypeParsers +{ + /// + /// Represents a parser for texture coordinate definitions in an OBJ file, responsible for parsing lines that define texture coordinates and storing them in the data store. + /// + public class TextureParser : TypeParserBase, ITypeParser + { + private readonly ITextureDataStore _dataStore; + + /// + /// Initializes a new instance of the . + /// + /// The data store to which the parsed texture coordinates will be added. + public TextureParser(ITextureDataStore 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.AddTexture(texture); + } + } +} \ No newline at end of file diff --git a/ObjLoader/TypeParsers/TypeParserBase.cs b/ObjLoader/TypeParsers/TypeParserBase.cs new file mode 100644 index 0000000..7410790 --- /dev/null +++ b/ObjLoader/TypeParsers/TypeParserBase.cs @@ -0,0 +1,22 @@ +using SharpEngine.Core.ObjLoader.TypeParsers; +using SharpEngine.Shared.Extensions; + +namespace SharpEngine.Core.ObjLoader.Loader.TypeParsers +{ + /// + /// Base implementation for OBJ file line type parsers. Provides a simple keyword matching helper. + /// + public abstract class TypeParserBase : ITypeParser + { + /// + /// The keyword this parser recognizes (e.g. "v", "vn", "f"). + /// + 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/ObjLoader/TypeParsers/UseMaterialParser.cs b/ObjLoader/TypeParsers/UseMaterialParser.cs new file mode 100644 index 0000000..aa4d6eb --- /dev/null +++ b/ObjLoader/TypeParsers/UseMaterialParser.cs @@ -0,0 +1,28 @@ +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.ObjLoader.TypeParsers; + +namespace SharpEngine.Core.ObjLoader.Loader.TypeParsers +{ + /// + /// Parses material usage lines ("usemtl") and assigns materials to the current group. + /// + public class UseMaterialParser : TypeParserBase, ITypeParser + { + private readonly IMaterialDataStore _dataStore; + + /// + /// Initializes a new instance of with the specified data store. + /// + /// The data store to use. + public UseMaterialParser(IMaterialDataStore 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/ObjLoader/TypeParsers/VertexParser.cs similarity index 52% rename from Engine/ObjLoader/TypeParsers/VertexParser.cs rename to ObjLoader/TypeParsers/VertexParser.cs index d6b4fcf..94159b3 100644 --- a/Engine/ObjLoader/TypeParsers/VertexParser.cs +++ b/ObjLoader/TypeParsers/VertexParser.cs @@ -1,16 +1,24 @@ -using ObjLoader.Loader.Common; -using ObjLoader.TypeParsers; +using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.ObjLoader.TypeParsers; +using SharpEngine.Shared.Extensions; + using System; -using Tutorial; -namespace ObjLoader.Loader.TypeParsers +namespace SharpEngine.Core.ObjLoader.Loader.TypeParsers { + /// + /// Parses vertex position lines ("v x y z") and adds them to the data store. + /// public class VertexParser : TypeParserBase, ITypeParser { - private readonly DataStore _dataStore; + private readonly IVertexDataStore _dataStore; - public VertexParser(DataStore dataStore) + /// + /// Initializes a new instance of with the specified data store to populate. + /// + /// The data store to populate. + public VertexParser(IVertexDataStore dataStore) { _dataStore = dataStore; } @@ -31,7 +39,8 @@ public override void Parse(string line) { Position = new System.Numerics.Vector3(x, y, z), }; - _dataStore.Vertices.Add(vertex); + + _dataStore.AddVertex(vertex); } } } \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/IComponent.cs b/SharpEngine.Core.Components/IComponent.cs similarity index 54% rename from Engine/SharpEngine.Core.Components/IComponent.cs rename to SharpEngine.Core.Components/IComponent.cs index fdeaa85..5a744ba 100644 --- a/Engine/SharpEngine.Core.Components/IComponent.cs +++ b/SharpEngine.Core.Components/IComponent.cs @@ -8,6 +8,10 @@ namespace SharpEngine.Core.Components; // TODO: #53 Entity Component System +/// +/// Represents a generic component used by the entity component system. +/// This interface is intentionally empty and serves as a marker for component types. +/// internal interface IComponent { } diff --git a/SharpEngine.Core.Components/ObjLoader/DataStore/IDataStore.cs b/SharpEngine.Core.Components/ObjLoader/DataStore/IDataStore.cs new file mode 100644 index 0000000..ddb9558 --- /dev/null +++ b/SharpEngine.Core.Components/ObjLoader/DataStore/IDataStore.cs @@ -0,0 +1,10 @@ +namespace SharpEngine.Core.Components.ObjLoader.DataStore +{ + /// + /// Represents a data store that can be used to store and retrieve data related to the OBJ file loading process, such as vertex data, material information, and other relevant details. + /// This interface can be implemented by various classes to provide different storage mechanisms (e.g., in-memory, file-based, database) for managing the data during the OBJ loading process. + /// + public interface IDataStore + { + } +} \ No newline at end of file diff --git a/SharpEngine.Core.Components/ObjLoader/DataStore/IFaceGroup.cs b/SharpEngine.Core.Components/ObjLoader/DataStore/IFaceGroup.cs new file mode 100644 index 0000000..1ccee9c --- /dev/null +++ b/SharpEngine.Core.Components/ObjLoader/DataStore/IFaceGroup.cs @@ -0,0 +1,19 @@ +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace SharpEngine.Core.Components.ObjLoader.DataStore +{ + /// + /// Contains definitions for the interface, which represents a group of faces in an OBJ model, allowing for the organization and management of faces based on their associated materials or other grouping criteria. + /// + public interface IFaceGroup + { + /// + /// Adds a face to the face group. + /// + /// + /// This method allows for the inclusion of a face, represented by the class, into the group, enabling the organization of faces based on their associated materials or other grouping criteria. + /// + /// The face to add to the group. + void AddFace(Face face); + } +} \ No newline at end of file diff --git a/SharpEngine.Core.Components/ObjLoader/DataStore/IGroupDataStore.cs b/SharpEngine.Core.Components/ObjLoader/DataStore/IGroupDataStore.cs new file mode 100644 index 0000000..69bc335 --- /dev/null +++ b/SharpEngine.Core.Components/ObjLoader/DataStore/IGroupDataStore.cs @@ -0,0 +1,14 @@ +namespace SharpEngine.Core.Components.ObjLoader.DataStore +{ + /// + /// Defines operations for managing groups of faces while parsing OBJ files. + /// + public interface IGroupDataStore : IDataStore + { + /// + /// Creates and activates a new face group with the provided name. + /// + /// The name of the group to push. + void PushGroup(string groupName); + } +} \ No newline at end of file diff --git a/SharpEngine.Core.Components/ObjLoader/DataStore/IMaterialLibrary.cs b/SharpEngine.Core.Components/ObjLoader/DataStore/IMaterialLibrary.cs new file mode 100644 index 0000000..7a85ab6 --- /dev/null +++ b/SharpEngine.Core.Components/ObjLoader/DataStore/IMaterialLibrary.cs @@ -0,0 +1,16 @@ +using SharpEngine.Core.Components.Properties; + +namespace SharpEngine.Core.Components.ObjLoader.DataStore +{ + /// + /// Represents a storage for materials discovered while parsing an OBJ's associated MTL file. + /// + public interface IMaterialLibrary + { + /// + /// Adds a material to the library. + /// + /// The material to add. + void Push(Material material); + } +} diff --git a/SharpEngine.Core.Components/ObjLoader/DataStore/INormalDataStore.cs b/SharpEngine.Core.Components/ObjLoader/DataStore/INormalDataStore.cs new file mode 100644 index 0000000..6c70e15 --- /dev/null +++ b/SharpEngine.Core.Components/ObjLoader/DataStore/INormalDataStore.cs @@ -0,0 +1,16 @@ +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace SharpEngine.Core.Components.ObjLoader.DataStore +{ + /// + /// Defines storage operations for normal vector data parsed from an OBJ file. + /// + public interface INormalDataStore + { + /// + /// Adds a normal vector to the data store. + /// + /// The normal vector to add. + void AddNormal(Normal normal); + } +} diff --git a/SharpEngine.Core.Components/ObjLoader/DataStore/ITextureDataStore.cs b/SharpEngine.Core.Components/ObjLoader/DataStore/ITextureDataStore.cs new file mode 100644 index 0000000..32ef8fb --- /dev/null +++ b/SharpEngine.Core.Components/ObjLoader/DataStore/ITextureDataStore.cs @@ -0,0 +1,16 @@ +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace SharpEngine.Core.Components.ObjLoader.DataStore +{ + /// + /// Defines storage operations for texture coordinate data parsed from an OBJ file. + /// + public interface ITextureDataStore + { + /// + /// Adds a texture coordinate to the underlying data store. + /// + /// The texture coordinate to add. + void AddTexture(TextureCoordinate texture); + } +} diff --git a/SharpEngine.Core.Components/ObjLoader/DataStore/IVertexDataStore.cs b/SharpEngine.Core.Components/ObjLoader/DataStore/IVertexDataStore.cs new file mode 100644 index 0000000..b143211 --- /dev/null +++ b/SharpEngine.Core.Components/ObjLoader/DataStore/IVertexDataStore.cs @@ -0,0 +1,16 @@ +using SharpEngine.Core.Components.Properties.Meshes.MeshData; + +namespace SharpEngine.Core.Components.ObjLoader.DataStore +{ + /// + /// Defines storage operations for vertex data parsed from an OBJ file. + /// + public interface IVertexDataStore + { + /// + /// Adds a vertex to the underlying data store. + /// + /// The vertex to add. + void AddVertex(Vertex vertex); + } +} diff --git a/Engine/SharpEngine.Core.Components/Properties/BoundingBox.cs b/SharpEngine.Core.Components/Properties/BoundingBox.cs similarity index 100% rename from Engine/SharpEngine.Core.Components/Properties/BoundingBox.cs rename to SharpEngine.Core.Components/Properties/BoundingBox.cs diff --git a/Engine/SharpEngine.Core.Components/Properties/ITransform.cs b/SharpEngine.Core.Components/Properties/ITransform.cs similarity index 100% rename from Engine/SharpEngine.Core.Components/Properties/ITransform.cs rename to SharpEngine.Core.Components/Properties/ITransform.cs diff --git a/Engine/SharpEngine.Core.Components/Properties/Material.cs b/SharpEngine.Core.Components/Properties/Material.cs similarity index 69% rename from Engine/SharpEngine.Core.Components/Properties/Material.cs rename to SharpEngine.Core.Components/Properties/Material.cs index 07c21be..5d01401 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Material.cs +++ b/SharpEngine.Core.Components/Properties/Material.cs @@ -8,14 +8,15 @@ namespace SharpEngine.Core.Components.Properties; /// /// Represents the material rendered onto a game object. /// -public class Material +public class Material : ICloneable, IEquatable { /// /// Initializes a new instance of . /// + /// The name assigned to the new material. /// The diffuse map texture of the material. /// The specular map texture of the material. Defaults to the diffuse map if not provided. - public Material(Texture diffuseMap, Texture? specularMap = null) + public Material(string materialName, Texture? diffuseMap = null, Texture? specularMap = null) : this(materialName) { DiffuseMap = diffuseMap; SpecularMap = specularMap ?? diffuseMap; @@ -37,19 +38,19 @@ public Material(string materialName) public string Name { get; set; } /// Gets or sets the diffuse map texture. - public Texture DiffuseMap { get; set; } + public Texture? DiffuseMap { get; set; } /// Gets or sets the path to the diffuse texture map. - public string DiffuseTextureMap { get; set; } + public string? DiffuseTextureMap { get; set; } /// Gets or sets the specular map texture. - public Texture SpecularMap { get; set; } + public Texture? SpecularMap { get; set; } /// Gets or sets the path to the specular texture map. - public string SpecularTextureMap { get; set; } + public string? SpecularTextureMap { get; set; } /// Gets a value indicating whether the material uses a specular map. - public bool UseSpecularMap => SpecularMap.Handle != DiffuseMap.Handle; + public bool UseSpecularMap => SpecularMap is not null && SpecularMap?.Handle != DiffuseMap?.Handle; /// The texture unit for the diffuse map. public const int DIFFUSE_UNIT = 0; @@ -82,22 +83,22 @@ public Material(string materialName) public int IlluminationModel { get; set; } /// Gets or sets the path to the ambient texture map. - public string AmbientTextureMap { get; set; } + public string? AmbientTextureMap { get; set; } /// Gets or sets the path to the specular highlight texture map. - public string SpecularHighlightTextureMap { get; set; } + public string? SpecularHighlightTextureMap { get; set; } /// Gets or sets the path to the bump map. - public string BumpMap { get; set; } + public string? BumpMap { get; set; } /// Gets or sets the path to the displacement map. - public string DisplacementMap { get; set; } + public string? DisplacementMap { get; set; } /// Gets or sets the path to the stencil decal map. - public string StencilDecalMap { get; set; } + public string? StencilDecalMap { get; set; } /// Gets or sets the path to the alpha texture map. - public string AlphaTextureMap { get; set; } + public string? AlphaTextureMap { get; set; } /// /// Sets the uniform values for the material in the specified shader. @@ -107,21 +108,50 @@ 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 (DiffuseMap is not null) + { + DiffuseMap.Use(TextureUnit.Texture0); + shader.SetInt("material.diffuse", DIFFUSE_UNIT); + } - if (UseSpecularMap) + if (SpecularMap is not null) { 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.SetInt("material.specular", DIFFUSE_UNIT); shader.SetFloat("material.shininess", 0); } } + + /// + public object Clone() + => MemberwiseClone(); + + /// + public override bool Equals(object? obj) + { + if (obj is null) + return false; + + if (ReferenceEquals(this, obj)) + return true; + + if (GetType() != obj.GetType()) + return false; + + return true; + } + + /// + public bool Equals(Material? other) => Equals(other); + + /// + public override int GetHashCode() + { + throw new NotImplementedException(); + } } diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/BufferObject.cs b/SharpEngine.Core.Components/Properties/Meshes/BufferObject.cs similarity index 83% rename from Engine/SharpEngine.Core.Components/Properties/Meshes/BufferObject.cs rename to SharpEngine.Core.Components/Properties/Meshes/BufferObject.cs index 1279b1d..9035976 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/BufferObject.cs +++ b/SharpEngine.Core.Components/Properties/Meshes/BufferObject.cs @@ -1,12 +1,12 @@ using Silk.NET.OpenGL; using System.Runtime.InteropServices; -namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +namespace SharpEngine.Core.Components.Properties.Meshes { /// - /// Represents a buffer object in OpenGL that manages memory allocation and data transfer for a specific data type. + /// Represents an OpenGL buffer object that manages memory allocation and data transfer for a specific value type. /// - /// Specifies the type of data stored in the buffer, which must be an unmanaged type for proper memory handling. + /// The unmanaged element type stored in the buffer. public class BufferObject : IDisposable where TDataType : unmanaged { private readonly uint _handle; diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs b/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs similarity index 92% rename from Engine/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs rename to SharpEngine.Core.Components/Properties/Meshes/Mesh.cs index e6f66fc..bf63131 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs +++ b/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs @@ -1,17 +1,15 @@ -using ObjLoader.Loader.Data.Elements; -using SharpEngine.Core.Attributes; +using SharpEngine.Core.Attributes; using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Components.Properties.Meshes.MeshData; using Silk.NET.OpenGL; -using Tutorial; - +using System.Diagnostics.CodeAnalysis; 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. +/// Represents a mesh composed of vertex data, indices, textures and materials which can be uploaded to GPU buffers. /// public class Mesh : IDisposable { @@ -74,17 +72,17 @@ public class Mesh : IDisposable /// /// Gets or sets the textures used by the mesh. /// - public IReadOnlyList Textures { get; set; } + public IReadOnlyList Textures { get; set; } = []; /// /// Gets or sets the legacy textures used by the mesh. /// - public IReadOnlyList Textures_Old { get; set; } + public IReadOnlyList Textures_Old { get; set; } = []; /// /// Gets or sets the Vertex Array Object (VAO) for the mesh. /// - public VertexArrayObject VAO { get; set; } + public VertexArrayObject VAO { get; private set; } /// /// Gets or sets the Vertex Buffer Object (VBO) for the mesh. @@ -146,6 +144,7 @@ public Mesh(GL gl) /// /// Allocates the required memory for the mesh and sets up the Vertex Array Object (VAO), Vertex Buffer Object (VBO), and Element Buffer Object (EBO). /// + [MemberNotNull(nameof(EBO), nameof(VBO), nameof(VAO))] public void SetupMesh() { EBO = new BufferObject(GL, Indices, BufferTargetARB.ElementArrayBuffer); diff --git a/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs b/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs new file mode 100644 index 0000000..39b7b76 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Meshes/MeshData/Face.cs @@ -0,0 +1,59 @@ +namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +{ + /// + /// Represents a face in a 3D mesh, which is defined by a list of vertices. + /// + /// + /// Each vertex is represented by a struct that contains indices for the vertex position, texture coordinate, and normal vector. + /// + public class Face + { + private readonly List _vertices = []; + + /// + /// Adds a vertex to the face. + /// + /// The vertex to add. + public void AddVertex(FaceVertex vertex) => _vertices.Add(vertex); + + /// + /// An indexer to access the vertices of the face by their index. + /// + /// The index of the vertex to access. + /// The vertex at the specified index. + public FaceVertex this[int i] => _vertices[i]; + + /// Gets the number of vertices in the face. + public int Count => _vertices.Count; + } + + /// + /// Represents a vertex in a face, which contains indices for the vertex position, texture coordinate, and normal vector. + /// + public struct FaceVertex + { + /// + /// Initializes a new instance of the . + /// + /// The index of the vertex position. + /// The index of the texture coordinate. + /// The index of the normal vector. + public FaceVertex(int vertexIndex, int textureIndex, int normalIndex) + { + VertexIndex = vertexIndex; + TextureIndex = textureIndex; + NormalIndex = normalIndex; + } + + /// + /// Gets or sets the index of the vertex position in the mesh's vertex list. + /// + public int VertexIndex { get; set; } + + /// Gets or sets the index of the texture coordinate in the mesh's texture coordinate list. + public int TextureIndex { get; set; } + + /// Gets or sets the index of the normal vector in the mesh's normal vector list. + public int NormalIndex { get; set; } + } +} \ No newline at end of file diff --git a/SharpEngine.Core.Components/Properties/Meshes/MeshData/Group.cs b/SharpEngine.Core.Components/Properties/Meshes/MeshData/Group.cs new file mode 100644 index 0000000..504d811 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Meshes/MeshData/Group.cs @@ -0,0 +1,38 @@ +using SharpEngine.Core.Components.ObjLoader.DataStore; + +namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +{ + /// + /// Represents a group of faces in a mesh, which can be used to organize faces that share the same material or other properties. + /// + public class Group : IFaceGroup + { + private readonly List _faces = []; + + /// + /// Initializes a new instance of the . + /// + /// The name of the group. + public Group(string name) + { + Name = name; + } + + /// + /// Gets the name of the group, which can be used to identify the group and associate it with specific materials or properties in the mesh. + /// + public string Name { get; private set; } + + /// Gets or sets the material associated with the group, which defines the appearance of the faces in the group when rendered. + public Material? Material { get; set; } + + /// Gets the list of faces that belong to the group. + public IList Faces => _faces; + + /// + /// Adds a face to the group. + /// + /// The face to add to the group. + public void AddFace(Face face) => _faces.Add(face); + } +} \ No newline at end of file diff --git a/SharpEngine.Core.Components/Properties/Meshes/MeshData/Normal.cs b/SharpEngine.Core.Components/Properties/Meshes/MeshData/Normal.cs new file mode 100644 index 0000000..44e220f --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Meshes/MeshData/Normal.cs @@ -0,0 +1,36 @@ +namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +{ + /// + /// Represents a normal vector in 3D space, which is used in mesh data to define the direction perpendicular to the surface of a triangle, affecting how light interacts with the surface for rendering purposes. + /// + public struct Normal + { + /// + /// Initializes a new instance of the struct with the specified x, y, and z components, representing the normal vector's direction in 3D space. + /// + /// The x-component of the normal vector. + /// The y-component of the normal vector. + /// The z-component of the normal vector. + public Normal(float x, float y, float z) : this() + { + X = x; + Y = y; + Z = z; + } + + /// + /// Gets the x-component of the normal vector, representing the horizontal direction in 3D space. + /// + public float X { get; private set; } + + /// + /// Gets the y-component of the normal vector, representing the vertical direction in 3D space. + /// + public float Y { get; private set; } + + /// + /// Gets the z-component of the normal vector, representing the depth direction in 3D space. + /// + public float Z { get; private set; } + } +} \ No newline at end of file diff --git a/SharpEngine.Core.Components/Properties/Meshes/MeshData/TextureCoordinate.cs b/SharpEngine.Core.Components/Properties/Meshes/MeshData/TextureCoordinate.cs new file mode 100644 index 0000000..c997bf8 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Meshes/MeshData/TextureCoordinate.cs @@ -0,0 +1,36 @@ +namespace SharpEngine.Core.Components.Properties.Meshes.MeshData +{ + /// + /// Represents a texture coordinate, which is used to map textures onto the surface of a 3D model. + /// Each texture coordinate consists of two components: X and Y, which range from 0 to 1, where (0,0) corresponds to the bottom-left corner of the texture and (1,1) corresponds to the top-right corner. + /// + public struct TextureCoordinate + { + /// + /// Initializes a new instance of the struct with the specified X and Y values. + /// + /// The X-component of the texture coordinate. + /// The Y-component of the texture coordinate. + public TextureCoordinate(float x, float y) : this() + { + X = x; + Y = y; + } + + /// + /// Gets the X-component of the texture coordinate, which represents the horizontal position on the texture map. + /// + /// + /// The value ranges from 0 to 1, where 0 corresponds to the left edge of the texture and 1 corresponds to the right edge. + /// + public float X { get; private set; } + + /// + /// Gets the Y-component of the texture coordinate, which represents the vertical position on the texture map. + /// + /// + /// The value ranges from 0 to 1, where 0 corresponds to the bottom edge of the texture and 1 corresponds to the top edge. + /// + public float Y { get; private set; } + } +} \ No newline at end of file diff --git a/SharpEngine.Core.Components/Properties/Meshes/MeshData/Vertex.cs b/SharpEngine.Core.Components/Properties/Meshes/MeshData/Vertex.cs new file mode 100644 index 0000000..9a5bdfd --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Meshes/MeshData/Vertex.cs @@ -0,0 +1,56 @@ +// 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 +{ + /// + /// Represents a vertex in a 3D mesh. + /// + public struct Vertex + { + /// + /// The position of the vertex in 3D space, represented as a containing the X, Y, and Z coordinates. + /// + public Vector3 Position; + + /// + /// The normal vector at the vertex, which is used for lighting calculations to determine how light interacts with the surface of the mesh. + /// + public Vector3 Normal; + + /// + /// The tangent vector at the vertex, which is used in conjunction with the normal and bitangent vectors to create a tangent space for normal mapping and other advanced shading techniques. + /// + public Vector3 Tangent; + + /// + /// The texture coordinates (UV coordinates) for the vertex, represented as a containing the U and V coordinates that map the vertex to a specific point on a texture image. + /// + public Vector2 TexCoords; + + /// + /// The bitangent vector at the vertex, which is used in conjunction with the normal and tangent vectors to create a tangent space for normal mapping and other advanced shading techniques. + /// + public Vector3 Bitangent; + + // TODO: #65 Skeletal mesh + + /// + /// The maximum number of bone influences that can affect a single vertex, which is typically set to 4 in many 3D graphics applications to balance performance and visual quality when animating skeletal meshes. + /// + public const int MAX_BONE_INFLUENCE = 4; + + /// + /// The IDs of the bones that influence this vertex, where each ID corresponds to a specific bone in the skeleton of a skeletal mesh. + /// The length of this array should not exceed to ensure efficient processing during animation. + /// + public int[] BoneIds; + + /// + /// The weights corresponding to each bone influence on this vertex, where each weight represents the degree of influence that a particular bone has on the vertex's position during skeletal animation. + /// + public float[] Weights; + } +} diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshExtensions.cs b/SharpEngine.Core.Components/Properties/Meshes/MeshExtensions.cs similarity index 93% rename from Engine/SharpEngine.Core.Components/Properties/Meshes/MeshExtensions.cs rename to SharpEngine.Core.Components/Properties/Meshes/MeshExtensions.cs index b28219a..908e955 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Meshes/MeshExtensions.cs +++ b/SharpEngine.Core.Components/Properties/Meshes/MeshExtensions.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace SharpEngine.Core.Entities.Properties.Meshes; +namespace SharpEngine.Core.Entities.Properties.Meshes; /// /// Contains extension methods for handling meshes. diff --git a/SharpEngine.Core.Components/Properties/Meshes/Model.cs b/SharpEngine.Core.Components/Properties/Meshes/Model.cs new file mode 100644 index 0000000..e5ba6b5 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Meshes/Model.cs @@ -0,0 +1,304 @@ +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.Entities.Properties.Meshes; +using EngineTexture = SharpEngine.Core.Components.Properties.Textures.Texture; +using EngineTextureType = SharpEngine.Core.Components.Properties.Textures.TextureType; + +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Components.Properties.Meshes +{ + /// + /// Represents a 3D model, which may consist of multiple meshes, materials, and textures. + /// + /// + /// This class is responsible for processing raw mesh data into a format suitable for rendering, including expanding vertices based on face definitions and building materials and textures as needed. + /// + public class Model : IDisposable + { + private readonly GL _gl; + + /// Gets the file path to where the model is stored. + public string Path { get; } + + /// Gets the list of meshes contained within the model. + public List Meshes { get; set; } = []; + + /// + /// Initializes a new instance of the Model class with the specified OpenGL context and model file path. + /// + /// The GL context used for rendering and resource management. + /// The file path of the model asset. + public Model(GL gl, string path) + { + _gl = gl; + Path = path; + } + + /// + /// Initializes a new instance of the Model class, stores the GL context and path, and processes the provided meshes. + /// + /// + /// Input meshes are enumerated and processed immediately to populate the Model's Meshes collection. + /// + /// The OpenGL context used for creating and managing rendering resources. + /// The file path of the model resource. + /// A sequence of meshes to be processed and stored; each mesh is converted via ProcessMesh. + public Model(GL gl, string path, IEnumerable meshes) + { + _gl = gl; + Path = path; + Meshes = [.. meshes.Select(ProcessMesh)]; + } + + /// + public void Dispose() + { + foreach (var mesh in Meshes) + mesh.Dispose(); + + GC.SuppressFinalize(this); + } + + /// + /// Produces a GPU-ready Mesh from the provided mesh by cloning when already processed or by expanding vertices and constructing indices, materials, and textures. + /// + /// Source mesh to process. Must not be null. + /// + /// A Mesh prepared for rendering: either a cloned processed mesh if the input already contains vertex buffer + /// data, or a newly constructed Mesh with expanded vertices, generated index buffer, and associated textures + /// and materials. + /// + /// Thrown if the input mesh is null. + public Mesh ProcessMesh(Mesh mesh) + { + ArgumentNullException.ThrowIfNull(mesh); + + // TODO: Instead of cloning the object, we should just update the values. + if (mesh.Vertices.Length > 0) + return CloneProcessedMesh(mesh); + + var vertices = ExpandVertices(mesh); + var indices = Enumerable.Range(0, vertices.Count).Select(index => (uint)index).ToArray(); + var materials = BuildMaterials(mesh).ToList(); + var textures = BuildTextures(materials).ToList(); + + // TODO: Instead of cloning the object, we should just update the values. + return new Mesh(_gl, BuildVertices(vertices), indices, textures) + { + Name = mesh.Name, + Vertices2 = vertices, + Normals2 = mesh.Normals2, + TextureCoordinates2 = mesh.TextureCoordinates2, + Groups = mesh.Groups, + Materials = materials, + }; + } + + private Mesh CloneProcessedMesh(Mesh template) + { + var materials = template.Materials.Select(CloneMaterial).ToList(); + var textures = materials.Count > 0 ? BuildTextures(materials) : CloneTextures(template.Textures); + + return new Mesh(_gl, template.Vertices, template.Indices, textures.ToList()) + { + Name = template.Name, + Vertices2 = template.Vertices2, + Normals2 = template.Normals2, + TextureCoordinates2 = template.TextureCoordinates2, + Groups = template.Groups, + Materials = materials, + }; + } + + private static List ExpandVertices(Mesh mesh) + { + var vertices = new List(); + + foreach (var faceVertex in EnumerateFaceVertices(mesh.Groups)) + vertices.Add(CreateVertex(mesh, faceVertex)); + + if (vertices.Count > 0) + return vertices; + + return [.. mesh.Vertices2.Select(vertex => + { + vertex.BoneIds ??= new int[Vertex.MAX_BONE_INFLUENCE]; + vertex.Weights ??= new float[Vertex.MAX_BONE_INFLUENCE]; + return vertex; + })]; + } + + private static IEnumerable EnumerateFaceVertices(IEnumerable groups) + { + foreach (var group in groups) + foreach (var face in group.Faces) + for (int i = 0; i < face.Count; i++) + yield return face[i]; + } + + private static Vertex CreateVertex(Mesh mesh, FaceVertex faceVertex) + => new() + { + BoneIds = new int[Vertex.MAX_BONE_INFLUENCE], + Weights = new float[Vertex.MAX_BONE_INFLUENCE], + Position = GetVertexPosition(mesh, faceVertex.VertexIndex), + Normal = GetNormal(mesh, faceVertex.NormalIndex), + TexCoords = GetTextureCoordinates(mesh, faceVertex.TextureIndex), + }; + + private static System.Numerics.Vector3 GetVertexPosition(Mesh mesh, int vertexIndex) + => vertexIndex > 0 && vertexIndex <= mesh.Vertices2.Count + ? mesh.Vertices2[vertexIndex - 1].Position + : default; + + private static System.Numerics.Vector3 GetNormal(Mesh mesh, int normalIndex) + => normalIndex > 0 && normalIndex <= mesh.Normals2.Count + ? new System.Numerics.Vector3( + mesh.Normals2[normalIndex - 1].X, + mesh.Normals2[normalIndex - 1].Y, + mesh.Normals2[normalIndex - 1].Z) + : default; + + private static System.Numerics.Vector2 GetTextureCoordinates(Mesh mesh, int textureIndex) + => textureIndex > 0 && textureIndex <= mesh.TextureCoordinates2.Count ? + new System.Numerics.Vector2(mesh.TextureCoordinates2[textureIndex - 1].X, mesh.TextureCoordinates2[textureIndex - 1].Y) : + default; + + private IEnumerable BuildMaterials(Mesh mesh) + { + var materialDefinitions = mesh.Groups + .Select(group => group.Material) + .Where(material => material is not null) + .DistinctBy(material => material!.Name) + .ToList(); + + if (materialDefinitions.Count == 0) + materialDefinitions = [.. mesh.Materials + .Where(material => material is not null) + .DistinctBy(material => material!.Name)]; + + foreach (var material in materialDefinitions) + { + var runtimeMaterial = CreateRuntimeMaterial(material!); + if (runtimeMaterial is not null) + yield return runtimeMaterial; + } + } + + private Material? CreateRuntimeMaterial(Material definition) + { + if (string.IsNullOrWhiteSpace(definition.DiffuseTextureMap)) + return null; + + var diffuseTexture = new EngineTexture(_gl, ResolveAssetPath(definition.DiffuseTextureMap), EngineTextureType.Diffuse); + EngineTexture? specularTexture = null; + + if (!string.IsNullOrWhiteSpace(definition.SpecularTextureMap)) + specularTexture = new EngineTexture(_gl, ResolveAssetPath(definition.SpecularTextureMap), EngineTextureType.Specular); + + return new Material(definition.Name, diffuseTexture, specularTexture) + { + Name = definition.Name, + AmbientColor = definition.AmbientColor, + DiffuseColor = definition.DiffuseColor, + SpecularColor = definition.SpecularColor, + SpecularCoefficient = definition.SpecularCoefficient, + Transparency = definition.Transparency, + IlluminationModel = definition.IlluminationModel, + DiffuseTextureMap = definition.DiffuseTextureMap, + SpecularTextureMap = definition.SpecularTextureMap, + AmbientTextureMap = definition.AmbientTextureMap, + SpecularHighlightTextureMap = definition.SpecularHighlightTextureMap, + AlphaTextureMap = definition.AlphaTextureMap, + BumpMap = definition.BumpMap, + DisplacementMap = definition.DisplacementMap, + StencilDecalMap = definition.StencilDecalMap, + }; + } + + private IEnumerable CloneTextures(IReadOnlyList textures) + => textures?.Select(texture => new EngineTexture(_gl, texture.Path, texture.Type)) ?? []; + + private Material CloneMaterial(Material material) + { + EngineTexture? diffuseTexture = null; + if (material.DiffuseMap != null) + diffuseTexture = new EngineTexture(_gl, material.DiffuseMap.Path, material.DiffuseMap.Type); + + EngineTexture? specularTexture = null; + if (material.UseSpecularMap) + specularTexture = new EngineTexture(_gl, material!.SpecularMap!.Path, material.SpecularMap.Type); + + return new Material(material.Name, diffuseTexture, specularTexture) + { + Name = material.Name, + AmbientColor = material.AmbientColor, + DiffuseColor = material.DiffuseColor, + SpecularColor = material.SpecularColor, + SpecularCoefficient = material.SpecularCoefficient, + Transparency = material.Transparency, + IlluminationModel = material.IlluminationModel, + DiffuseTextureMap = material.DiffuseTextureMap, + SpecularTextureMap = material.SpecularTextureMap, + AmbientTextureMap = material.AmbientTextureMap, + SpecularHighlightTextureMap = material.SpecularHighlightTextureMap, + AlphaTextureMap = material.AlphaTextureMap, + BumpMap = material.BumpMap, + DisplacementMap = material.DisplacementMap, + StencilDecalMap = material.StencilDecalMap, + Specular = material.Specular, + Shininess = material.Shininess, + }; + } + + private static IEnumerable BuildTextures(IEnumerable materials) + => materials + .SelectMany(GetMaterialEngineTextures) + .Distinct(); + private static EngineTexture[] GetMaterialEngineTextures(Material material) + { + ArgumentNullException.ThrowIfNull(material); + + var materials = new List(); + if (material.UseSpecularMap) + materials.Add(material.SpecularMap!); + + if (material.DiffuseMap != null) + materials.Add(material.DiffuseMap); + + return [.. materials]; + } + + private string ResolveAssetPath(string assetPath) + { + if (System.IO.Path.IsPathRooted(assetPath) || string.IsNullOrWhiteSpace(Path)) + return assetPath; + + var directory = System.IO.Path.GetDirectoryName(Path); + + return string.IsNullOrWhiteSpace(directory) ? + assetPath : System.IO.Path.Combine(directory, assetPath); + } + + private static float[] BuildVertices(IEnumerable 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]; + } + } +} diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/ObjMeshData.cs b/SharpEngine.Core.Components/Properties/Meshes/ObjMeshData.cs similarity index 100% rename from Engine/SharpEngine.Core.Components/Properties/Meshes/ObjMeshData.cs rename to SharpEngine.Core.Components/Properties/Meshes/ObjMeshData.cs diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/VertexArrayObject.cs b/SharpEngine.Core.Components/Properties/Meshes/VertexArrayObject.cs similarity index 100% rename from Engine/SharpEngine.Core.Components/Properties/Meshes/VertexArrayObject.cs rename to SharpEngine.Core.Components/Properties/Meshes/VertexArrayObject.cs diff --git a/Engine/SharpEngine.Core.Components/Properties/Meshes/VertexData.cs b/SharpEngine.Core.Components/Properties/Meshes/VertexData.cs similarity index 100% rename from Engine/SharpEngine.Core.Components/Properties/Meshes/VertexData.cs rename to SharpEngine.Core.Components/Properties/Meshes/VertexData.cs diff --git a/Engine/SharpEngine.Core.Components/Properties/Quaternion.cs b/SharpEngine.Core.Components/Properties/Quaternion.cs similarity index 100% rename from Engine/SharpEngine.Core.Components/Properties/Quaternion.cs rename to SharpEngine.Core.Components/Properties/Quaternion.cs diff --git a/Engine/SharpEngine.Core.Components/Properties/Shaders/Shader.cs b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs similarity index 81% rename from Engine/SharpEngine.Core.Components/Properties/Shaders/Shader.cs rename to SharpEngine.Core.Components/Properties/Shaders/Shader.cs index 13aea94..06e8322 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Shaders/Shader.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs @@ -1,6 +1,6 @@ -using SharpEngine.Shared; +using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; using Silk.NET.OpenGL; -using System.Diagnostics.CodeAnalysis; namespace SharpEngine.Core.Shaders; @@ -21,9 +21,11 @@ public partial class Shader : IDisposable /// Gets or sets the path to the fragment shader file. public string FragPath { get; set; } + private readonly GL _gl; + private readonly ILogger _logger; + private Dictionary _uniformLocations = []; private bool disposedValue; - private readonly GL _gl; /// /// Initializes a new instance of . @@ -32,14 +34,16 @@ public partial class Shader : IDisposable /// 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 OpenGL context. /// 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; + Name = name; + + _logger = LoggingExtensions.CreateLogger(); // There are several different types of shaders, but the only two you need for basic rendering are the vertex and fragment shaders. // The vertex shader is responsible for moving around vertices, and uploading that data to the fragment shader. @@ -48,10 +52,10 @@ public Shader(GL gl, string vertPath, string fragPath, string name) // 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."); + _logger.LogWarning("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."); + _logger.LogWarning("Fragment shaders should have the file extension '.frag' for easier manageability."); VertPath = vertPath; FragPath = fragPath; @@ -65,7 +69,8 @@ protected virtual void Dispose(bool disposing) if (disposing) { - _gl.DeleteProgram(Handle); + // TODO: Collect handles in a separate container with proper access to the shared GL context. + //_gl.DeleteProgram(Handle); Handle = 0; _uniformLocations.Clear(); } diff --git a/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderAttributes.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderAttributes.cs similarity index 85% rename from Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderAttributes.cs rename to SharpEngine.Core.Components/Properties/Shaders/ShaderAttributes.cs index 29c14ac..d6fbfd2 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderAttributes.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderAttributes.cs @@ -18,7 +18,9 @@ public static class ShaderAttributes /// Represents the model matrix attribute. public const string Model = "uModel"; + /// Represents the view matrix attribute. public const string View = "uView"; + /// Represents the projection matrix attribute. public const string Projection = "uProjection"; } diff --git a/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs similarity index 91% rename from Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs rename to SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs index 830a2aa..f9fb638 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs @@ -1,4 +1,4 @@ -using SharpEngine.Shared; +using Microsoft.Extensions.Logging; using Silk.NET.OpenGL; using System.Numerics; using System.Text.RegularExpressions; @@ -19,13 +19,13 @@ public Shader Initialize() // Load and compile shader if (!LoadShader(ShaderType.VertexShader, VertPath, out uint vertexShader)) { - Debug.Log.Error("Unable to load vertex shader."); + _logger.LogError("Unable to load vertex shader."); return this; } if (!LoadShader(ShaderType.FragmentShader, FragPath, out uint fragmentShader)) { - Debug.Log.Error("Unable to load fragment shader."); + _logger.LogError("Unable to load fragment shader."); return this; } @@ -50,7 +50,7 @@ public Shader Initialize() if (!shaderLinked) { - Debug.Log.Information("Unable to link shader program."); + _logger.LogInformation("Unable to link shader program."); return this; } @@ -66,7 +66,7 @@ private bool LoadShader(ShaderType shaderType, string shaderPath, out uint shade { if (!File.Exists(shaderPath)) { - Debug.Log.Information("Shader file not found: {Path}", shaderPath); + _logger.LogInformation("Shader file not found: {Path}", shaderPath); shader = 0; return false; @@ -81,7 +81,7 @@ private bool LoadShader(ShaderType shaderType, string shaderPath, out uint shade if (!CompileShader(shader)) { - Debug.Log.Information("Unable to load {Type} shader from '{Path}'.", shaderType, shaderPath); + _logger.LogInformation("Unable to load {Type} shader from '{Path}'.", shaderType, shaderPath); return false; } @@ -133,7 +133,7 @@ private bool CompileShader(uint shader) { // 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); + _logger.LogError("Error occurred whilst compiling Shader({Shader}).\n\n{Log}", shader, infoLog); return false; } @@ -149,7 +149,7 @@ private bool LinkProgram(uint program) if (statusCode != (int)GLEnum.True) { string infoLog = _gl.GetProgramInfoLog(program); - Debug.Log.Error("Error occurred whilst linking Program({Program}): {Info}", program, infoLog); + _logger.LogError("Error occurred whilst linking Program({Program}): {Info}", program, infoLog); return false; } @@ -174,7 +174,7 @@ 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); + _logger.LogWarning("Attribute '{Attribute}' not found in shader program.", attribName); return false; } @@ -194,7 +194,7 @@ 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); + _logger.LogInformation("Uniform '{UniformName}' not found in shader '{ShaderName}'.", uniformName, Name); return false; } diff --git a/SharpEngine.Core.Components/Properties/Textures/Texture.cs b/SharpEngine.Core.Components/Properties/Textures/Texture.cs new file mode 100644 index 0000000..66213d3 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Textures/Texture.cs @@ -0,0 +1,66 @@ +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Components.Properties.Textures; + +/// +/// Represents a texture resource managed in OpenGL with associated metadata. +/// +public partial class Texture : IDisposable, IEquatable +{ + /// The OpenGL handle for the texture. + public readonly uint Handle; + + /// Gets the type of the texture. + public readonly TextureType Type; + + /// Gets the path to the texture file. + public readonly string Path; + + private readonly GL _gl; + + /// + /// Initializes a new instance of . + /// + /// The OpenGL context where this texture should be available. + /// The path to the texture file. + /// The type of the texture. + public Texture(GL gl, string path, TextureType type = TextureType.Diffuse) + { + _gl = gl; + Handle = _gl.GenTexture(); + + Path = path; + Type = type; + + Initialize(); + } + + /// + /// Creates a deep clone of the texture using the same OpenGL context. + /// + /// Assumes that the OpenGL context is the same as the original texture. + public Texture DeepClone() + => new(_gl, Path, Type); + + /// + public bool Equals(Texture? other) + { + if (other is null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + return Handle == other.Handle && + Type == other.Type && + Path == other.Path; + } + + /// + public override bool Equals(object? obj) + => Equals(obj as Texture); + + /// + public override int GetHashCode() + => HashCode.Combine(Handle, Type, Path); +} diff --git a/Engine/SharpEngine.Core.Components/Properties/Textures/TextureExtensions.cs b/SharpEngine.Core.Components/Properties/Textures/TextureExtensions.cs similarity index 84% rename from Engine/SharpEngine.Core.Components/Properties/Textures/TextureExtensions.cs rename to SharpEngine.Core.Components/Properties/Textures/TextureExtensions.cs index a060f8c..81c3f1f 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Textures/TextureExtensions.cs +++ b/SharpEngine.Core.Components/Properties/Textures/TextureExtensions.cs @@ -3,8 +3,11 @@ namespace SharpEngine.Core.Components.Properties.Textures; -public partial class Texture +public partial class Texture : IDisposable { + /// + /// Initializes the texture by loading the image data and creating the OpenGL texture. + /// public void Initialize() { // Bind the handle @@ -30,14 +33,17 @@ public void Initialize() _gl.GenerateMipmap(GLEnum.Texture2D); } + /// + /// Configures the currently bound 2D texture. + /// 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.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBaseLevel, 0); // TODO: Calculate this based on the texture size + _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, 8); // TODO: Calculate this based on the texture size _gl.GenerateMipmap(TextureTarget.Texture2D); } @@ -55,6 +61,7 @@ public void Use(TextureUnit unit = TextureUnit.Texture0) _gl.BindTexture(TextureTarget.Texture2D, Handle); } + /// public void Dispose() { _gl.DeleteTexture(Handle); diff --git a/SharpEngine.Core.Components/Properties/Textures/TextureType.cs b/SharpEngine.Core.Components/Properties/Textures/TextureType.cs new file mode 100644 index 0000000..9fd26b1 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Textures/TextureType.cs @@ -0,0 +1,27 @@ +namespace SharpEngine.Core.Components.Properties.Textures; + +/// +/// Represents the semantic role of a texture in a material. +/// +public enum TextureType +{ + /// + /// A texture that defines the base color of a material, affecting how it appears under direct lighting. + /// + Diffuse, + + /// + /// The texture that defines the shininess and reflectivity of a material, affecting how it interacts with light and creates highlights. + /// + Specular, + + /// + /// The texture that defines the normal vectors of a surface, which are used to create the illusion of surface detail and depth without adding additional geometry. + /// + Ambient, + + /// + /// The texture that defines the height information of a surface, which can be used to create parallax effects or to simulate surface displacement in advanced rendering techniques. + /// + Height +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/Properties/Transform.cs b/SharpEngine.Core.Components/Properties/Transform.cs similarity index 88% rename from Engine/SharpEngine.Core.Components/Properties/Transform.cs rename to SharpEngine.Core.Components/Properties/Transform.cs index 3f2f075..be8462d 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Transform.cs +++ b/SharpEngine.Core.Components/Properties/Transform.cs @@ -12,6 +12,11 @@ public class Transform : ITransform /// Initializes a new instance of . /// public Transform() { } + + /// + /// Initializes a new instance of . + /// + /// The initial position of the game object. public Transform(Vector3 position) { Position = position; diff --git a/Engine/SharpEngine.Core.Components/Properties/Transform2D.cs b/SharpEngine.Core.Components/Properties/Transform2D.cs similarity index 100% rename from Engine/SharpEngine.Core.Components/Properties/Transform2D.cs rename to SharpEngine.Core.Components/Properties/Transform2D.cs diff --git a/Engine/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj similarity index 51% rename from Engine/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj rename to SharpEngine.Core.Components/SharpEngine.Core.Components.csproj index 3ba046b..f411581 100644 --- a/Engine/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj +++ b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj @@ -1,7 +1,7 @@ - + - net8.0 + $(DotNetTargetFramework) enable enable @@ -11,23 +11,15 @@ true - - - - - - + + - - - - - + diff --git a/Engine/SharpEngine.Core.Numerics/Float.cs b/SharpEngine.Core.Numerics/Float.cs similarity index 69% rename from Engine/SharpEngine.Core.Numerics/Float.cs rename to SharpEngine.Core.Numerics/Float.cs index 333f856..f1f19c7 100644 --- a/Engine/SharpEngine.Core.Numerics/Float.cs +++ b/SharpEngine.Core.Numerics/Float.cs @@ -1,5 +1,8 @@ namespace SharpEngine.Core.Numerics; +/// +/// Contains constants and utility methods related to floating-point numbers. +/// public static class Float { /// diff --git a/Engine/SharpEngine.Core.Numerics/HashCodeHelper.cs b/SharpEngine.Core.Numerics/HashCodeHelper.cs similarity index 51% rename from Engine/SharpEngine.Core.Numerics/HashCodeHelper.cs rename to SharpEngine.Core.Numerics/HashCodeHelper.cs index 601d499..d8fa3e3 100644 --- a/Engine/SharpEngine.Core.Numerics/HashCodeHelper.cs +++ b/SharpEngine.Core.Numerics/HashCodeHelper.cs @@ -4,11 +4,18 @@ namespace SharpEngine.Core.Numerics { + /// + /// Provides helper methods for combining hash codes in a deterministic manner. + /// This is used internally by numeric types to produce stable hash codes based on multiple fields. + /// internal static class HashCodeHelper { /// - /// Combines two hash codes, useful for combining hash codes of individual vector elements + /// Combines two hash codes into a single hash code. /// + /// The first hash code. + /// The second hash code. + /// A combined hash code value. internal static int CombineHashCodes(int h1, int h2) => (((h1 << 5) + h1) ^ h2); } diff --git a/Engine/SharpEngine.Core.Numerics/IVector.cs b/SharpEngine.Core.Numerics/IVector.cs similarity index 88% rename from Engine/SharpEngine.Core.Numerics/IVector.cs rename to SharpEngine.Core.Numerics/IVector.cs index a620e8d..939cf80 100644 --- a/Engine/SharpEngine.Core.Numerics/IVector.cs +++ b/SharpEngine.Core.Numerics/IVector.cs @@ -10,6 +10,4 @@ public interface IVector /// 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/SharpEngine.Core.Numerics/JitIntrinsicAttribute.cs similarity index 100% rename from Engine/SharpEngine.Core.Numerics/JitIntrinsicAttribute.cs rename to SharpEngine.Core.Numerics/JitIntrinsicAttribute.cs diff --git a/Engine/SharpEngine.Core.Numerics/SharpEngine.Core.Numerics.csproj b/SharpEngine.Core.Numerics/SharpEngine.Core.Numerics.csproj similarity index 81% rename from Engine/SharpEngine.Core.Numerics/SharpEngine.Core.Numerics.csproj rename to SharpEngine.Core.Numerics/SharpEngine.Core.Numerics.csproj index 3082977..8e540fa 100644 --- a/Engine/SharpEngine.Core.Numerics/SharpEngine.Core.Numerics.csproj +++ b/SharpEngine.Core.Numerics/SharpEngine.Core.Numerics.csproj @@ -1,7 +1,7 @@  - net8.0 + $(DotNetTargetFramework) enable enable diff --git a/Engine/SharpEngine.Core.Numerics/Vector2.cs b/SharpEngine.Core.Numerics/Vector2.cs similarity index 100% rename from Engine/SharpEngine.Core.Numerics/Vector2.cs rename to SharpEngine.Core.Numerics/Vector2.cs diff --git a/Engine/SharpEngine.Core.Numerics/Vector2_Intrinsics.cs b/SharpEngine.Core.Numerics/Vector2_Intrinsics.cs similarity index 100% rename from Engine/SharpEngine.Core.Numerics/Vector2_Intrinsics.cs rename to SharpEngine.Core.Numerics/Vector2_Intrinsics.cs diff --git a/Engine/SharpEngine.Core.Numerics/Vector3.cs b/SharpEngine.Core.Numerics/Vector3.cs similarity index 99% rename from Engine/SharpEngine.Core.Numerics/Vector3.cs rename to SharpEngine.Core.Numerics/Vector3.cs index da6f282..8e5fc83 100644 --- a/Engine/SharpEngine.Core.Numerics/Vector3.cs +++ b/SharpEngine.Core.Numerics/Vector3.cs @@ -304,18 +304,6 @@ public static Vector3 Transform(Vector3 position, Matrix4x4 matrix) (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. /// @@ -344,6 +332,19 @@ public static Vector3 Transform(Vector3 value, Quaternion rotation) (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))); } + + /// + /// 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)); + #endregion Public Static Methods #region Public operator methods diff --git a/Engine/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs b/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs similarity index 100% rename from Engine/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs rename to SharpEngine.Core.Numerics/Vector3_Intrinsics.cs diff --git a/Engine/SharpEngine.Core/Abstractions/IGame.cs b/SharpEngine.Core/Abstractions/IGame.cs similarity index 63% rename from Engine/SharpEngine.Core/Abstractions/IGame.cs rename to SharpEngine.Core/Abstractions/IGame.cs index de0b798..0fad3ee 100644 --- a/Engine/SharpEngine.Core/Abstractions/IGame.cs +++ b/SharpEngine.Core/Abstractions/IGame.cs @@ -1,7 +1,7 @@ -using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Enums; - +using SharpEngine.Core.Scenes; using Silk.NET.Input; namespace SharpEngine.Core.Interfaces; @@ -10,8 +10,41 @@ namespace SharpEngine.Core.Interfaces; /// Contains definitions the Game class must implement. /// To consider: move into a abstract class so that the game doesn't necessarily have to implement all methods. /// -public class Game +public abstract class Game { + /// + /// Initializes a new instance of the . + /// + /// + /// Sets up a default scene and camera. + /// + public Game() + { + Scene = new Scene(); + Camera = new(new System.Numerics.Vector3(0), new DefaultViewSettings()); + } + + /// + /// Initializes a new instance of the . + /// + /// + /// Sets up a default camera. + /// + public Game(Scene scene) + { + Scene = scene; + Camera = new(new System.Numerics.Vector3(0), new DefaultViewSettings()); + } + + /// + /// Initializes a new instance of the . + /// + public Game(Scene scene, CameraView camera) + { + Scene = scene; + Camera = camera; + } + /// /// Gets or sets the core settings of the game. /// @@ -20,7 +53,12 @@ public class Game /// /// Gets or sets the camera of the game. /// - public CameraView Camera { get; set; } = new(new System.Numerics.Vector3(0), new DefaultViewSettings()); + public CameraView Camera { get; set; } = CameraView.CreateDefault(); + + /// + /// Gets or sets the scene loaded in the game. + /// + public Scene Scene { get; init; } /// /// Executed when a mouse button is pressed. diff --git a/Engine/SharpEngine.Core/Abstractions/IRenderable.cs b/SharpEngine.Core/Abstractions/IRenderable.cs similarity index 66% rename from Engine/SharpEngine.Core/Abstractions/IRenderable.cs rename to SharpEngine.Core/Abstractions/IRenderable.cs index e248e60..72eb7e8 100644 --- a/Engine/SharpEngine.Core/Abstractions/IRenderable.cs +++ b/SharpEngine.Core/Abstractions/IRenderable.cs @@ -1,6 +1,4 @@ -using SharpEngine.Core.Entities.Properties.Meshes; - -namespace SharpEngine.Core.Interfaces; +namespace SharpEngine.Core.Interfaces; // TODO: #55 Make an abstract class and move Render method here? diff --git a/Engine/SharpEngine.Core/Abstractions/ISettings.cs b/SharpEngine.Core/Abstractions/ISettings.cs similarity index 98% rename from Engine/SharpEngine.Core/Abstractions/ISettings.cs rename to SharpEngine.Core/Abstractions/ISettings.cs index 23888d2..6800463 100644 --- a/Engine/SharpEngine.Core/Abstractions/ISettings.cs +++ b/SharpEngine.Core/Abstractions/ISettings.cs @@ -1,6 +1,5 @@ using SharpEngine.Core.Renderers; using Silk.NET.Input; -using Silk.NET.Windowing; namespace SharpEngine.Core.Interfaces; diff --git a/Engine/SharpEngine.Core/Audio/Audio.cs b/SharpEngine.Core/Audio/Audio.cs similarity index 100% rename from Engine/SharpEngine.Core/Audio/Audio.cs rename to SharpEngine.Core/Audio/Audio.cs diff --git a/Engine/SharpEngine.Core/Audio/AudioBuffer.cs b/SharpEngine.Core/Audio/AudioBuffer.cs similarity index 100% rename from Engine/SharpEngine.Core/Audio/AudioBuffer.cs rename to SharpEngine.Core/Audio/AudioBuffer.cs diff --git a/Engine/SharpEngine.Core/Audio/AudioDevice.cs b/SharpEngine.Core/Audio/AudioDevice.cs similarity index 100% rename from Engine/SharpEngine.Core/Audio/AudioDevice.cs rename to SharpEngine.Core/Audio/AudioDevice.cs diff --git a/Engine/SharpEngine.Core/Audio/AudioFileExtensions.cs b/SharpEngine.Core/Audio/AudioFileExtensions.cs similarity index 100% rename from Engine/SharpEngine.Core/Audio/AudioFileExtensions.cs rename to SharpEngine.Core/Audio/AudioFileExtensions.cs diff --git a/Engine/SharpEngine.Core/Audio/AudioPlayerBase.cs b/SharpEngine.Core/Audio/AudioPlayerBase.cs similarity index 100% rename from Engine/SharpEngine.Core/Audio/AudioPlayerBase.cs rename to SharpEngine.Core/Audio/AudioPlayerBase.cs diff --git a/Engine/SharpEngine.Core/Audio/AudioProperties.cs b/SharpEngine.Core/Audio/AudioProperties.cs similarity index 100% rename from Engine/SharpEngine.Core/Audio/AudioProperties.cs rename to SharpEngine.Core/Audio/AudioProperties.cs diff --git a/Engine/SharpEngine.Core/Audio/AudioSource.cs b/SharpEngine.Core/Audio/AudioSource.cs similarity index 82% rename from Engine/SharpEngine.Core/Audio/AudioSource.cs rename to SharpEngine.Core/Audio/AudioSource.cs index 9e59179..d0a3669 100644 --- a/Engine/SharpEngine.Core/Audio/AudioSource.cs +++ b/SharpEngine.Core/Audio/AudioSource.cs @@ -3,7 +3,7 @@ namespace SharpEngine.Core.Audio; /// -/// Represents a source for audio +/// Represents a source for audio that can be played, stopped and queried. /// public class AudioSource { @@ -21,9 +21,9 @@ public AudioSource(AL al) } /// - /// Gets the source identifier. + /// Gets the numeric identifier for the OpenAL source. /// - /// The identifier to the source. + /// The identifier of the underlying OpenAL source. public uint Get() => _source; /// diff --git a/Engine/SharpEngine.Core/Audio/WavData.cs b/SharpEngine.Core/Audio/WavData.cs similarity index 100% rename from Engine/SharpEngine.Core/Audio/WavData.cs rename to SharpEngine.Core/Audio/WavData.cs diff --git a/Engine/SharpEngine.Core/Audio/WavPlayer.cs b/SharpEngine.Core/Audio/WavPlayer.cs similarity index 92% rename from Engine/SharpEngine.Core/Audio/WavPlayer.cs rename to SharpEngine.Core/Audio/WavPlayer.cs index a686bea..7869f28 100644 --- a/Engine/SharpEngine.Core/Audio/WavPlayer.cs +++ b/SharpEngine.Core/Audio/WavPlayer.cs @@ -1,4 +1,4 @@ -using SharpEngine.Shared; +using Microsoft.Extensions.Logging; using Silk.NET.OpenAL; using System; @@ -12,6 +12,8 @@ namespace SharpEngine.Core.Audio; /// public class WavPlayer : AudioPlayerBase { + private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + /// protected override string FileExtension => ".wav"; @@ -41,7 +43,7 @@ public override void Play(string filePath) if (!CheckHeader(file, ref index, WavConstants.RiffHeader)) { - Debug.Log.Warning("Given file is not in RIFF format"); + Logger.LogWarning("Given file is not in RIFF format"); return; } @@ -49,7 +51,7 @@ public override void Play(string filePath) if (!CheckHeader(file, ref index, WavConstants.WaveHeader)) { - Debug.Log.Warning("Given file is not in WAVE format"); + Logger.LogWarning("Given file is not in WAVE format"); return; } diff --git a/SharpEngine.Core/Components/ObjLoader/DataStore/IVertexDataStore.cs b/SharpEngine.Core/Components/ObjLoader/DataStore/IVertexDataStore.cs new file mode 100644 index 0000000..e69de29 diff --git a/SharpEngine.Core/DependencyInjection/App.cs b/SharpEngine.Core/DependencyInjection/App.cs new file mode 100644 index 0000000..3b9b7cb --- /dev/null +++ b/SharpEngine.Core/DependencyInjection/App.cs @@ -0,0 +1,100 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.Windowing; + +using System; +using System.Threading; + +namespace SharpEngine.Core.DependencyInjection; + +/// +/// Represents the application entry point responsible for resolving required services and running the main loop or window. +/// +public class App +{ + private static IConfiguration? _configuration; + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider for dependency injection. + /// The application configuration. + public App(IServiceProvider serviceProvider, IConfiguration configuration) + { + _serviceProvider = serviceProvider; + _configuration = configuration; + } + + /// + /// Runs the application by resolving required services and starting either a window or a main processing loop. + /// + /// Signals that the operation should be canceled. + public void Run(CancellationToken cancellationToken) + { + var logger = _serviceProvider.GetService>()!; + + try + { + + logger.LogInformation("App started. Resolving services."); + + if (cancellationToken.IsCancellationRequested) + return; + + var windowFactory = _serviceProvider.GetService(); + if (windowFactory is not null && windowFactory.RegisteredWindows.Count > 0) + { + using var factoryWindow = windowFactory.CreateWindow(); + + logger.LogInformation("Required services resolved."); + logger.LogInformation("Starting window."); + + factoryWindow.Initialize(); + factoryWindow.Run(); + return; + } + + var window = _serviceProvider.GetService(); + if (window is not null) + { + logger.LogInformation("Required services resolved."); + logger.LogInformation("Starting window."); + + using (window) + { + window.Initialize(); + window.Run(); + } + + return; + } + + // This is just an example of how to use the scopes. In App we should always use '_serviceProvider'. + var scopeFactory = _serviceProvider.GetRequiredService(); + using var scope = scopeFactory.CreateScope(); + + logger.LogInformation("Required services resolved."); + logger.LogInformation("Entering main loop."); + + bool running = !cancellationToken.IsCancellationRequested; + while (running) + { + logger.LogInformation("frame tick."); + + Thread.Sleep(1000); // Simulate frame delay + } + } + catch (Exception ex) + { + logger.LogError(ex, "Exception occurred."); + } + } + + /// + /// Runs the application without a . + /// + public void Run() => Run(CancellationToken.None); +} diff --git a/SharpEngine.Core/DependencyInjection/AppBuilder.cs b/SharpEngine.Core/DependencyInjection/AppBuilder.cs new file mode 100644 index 0000000..9cf7f30 --- /dev/null +++ b/SharpEngine.Core/DependencyInjection/AppBuilder.cs @@ -0,0 +1,68 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace SharpEngine.Core.DependencyInjection; + +/// +/// Represents a Dependency Injection builder for configuring services and building the application. +/// +public class AppBuilder +{ + private readonly IServiceCollection _services = new ServiceCollection(); + private IConfiguration? _configuration; + + private App? App; + + /// + /// Configures the application using the specified configuration action. + /// + /// An action to configure the configuration builder. + /// The current instance for method chaining. + public AppBuilder Configure(Action configure) + { + var configBuilder = new ConfigurationBuilder(); + configure(configBuilder); + + Configure(configBuilder); + return this; + } + + private void Configure(ConfigurationBuilder? configBuilder = null) + { + var builder = configBuilder ?? new ConfigurationBuilder(); + + _configuration = builder.Build(); + _services.AddSingleton(_configuration); + } + + /// + /// Configures the application services using the specified configuration action. + /// + /// The configuration action to apply to the service collection. + /// The current instance for method chaining. + public AppBuilder ConfigureServices(Action configure) + { + configure(_services); + return this; + } + + /// + /// Builds the App instance if not already built. + /// + /// + /// This method lazily initializes and caches the App instance. + /// + /// The App instance. + public App Build() + { + if (App != null) + return App; + + if (_configuration == null) + Configure(); + + App = new App(_services.BuildServiceProvider(), _configuration!); + return App; + } +} \ No newline at end of file diff --git a/SharpEngine.Core/DependencyInjection/ServiceCollectionExtensions.cs b/SharpEngine.Core/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..72ac3d8 --- /dev/null +++ b/SharpEngine.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,46 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using SharpEngine.Core.Windowing; +using System; +using System.Linq; + +namespace SharpEngine.Core.DependencyInjection; + +/// +/// Provides window-related dependency injection registrations. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Registers a window factory and an optional configuration callback. + /// + /// The service collection. + /// Creates the window instance. + /// Configures the created window. + /// The optional registration name. + /// Whether this registration should be used as the default app window. + /// The service collection. + public static IServiceCollection AddWindow( + this IServiceCollection services, + Func factory, + Action? configure = null, + string? name = null, + bool isDefault = false) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(factory); + + services.TryAddSingleton(); + + var registrationName = name ?? $"window-{services.Count(service => service.ServiceType == typeof(WindowRegistration))}"; + services.AddSingleton(new WindowRegistration + { + Name = registrationName, + Factory = factory, + Configure = configure, + IsDefault = isDefault + }); + + return services; + } +} diff --git a/SharpEngine.Core/Engine.cs b/SharpEngine.Core/Engine.cs new file mode 100644 index 0000000..dde1280 --- /dev/null +++ b/SharpEngine.Core/Engine.cs @@ -0,0 +1,80 @@ +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.Handlers; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Windowing; +using SharpEngine.Telemetry; + +using System.Threading.Tasks; + +namespace SharpEngine.Core; + +/// +/// Manages the engine's services and provides methods to initialize, register handlers, and shut down asynchronously. +/// +public static class Engine +{ + /// + /// Gets the manager responsible for handling engine services. + /// + public static EngineServiceManager Services { get; private set; } = new(); + + private static bool _initialized = false; + + private readonly static ILogger _logger; + + static Engine() + { + Initialize(); + _logger = LoggingExtensions.CreateLogger(typeof(Engine)); + } + + /// + /// Initializes the engine for use. + /// + public static void Initialize() + { + _logger.LogDebug("Initializing engine..."); + + if (_initialized) + { + _logger.LogWarning("Reinitializing engine."); + Services.StopAllAsync().Wait(); + } + + _initialized = true; + _logger.LogDebug("Engine successfully initialized."); + } + + /// + /// Creates and initializes a new window using the provided context and registers the window handler. + /// + /// The game context provides access to the current scene and camera settings for window initialization. + /// Returns the newly created instance. + public static Window Initialize(Game game) + { + var window = new Window(game); + + Initialize(); + Services.RegisterHandler(new WindowHandler(window)); + + return window; + } + + /// + /// Stops all engine services and shuts down the engine asynchronously. + /// + /// A that completes when shutdown finishes. + public static async Task ShutdownAsync() + { + if (Services == null) + return; + + _logger.LogDebug("Shutting down engine..."); + + await Services.StopAllAsync(); + + _initialized = false; + _logger.LogDebug("Engine successfully shut down."); + } +} \ No newline at end of file diff --git a/SharpEngine.Core/EngineServiceManager.cs b/SharpEngine.Core/EngineServiceManager.cs new file mode 100644 index 0000000..d926a94 --- /dev/null +++ b/SharpEngine.Core/EngineServiceManager.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.Handlers; +using SharpEngine.Telemetry; + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SharpEngine.Core; + +/// +/// Contains all the engine handlers and manages their lifecycle. +/// +public class EngineServiceManager +{ + private readonly List handlers = []; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of . + /// + /// The logger to use for logging messages. + public EngineServiceManager(ILogger? logger = null) + { + _logger = logger ?? LoggingExtensions.CreateLogger(); + } + + /// + /// Registers a new engine handler and starts its operation. + /// + /// The handler to register. + public void RegisterHandler(EngineHandler handler) + { + _logger.LogDebug("Registering handler: '{Handler}'.", handler.GetType().Name); + + handlers.Add(handler); + handler.Start(); + + _logger.LogDebug("Handler '{Handler}' registered successfully.", handler.GetType().Name); + } + + /// + /// Stops all active handlers asynchronously by calling their StopAsync method. + /// + /// A representing the asynchronous operation. + public async Task StopAllAsync() + { + var stopTasks = handlers.Select(handler => handler.StopAsync()); + await Task.WhenAll(stopTasks); + } +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Entities/BezierCurve.cs b/SharpEngine.Core/Entities/BezierCurve.cs similarity index 100% rename from Engine/SharpEngine.Core/Entities/BezierCurve.cs rename to SharpEngine.Core/Entities/BezierCurve.cs diff --git a/SharpEngine.Core/Entities/GameObject.cs b/SharpEngine.Core/Entities/GameObject.cs new file mode 100644 index 0000000..1b52fea --- /dev/null +++ b/SharpEngine.Core/Entities/GameObject.cs @@ -0,0 +1,249 @@ +using SharpEngine.Core.Attributes; +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; +using Shader = SharpEngine.Core.Shaders.Shader; +using EngineTexture = SharpEngine.Core.Components.Properties.Textures.Texture; + +using Silk.NET.OpenGL; +using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SharpEngine.Core.Entities; + +/// +/// Represents a game object in the scene. +/// +public class GameObject : EmptyNode, IRenderable +{ + private readonly object _modelCacheLock = new(); + private readonly Dictionary _modelByShareGroup = []; + + private readonly string _shaderVertPath; + private readonly string _shaderFragPath; + private readonly string _shaderName; + + /// + /// Initializes a new instance of the . + /// + public GameObject() : base(string.Empty) + { + BoundingBox = BoundingBox.CalculateBoundingBox(Transform); + + _shaderVertPath = _Resources.Default.VertexShader; + _shaderFragPath = _Resources.Default.FragmentShader; + _shaderName = "lighting"; + } + + /// + /// Initializes a new instance of the class with a model. + /// + /// The model. + public GameObject(Model model) : base(string.Empty) + { + Model = model; + BoundingBox = BoundingBox.CalculateBoundingBox(Transform); + + _shaderVertPath = _Resources.Default.VertexShader; + _shaderFragPath = _Resources.Default.FragmentShader; + _shaderName = "lighting"; + } + + /// + /// Initializes a new instance of the with specified textures and shaders. + /// + /// The shader to be used by the game object. + /// The model of the game object. + public GameObject(Shader shader, Model model) : base(string.Empty) + { + Model = model; + BoundingBox = BoundingBox.CalculateBoundingBox(Transform); + Shader = shader; + + _shaderVertPath = shader.VertPath; + _shaderFragPath = shader.FragPath; + _shaderName = shader.Name; + } + + /// + /// Gets or sets the shader of the game object. + /// + public Shader? Shader { get; set; } + + /// + /// Gets or sets the mesh of the game object. + /// + public Model? 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; } + + /// + /// Sets shader uniforms for model, view, and projection matrices. + /// + /// The camera view used to retrieve view and projection matrices. + protected virtual void SetShaderUniforms(CameraView camera) + { + if (Shader is null) + throw new NullReferenceException(nameof(Shader)); + + 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; + + var glInstance = window.GetGL(); + + // Ensure this instance is using a shader compiled for the correct context/share-group. + Shader = ShaderService.Instance.LoadShader(window, _shaderVertPath, _shaderFragPath, _shaderName); + + var modelToRender = GetOrCreateModelForWindow(window, glInstance); + if (modelToRender is null) + return Task.CompletedTask; + + foreach (var mesh in modelToRender.Meshes) + { + mesh.Bind(); + + foreach (var texture in mesh.Textures) + texture.Use(); + + Shader.Use(); + SetShaderUniforms(camera); + + foreach (var material in mesh.Materials) + material.SetUniformValues(Shader); + + if (mesh.Indices.Length > 0) + glInstance.DrawElements(PrimitiveType.Triangles, (uint)mesh.Indices.Length, DrawElementsType.UnsignedInt, []); + else + glInstance.DrawArrays(PrimitiveType.Triangles, 0, (uint)(mesh.Vertices.Length / (VertexData.VerticesSize + VertexData.NormalsSize + VertexData.TexCoordsSize))); + } + + return Task.CompletedTask; + } + + private static object GetShareGroupKey(Window window) + => (object?)window.SharedContext ?? (object?)window.GLContext ?? window; + + private Model? GetOrCreateModelForWindow(Window window, GL gl) + { + if (Model is null) + return null; + + var shareGroupKey = GetShareGroupKey(window); + + lock (_modelCacheLock) + { + if (_modelByShareGroup.TryGetValue(shareGroupKey, out var cachedModel)) + return cachedModel; + + // If this model was created using the same GL instance, we can reuse it for this share group. + // Otherwise, build a copy of the model bound to the GL instance for this window. + var canReuseModel = Model.Meshes.Count > 0 && ReferenceEquals(Model.Meshes[0].GL, gl); + + var modelForWindow = canReuseModel ? Model : CloneModel(gl, Model); + _modelByShareGroup[shareGroupKey] = modelForWindow; + + return modelForWindow; + } + } + + private static Model CloneModel(GL gl, Model template) + { + // Create an empty model and populate it with meshes recreated against the provided GL instance. + // This allows rendering on windows that don't share the original context. + var clone = new Model(gl, string.Empty); + + foreach (var mesh in template.Meshes) + clone.Meshes.Add(CloneMesh(gl, mesh)); + + return clone; + } + + private static Mesh CloneMesh(GL gl, Mesh template) + { + var textures = template.Textures is null ? + new List() : + [.. template.Textures.Select(t => new EngineTexture(gl, t.Path, t.Type))]; + + var clone = new Mesh(gl, template.Vertices, template.Indices, textures) + { + Name = template.Name, + }; + + if (template.Materials is { Count: > 0 }) + clone.Materials = [.. template.Materials.Select(m => CloneMaterial(gl, m))]; + + return clone; + } + + private static Material CloneMaterial(GL gl, Material template) + { + ArgumentNullException.ThrowIfNull(template); + var diffuse = new EngineTexture(gl, template.DiffuseMap!.Path, template.DiffuseMap!.Type); + + EngineTexture? specular = null; + if (template.UseSpecularMap) + specular = new EngineTexture(gl, template.SpecularMap!.Path, template.SpecularMap!.Type); + + var clone = new Material(template.Name, diffuse, specular) + { + Name = template.Name, + DiffuseTextureMap = template.DiffuseTextureMap, + SpecularTextureMap = template.SpecularTextureMap, + Specular = template.Specular, + Shininess = template.Shininess, + AmbientColor = template.AmbientColor, + DiffuseColor = template.DiffuseColor, + SpecularColor = template.SpecularColor, + SpecularCoefficient = template.SpecularCoefficient, + Transparency = template.Transparency, + IlluminationModel = template.IlluminationModel, + AmbientTextureMap = template.AmbientTextureMap, + SpecularHighlightTextureMap = template.SpecularHighlightTextureMap, + BumpMap = template.BumpMap, + DisplacementMap = template.DisplacementMap, + StencilDecalMap = template.StencilDecalMap, + AlphaTextureMap = template.AlphaTextureMap, + }; + + return clone; + } +} diff --git a/Engine/SharpEngine.Core/Entities/Lights/DirectionalLight.cs b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs similarity index 81% rename from Engine/SharpEngine.Core/Entities/Lights/DirectionalLight.cs rename to SharpEngine.Core/Entities/Lights/DirectionalLight.cs index c7c0701..b37be9a 100644 --- a/Engine/SharpEngine.Core/Entities/Lights/DirectionalLight.cs +++ b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs @@ -1,9 +1,8 @@ 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; @@ -26,13 +25,12 @@ public DirectionalLight() { Diffuse = new Vector3(0.4f, 0.4f, 0.4f); Specular = new Vector3(0.5f, 0.5f, 0.5f); - - Shader = ShaderService.Instance.LoadShader(Default.VertexShader, Default.FragmentShader, "lighting"); } /// - public Task Render(CameraView camera, Window window) + public override Task Render(CameraView camera, Window window) { + Shader = ShaderService.Instance.LoadShader(window, Default.VertexShader, Default.FragmentShader, "lighting"); Shader.SetVector3("dirLight.direction", Direction); Shader.SetVector3("dirLight.ambient", Ambient); Shader.SetVector3("dirLight.diffuse", Diffuse); diff --git a/Engine/SharpEngine.Core/Entities/Lights/Light.cs b/SharpEngine.Core/Entities/Lights/Light.cs similarity index 100% rename from Engine/SharpEngine.Core/Entities/Lights/Light.cs rename to SharpEngine.Core/Entities/Lights/Light.cs diff --git a/Engine/SharpEngine.Core/Entities/Lights/PointLight.cs b/SharpEngine.Core/Entities/Lights/PointLight.cs similarity index 83% rename from Engine/SharpEngine.Core/Entities/Lights/PointLight.cs rename to SharpEngine.Core/Entities/Lights/PointLight.cs index 0522f97..0b6ef21 100644 --- a/Engine/SharpEngine.Core/Entities/Lights/PointLight.cs +++ b/SharpEngine.Core/Entities/Lights/PointLight.cs @@ -1,8 +1,9 @@ using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; -using SharpEngine.Core.Extensions; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; + +using System; using System.Numerics; using System.Threading.Tasks; @@ -28,7 +29,7 @@ public PointLight(Vector3 position, int index) _index = index; - LampShader = new LampShader(); + LampShader = new LampShader(Window.SharedGL); } @@ -51,8 +52,13 @@ public PointLight(Vector3 position, int index) /// public float Quadratic { get; set; } + /// + /// protected override void SetShaderUniforms(CameraView camera) { + if (Shader == null) + throw new NullReferenceException(nameof(Shader)); + Shader.SetVector3($"pointLights[{_index}].position", (Vector3)Transform.Position); Shader.SetVector3($"pointLights[{_index}].ambient", Ambient); Shader.SetVector3($"pointLights[{_index}].diffuse", Diffuse); @@ -65,8 +71,9 @@ protected override void SetShaderUniforms(CameraView camera) } /// - public Task Render(CameraView camera, Window window) + public override Task Render(CameraView camera, Window window) { + Shader = ShaderService.Instance.LoadShader(window, Default.VertexShader, Default.FragmentShader, "lighting"); SetShaderUniforms(camera); return Task.CompletedTask; diff --git a/Engine/SharpEngine.Core/Entities/Lights/SpotLight.cs b/SharpEngine.Core/Entities/Lights/SpotLight.cs similarity index 88% rename from Engine/SharpEngine.Core/Entities/Lights/SpotLight.cs rename to SharpEngine.Core/Entities/Lights/SpotLight.cs index 59ad0fe..1da0b41 100644 --- a/Engine/SharpEngine.Core/Entities/Lights/SpotLight.cs +++ b/SharpEngine.Core/Entities/Lights/SpotLight.cs @@ -1,8 +1,9 @@ using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; -using SharpEngine.Core.Extensions; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; + +using System; using System.Numerics; using System.Threading.Tasks; @@ -25,7 +26,6 @@ public SpotLight() Linear = 0.09f; Quadratic = 0.032f; - Shader = ShaderService.Instance.LoadShader(Default.VertexShader, Default.FragmentShader, "lighting"); } /// @@ -58,8 +58,13 @@ public SpotLight() /// public float Quadratic { get; set; } + /// + /// protected override void SetShaderUniforms(CameraView camera) { + if (Shader == null) + throw new NullReferenceException(nameof(Shader)); + Shader.SetVector3("spotLight.position", (Vector3)Transform.Position); Shader.SetVector3("spotLight.direction", Direction); Shader.SetVector3("spotLight.ambient", Ambient); @@ -77,6 +82,7 @@ protected override void SetShaderUniforms(CameraView camera) /// public override Task Render(CameraView camera, Window window) { + Shader = ShaderService.Instance.LoadShader(window, Default.VertexShader, Default.FragmentShader, "lighting"); SetShaderUniforms(camera); return Task.CompletedTask; diff --git a/SharpEngine.Core/Entities/MeshService.cs b/SharpEngine.Core/Entities/MeshService.cs new file mode 100644 index 0000000..ea51e4d --- /dev/null +++ b/SharpEngine.Core/Entities/MeshService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace SharpEngine.Core.Entities.Properties.Meshes; + +/// +/// Service responsible for caching and providing meshes bound to GPU resources. +/// +public class MeshService : IMeshService +{ + /// A global instance of the service. + public static readonly MeshService Instance = new(); + + private readonly Dictionary Meshes = []; + + private MeshService() { } + + /// + public Mesh LoadMesh(string identifier, Mesh mesh) + { + if (Meshes.TryGetValue(identifier, out var cachedMesh)) + return cachedMesh; + + Meshes.Add(identifier, mesh); + return mesh; + } +} + +/// +/// Provides methods to load and retrieve Mesh instances from a cache keyed by an identifier. +/// +public interface IMeshService +{ + /// + /// Loads a mesh into the mesh cache or returns an existing cached instance for the identifier. + /// + /// The cache key used to identify the mesh. + /// The mesh to cache if not already present. + /// The cached or newly stored instance. + Mesh LoadMesh(string identifier, Mesh mesh); +} \ No newline at end of file diff --git a/SharpEngine.Core/Entities/Particle.cs b/SharpEngine.Core/Entities/Particle.cs new file mode 100644 index 0000000..542d126 --- /dev/null +++ b/SharpEngine.Core/Entities/Particle.cs @@ -0,0 +1,35 @@ +using System; + +namespace SharpEngine.Core.Entities; + +/// +/// Represents a particle emitted by a particle system. +/// +public class Particle : GameObject +{ + /// + /// Initializes a new instance of with a specified lifetime. + /// + /// Determines how long the particle should stay on the screen. + public Particle(int lifeTimeMilliseconds) + { + LifeTimeMilliseconds = lifeTimeMilliseconds; + StartTimeTicks = DateTime.UtcNow.Ticks; + } + + /// + /// Gets or sets the time when the particle was emitted, represented in ticks. + /// + /// + /// This value is used to determine when the particle should be removed based on its lifetime. + /// + public long StartTimeTicks { get; private set; } + + /// + /// Gets or sets the lifetime of the particle in milliseconds. + /// + /// + /// This value is used to determine when the particle should be removed based on its start time. + /// + public int LifeTimeMilliseconds { get; private set; } +} diff --git a/Engine/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs b/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs similarity index 97% rename from Engine/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs rename to SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs index b0dded0..373fda3 100644 --- a/Engine/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs +++ b/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs @@ -1,7 +1,6 @@ using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using System; -using System.Numerics; namespace SharpEngine.Core.Entities.UI.Layouts; diff --git a/Engine/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs b/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs similarity index 96% rename from Engine/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs rename to SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs index 452cc3e..3f7a339 100644 --- a/Engine/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs +++ b/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs @@ -1,6 +1,4 @@ -using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Numerics; -using SharpEngine.Core.Scenes; +using SharpEngine.Core.Scenes; using System; namespace SharpEngine.Core.Entities.UI.Layouts; diff --git a/Engine/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs b/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs similarity index 100% rename from Engine/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs rename to SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs diff --git a/SharpEngine.Core/Entities/UI/UIElement.cs b/SharpEngine.Core/Entities/UI/UIElement.cs new file mode 100644 index 0000000..50d498a --- /dev/null +++ b/SharpEngine.Core/Entities/UI/UIElement.cs @@ -0,0 +1,162 @@ +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.Textures; +using SharpEngine.Core.Windowing; +using SharpEngine.Core._Resources; +using Vector2 = SharpEngine.Core.Numerics.Vector2; +using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; + +using Silk.NET.OpenGL; +using System.Collections.Generic; +using System.Numerics; +using System.Threading.Tasks; + +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); + } + + private readonly UIShader _uiShader = new(); + private readonly Texture _texture = TextureService.Instance.LoadTexture(Default.DebugTexture); + + /// 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; } + + /// + /// Gets the most recently used VAO for this element. + /// + /// + /// UIElement maintains VAOs per OpenGL context. This property is updated on render. + /// + public uint VAO { get; private set; } + + private sealed record SharedBuffers(uint Vbo, uint Ebo, int IndexCount); + private sealed record ContextState(uint Vao); + + private readonly object _gpuLock = new(); + private readonly Dictionary _sharedBuffersByShareGroup = []; + private readonly Dictionary _contextStateByContext = []; + + private static object GetShareGroupKey(Window window) + => (object?)window.SharedContext ?? (object?)window.GLContext ?? (object)window; + + private static object GetContextKey(Window window) + => (object?)window.GLContext ?? (object)window; + + private SharedBuffers EnsureSharedBuffers(GL gl, Window window) + { + var shareGroupKey = GetShareGroupKey(window); + + lock (_gpuLock) + { + if (_sharedBuffersByShareGroup.TryGetValue(shareGroupKey, out var buffers)) + return buffers; + + // Buffers are shareable across contexts in the same share group. + var vbo = gl.GenBuffer(); + gl.BindBuffer(GLEnum.ArrayBuffer, vbo); + gl.BufferData(GLEnum.ArrayBuffer, Mesh.GetVertices(), GLEnum.StaticDraw); + + var ebo = gl.GenBuffer(); + gl.BindBuffer(GLEnum.ElementArrayBuffer, ebo); + gl.BufferData(GLEnum.ElementArrayBuffer, Mesh.Indices, GLEnum.StaticDraw); + + buffers = new SharedBuffers(vbo, ebo, Mesh.Indices.Length); + _sharedBuffersByShareGroup[shareGroupKey] = buffers; + + return buffers; + } + } + + private ContextState EnsureContextState(GL gl, Window window, SharedBuffers sharedBuffers) + { + var contextKey = GetContextKey(window); + + lock (_gpuLock) + { + if (_contextStateByContext.TryGetValue(contextKey, out var state)) + return state; + + // VAOs are generally not shared between contexts. + var vao = gl.GenVertexArray(); + gl.BindVertexArray(vao); + + gl.BindBuffer(GLEnum.ArrayBuffer, sharedBuffers.Vbo); + gl.BindBuffer(GLEnum.ElementArrayBuffer, sharedBuffers.Ebo); + + _uiShader.EnsureInitialized(window); + _uiShader.Shader!.Use(); + _uiShader.SetAttributes(gl); + + state = new ContextState(vao); + _contextStateByContext[contextKey] = state; + + return state; + } + } + + Matrix4x4 OrthoMatrix = Matrix4x4.CreateOrthographicOffCenter(-1, 1, -1, 1, -1, 1); + + /// + /// Render the UI element. + /// + public override Task Render(CameraView camera, Window window) + { + var gl = window.GetGL(); + + _uiShader.EnsureInitialized(window); + + var sharedBuffers = EnsureSharedBuffers(gl, window); + var contextState = EnsureContextState(gl, window, sharedBuffers); + + _uiShader.Shader!.Use(); + gl.BindVertexArray(contextState.Vao); + _texture.Use(TextureUnit.Texture0); + + VAO = contextState.Vao; + + // TODO: #75 These should come from somewhere else. + const float screenWidth = 1280; + const float screenHeight = 720; + + _uiShader.Shader!.SetFloat("width", Width); + _uiShader.Shader!.SetFloat("height", Height); + _uiShader.Shader!.SetVector2("screenSize", new System.Numerics.Vector2(screenWidth, screenHeight)); + _uiShader.Shader!.SetVector2("position", (System.Numerics.Vector2)Transform.Position); + _uiShader.Shader!.SetFloat("rotation", Math.DegreesToRadians(Transform.Rotation.Angle)); + _uiShader.Shader!.SetInt("texture1", 0); + _uiShader.Shader!.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); + _uiShader.Shader!.SetMatrix4("orthoMatrix", OrthoMatrix); // Pass the orthographic matrix to the shader + + gl.DrawElements(PrimitiveType.Triangles, (uint)sharedBuffers.IndexCount, DrawElementsType.UnsignedInt, []); + + return Task.CompletedTask; + } +} diff --git a/Engine/SharpEngine.Core/Entities/Views/CameraView.cs b/SharpEngine.Core/Entities/Views/CameraView.cs similarity index 92% rename from Engine/SharpEngine.Core/Entities/Views/CameraView.cs rename to SharpEngine.Core/Entities/Views/CameraView.cs index d98a3ff..16ea0fb 100644 --- a/Engine/SharpEngine.Core/Entities/Views/CameraView.cs +++ b/SharpEngine.Core/Entities/Views/CameraView.cs @@ -1,4 +1,3 @@ -using SharpEngine.Core.Components.Properties; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Shaders; using System.Numerics; @@ -25,6 +24,16 @@ public CameraView(Vector3 position, IViewSettings settings) : base(settings) AspectRatio = settings.WindowOptions.Size.X / (float)settings.WindowOptions.Size.Y; } + /// + /// Creates a default camera view with the specified position and settings, or default values if not provided. + /// + /// The initial position of the camera. If not provided, defaults to (0,0,0). + /// The settings for the camera. If not provided, defaults to . + /// A new instance of . + public static CameraView CreateDefault(Vector3? position = null, IViewSettings? settings = null) + => new(position ?? new Vector3(0), + settings ?? new DefaultViewSettings()); + // Rotation around the X axis (radians) private float _pitch; diff --git a/Engine/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs b/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs similarity index 99% rename from Engine/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs rename to SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs index cef7852..03d03f7 100644 --- a/Engine/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs +++ b/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs @@ -1,4 +1,5 @@ using SharpEngine.Core.Renderers; + using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.Windowing; diff --git a/Engine/SharpEngine.Core/Entities/Views/Settings/IViewSettings.cs b/SharpEngine.Core/Entities/Views/Settings/IViewSettings.cs similarity index 100% rename from Engine/SharpEngine.Core/Entities/Views/Settings/IViewSettings.cs rename to SharpEngine.Core/Entities/Views/Settings/IViewSettings.cs diff --git a/Engine/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs b/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs similarity index 100% rename from Engine/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs rename to SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs diff --git a/Engine/SharpEngine.Core/Entities/Views/View.cs b/SharpEngine.Core/Entities/Views/View.cs similarity index 93% rename from Engine/SharpEngine.Core/Entities/Views/View.cs rename to SharpEngine.Core/Entities/Views/View.cs index 02aecba..b037f4e 100644 --- a/Engine/SharpEngine.Core/Entities/Views/View.cs +++ b/SharpEngine.Core/Entities/Views/View.cs @@ -1,9 +1,8 @@ -using SharpEngine.Core.Components.Properties; -using SharpEngine.Core.Entities.Views.Settings; +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; @@ -22,8 +21,13 @@ public View(IViewSettings settings) Settings = settings; } + /// Gets the front vector of the camera. protected Vector3 _front = -Vector3.UnitZ; + + /// Gets the up vector of the camera. protected Vector3 _up = Vector3.UnitY; + + /// Gets the right vector of the camera. protected Vector3 _right = Vector3.UnitX; /// Gets the front vector of the camera. diff --git a/Engine/SharpEngine.Core/Enums/MouseWheelScrollDirection.cs b/SharpEngine.Core/Enums/MouseWheelScrollDirection.cs similarity index 100% rename from Engine/SharpEngine.Core/Enums/MouseWheelScrollDirection.cs rename to SharpEngine.Core/Enums/MouseWheelScrollDirection.cs diff --git a/SharpEngine.Core/Extensions/AssemblyExtensions.cs b/SharpEngine.Core/Extensions/AssemblyExtensions.cs new file mode 100644 index 0000000..39a3315 --- /dev/null +++ b/SharpEngine.Core/Extensions/AssemblyExtensions.cs @@ -0,0 +1,18 @@ +using System; +using System.Reflection; + +namespace SharpEngine.Core.Extensions; + +/// +/// Contains extension methods for the class, providing additional functionality related to assembly metadata and versioning. +/// +public static class AssemblyExtensions +{ + /// + /// Gets the version of the assembly as specified in its metadata. + /// + /// The assembly to get the version for. + /// The version of the assembly. + public static Version GetVersion(this Assembly assembly) + => assembly.GetName().Version!; +} diff --git a/Engine/SharpEngine.Core/Extensions/CollectionExtensions.cs b/SharpEngine.Core/Extensions/CollectionExtensions.cs similarity index 100% rename from Engine/SharpEngine.Core/Extensions/CollectionExtensions.cs rename to SharpEngine.Core/Extensions/CollectionExtensions.cs diff --git a/Engine/SharpEngine.Core/Extensions/PathExtensions.cs b/SharpEngine.Core/Extensions/PathExtensions.cs similarity index 100% rename from Engine/SharpEngine.Core/Extensions/PathExtensions.cs rename to SharpEngine.Core/Extensions/PathExtensions.cs diff --git a/SharpEngine.Core/Handlers/EngineHandler.cs b/SharpEngine.Core/Handlers/EngineHandler.cs new file mode 100644 index 0000000..212c54c --- /dev/null +++ b/SharpEngine.Core/Handlers/EngineHandler.cs @@ -0,0 +1,161 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Handlers +{ + + /// + /// Base class for long-running engine handlers that run asynchronously. + /// + /// + /// Implementations should override to perform the handler's work. + /// The handler can be started with and stopped with . + /// + /// Consumers can await to observe when the handler has finished executing. + /// + public abstract class EngineHandler : IAsyncDisposable + { + private Task? _runner; + private CancellationTokenSource? _cts; + private readonly TaskCompletionSource _completionSource = new(); + private DateTime _startTime; + + /// + /// Logger for recording diagnostic and operational messages for the handler. + /// + /// + /// Provides structured, category-specific logging for EngineHandler and its derived classes. + /// + protected readonly ILogger Logger; + + /// + /// Initializes a new instance of . + /// + /// A logger for logging handler events. + protected EngineHandler(ILogger logger) + { + Logger = logger; + } + + /// + /// Gets the current lifecycle state of the handler. + /// + public EngineHandlerState State { get; private set; } = EngineHandlerState.NotStarted; + + /// + /// Gets the total execution duration of the handler once it has stopped or faulted. + /// + public TimeSpan ExecutionDuration => State is EngineHandlerState.Stopped or EngineHandlerState.Faulted ? + DateTime.UtcNow - _startTime : + TimeSpan.Zero; + + /// + /// Starts the handler and begins executing on a background task. + /// + /// Thrown if the handler has already been started. + public void Start() + { + if (State != EngineHandlerState.NotStarted) + throw new InvalidOperationException("Handler already started."); + + _cts = new CancellationTokenSource(); + _startTime = DateTime.UtcNow; + _runner = Task.Run(() => RunInternalAsync(_cts.Token)); + } + + /// + /// Requests cancellation and waits for the handler to stop. + /// + /// + /// If the handler has not been started or is already stopped, this method returns immediately. + /// + public async Task StopAsync() + { + if (_cts == null || _runner == null || State == EngineHandlerState.Stopped) + { + Logger.LogDebug("[EngineHandler] '{Name}' is not running.", GetType().Name); + return; + } + + await _cts.CancelAsync(); + + try + { + await _runner; + } + catch (OperationCanceledException) + { + Logger.LogDebug("[EngineHandler] '{Name}' was canceled.", GetType().Name); + } + + await OnStopAsync(); + State = EngineHandlerState.Stopped; + + Logger.LogDebug("[EngineHandler] '{Name}' stopped.", GetType().Name); + } + + /// + /// Asynchronously waits for the handler to complete execution, either by stopping normally, + /// being cancelled, or faulting. + /// + /// A task that completes when the handler has finished. + public async Task WaitForCompletionAsync() => await _completionSource.Task; + + /// + /// Executes the handler's work. Implementations should honor the provided + /// and return promptly when it signals cancellation. + /// + /// The cancellation token that is signaled when the handler should stop. + protected abstract Task ExecuteAsync(CancellationToken token); + + /// + /// Optional override called before the execution of the handler. + /// + protected virtual Task OnInitializedAsync() => Task.CompletedTask; + + /// + /// Optional override called after the handler has stopped or been cancelled. + /// + protected virtual Task OnStopAsync() => Task.CompletedTask; + + private async Task RunInternalAsync(CancellationToken token) + { + try + { + State = EngineHandlerState.Running; + await OnInitializedAsync(); + await ExecuteAsync(token); + State = EngineHandlerState.Stopped; + } + catch (OperationCanceledException) + { + State = EngineHandlerState.Stopped; + Logger.LogDebug("[EngineHandler] '{Name}' was canceled.", GetType().Name); + } + catch (Exception ex) + { + State = EngineHandlerState.Faulted; + Logger.LogError(ex, "[EngineHandler] Unhandled exception in '{Name}': {Exception}", GetType().Name, ex.Message); + } + finally + { + _completionSource.TrySetResult(); + } + } + + /// + /// + /// Disposes internal resources used by the handler. Implementations that + /// require asynchronous disposal should override this method. + /// + public virtual ValueTask DisposeAsync() + { + _cts?.Dispose(); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } + } +} + \ No newline at end of file diff --git a/SharpEngine.Core/Handlers/EngineHandlerState.cs b/SharpEngine.Core/Handlers/EngineHandlerState.cs new file mode 100644 index 0000000..44153c4 --- /dev/null +++ b/SharpEngine.Core/Handlers/EngineHandlerState.cs @@ -0,0 +1,25 @@ +/// +/// Represents the lifecycle state of an engine handler. +/// +public enum EngineHandlerState +{ + /// + /// The engine handler has not been started yet. + /// + NotStarted, + + /// + /// The engine handler is currently running. + /// + Running, + + /// + /// The engine handler has been stopped gracefully. + /// + Stopped, + + /// + /// The engine handler has encountered an error and is in a faulted state. + /// + Faulted +} diff --git a/SharpEngine.Core/Handlers/ParticleEmitter.cs b/SharpEngine.Core/Handlers/ParticleEmitter.cs new file mode 100644 index 0000000..3d62aaf --- /dev/null +++ b/SharpEngine.Core/Handlers/ParticleEmitter.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Entities; +using System; +using System.Collections.Generic; + +using System.Threading; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Handlers; + +internal class ParticleEmitter : EngineHandler +{ + public int EmissionRateMilliseconds { get; set; } + public int ParticleLifeTimeMilliseconds { get; set; } + + private readonly List _particles = new(); + private readonly object _particlesLock = new(); + + private readonly SemaphoreSlim _pauseSemaphore = new(1, 1); + private bool _isPaused = false; + + /// + /// Initializes a new instance of . + /// + /// The logger to use for logging events. + public ParticleEmitter(ILogger logger) : base(logger) { } + + /// + protected override async Task ExecuteAsync(CancellationToken token) + { + // Run disposer and emitter concurrently so particles are emitted and + // disposed in parallel while respecting pause behavior. + await Task.WhenAll(StartDisposer(token), StartEmitter(token)); + } + + private async Task StartEmitter(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + // Wait if emitting is paused + await _pauseSemaphore.WaitAsync(token); + _pauseSemaphore.Release(); + + EmitParticle(); + + // Wait for the next emission + await Task.Delay(EmissionRateMilliseconds, token); + } + } + + private async Task StartDisposer(CancellationToken token) + { + // Periodically remove expired particles. This loop honors the same + // pause semaphore so disposal pauses when emitter is paused. + while (!token.IsCancellationRequested) + { + // Wait if emitting is paused + await _pauseSemaphore.WaitAsync(token); + _pauseSemaphore.Release(); + + // Remove expired particles + var nowTicks = DateTime.UtcNow.Ticks; + lock (_particlesLock) + { + _particles.RemoveAll(p => (nowTicks - p.StartTimeTicks) / TimeSpan.TicksPerMillisecond >= p.LifeTimeMilliseconds); + } + + // Avoid a tight loop — check disposal every 50ms + await Task.Delay(50, token); + } + } + + private void EmitParticle() + { + var particle = new Particle(ParticleLifeTimeMilliseconds); + lock (_particlesLock) + { + _particles.Add(particle); + } + } + + /// + /// Pauses the emission of particles. + /// + /// + /// If already paused, this method does nothing. + /// + public void Pause() + { + if (_isPaused) + return; + + _isPaused = true; + _pauseSemaphore.Wait(); + } + + /// + /// Resumes the emission of particles. + /// + /// + /// If not paused, this method does nothing. + /// + public void Resume() + { + if (!_isPaused) + return; + + _pauseSemaphore.Release(); + _isPaused = false; + } +} \ No newline at end of file diff --git a/SharpEngine.Core/Handlers/WindowHandler.cs b/SharpEngine.Core/Handlers/WindowHandler.cs new file mode 100644 index 0000000..ef2d1ec --- /dev/null +++ b/SharpEngine.Core/Handlers/WindowHandler.cs @@ -0,0 +1,244 @@ +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Telemetry; +using SharpEngine.Core.Windowing; + +using Silk.NET.Input; +using Silk.NET.Maths; +using Silk.NET.Windowing; + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Handlers; + +/// +/// Manages application windows and their input contexts. +/// +/// This handler maintains queue of window creation requests, starts a background task to process that queue, +/// and runs the per-frame update/render loop for all active windows until cancellation is requested. +/// +/// +/// Windows are created and enqueued on a background task. +/// +/// The handler will call DoEvents, DoUpdate and DoRender on each managed window every loop iteration. +/// When a window is closing it will be disposed and removed from the managed list. +/// +public class WindowHandler : EngineHandler +{ + private static readonly List _windows = []; + private static readonly List _inputContexts = []; + private static readonly ConcurrentQueue _windowQueue = []; + private static readonly CancellationTokenSource _cancellationTokenSource = new(); + + private readonly ILogger _logger; + + private SilkWindow? _mainWindow; + + /// + /// Gets the main window registered with this handler, if any. + /// + public SilkWindow? MainWindow => _mainWindow; + + /// + /// Initializes a new instance of the + /// + /// + /// Starts the window queue as a background task. + /// + /// Optional logger instance; if null a default logger will be created. + public WindowHandler(ILogger? logger = null) : base(logger ?? LoggingExtensions.CreateLogger()) + { + _logger = logger ?? LoggingExtensions.CreateLogger(); + + // Enqueue a default window creation request when no explicit window is provided. + StartWindowQueueTask(WindowOptions.Default); + } + + /// + /// Initializes a new instance of the + /// + /// + /// Starts the window queue as a background task. + /// + /// An existing window instance. + /// Optional logger instance; if null a default logger will be created. + public WindowHandler(SilkWindow window, ILogger? logger = null) : base(logger ?? LoggingExtensions.CreateLogger()) + { + _logger = logger ?? LoggingExtensions.CreateLogger(); + + // Register the provided window as the main window so it is managed immediately. + RegisterWindow(window, isMain: true); + + // Start the queue task but do not enqueue an additional default window. + StartWindowQueueTask(window.Settings.WindowOptions); + } + + /// + /// Starts a background task that enqueues an initial and runs until the shared cancellation token is requested. + /// + /// Optional window options to enqueue; if null is used. + private void StartWindowQueueTask(WindowOptions? options = null) + { + Task.Run(async () => + { + // Only enqueue when an explicit options value is provided. The caller + // controls whether a default window should be queued. + if (options is not null) + _windowQueue.Enqueue(options.Value); + + while (!_cancellationTokenSource.IsCancellationRequested) + { + await Task.Delay(1000); + _logger.LogDebug("Running loop on background thread..."); + } + }); + } + + /// + /// + /// Runs the main update loop for managed windows. + /// + /// + /// This method will continue to run until the provided cancellation is cancelled. + /// + /// A that, when cancelled, ends the loop. + protected override Task ExecuteAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + for (int i = 0; i < _windows.Count; i++) + UpdateWindow(ref i); + + DequeueWindows(); + } + + return Task.FromCanceled(token); + } + + /// + /// Advances a single window through its event, update and render phases. + /// + /// + /// If the window is closing it will be disposed and removed from the managed list. + /// + /// + /// Reference to the index of the window in the internal list; + /// this method may decrement the index if the window is removed. + /// + 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(); + } + } + + /// + /// Dequeues any pending window creation requests and enqueues the resulting windows for management. + /// No action is taken if cancellation has been requested. + /// + private static void DequeueWindows() + { + if (_cancellationTokenSource.IsCancellationRequested) + return; + + while (_windowQueue.TryDequeue(out var options)) + EnqueueWindow(options); + } + + /// + /// Creates and initializes a new instance using the provided . + /// + /// The created window's position will be offset based on the current number of managed windows to help avoid overlap. + /// + /// Options to use when creating the window. + /// A newly initialized . + private static Windowing.Window CreateWindow(WindowOptions options) + { + var viewSettings = new DefaultViewSettings() with + { + WindowOptions = options with + { + Title = options.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 Windowing.Window(new(), viewSettings, LoggingExtensions.CreateLogger()); + window.Initialize(); + + return window; + } + + /// + /// Creates a new window from the provided options, wires up mouse click handlers, and begins managing the window and its input context. + /// + /// Options used to create the new window. + private static void EnqueueWindow(WindowOptions options) + { + var window = CreateWindow(options); + foreach (var mouse in window!.Input!.Mice) + mouse.Click += Mouse_Click; + + _inputContexts.Add(window.Input); + _windows.Add(window); + } + + /// + /// Registers an existing window instance with the handler and optionally marks it as the main window. + /// + /// The window instance to register. + /// Whether this window should be considered the main window. + private void RegisterWindow(SilkWindow window, bool isMain = false) + { + if (window is null) + return; + + if (window.Input is not null) + { + foreach (var mouse in window.Input.Mice) + mouse.Click += Mouse_Click; + + _inputContexts.Add(window.Input); + } + + _windows.Add(window); + + if (isMain) + _mainWindow = window; + } + + /// + /// Global mouse click handler that enqueues a request to create a new default window. + /// + /// The mouse instance that raised the click event. + /// The mouse button that was clicked. + /// The pointer position when the click occurred. + private static void Mouse_Click(IMouse args1, Silk.NET.Input.MouseButton arg2, System.Numerics.Vector2 arg3) + { + _windowQueue.Enqueue(WindowOptions.Default); + } +} diff --git a/SharpEngine.Core/Primitives/Cube.cs b/SharpEngine.Core/Primitives/Cube.cs new file mode 100644 index 0000000..76ac6ae --- /dev/null +++ b/SharpEngine.Core/Primitives/Cube.cs @@ -0,0 +1,265 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Components.Properties.Textures; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Textures; +using SharpEngine.Core.Windowing; + +using System.Collections.Generic; + +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.SharedGL) + { + Vertices = [.. Vertices], + Normals = [.. Normals], + TextureCoordinates = [.. TextureCoordinates], + Indices = [.. Indices], + Textures = [defaultTexture], + // Materials = [MaterialService.Instance.LoadMaterial(Default.DebugMaterial)], + Materials = [new("Debug", defaultTexture)] + }; + + Mesh = MeshService.Instance.LoadMesh(nameof(Cube), mesh); + Model = new(Window.SharedGL, string.Empty, [Mesh]); + + _loaded = true; + } + + private readonly static bool _loaded; + + /// The loaded model of the cube. + public static Model Model { get; private set; } = null!; + + /// The cube mesh. + public static readonly Mesh Mesh = null!; + + private static readonly 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, + ]; + + private readonly 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, + ]; + + private readonly 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 + ]; + + private readonly 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 + ]; + + /// + /// Creates a model with a mesh containing diffuse and optional specular texture maps. + /// + /// The file path to the diffuse texture map. + /// The file path to the specular texture map, or to omit specular mapping. + /// A new model instance containing a mesh with the specified texture maps. + public static Model CreateModel(string diffuseMapFile, string? specularMapFile = null) + { + var diffuseTexture = TextureService.Instance.LoadTexture(diffuseMapFile, TextureType.Diffuse); + var specularTexture = string.IsNullOrWhiteSpace(specularMapFile) ? + null : TextureService.Instance.LoadTexture(specularMapFile, TextureType.Specular); + + var material = new Material("Debug", diffuseTexture, specularTexture); + var textures = new List { diffuseTexture }; + + if (specularTexture is not null && specularTexture.Handle != diffuseTexture.Handle) + textures.Add(specularTexture); + + var mesh = new Mesh(Window.SharedGL, BuildVertices(), [], textures) + { + Name = Mesh.Name, + Materials = [material] + }; + + var model = new Model(Window.SharedGL, string.Empty); + model.Meshes.Add(mesh); + + return model; + } + + private static float[] BuildVertices() + { + var vertices = new List(Vertices.Length + Normals.Length + TextureCoordinates.Length); + + for (int i = 0; i < Vertices.Length / 3; i++) + { + var vertexIndex = i * 3; + var texCoordIndex = i * 2; + + vertices.Add(Vertices[vertexIndex]); + vertices.Add(Vertices[vertexIndex + 1]); + vertices.Add(Vertices[vertexIndex + 2]); + + vertices.Add(Normals[vertexIndex]); + vertices.Add(Normals[vertexIndex + 1]); + vertices.Add(Normals[vertexIndex + 2]); + + vertices.Add(TextureCoordinates[texCoordIndex]); + vertices.Add(TextureCoordinates[texCoordIndex + 1]); + } + + return [.. vertices]; + } +} diff --git a/Engine/SharpEngine.Core/Primitives/Plane.cs b/SharpEngine.Core/Primitives/Plane.cs similarity index 93% rename from Engine/SharpEngine.Core/Primitives/Plane.cs rename to SharpEngine.Core/Primitives/Plane.cs index ba046a5..854ae7a 100644 --- a/Engine/SharpEngine.Core/Primitives/Plane.cs +++ b/SharpEngine.Core/Primitives/Plane.cs @@ -9,7 +9,7 @@ namespace SharpEngine.Core.Primitives; public static class Plane { /// The plane mesh. - public static Mesh Mesh { get; } = new(Window.GL) + public static Mesh Mesh { get; } = new(Window.SharedGL) { Vertices = [ diff --git a/Engine/SharpEngine.Core/Primitives/PrimitiveFactory.cs b/SharpEngine.Core/Primitives/PrimitiveFactory.cs similarity index 77% rename from Engine/SharpEngine.Core/Primitives/PrimitiveFactory.cs rename to SharpEngine.Core/Primitives/PrimitiveFactory.cs index 633580d..1148e59 100644 --- a/Engine/SharpEngine.Core/Primitives/PrimitiveFactory.cs +++ b/SharpEngine.Core/Primitives/PrimitiveFactory.cs @@ -1,11 +1,9 @@ -using SharpEngine.Core.Entities; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Entities; using SharpEngine.Core.Entities.Properties; -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; @@ -33,18 +31,17 @@ public static GameObject Create(PrimitiveType primitiveType, Vector3 position) /// 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) { - // var material = diffuseMapFile, specularMapFile, - - Model_Old model = primitiveType switch + Model model = primitiveType switch { - PrimitiveType.Cube => Cube.Model, + PrimitiveType.Cube => Cube.CreateModel(diffuseMapFile, specularMapFile), // 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) + // Shader compilation is context-dependent; the GameObject will acquire the correct shader per-window during rendering. + return new GameObject() { + Model = model, Transform = new Transform((Numerics.Vector3)position), }; } diff --git a/Engine/SharpEngine.Core/Primitives/PrimitiveType.cs b/SharpEngine.Core/Primitives/PrimitiveType.cs similarity index 100% rename from Engine/SharpEngine.Core/Primitives/PrimitiveType.cs rename to SharpEngine.Core/Primitives/PrimitiveType.cs diff --git a/Engine/SharpEngine.Core/Ray.cs b/SharpEngine.Core/Ray.cs similarity index 100% rename from Engine/SharpEngine.Core/Ray.cs rename to SharpEngine.Core/Ray.cs diff --git a/SharpEngine.Core/Renderers/ParticleRenderer.cs b/SharpEngine.Core/Renderers/ParticleRenderer.cs new file mode 100644 index 0000000..53f6d93 --- /dev/null +++ b/SharpEngine.Core/Renderers/ParticleRenderer.cs @@ -0,0 +1,25 @@ +using SharpEngine.Core.Interfaces; +using System; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Renderers; + +/// +/// Renders particle effects. +/// +public class ParticleRenderer : RendererBase +{ + /// + /// Initializes a new instance of the . + /// + /// The settings for the particle renderer. + public ParticleRenderer(ISettings settings) : base(settings) + { + } + + /// + public override RenderFlags RenderFlag => RenderFlags.All; + + /// + public override Task Render() => throw new NotImplementedException(); +} diff --git a/Engine/SharpEngine.Core/Renderers/RenderFlags.cs b/SharpEngine.Core/Renderers/RenderFlags.cs similarity index 100% rename from Engine/SharpEngine.Core/Renderers/RenderFlags.cs rename to SharpEngine.Core/Renderers/RenderFlags.cs diff --git a/Engine/SharpEngine.Core/Renderers/Renderer.cs b/SharpEngine.Core/Renderers/Renderer.cs similarity index 69% rename from Engine/SharpEngine.Core/Renderers/Renderer.cs rename to SharpEngine.Core/Renderers/Renderer.cs index 91f4417..873a359 100644 --- a/Engine/SharpEngine.Core/Renderers/Renderer.cs +++ b/SharpEngine.Core/Renderers/Renderer.cs @@ -1,11 +1,13 @@ using SharpEngine.Core.Entities; +using SharpEngine.Core.Entities.Lights; using SharpEngine.Core.Entities.Properties; 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 Microsoft.Extensions.Logging; using Silk.NET.OpenGL; using System; @@ -19,12 +21,14 @@ namespace SharpEngine.Core.Renderers; /// public class Renderer : RendererBase { - private readonly LampShader _lampShader; - private readonly LightingShader _lightingShader; + private LampShader _lampShader = null!; + private LightingShader _lightingShader = null!; private readonly CameraView _camera; private readonly Scene _scene; - private readonly Window _window; + private readonly ILogger _logger; + + private GL _gl = null!; // TODO: #7 Property for specific type of objects // No heavy iteration reads for filtering, @@ -40,18 +44,24 @@ 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, Window window, ISettings settings, Scene scene) : base(settings) + /// The logger for the renderer. + public Renderer(CameraView camera, ISettings settings, Scene scene, ILogger logger) : base(settings) { _camera = camera; _scene = scene; - _window = window; + _logger = logger; + } + + /// + protected override void OnWindowAttached(Window window) + { + _gl = window.GetGL(); // TODO: #5 These should be refactored out. The minimum build shouldn't need to use these. - _lightingShader = new LightingShader(); - _lampShader = new LampShader(); + _lightingShader = new LightingShader(_gl); + _lampShader = new LampShader(_gl); } /// @@ -62,33 +72,37 @@ public override Task Render() try { - Window.GL.Enable(EnableCap.DepthTest); + _gl.Enable(EnableCap.DepthTest); // Enable image transparency. // TODO: #62 Needs testing. - Window.GL.Enable(EnableCap.Blend); - Window.GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + _gl.Enable(EnableCap.Blend); + _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); _camera.SetShaderUniforms(_lightingShader.Shader!); - Window.GL.BindVertexArray(_lightingShader.Vao); + _gl.BindVertexArray(_lightingShader.Vao); + var lightRenderTasks = _scene.IterateAsync(_scene.Root.Children, RenderLight); + Task.WaitAll([.. lightRenderTasks]); + + // TODO: Streamline this part where the game objects are rendered var gameObjectRenderTasks = _scene.IterateAsync(_scene.Root.Children, RenderGameObject); var renderTask = Task.WhenAll(gameObjectRenderTasks); - Window.GL.BindVertexArray(_lampShader.Vao); + _gl.BindVertexArray(_lampShader.Vao); return renderTask; } catch (Exception ex) { - Debug.Log.Error(ex, "{Message}", ex.Message); + _logger.LogError(ex, "{Message}", ex.Message); return Task.FromException(ex); } } private Task RenderGameObject(SceneNode node) { - if (node is not GameObject gameObject) + if (node is not GameObject gameObject || node is Light) return Task.CompletedTask; // TODO: #7 Fix culling for blocks that are partially in view @@ -97,8 +111,16 @@ private Task RenderGameObject(SceneNode node) return Task.CompletedTask; // TODO: #7 Skip blocks that are behind others relative to the camera - return gameObject.Render(_camera, _window); - // gameObject.Render(_camera, _window); + return gameObject.Render(_camera, Window); + // gameObject.Render(_camera, Window); + } + + private Task RenderLight(SceneNode node) + { + if (node is not Light light) + return Task.CompletedTask; + + return light.Render(_camera, Window); } private static bool IsInViewFrustum(BoundingBox boundingBox, CameraView camera) diff --git a/Engine/SharpEngine.Core/Renderers/RendererBase.cs b/SharpEngine.Core/Renderers/RendererBase.cs similarity index 64% rename from Engine/SharpEngine.Core/Renderers/RendererBase.cs rename to SharpEngine.Core/Renderers/RendererBase.cs index 1491fab..ea99872 100644 --- a/Engine/SharpEngine.Core/Renderers/RendererBase.cs +++ b/SharpEngine.Core/Renderers/RendererBase.cs @@ -1,5 +1,6 @@ -using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Windowing; + using System; using System.Threading.Tasks; @@ -13,6 +14,9 @@ public abstract class RendererBase : IDisposable /// Gets or sets the settings for the renderer. protected ISettings Settings; + /// Gets the window attached to the renderer. + protected Window Window { get; private set; } = null!; + /// /// Initializes a new instance of . /// @@ -27,6 +31,24 @@ protected RendererBase(ISettings settings) /// public abstract RenderFlags RenderFlag { get; } + /// + /// Attaches the renderer to a window after both have been constructed. + /// + /// The window that owns the renderer. + public void AttachWindow(Window window) + { + ArgumentNullException.ThrowIfNull(window); + + Window = window; + OnWindowAttached(window); + } + + /// + /// Allows derived renderers to react when a window is attached. + /// + /// The attached window. + protected virtual void OnWindowAttached(Window window) { } + /// /// Initializes the renderer. /// diff --git a/SharpEngine.Core/Renderers/TextRenderer.cs b/SharpEngine.Core/Renderers/TextRenderer.cs new file mode 100644 index 0000000..15670c8 --- /dev/null +++ b/SharpEngine.Core/Renderers/TextRenderer.cs @@ -0,0 +1,31 @@ +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Scenes; + +using System.Threading.Tasks; + +namespace SharpEngine.Core.Renderers; + +/// +/// A renderer responsible for rendering text in the scene. +/// +internal class TextRenderer : RendererBase +{ + /// + /// Initializes a new instance of the TextRenderer class with the specified camera, settings, and scene. + /// + /// CameraView that provides view and projection information for rendering. + /// ISettings that configures renderer behavior. + /// Scene that contains renderable text elements. + public TextRenderer(CameraView camera, ISettings settings, Scene scene) : base(settings) + { + } + + public override RenderFlags RenderFlag => RenderFlags.Text; + + /// + public override Task Render() + { + return Task.CompletedTask; + } +} diff --git a/Engine/SharpEngine.Core/Renderers/UIRenderer.cs b/SharpEngine.Core/Renderers/UIRenderer.cs similarity index 56% rename from Engine/SharpEngine.Core/Renderers/UIRenderer.cs rename to SharpEngine.Core/Renderers/UIRenderer.cs index 54ee5da..931b0c5 100644 --- a/Engine/SharpEngine.Core/Renderers/UIRenderer.cs +++ b/SharpEngine.Core/Renderers/UIRenderer.cs @@ -2,9 +2,9 @@ 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 Microsoft.Extensions.Logging; using Silk.NET.OpenGL; using System; @@ -19,7 +19,9 @@ public class UIRenderer : RendererBase { private readonly Scene _scene; private readonly CameraView _camera; - private readonly Window _window; + private readonly ILogger _logger; + + private GL _gl = null!; /// public override RenderFlags RenderFlag => RenderFlags.UIRenderer; @@ -27,11 +29,25 @@ public class UIRenderer : RendererBase /// /// Initializes a new instance of . /// - public UIRenderer(CameraView camera, Window window, ISettings settings, Scene scene) : base(settings) + public UIRenderer(CameraView camera, ISettings settings, Scene scene) + : this(camera, settings, scene, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) + { + } + + /// + /// Initializes a new instance of . + /// + public UIRenderer(CameraView camera, ISettings settings, Scene scene, ILogger logger) : base(settings) { _scene = scene; _camera = camera; - _window = window; + _logger = logger; + } + + /// + protected override void OnWindowAttached(Window window) + { + _gl = window.GetGL(); } /// @@ -42,19 +58,19 @@ public override Task Render() try { - Window.GL.Enable(EnableCap.DepthTest); - Window.GL.DepthFunc(DepthFunction.Less); + _gl.Enable(EnableCap.DepthTest); + _gl.DepthFunc(DepthFunction.Less); // Disable face culling to render both sides of the quad - Window.GL.Disable(EnableCap.CullFace); + _gl.Disable(EnableCap.CullFace); - var uiElementRenderTasks = _scene.IterateAsync(_scene.UIElements, elem => elem.Render(_camera, _window)); + var uiElementRenderTasks = _scene.IterateAsync(_scene.UIElements, elem => elem.Render(_camera, Window)); return Task.WhenAll(uiElementRenderTasks); } catch (Exception ex) { - Debug.Log.Error(ex, "{Message}", ex.Message); + _logger.LogError(ex, "{Message}", ex.Message); return Task.FromException(ex); } } diff --git a/Engine/SharpEngine.Core/Scenes/Scene.cs b/SharpEngine.Core/Scenes/Scene.cs similarity index 97% rename from Engine/SharpEngine.Core/Scenes/Scene.cs rename to SharpEngine.Core/Scenes/Scene.cs index e53d524..ff4db26 100644 --- a/Engine/SharpEngine.Core/Scenes/Scene.cs +++ b/SharpEngine.Core/Scenes/Scene.cs @@ -1,7 +1,6 @@ -using SharpEngine.Core.Entities; +using Microsoft.Extensions.Logging; using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Entities.UI; -using SharpEngine.Shared; + using System; using System.Collections.Generic; using System.Linq; @@ -15,6 +14,8 @@ namespace SharpEngine.Core.Scenes; /// public class Scene { + private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + /// The file extension by which saved scenes are associated with. public const string SceneFileExtension = "sharpscene"; @@ -183,7 +184,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) { - Debug.Log.Debug(sceneFile); + Logger.LogDebug("Loading scene from {SceneFile}", sceneFile); var loadedScene = JsonSerializer.Deserialize(sceneFile); diff --git a/Engine/SharpEngine.Core/Scenes/SceneNode.cs b/SharpEngine.Core/Scenes/SceneNode.cs similarity index 100% rename from Engine/SharpEngine.Core/Scenes/SceneNode.cs rename to SharpEngine.Core/Scenes/SceneNode.cs diff --git a/SharpEngine.Core/Shaders/LampShader.cs b/SharpEngine.Core/Shaders/LampShader.cs new file mode 100644 index 0000000..f7ce4e1 --- /dev/null +++ b/SharpEngine.Core/Shaders/LampShader.cs @@ -0,0 +1,43 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Entities.Properties.Meshes; + +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +internal class LampShader : ShaderBase +{ + private readonly GL _gl; + + /// + /// Initializes a new instance of using the provided OpenGL context. + /// + /// The OpenGL context to use for shader and VAO creation. + public LampShader(GL gl) + { + _gl = gl; + + Shader = ShaderService.Instance.LoadShader(_gl, Default.VertexShader, Default.LightShader, "lamp"); + + Vao = _gl.GenVertexArray(); + _gl.BindVertexArray(Vao); + + SetAttributes(_gl); + } + + /// + public override bool SetAttributes(GL gl) + { + if (!base.SetAttributes(gl)) + return false; + + if (!Shader!.TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) + return false; + + var positionLocationUint = (uint)positionLocation; + _gl.EnableVertexAttribArray(positionLocationUint); + _gl.VertexAttribPointer(positionLocationUint, 3, VertexAttribPointerType.Float, false, VertexData.Stride, 0); + + return true; + } +} diff --git a/SharpEngine.Core/Shaders/LightingShader.cs b/SharpEngine.Core/Shaders/LightingShader.cs new file mode 100644 index 0000000..55b5e5e --- /dev/null +++ b/SharpEngine.Core/Shaders/LightingShader.cs @@ -0,0 +1,56 @@ +using SharpEngine.Core._Resources; + +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +internal class LightingShader : ShaderBase +{ + private readonly GL _gl; + + /// + /// Initializes a new instance of . + /// + /// The OpenGL context used to create and configure the shader program. + public LightingShader(GL gl) + { + _gl = gl; + + Shader = ShaderService.Instance.LoadShader(_gl, Default.VertexShader, Default.FragmentShader, "lighting"); + + Vao = _gl.GenVertexArray(); + _gl.BindVertexArray(Vao); + + SetAttributes(_gl); + } + + /// + public override bool SetAttributes(GL gl) + { + if (!base.SetAttributes(gl)) + return false; + + /*if (!Shader!.TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) + return false; + + var positionLocationUint = (uint)positionLocation; + _gl.EnableVertexAttribArray(positionLocationUint); + _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; + _gl.EnableVertexAttribArray(normalLocationUint); + _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; + _gl.EnableVertexAttribArray(texCoordLocationUint); + _gl.VertexAttribPointer(texCoordLocationUint, VertexData.TexCoordsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.TexCoordsOffset); + */ + return true; + } +} diff --git a/Engine/SharpEngine.Core/Shaders/ShaderBase.cs b/SharpEngine.Core/Shaders/ShaderBase.cs similarity index 69% rename from Engine/SharpEngine.Core/Shaders/ShaderBase.cs rename to SharpEngine.Core/Shaders/ShaderBase.cs index e7fd389..492217a 100644 --- a/Engine/SharpEngine.Core/Shaders/ShaderBase.cs +++ b/SharpEngine.Core/Shaders/ShaderBase.cs @@ -1,6 +1,5 @@ -using SharpEngine.Core.Components.Properties; -using SharpEngine.Shared; -using System; +using Microsoft.Extensions.Logging; +using Silk.NET.OpenGL; namespace SharpEngine.Core.Shaders; @@ -9,6 +8,8 @@ namespace SharpEngine.Core.Shaders; /// public abstract class ShaderBase { + private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + /// Gets the shader. public Shader? Shader { get; protected set; } @@ -21,11 +22,11 @@ public abstract class ShaderBase /// /// if the attributes were set successfully; otherwise . /// - public virtual bool SetAttributes() + public virtual bool SetAttributes(GL gl) { if (Shader is null) { - Debug.Log.Error("Unable to set shader attributes, shader not found."); + Logger.LogError("Unable to set shader attributes, shader not found."); return false; } diff --git a/SharpEngine.Core/Shaders/ShaderService.cs b/SharpEngine.Core/Shaders/ShaderService.cs new file mode 100644 index 0000000..74fee61 --- /dev/null +++ b/SharpEngine.Core/Shaders/ShaderService.cs @@ -0,0 +1,134 @@ +using Microsoft.Extensions.Logging; +using System.Collections.Generic; +using System.IO; + +using SharpEngine.Core.Windowing; +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +/// +/// Contains all the shaders used in the game. +/// +public class ShaderService +{ + private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + + /// + /// Gets the singleton instance of the . + /// + public static ShaderService Instance { get; } = new ShaderService(); + + private readonly Dictionary _shaderCache = []; + + private readonly record struct ShaderCacheKey(string Name, object ShareGroupKey); + + /// + /// Gets or sets whether there are shaders to load. + /// + public bool HasShadersToLoad { get; set; } = true; + + /// + /// Private constructor to prevent instantiation. + /// + private ShaderService() { } + + /// + /// Gets all the shaders in the cache. + /// + /// All the shaders found from the cache. + public List GetAll() + { + HasShadersToLoad = false; + return [.. _shaderCache.Values]; + } + + /// + /// Gets a shader by its name. + /// + /// The name of the shader to be found. + /// The found shader. + /// + /// Thrown if a shader by that is not found. + /// This exception is thrown to make sure there are no unexpected issues made by the developer. + /// + public Shader GetByName(string name) + { + // Legacy API: return first shader found with this name. + foreach (var kvp in _shaderCache) + if (kvp.Key.Name == name) + return kvp.Value; + + throw new KeyNotFoundException($"Shader with name {name} not found in cache."); + } + + /// + /// Loads a shader from the specified vertex and fragment paths.
+ /// If the shader is loaded already, adds it to the cache. + ///
+ /// The game window. + /// The vertex shader full path. + /// The fragment shader full path. + /// A name identifier for the shader. + /// A shader with the given name. + /// Thrown when either the vertex or fragment shader is not found. + public Shader LoadShader(Window window, string vertPath, string fragPath, string name) + => LoadShader(window.GetGL(), GetShareGroupKey(window), vertPath, fragPath, name); + + /// + /// Loads a shader from the specified vertex and fragment paths.
+ /// If the shader is loaded already, adds it to the cache. + ///
+ /// The OpenGL context. + /// The vertex shader full path. + /// The fragment shader full path. + /// A name identifier for the shader. + /// A shader with the given name. + /// Thrown when either the vertex or fragment shader is not found. + public Shader LoadShader(GL gl, string vertPath, string fragPath, string name) + => LoadShader(gl, shareGroupKey: gl, vertPath, fragPath, name); + + /// + /// Loads a shader from the specified vertex and fragment paths.
+ /// If the shader is loaded already, adds it to the cache. + ///
+ /// The OpenGL context. + /// A key representing the share group for the shader. + /// The vertex shader full path. + /// The fragment shader full path. + /// A name identifier for the shader. + /// A shader with the given name. + /// Thrown when either the vertex or fragment shader is not found. + public Shader LoadShader(GL gl, object shareGroupKey, string vertPath, string fragPath, string name) + { + var cacheKey = new ShaderCacheKey(name, shareGroupKey); + + // Check if the shader is already in the cache for this share group. + if (_shaderCache.TryGetValue(cacheKey, out var cachedShader)) + return cachedShader; + + if (!File.Exists(vertPath)) + { + Logger.LogInformation("Vertex shader file not found: {VertPath}", vertPath); + throw new FileNotFoundException($"Vertex shader file not found: {vertPath}"); + } + + if (!File.Exists(fragPath)) + { + Logger.LogInformation("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. + // Shader program objects are shareable across contexts *only* when those contexts share. + var shader = new Shader(gl, vertPath, fragPath, name).Initialize(); + _shaderCache[cacheKey] = shader; + + HasShadersToLoad = true; + + return shader; + } + + private static object GetShareGroupKey(Window window) + => (object?)window.SharedContext ?? (object?)window.GLContext ?? (object)window; +} diff --git a/SharpEngine.Core/Shaders/UIShader.cs b/SharpEngine.Core/Shaders/UIShader.cs new file mode 100644 index 0000000..50e80a6 --- /dev/null +++ b/SharpEngine.Core/Shaders/UIShader.cs @@ -0,0 +1,50 @@ +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Windowing; +using SharpEngine.Core._Resources; + +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +/// +/// Represents a shader used for rendering UI elements. +/// +/// +/// This shader is responsible for rendering 2D UI components on the screen, such as buttons, panels, and other interface elements. +/// It is designed to work with the specific vertex and fragment shaders defined for UI rendering in the game engine. +/// +internal class UIShader : ShaderBase +{ + /// + /// Ensures that the shader is initialized. + /// + /// + /// This method should be called before using the shader to ensure that it is properly loaded and ready for use. + /// + /// The window where the shader will be used. + public void EnsureInitialized(Window window) + { + if (Shader is not null) + return; + + Shader = ShaderService.Instance.LoadShader(window, Default.UIVertexShader, Default.UIFragmentShader, nameof(UIShader)); + } + + /// + public override bool SetAttributes(GL gl) + { + if (!base.SetAttributes(gl)) + return false; + + gl.EnableVertexAttribArray(0); + gl.VertexAttribPointer(0, VertexData.VerticesSize, VertexAttribPointerType.Float, false, VertexData.Stride, 0); + + gl.EnableVertexAttribArray(1); + gl.VertexAttribPointer(1, VertexData.NormalsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.NormalsOffset); + + gl.EnableVertexAttribArray(2); + gl.VertexAttribPointer(2, VertexData.TexCoordsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.TexCoordsOffset); + + return true; + } +} diff --git a/Engine/SharpEngine.Core/SharpEngine.Core.csproj b/SharpEngine.Core/SharpEngine.Core.csproj similarity index 75% rename from Engine/SharpEngine.Core/SharpEngine.Core.csproj rename to SharpEngine.Core/SharpEngine.Core.csproj index 8633518..04fb6cb 100644 --- a/Engine/SharpEngine.Core/SharpEngine.Core.csproj +++ b/SharpEngine.Core/SharpEngine.Core.csproj @@ -1,6 +1,8 @@  + $(DotNetTargetFramework) + Everything needed to build games using a purely C# game engine. AnyCPU;x64;x86 @@ -19,20 +21,24 @@ - - - - - + + + + + - + + + + + + - diff --git a/Engine/SharpEngine.Core/Textures/TextureService.cs b/SharpEngine.Core/Textures/TextureService.cs similarity index 88% rename from Engine/SharpEngine.Core/Textures/TextureService.cs rename to SharpEngine.Core/Textures/TextureService.cs index 4c40568..2f25e90 100644 --- a/Engine/SharpEngine.Core/Textures/TextureService.cs +++ b/SharpEngine.Core/Textures/TextureService.cs @@ -1,9 +1,9 @@ using System.IO; using System.Collections.Generic; +using SharpEngine.Core.Components.Properties.Textures; using SharpEngine.Core.Windowing; using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; -using TextureType = Silk.NET.Assimp.TextureType; namespace SharpEngine.Core.Textures; @@ -25,6 +25,7 @@ private TextureService() { } /// Loads a texture from the specified path. /// /// the full path to the texture. + /// The type of the texture. /// The loaded texture program. public Texture LoadTexture(string path, TextureType textureType = TextureType.Diffuse) { @@ -39,7 +40,7 @@ public Texture LoadTexture(string path, TextureType textureType = TextureType.Di return cachedTexture; // Generate handle - var texture = new Texture(Window.GL, path, textureType); + var texture = new Texture(Window.SharedGL, path, textureType); // Add it to the cache _textureCache[path] = texture; diff --git a/Engine/SharpEngine.Core/Windowing/Frame.cs b/SharpEngine.Core/Windowing/Frame.cs similarity index 100% rename from Engine/SharpEngine.Core/Windowing/Frame.cs rename to SharpEngine.Core/Windowing/Frame.cs diff --git a/SharpEngine.Core/Windowing/IWindowFactory.cs b/SharpEngine.Core/Windowing/IWindowFactory.cs new file mode 100644 index 0000000..881042c --- /dev/null +++ b/SharpEngine.Core/Windowing/IWindowFactory.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace SharpEngine.Core.Windowing; + +/// +/// Creates configured window instances from DI registrations. +/// +public interface IWindowFactory +{ + /// + /// Gets the registered window names. + /// + IReadOnlyList RegisteredWindows { get; } + + /// + /// Creates a configured window instance. + /// + /// The optional window registration name. + /// A configured . + Window CreateWindow(string? name = null); + + /// + /// Creates one window for each registration. + /// + /// The configured windows. + IReadOnlyList CreateAllWindows(); +} diff --git a/Engine/SharpEngine.Core/Windowing/SilkWindow.cs b/SharpEngine.Core/Windowing/SilkWindow.cs similarity index 95% rename from Engine/SharpEngine.Core/Windowing/SilkWindow.cs rename to SharpEngine.Core/Windowing/SilkWindow.cs index 4301fa0..1f5e1dd 100644 --- a/Engine/SharpEngine.Core/Windowing/SilkWindow.cs +++ b/SharpEngine.Core/Windowing/SilkWindow.cs @@ -1,3 +1,5 @@ +using SharpEngine.Core.Entities.Views.Settings; + using Silk.NET.Core; using Silk.NET.Core.Contexts; using Silk.NET.Input; @@ -13,6 +15,9 @@ namespace SharpEngine.Core.Windowing; +// TODO: The warnings produced by enabling this need to be implemented. +#pragma warning disable CS0067 // Event is required by IWindow but not used by this implementation + /// /// Represents an abstraction for the interface. /// @@ -38,6 +43,11 @@ public bool IsClosing set => CurrentWindow.IsClosing = value; } + /// + /// Gets the settings for the current window. + /// + public IViewSettings Settings { get; protected set; } = ViewSettings.Default; + /// Gets or sets the input context for the window. public IInputContext? Input { get; protected set; } @@ -159,6 +169,7 @@ public bool VSync set => CurrentWindow.ShouldSwapAutomatically = value; } + /// Gets a value indicating whether the window is focused. public bool IsFocused { get; } /// diff --git a/Engine/SharpEngine.Core/Windowing/Window.cs b/SharpEngine.Core/Windowing/Window.cs similarity index 57% rename from Engine/SharpEngine.Core/Windowing/Window.cs rename to SharpEngine.Core/Windowing/Window.cs index c566254..903f3df 100644 --- a/Engine/SharpEngine.Core/Windowing/Window.cs +++ b/SharpEngine.Core/Windowing/Window.cs @@ -1,4 +1,3 @@ -using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Enums; @@ -6,6 +5,12 @@ using SharpEngine.Core.Renderers; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; +using SharpEngine.Core.Interfaces; +using Shader = SharpEngine.Core.Shaders.Shader; + +using SharpEngine.Shared.Dto; + +using Microsoft.Extensions.Logging; using Silk.NET.Input; using Silk.NET.Maths; @@ -18,10 +23,6 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; -using System.Threading.Tasks; -using Shader = SharpEngine.Core.Shaders.Shader; -using Silk.NET.GLFW; -using SharpEngine.Shared; namespace SharpEngine.Core.Windowing; @@ -30,8 +31,11 @@ namespace SharpEngine.Core.Windowing; /// public class Window : SilkWindow { + private const string _iconPath = "_Resources/icon.png"; private bool _windowInitialized; private bool _initialized; + private readonly RendererBase[] _registeredRenderers; + private readonly ILogger _logger; private IEnumerable _renderers = []; private ImGuiController? _imGuiController; @@ -41,11 +45,6 @@ public class Window : SilkWindow /// 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; @@ -67,29 +66,84 @@ public class Window : SilkWindow protected Scene Scene { get; private set; } /// The OpenGL context. - public static GL GL; + private GL _gl = null!; + + private static GL? _sharedGl; + + /// + /// Gets the shared OpenGL instance used for resource creation when windows share an OpenGL context. + /// + /// + /// This is primarily for backward compatibility with code that assumed a single global GL instance. + /// For multi-window support, prefer using and ensuring the correct context is current. + /// + public static GL SharedGL + => _sharedGl ?? throw new InvalidOperationException("No OpenGL context has been created yet."); + + /// + /// Backward compatible alias for . + /// + public static GL GL => SharedGL; - // 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; - + public GL GetGL() => _gl; + private void SetGL(GL gl) => _gl = gl; + + /// + /// Initializes a new instance of . + /// + /// The camera the window should render from. + /// Contains the game scene. + /// The settings for the window. + public Window(CameraView camera, Scene scene, IViewSettings settings) : + this(camera, scene, settings, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) { } + /// /// Initializes a new instance of . /// /// The camera the window should render from. /// Contains the game scene. /// The settings for the window. - public Window(CameraView camera, Scene scene, IViewSettings settings) + /// The logger for the window. + /// The renderers for the window. + public Window(CameraView camera, Scene scene, IViewSettings settings, ILogger logger, IEnumerable? renderers = null) { + // TODO: + // Should the developer need to call for a window initialization? + // Meaning should should we move this project loading part to a separate function? + Scene = scene; Settings = settings; Camera = camera; + _registeredRenderers = renderers?.ToArray() ?? []; + _logger = logger; + + CheckEngineVersion(); + + // NOTE: Window initialization is intentionally not performed automatically + // here. Call InitializeWindow() explicitly when ready to create the underlying + // native window and load resources. See the TODO in the project for reasoning. } + private void CheckEngineVersion() + { + var project = new Project(); + var currentAssemlyVersion = typeof(Window).Assembly.GetVersion(); + + if (currentAssemlyVersion != project.EngineVersion) + _logger.LogWarning("The current engine version ({CurrentVersion}) does not match the project engine version ({ProjectVersion}). This may lead to unexpected behavior.", currentAssemlyVersion, project.EngineVersion); + } + + /// + /// Initializes a new instance of . + /// + /// The game instance. + public Window(Game game) + : this(game.Scene, game.Camera.Settings, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) { } + /// /// Initializes a new instance of . /// @@ -98,17 +152,22 @@ public Window(CameraView camera, Scene scene, IViewSettings settings) /// /// Contains the game scene. /// The settings for the window. - public Window(Scene scene, IViewSettings settings) + /// The logger for the window. + /// The renderers for the window. + public Window(Scene scene, IViewSettings settings, ILogger logger, IEnumerable? renderers = null) { Scene = scene; Settings = settings; Camera = new(Vector3.One, settings); + _registeredRenderers = renderers?.ToArray() ?? []; + _logger = logger; + + // Initialization is deferred. Call Initialize() when you want the + // native window to be created and resources to be loaded. } - /// - /// Initializes the ga - /// - public void InitializeWindow() + /// + public override void Initialize() { if (_initialized) return; @@ -136,7 +195,7 @@ public override void Run(Action onFrame) } catch (Exception ex) { - Debug.Log.Error(ex, "Error running window: {Message}", ex.Message); + _logger.LogError(ex, "Error running window: {Message}", ex.Message); } } @@ -148,43 +207,36 @@ public override void OnLoad() var context = CurrentWindow.CreateOpenGL(); SetGL(context); + // Capture the first created GL as the shared GL. This enables resource caches to work across windows + // when those windows are created with a shared OpenGL context. + _sharedGl ??= context; + Input = CurrentWindow.CreateInput(); + + // TODO: Skip calling this for secondary windows? CurrentWindow.MakeCurrent(); - SetWindowIcon(PathExtensions.GetAssemblyPath("_Resources/icon.png")); + SetWindowIcon(PathExtensions.GetAssemblyPath(_iconPath)); AssignInputEvents(); - GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f); + _gl.ClearColor(0.2f, 0.3f, 0.3f, 1.0f); - // Load all meshes from the mesh cache - // MeshService.Instance.LoadMesh("cube", Primitives.Cube.Mesh); - - // Using reflection, find all renderers that implement the RendererBase. - var rendererTypes = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(assembly => assembly.GetTypes()) - .Where(type => type.IsSubclassOf(typeof(RendererBase)) && !type.IsAbstract); - - foreach (var type in rendererTypes) - { - // Make sure the renderer has the correct constructor parameters! - // 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); - } + _renderers = CreateRenderers(); foreach (var renderer in _renderers) + { + renderer.AttachWindow(this); renderer.Initialize(); + } - _imGuiController = new ImGuiController(GL, CurrentWindow, Input); + _imGuiController = new ImGuiController(_gl, CurrentWindow, Input); _initialized = true; } catch (Exception ex) { - Debug.Log.Information(ex, "Error loading window: {Message}", ex.Message); + _logger.LogInformation(ex, "Error loading window: {Message}", ex.Message); } base.OnLoad(); @@ -207,17 +259,18 @@ protected void RenderFrame(Frame frame) _imGuiController?.Update((float)frame.FrameTime); - GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + _gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); ToggleWireFrame(Settings.UseWireFrame); UseShaders(); - var renderTasks = _renderers.Where(renderer => Settings.RendererFlags.HasFlag(renderer.RenderFlag)) - .Select(renderer => renderer.Render()) - .ToList(); + var activeRenderers = _renderers.Where(renderer => Settings.RendererFlags.HasFlag(renderer.RenderFlag)) + .OrderBy(renderer => renderer.RenderFlag) + .ToList(); - Task.WaitAll([.. renderTasks]); + foreach (var renderer in activeRenderers) + renderer.Render().GetAwaiter().GetResult(); AfterRender(frame); @@ -225,16 +278,35 @@ protected void RenderFrame(Frame frame) } catch (Exception ex) { - Debug.Log.Information(ex.Message); + _logger.LogInformation("{Message}", ex.Message); } } + private RendererBase[] CreateRenderers() + { + // Return the renderers that have been registered + if (_registeredRenderers.Length > 0) + return _registeredRenderers; + + // When no renderers have been registered, fall back to use all that are discoverable. + var rendererTypes = AppDomain.CurrentDomain.GetAssemblies() + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => type.IsSubclassOf(typeof(RendererBase)) && !type.IsAbstract); + + return [.. rendererTypes + .Select(type => + { + var requiredArguments = new object[] { Camera, Settings, Scene }; + return (RendererBase)Activator.CreateInstance(type, requiredArguments)!; + })]; + } + /// /// Toggles the renderer between wireframe and fill mode. /// /// Determines whether objects should be rendered in wireframe. - private static void ToggleWireFrame(bool useWireFrame) - => GL.PolygonMode(GLEnum.FrontAndBack, useWireFrame ? PolygonMode.Line : PolygonMode.Fill); + private void ToggleWireFrame(bool useWireFrame) + => _gl.PolygonMode(GLEnum.FrontAndBack, useWireFrame ? PolygonMode.Line : PolygonMode.Fill); private List _shaders = []; @@ -255,7 +327,7 @@ protected void OnUpdateFrame(Frame frame) } if (Settings.PrintFrameRate) - Debug.Log.Information($"FPS: {frame.FrameRate}"); + _logger.LogInformation("FPS: {FrameRate}", frame.FrameRate); // TODO: #21 Handle multiple mice? var mouse = Input?.Mice[0]; @@ -283,7 +355,7 @@ private void AssignInputEvents() { if (Input is null) { - Debug.Log.Information("Input is null. No input events will be assigned."); + _logger.LogInformation("Input is null. No input events will be assigned."); return; } @@ -325,7 +397,7 @@ protected void OnMouseWheel(IMouse mouse, ScrollWheel sw) /// protected void OnResize(Vector2D size) { - GL.Viewport(size); + _gl.Viewport(size); if (size != Vector2D.Zero) Camera.AspectRatio = (float)size.X / size.Y; diff --git a/SharpEngine.Core/Windowing/WindowFactory.cs b/SharpEngine.Core/Windowing/WindowFactory.cs new file mode 100644 index 0000000..f2774ea --- /dev/null +++ b/SharpEngine.Core/Windowing/WindowFactory.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SharpEngine.Core.Windowing; + +/// +/// Creates window instances from a collection of entries. +/// +/// The service provider used to resolve window dependencies. +/// The registrations that describe available windows. +internal sealed class WindowFactory(IServiceProvider serviceProvider, IEnumerable registrations) : IWindowFactory +{ + private readonly IServiceProvider _serviceProvider = serviceProvider; + private readonly IReadOnlyList _registrations = registrations.ToArray(); + + /// + /// Gets the names of all registered windows. + /// + public IReadOnlyList RegisteredWindows => [.. _registrations.Select(registration => registration.Name)]; + + /// + /// Creates a window by its registration name. + /// + /// + /// If is null, the default registration (or the first registration if none are marked default) is used. + /// + /// Optional name of the window registration to create. + /// The created instance. + /// Thrown when no registrations exist or when a named registration cannot be found. + public Window CreateWindow(string? name = null) + { + var registration = ResolveRegistration(name); + var window = registration.Factory(_serviceProvider); + registration.Configure?.Invoke(_serviceProvider, window); + return window; + } + + /// + /// Creates instances for all registered windows in the order they were registered. + /// + /// A read-only list containing all created window instances. + public IReadOnlyList CreateAllWindows() + => [.. _registrations.Select(registration => CreateWindow(registration.Name))]; + + /// + /// Resolves the appropriate registration for the given name. + /// + /// The optional registration name to resolve. + /// + /// The matching or returns the default registration when is null. + /// + /// Thrown when no registrations exist or when a named registration cannot be found. + private WindowRegistration ResolveRegistration(string? name) + { + if (_registrations.Count == 0) + throw new InvalidOperationException("No windows have been registered. Call AddWindow during service configuration."); + + if (name is not null) + { + var namedRegistration = _registrations.FirstOrDefault(registration => string.Equals(registration.Name, name, StringComparison.OrdinalIgnoreCase)); + return namedRegistration ?? throw new InvalidOperationException($"No window named '{name}' has been registered."); + } + + return _registrations.FirstOrDefault(registration => registration.IsDefault) ?? _registrations[0]; + } +} diff --git a/SharpEngine.Core/Windowing/WindowRegistration.cs b/SharpEngine.Core/Windowing/WindowRegistration.cs new file mode 100644 index 0000000..f6c64c4 --- /dev/null +++ b/SharpEngine.Core/Windowing/WindowRegistration.cs @@ -0,0 +1,22 @@ +using System; + +namespace SharpEngine.Core.Windowing; + +/// +/// Holds registration information for a window type. +/// +internal sealed class WindowRegistration +{ + /// Gets or initializes the name for the window. + public required string Name { get; init; } + + /// Gets or initializes the factory delegate used to create the window instance. + /// The service provider is supplied to allow resolving dependencies required by the window. + public required Func Factory { get; init; } + + /// Gets or initializes the optional configuration action invoked immediately after the window is created. + public Action? Configure { get; init; } + + /// Gets or initializes a value indicating whether this registration should be used as the default when no specific window name is requested. + public bool IsDefault { get; init; } +} diff --git a/Engine/SharpEngine.Core/_Resources/Default.cs b/SharpEngine.Core/_Resources/Default.cs similarity index 77% rename from Engine/SharpEngine.Core/_Resources/Default.cs rename to SharpEngine.Core/_Resources/Default.cs index ef3e138..88fc100 100644 --- a/Engine/SharpEngine.Core/_Resources/Default.cs +++ b/SharpEngine.Core/_Resources/Default.cs @@ -16,8 +16,12 @@ public static class Default /// Gets the path to the lighting fragment shader. public static string FragmentShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\lighting.frag"); + /// Gets the path to the default shader used for rendering light sources. public static string LightShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\shader.frag"); - + + /// Gets the path to the default vertex shader used for rendering UI elements. public static string UIVertexShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\uiShader.vert"); + + /// Gets the path to the default fragment shader used for rendering UI elements. public static string UIFragmentShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\uiShader.frag"); } diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/Light/DirectionalLight.glsl b/SharpEngine.Core/_Resources/Shaders/Light/DirectionalLight.glsl similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/Light/DirectionalLight.glsl rename to SharpEngine.Core/_Resources/Shaders/Light/DirectionalLight.glsl diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/Light/PointLight.glsl b/SharpEngine.Core/_Resources/Shaders/Light/PointLight.glsl similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/Light/PointLight.glsl rename to SharpEngine.Core/_Resources/Shaders/Light/PointLight.glsl diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/Light/SpotLight.glsl b/SharpEngine.Core/_Resources/Shaders/Light/SpotLight.glsl similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/Light/SpotLight.glsl rename to SharpEngine.Core/_Resources/Shaders/Light/SpotLight.glsl diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/Material.glsl b/SharpEngine.Core/_Resources/Shaders/Material.glsl similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/Material.glsl rename to SharpEngine.Core/_Resources/Shaders/Material.glsl diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/lighting.frag b/SharpEngine.Core/_Resources/Shaders/lighting.frag similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/lighting.frag rename to SharpEngine.Core/_Resources/Shaders/lighting.frag diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/shader.frag b/SharpEngine.Core/_Resources/Shaders/shader.frag similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/shader.frag rename to SharpEngine.Core/_Resources/Shaders/shader.frag diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/shader.vert b/SharpEngine.Core/_Resources/Shaders/shader.vert similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/shader.vert rename to SharpEngine.Core/_Resources/Shaders/shader.vert diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/shader2.vert b/SharpEngine.Core/_Resources/Shaders/shader2.vert similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/shader2.vert rename to SharpEngine.Core/_Resources/Shaders/shader2.vert diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.frag b/SharpEngine.Core/_Resources/Shaders/uiShader.frag similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/uiShader.frag rename to SharpEngine.Core/_Resources/Shaders/uiShader.frag diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.vert b/SharpEngine.Core/_Resources/Shaders/uiShader.vert similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Shaders/uiShader.vert rename to SharpEngine.Core/_Resources/Shaders/uiShader.vert diff --git a/Engine/SharpEngine.Core/_Resources/Textures/debug.jpg b/SharpEngine.Core/_Resources/Textures/debug.jpg similarity index 100% rename from Engine/SharpEngine.Core/_Resources/Textures/debug.jpg rename to SharpEngine.Core/_Resources/Textures/debug.jpg diff --git a/Engine/SharpEngine.Core/_Resources/icon.png b/SharpEngine.Core/_Resources/icon.png similarity index 100% rename from Engine/SharpEngine.Core/_Resources/icon.png rename to SharpEngine.Core/_Resources/icon.png diff --git a/SharpEngine.Shared/Debug.cs b/SharpEngine.Shared/Debug.cs new file mode 100644 index 0000000..43c2f7a --- /dev/null +++ b/SharpEngine.Shared/Debug.cs @@ -0,0 +1,79 @@ +using Serilog; +using Serilog.Events; +using System; +using System.Numerics; + +namespace SharpEngine.Shared; + +/// +/// Contains methods for debugging the application. +/// +public static class Debug +{ + /// + /// A logger instance used for debugging the application. + /// + public static ILogger Log { get; set; } + + private static LogEventLevel _logLevel = LogEventLevel.Information; + + /// + /// Initializes the class instance. + /// + static Debug() + { + SetLogger(); + } + + /// + /// Updates the logger instance. + /// + public static void SetLogger() + => Log = new LoggerConfiguration() + .MinimumLevel.Is(_logLevel) + .WriteTo.Console() + .CreateLogger(); + + /// + /// Sets the logging level for the application. + /// + /// + /// This affects the granularity of log messages generated. + /// + /// Specifies the severity level of log messages to be recorded. + public static void SetLogLevel(LogEventLevel logLevel) + { + _logLevel = logLevel; + SetLogger(); + } + +#if DEBUG + + /// + /// Draws a line on the screen. + /// + /// The starting point for the line. + /// The direction where the vector needs to be drawn. + /// Determines how long the line needs to be. + /// Determines how wide of a vector needs to be drawn. + /// Thrown when the method is called, indicating that the implementation is not yet provided. + public static void DrawLine(Vector3 startPoint, Vector3 direction, float length, float width = 1) + { + // https://math.stackexchange.com/questions/1286489/how-to-find-direction-and-normal-vector + throw new NotImplementedException(); + } + + /// + /// Draws a box in 3D space defined by two corner points. + /// + /// Specifies one corner of the box in 3D coordinates. + /// Specifies the opposite corner of the box in 3D coordinates. + /// Determines whether the box is rendered as a solid shape or as a wireframe. + /// Thrown when the method has not been implemented yet. + public static void DrawBox(Vector3 min, Vector3 max, bool wireframe = true) + { + throw new NotImplementedException(); + } + +#endif +} \ No newline at end of file diff --git a/Engine/SharpEngine.slnx b/SharpEngine.slnx similarity index 74% rename from Engine/SharpEngine.slnx rename to SharpEngine.slnx index 32cc136..4cfa5e7 100644 --- a/Engine/SharpEngine.slnx +++ b/SharpEngine.slnx @@ -1,9 +1,15 @@ + + + + + + @@ -18,14 +24,17 @@ + + + - - + + @@ -38,12 +47,17 @@ - - + + + + + + + + - - - + + @@ -55,10 +69,5 @@ - - - - - - + diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props new file mode 100644 index 0000000..3ad3602 --- /dev/null +++ b/Tests/Directory.Build.props @@ -0,0 +1,10 @@ + + + + + + False + + + + \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs similarity index 85% rename from Engine/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs rename to Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs index 39c13c4..be3028c 100644 --- a/Engine/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs @@ -1,32 +1,30 @@ using System.Collections.Generic; -using System.IO; -using System.Text; -using NUnit.Framework; -using ObjLoader.Loader.Data; using System.Linq; + +using NUnit.Framework; using FluentAssertions; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; -using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties; -using ObjLoader.Loaders.MaterialLoader; +using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; +using System.Diagnostics.CodeAnalysis; -namespace ObjLoader.Test.Loaders +namespace SharpEngine.Core.ObjLoader.Tests.Loaders { [TestFixture] public class MaterialLibraryLoaderTests { - private MaterialLibrarySpy _materialLibrarySpy; + private readonly MaterialLibrarySpy _materialLibrarySpy; + private readonly MaterialLibraryLoader _materialLibraryLoader; + private Material _firstMaterial; private Material _secondMaterial; private const float Epsilon = 0.000001f; - private MaterialLibraryLoader _materialLibraryLoader; - [SetUp] - public void SetUp() + public MaterialLibraryLoaderTests() { _materialLibrarySpy = new MaterialLibrarySpy(); - // _materialLibraryLoader = new MaterialLibraryLoader(_materialLibrarySpy); + _materialLibraryLoader = new MaterialLibraryLoader(null!); } [Test] @@ -102,12 +100,10 @@ public void Sets_correct_texure_maps() _firstMaterial.StencilDecalMap.Should().BeEquivalentTo("lenna_stencil.tga"); } + [MemberNotNull(nameof(_firstMaterial), nameof(_secondMaterial))] private void LoadMaterial() { - var data = Encoding.ASCII.GetBytes(MaterialLibrary); - var materialStream = new MemoryStream(data); - - _materialLibraryLoader.Load(materialStream); + _materialLibraryLoader.ParseFile(MaterialLibrary); _firstMaterial = _materialLibrarySpy.Materials.First(); _secondMaterial = _materialLibrarySpy.Materials.ElementAt(1); } diff --git a/Engine/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs b/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs similarity index 82% rename from Engine/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs rename to Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs index 8e4190a..56e9765 100644 --- a/Engine/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs @@ -2,13 +2,15 @@ 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; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; + +using CoreObjLoader = SharpEngine.Core.ObjLoader.Loaders.ObjLoader.ObjLoader; -namespace ObjLoader.Test.Loaders +namespace SharpEngine.Core.ObjLoader.Tests.Loaders { [TestFixture] public class ObjLoaderTests @@ -25,7 +27,7 @@ public Stream Open(string materialFilePath) } } - private ObjLoader.Loaders.ObjLoader.ObjLoader _loader; + private CoreObjLoader _loader; private Mesh _loadResult; private DataStore _textureDataStore; @@ -53,12 +55,12 @@ public void SetUp() _materialStreamProviderSpy = new MaterialStreamProviderSpy(); _materialStreamProviderSpy.StreamToReturn = CreateMemoryStreamFromString(MaterialLibraryString); - _materialLibraryLoader = new MaterialLibraryLoader("", _textureDataStore); - _materialLibraryLoaderFacade = new MaterialLibraryLoaderFacade(_materialLibraryLoader); - // _materialLibraryParser = new MaterialLibraryParser(_materialLibraryLoaderFacade); + _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); + _loader = new CoreObjLoader("", _textureDataStore); } [Test] @@ -77,17 +79,17 @@ public void Loads_object_and_material_correctly() var group = _loadResult.Groups.First(); group.Faces.Should().HaveCount(12); - group.Material.Name.Should().BeEquivalentTo("cube_material"); + 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); + _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); + _loader = new CoreObjLoader("", _textureDataStore); Load(); @@ -107,7 +109,7 @@ private void Load() { var objectStream = CreateMemoryStreamFromString(ObjectFileString); - // _loadResult = _loader.Load(objectStream); + _loadResult = _loader.Load(null!).First(); } private Stream CreateMemoryStreamFromString(string str) diff --git a/Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj b/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj similarity index 82% rename from Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj rename to Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj index 7725988..6a619d7 100644 --- a/Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj +++ b/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + $(DotNetTargetFramework) @@ -11,6 +11,10 @@ + + + CS8618 + diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs similarity index 94% rename from Engine/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs index 64ba447..df5b03a 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs @@ -1,11 +1,11 @@ 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 +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; + +namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { [TestFixture] public class FaceParserTests diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs similarity index 76% rename from Engine/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs index 97ea846..061dd2d 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs @@ -1,23 +1,21 @@ using FluentAssertions; using NUnit.Framework; -using ObjLoader.Loader.Data; -using ObjLoader.Loader.TypeParsers; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; -namespace ObjLoader.Test.TypeParsers +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; + +namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { [TestFixture] public class GroupParserTests { - private GroupDataStoreMock _groupDataStoreMock; - private GroupParser _groupParser; + private readonly GroupDataStoreMock _groupDataStoreMock; + private readonly GroupParser _groupParser; - [SetUp] - public void SetUp() + public GroupParserTests() { _groupDataStoreMock = new GroupDataStoreMock(); - - // _groupParser = new GroupParser(_groupDataStoreMock); + _groupParser = new GroupParser(_groupDataStoreMock); } [Test] diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs similarity index 75% rename from Engine/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs index 91089f1..3c974a6 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs @@ -1,21 +1,20 @@ using FluentAssertions; using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; -using ObjLoader.Loaders.MaterialLoader; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; -namespace ObjLoader.Test.TypeParsers +namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { [TestFixture] public class MaterialLibraryParserTests { - private MaterialLibraryLoaderFacadeSpy _materialLibraryLoaderFacadeSpy; - private MaterialLibraryParser _parser; + private readonly MaterialLibraryLoaderFacadeSpy _materialLibraryLoaderFacadeSpy; + private readonly MaterialLibraryParser _parser; - [SetUp] - public void SetUp() + public MaterialLibraryParserTests() { _materialLibraryLoaderFacadeSpy = new MaterialLibraryLoaderFacadeSpy(); - // _parser = new MaterialLibraryParser(_materialLibraryLoaderFacadeSpy); + _parser = new MaterialLibraryParser(_materialLibraryLoaderFacadeSpy); } [Test] diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs similarity index 80% rename from Engine/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs index f9e03c7..f047eb5 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs @@ -1,23 +1,22 @@ using FluentAssertions; using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; + +using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; -namespace ObjLoader.Test.TypeParsers +namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { [TestFixture] public class NormalParserTests { - private NormalParser _normalParser; - private NormalDataStoreMock _normalDataStoreMock; + private readonly NormalParser _normalParser; + private readonly NormalDataStoreMock _normalDataStoreMock; - [SetUp] - public void SetUp() + public NormalParserTests() { _normalDataStoreMock = new NormalDataStoreMock(); - - // _normalParser = new NormalParser(_normalDataStoreMock); + _normalParser = new NormalParser(_normalDataStoreMock); } [Test] diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs similarity index 80% rename from Engine/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs index 539308b..4b9ffd9 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs @@ -1,23 +1,23 @@ using FluentAssertions; using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; + +using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; -namespace ObjLoader.Test.TypeParsers +namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { [TestFixture] public class TextureParserTests { - private TextureDataStoreMock _textureDataStoreMock; - private TextureParser _textureParser; + private readonly TextureDataStoreMock _textureDataStoreMock; + private readonly TextureParser _textureParser; - [SetUp] - public void SetUp() + public TextureParserTests() { _textureDataStoreMock = new TextureDataStoreMock(); - // _textureParser = new TextureParser(_textureDataStoreMock); + _textureParser = new TextureParser(_textureDataStoreMock); } [Test] diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs similarity index 75% rename from Engine/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs index 863fac8..29cbaa8 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs @@ -1,20 +1,19 @@ using FluentAssertions; using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; -namespace ObjLoader.Test.TypeParsers +namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { [TestFixture] public class UseMaterialParserTests { - private ElementGroupSpy _elementGroupSpy; - private UseMaterialParser _parser; + private readonly ElementGroupSpy _elementGroupSpy; + private readonly UseMaterialParser _parser; - [SetUp] - public void SetUp() + public UseMaterialParserTests() { _elementGroupSpy = new ElementGroupSpy(); - // _parser = new UseMaterialParser(_elementGroupSpy); + _parser = new UseMaterialParser(_elementGroupSpy); } [Test] @@ -44,7 +43,7 @@ public void Parses_usemtl_line_correctly_1() _elementGroupSpy.MaterialName.Should().BeEquivalentTo("materialName"); } - private class ElementGroupSpy // : IElementGroup + private class ElementGroupSpy : IMaterialDataStore { public string MaterialName { get; private set; } diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs similarity index 80% rename from Engine/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs index ba27316..8009a66 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs @@ -1,23 +1,22 @@ using FluentAssertions; using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; + +using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; -namespace ObjLoader.Test.TypeParsers +namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { [TestFixture] public class VertexParserTests { - private VertexDataStoreMock _vertexDataStoreMock; - private VertexParser _vertexParser; + private readonly VertexDataStoreMock _vertexDataStoreMock; + private readonly VertexParser _vertexParser; - [SetUp] - public void SetUp() + public VertexParserTests() { _vertexDataStoreMock = new VertexDataStoreMock(); - - // _vertexParser = new VertexParser(_vertexDataStoreMock); + _vertexParser = new VertexParser(_vertexDataStoreMock); } [Test] @@ -64,6 +63,5 @@ public void AddVertex(Vertex vertex) ParsedVertex = vertex; } } - } } \ No newline at end of file diff --git a/Engine/Tests/SharpEngine.Core.Tests/Class1.cs b/Tests/SharpEngine.Core.Tests/Class1.cs similarity index 100% rename from Engine/Tests/SharpEngine.Core.Tests/Class1.cs rename to Tests/SharpEngine.Core.Tests/Class1.cs diff --git a/Engine/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj similarity index 71% rename from Engine/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj rename to Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj index fa71b7a..bbcf245 100644 --- a/Engine/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj +++ b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + $(DotNetTargetFramework) enable enable diff --git a/Engine/Tests/SharpEngine.Editor.Tests/Class1.cs b/Tests/SharpEngine.Editor.Tests/Class1.cs similarity index 100% rename from Engine/Tests/SharpEngine.Editor.Tests/Class1.cs rename to Tests/SharpEngine.Editor.Tests/Class1.cs diff --git a/Engine/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj b/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj similarity index 71% rename from Engine/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj rename to Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj index fa71b7a..bbcf245 100644 --- a/Engine/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj +++ b/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + $(DotNetTargetFramework) enable enable diff --git a/Engine/Tests/SharpEngine.Shared.Tests/Class1.cs b/Tests/SharpEngine.Shared.Tests/Class1.cs similarity index 100% rename from Engine/Tests/SharpEngine.Shared.Tests/Class1.cs rename to Tests/SharpEngine.Shared.Tests/Class1.cs diff --git a/Engine/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj b/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj similarity index 100% rename from Engine/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj rename to Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj From ea423f1cb832f6e05104b2669b11a1160bda51e9 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 16:04:30 +0300 Subject: [PATCH 03/10] #87 Basic perlin noise (#133) * #87 Basic perlin noise * #87 Documentation and cleanup --------- Co-authored-by: Antti Veikkolainen --- SharpEngine.Core.Numerics/MathExtensions.cs | 64 ++++++ .../Noise/INoiseGenerator.cs | 45 +++++ .../Noise/PerlinNoiseGenerator.cs | 191 ++++++++++++++++++ 3 files changed, 300 insertions(+) create mode 100644 SharpEngine.Core.Numerics/MathExtensions.cs create mode 100644 SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs create mode 100644 SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs diff --git a/SharpEngine.Core.Numerics/MathExtensions.cs b/SharpEngine.Core.Numerics/MathExtensions.cs new file mode 100644 index 0000000..21881ae --- /dev/null +++ b/SharpEngine.Core.Numerics/MathExtensions.cs @@ -0,0 +1,64 @@ +namespace SharpEngine.Core.Numerics; + +/// +/// Provides mathematical utility functions. +/// +public static partial class MathExtensions +{ + /// + /// Returns the largest integer less than or equal to the specified float value. + /// + /// The float value. + /// The largest integer less than or equal to the specified float value. + public static int FastFloor(float value) + { + int integer = (int)value; + + return value < integer ? integer - 1 : integer; + } + + /// + /// Smooths the input value. + /// + /// The input value. + /// The smoothed value. + public static float Fade(float t) + { + return t * t * t * (t * (t * 6f - 15f) + 10f); + } + + /// + /// Performs linear interpolation between two values. + /// + /// The start value. + /// The end value. + /// The interpolation factor. + /// The interpolated value. + public static float Lerp(float a, float b, float t) + { + return a + t * (b - a); + } + + /// + /// Computes the gradient for a given hash and coordinates. + /// + /// The hash value. + /// The x coordinate. + /// The y coordinate. + /// The z coordinate. + /// The computed gradient. + public static float Grad(int hash, float x, float y, float z) + { + int h = hash & 15; + + float u = h < 8 ? x : y; + float v = h < 4 + ? y + : h is 12 or 14 + ? x + : z; + + return ((h & 1) == 0 ? u : -u) + + ((h & 2) == 0 ? v : -v); + } +} diff --git a/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs b/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs new file mode 100644 index 0000000..d285dc2 --- /dev/null +++ b/SharpEngine.Core.Numerics/Noise/INoiseGenerator.cs @@ -0,0 +1,45 @@ +namespace SharpEngine.Core.Numerics.Noise; + +/// +/// Defines a noise generator with controllable fractal parameters. +/// +public interface INoiseGenerator +{ + /// Gets or sets the scale factor applied to the input coordinates. + /// + /// Higher values make the noise appear more zoomed out. + /// + float Scale { get; set; } + + /// Gets or sets the number of octaves (noise layers) to combine. + int Octaves { get; set; } + + /// + /// Gets or sets the persistence, + /// which controls how much each octave contributes relative to the previous one (amplitude decay per octave). + /// + float Persistence { get; set; } + + /// + /// Gets or sets the lacunarity, + /// which controls how quickly the frequency increases per octave. + /// + float Lacunarity { get; set; } + + /// + /// Samples the noise at the given 2D coordinates. + /// + /// The X coordinate. + /// The Z coordinate. + /// A normalized value in the range [0, 1]. + float Sample2D(float x, float z); + + /// + /// Samples the noise at the given 3D coordinates. + /// + /// The X coordinate. + /// The Y coordinate. + /// The Z coordinate. + /// A normalized value in the range [0, 1]. + float Sample3D(float x, float y, float z); +} diff --git a/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs new file mode 100644 index 0000000..a961cd8 --- /dev/null +++ b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs @@ -0,0 +1,191 @@ +using MathEx = SharpEngine.Core.Numerics.MathExtensions; + +namespace SharpEngine.Core.Numerics.Noise; + +/// +/// Deterministic Perlin noise generator with fractal octave support. +/// +public sealed class PerlinNoiseGenerator : INoiseGenerator +{ + private readonly int[] _permutation = new int[512]; + + private float _scale = 32f; + private int _octaves = 4; + private float _persistence = 0.5f; + private float _lacunarity = 2f; + + /// + /// Creates a new Perlin noise generator. + /// + /// The seed used to generate the permutation table. + public PerlinNoiseGenerator(int seed = 1337) + { + InitializePermutation(seed); + } + + /// + public float Scale + { + get => _scale; + set => _scale = value <= 0f ? 0.0001f : value; + } + + /// + public int Octaves + { + get => _octaves; + set => _octaves = Math.Max(1, value); + } + + /// + public float Persistence + { + get => _persistence; + set => _persistence = Math.Max(0f, value); + } + + /// + public float Lacunarity + { + get => _lacunarity; + set => _lacunarity = Math.Max(0.0001f, value); + } + + /// + public float Sample2D(float x, float z) + { + float total = 0f; + float amplitude = 1f; + float frequency = 1f; + float maxAmplitude = 0f; + + for (int i = 0; i < Octaves; i++) + { + float sampleX = x / Scale * frequency; + float sampleZ = z / Scale * frequency; + + float noise = Perlin(sampleX, 0f, sampleZ); + + total += noise * amplitude; + maxAmplitude += amplitude; + + amplitude *= Persistence; + frequency *= Lacunarity; + } + + float normalized = total / maxAmplitude; + + return ToUnitRange(normalized); + } + + /// + public float Sample3D(float x, float y, float z) + { + float total = 0f; + float amplitude = 1f; + float frequency = 1f; + float maxAmplitude = 0f; + + for (int i = 0; i < Octaves; i++) + { + float sampleX = x / Scale * frequency; + float sampleY = y / Scale * frequency; + float sampleZ = z / Scale * frequency; + + float noise = Perlin(sampleX, sampleY, sampleZ); + + total += noise * amplitude; + maxAmplitude += amplitude; + + amplitude *= Persistence; + frequency *= Lacunarity; + } + + float normalized = total / maxAmplitude; + + return ToUnitRange(normalized); + } + + private void InitializePermutation(int seed) + { + int[] source = new int[256]; + + for (int i = 0; i < source.Length; i++) + source[i] = i; + + Random random = new(seed); + + for (int i = source.Length - 1; i > 0; i--) + { + int swapIndex = random.Next(i + 1); + + (source[i], source[swapIndex]) = (source[swapIndex], source[i]); + } + + for (int i = 0; i < 512; i++) + _permutation[i] = source[i & 255]; + } + + private float Perlin(float x, float y, float z) + { + int xi = MathEx.FastFloor(x) & 255; + int yi = MathEx.FastFloor(y) & 255; + int zi = MathEx.FastFloor(z) & 255; + + float xf = x - MathEx.FastFloor(x); + float yf = y - MathEx.FastFloor(y); + float zf = z - MathEx.FastFloor(z); + + float u = MathEx.Fade(xf); + float v = MathEx.Fade(yf); + float w = MathEx.Fade(zf); + + var yiOffset = _permutation[xi] + yi; + + int aaa = _permutation[_permutation[yiOffset] + zi]; + int aba = _permutation[_permutation[yiOffset + 1] + zi]; + int aab = _permutation[_permutation[yiOffset] + zi + 1]; + int abb = _permutation[_permutation[yiOffset + 1] + zi + 1]; + + var yiOffsetNext = _permutation[xi + 1] + yi; + + int baa = _permutation[_permutation[yiOffsetNext] + zi]; + int bba = _permutation[_permutation[yiOffsetNext + 1] + zi]; + int bab = _permutation[_permutation[yiOffsetNext] + zi + 1]; + int bbb = _permutation[_permutation[yiOffsetNext + 1] + zi + 1]; + + float x1 = MathEx.Lerp( + MathEx.Grad(aaa, xf, yf, zf), + MathEx.Grad(baa, xf - 1f, yf, zf), + u); + + float x2 = MathEx.Lerp( + MathEx.Grad(aba, xf, yf - 1f, zf), + MathEx.Grad(bba, xf - 1f, yf - 1f, zf), + u); + + float y1 = MathEx.Lerp(x1, x2, v); + + x1 = MathEx.Lerp( + MathEx.Grad(aab, xf, yf, zf - 1f), + MathEx.Grad(bab, xf - 1f, yf, zf - 1f), + u); + + x2 = MathEx.Lerp( + MathEx.Grad(abb, xf, yf - 1f, zf - 1f), + MathEx.Grad(bbb, xf - 1f, yf - 1f, zf - 1f), + u); + + float y2 = MathEx.Lerp(x1, x2, v); + + return MathEx.Lerp(y1, y2, w); + } + + private static float ToUnitRange(float value) + { + // Perlin is roughly in [-1, 1], so remap to [0, 1]. + float result = value * 0.5f + 0.5f; + + return Math.Clamp(result, 0f, 1f); + } +} \ No newline at end of file From 100155c0be6dbb0577ce8bd9850230e7f215fd11 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 19:37:46 +0300 Subject: [PATCH 04/10] #109 Improve testability and cleanup (#135) * #109 Add initial unit tests * #109 Audio player updates Added pause, resume and loading new audios on top of an existing player * #109 Fix windowing * #109 Update scene calls --------- Co-authored-by: Antti Veikkolainen --- Examples/Minimal/Program.cs | 1 + Examples/MultipleWindows/Program.cs | 9 +- ObjLoader/DataStore.cs | 4 + ObjLoader/IMaterialDataStore.cs | 13 ++- ObjLoader/Loaders/IFileManager.cs | 13 +++ ObjLoader/Loaders/LoaderBase.cs | 17 +++- .../MaterialLoader/MaterialLibraryLoader.cs | 12 ++- ObjLoader/Loaders/ObjLoader/ObjLoader.cs | 7 +- .../Loaders/ObjLoader/ObjLoaderFactory.cs | 35 +++++-- ObjLoader/SharpEngine.Core.ObjLoader.csproj | 1 + SharpEngine.Core.ArchitectureTests/Class1.cs | 6 ++ .../SharpEngine.Core.ArchitectureTests.csproj | 9 ++ SharpEngine.Core/Audio/Audio.cs | 27 +++++- SharpEngine.Core/Audio/AudioBuffer.cs | 2 +- .../Audio/{WavData.cs => AudioData.cs} | 4 +- SharpEngine.Core/Audio/AudioDevice.cs | 8 ++ SharpEngine.Core/Audio/AudioPlayerBase.cs | 32 ++++++- SharpEngine.Core/Audio/AudioSource.cs | 23 +++++ SharpEngine.Core/Audio/Mp3Player.cs | 74 +++++++++++++++ SharpEngine.Core/Audio/WavPlayer.cs | 4 +- .../ObjLoader/DataStore/IVertexDataStore.cs | 0 .../Extensions/AssemblyExtensions.cs | 3 +- SharpEngine.Core/Handlers/WindowHandler.cs | 16 ++-- .../Renderers/ParticleRenderer.cs | 25 ++++- SharpEngine.Core/Scenes/Scene.cs | 19 +++- SharpEngine.Core/SharpEngine.Core.csproj | 2 + SharpEngine.Core/Windowing/SilkWindow.cs | 8 ++ SharpEngine.Core/Windowing/Window.cs | 93 +++++++++++++------ SharpEngine.slnx | 17 +++- Tests/Directory.Build.props | 3 +- .../Loaders/MaterialLibraryLoaderTests.cs | 29 +++--- .../ObjLoader.Test/Loaders/ObjLoaderTests.cs | 80 +++++----------- .../SharpEngine.Core.ObjLoader.Tests.csproj | 9 +- .../TypeParsers/FaceParserTests.cs | 23 ++--- .../TypeParsers/GroupParserTests.cs | 9 +- .../TypeParsers/MaterialLibraryParserTests.cs | 10 +- .../TypeParsers/NormalParserTests.cs | 9 +- .../TypeParsers/TextureParserTests.cs | 9 +- .../TypeParsers/UseMaterialParserTests.cs | 16 +++- .../TypeParsers/VertexParserTests.cs | 9 +- .../Class1.cs | 6 ++ .../SharpEngine.Core.Components.Tests.csproj | 13 +++ .../SharpEngine.Core.Numerics.Tests/Class1.cs | 6 ++ .../SharpEngine.Core.Numerics.Tests.csproj | 13 +++ Tests/SharpEngine.Core.Tests/Class1.cs | 6 -- .../Extensions/AssemblyExtensionsTests.cs | 40 ++++++++ .../SharpEngine.Core.Tests.csproj | 25 +++++ .../SharpEngine.Shared.Tests.csproj | 2 +- 48 files changed, 600 insertions(+), 201 deletions(-) create mode 100644 ObjLoader/Loaders/IFileManager.cs create mode 100644 SharpEngine.Core.ArchitectureTests/Class1.cs create mode 100644 SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj rename SharpEngine.Core/Audio/{WavData.cs => AudioData.cs} (90%) create mode 100644 SharpEngine.Core/Audio/Mp3Player.cs delete mode 100644 SharpEngine.Core/Components/ObjLoader/DataStore/IVertexDataStore.cs create mode 100644 Tests/SharpEngine.Core.Components.Tests/Class1.cs create mode 100644 Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj create mode 100644 Tests/SharpEngine.Core.Numerics.Tests/Class1.cs create mode 100644 Tests/SharpEngine.Core.Numerics.Tests/SharpEngine.Core.Numerics.Tests.csproj delete mode 100644 Tests/SharpEngine.Core.Tests/Class1.cs create mode 100644 Tests/SharpEngine.Core.Tests/Extensions/AssemblyExtensionsTests.cs diff --git a/Examples/Minimal/Program.cs b/Examples/Minimal/Program.cs index 26310ec..36a530e 100644 --- a/Examples/Minimal/Program.cs +++ b/Examples/Minimal/Program.cs @@ -16,6 +16,7 @@ public static void Main(string[] _) var game = new Minimal(); using var window = new Window(game); + window.Initialize(); window.Run(); } } diff --git a/Examples/MultipleWindows/Program.cs b/Examples/MultipleWindows/Program.cs index 67751b3..b1dfc16 100644 --- a/Examples/MultipleWindows/Program.cs +++ b/Examples/MultipleWindows/Program.cs @@ -117,10 +117,13 @@ private static Window CreateWindow() private static void EnqueueWindow() { var window = CreateWindow(); - foreach (var mouse in window!.Input!.Mice) - mouse.Click += Mouse_Click; - _inputContexts.Add(window.Input); + // Prefer the higher-level Window event to avoid wiring low-level input handlers. + window.OnButtonMouseDown += (mouse, button) => Mouse_Click(mouse, (MouseButton)button, mouse.Position); + + if (window.Input is not null) + _inputContexts.Add(window.Input); + _windows.Add(window); } diff --git a/ObjLoader/DataStore.cs b/ObjLoader/DataStore.cs index 5833552..ec82bc1 100644 --- a/ObjLoader/DataStore.cs +++ b/ObjLoader/DataStore.cs @@ -79,5 +79,9 @@ public void AddNormal(Normal normal) /// public void AddVertex(Vertex vertex) => Vertices.Add(vertex); + + /// + public void AddMaterial(Material currentMaterial) + => Materials.Add(currentMaterial); } } \ No newline at end of file diff --git a/ObjLoader/IMaterialDataStore.cs b/ObjLoader/IMaterialDataStore.cs index 38f3168..9424ed5 100644 --- a/ObjLoader/IMaterialDataStore.cs +++ b/ObjLoader/IMaterialDataStore.cs @@ -1,14 +1,21 @@ -namespace SharpEngine.Core.ObjLoader; +using SharpEngine.Core.Components.Properties; + +namespace SharpEngine.Core.ObjLoader; /// /// Contains definitions for the material data store, which provides methods to manage material information during the OBJ file loading process. /// public interface IMaterialDataStore { + /// + /// Adds a material to the material data store. + /// + /// The material to add. + void AddMaterial(Material currentMaterial); + /// /// Sets the material for the current group. /// /// The name of the material to set. - public void SetMaterial(string materialName); - + void SetMaterial(string materialName); } \ No newline at end of file diff --git a/ObjLoader/Loaders/IFileManager.cs b/ObjLoader/Loaders/IFileManager.cs new file mode 100644 index 0000000..21e33eb --- /dev/null +++ b/ObjLoader/Loaders/IFileManager.cs @@ -0,0 +1,13 @@ +using System.IO; + +namespace SharpEngine.Core.ObjLoader.Loader.Loaders +{ + /// + /// Represents an abstraction of a stream reader for testability. + /// + public interface IFileManager + { + /// + StreamReader StreamReader(string path); + } +} \ No newline at end of file diff --git a/ObjLoader/Loaders/LoaderBase.cs b/ObjLoader/Loaders/LoaderBase.cs index 4ecbabc..5253b8f 100644 --- a/ObjLoader/Loaders/LoaderBase.cs +++ b/ObjLoader/Loaders/LoaderBase.cs @@ -1,4 +1,5 @@ using System.IO; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Loader.Loaders { @@ -7,6 +8,18 @@ namespace SharpEngine.Core.ObjLoader.Loader.Loaders /// public abstract class LoaderBase { + private readonly IFileStreamFactory _fileStreamFactory; + private readonly IFileManager _fileManager; + + /// + /// Initializes a new instance of the . + /// + protected LoaderBase(IFileStreamFactory fileStreamFactory, IFileManager fileManager) + { + _fileStreamFactory = fileStreamFactory; + _fileManager = fileManager; + } + /// /// Parses the file at the specified path and processes each line via . /// @@ -16,8 +29,8 @@ public abstract class LoaderBase /// The path of the file to open and parse. public void ParseFile(string path) { - using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read); - using var lineStreamReader = new StreamReader(fileStream); + using var fileStream = _fileStreamFactory.FileSystem.FileStream.New(path, FileMode.Open, FileAccess.Read); + using var lineStreamReader = _fileManager.StreamReader(path); while (!lineStreamReader.EndOfStream) { diff --git a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs index 39aa7d3..e2dcb91 100644 --- a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs +++ b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs @@ -1,9 +1,11 @@ -using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.ObjLoader.DataStore; +using SharpEngine.Core.Components.Properties; using SharpEngine.Core.ObjLoader.Loader.Loaders; using SharpEngine.Shared.Extensions; using System; using System.Collections.Generic; +using System.IO.Abstractions; using System.Numerics; namespace SharpEngine.Core.ObjLoader.Loaders.MaterialLoader @@ -19,7 +21,7 @@ namespace SharpEngine.Core.ObjLoader.Loaders.MaterialLoader /// public class MaterialLibraryLoader : LoaderBase { - private readonly DataStore _dataStore; + private readonly IMaterialDataStore _dataStore; private readonly Dictionary> _parseActionDictionary = []; private readonly List _unrecognizedLines = []; @@ -35,7 +37,9 @@ public class MaterialLibraryLoader : LoaderBase /// Ns, d, Tr, illum, map_*, bump, disp, decal) to populate Material instances. /// /// DataStore used to register and store parsed materials and texture references. - public MaterialLibraryLoader(DataStore dataStore) + /// Represents a factory for creating file streams. + /// Represents a manager for handling file operations. + public MaterialLibraryLoader(IMaterialDataStore dataStore, IFileStreamFactory fileStreamFactory, IFileManager fileManager) : base(fileStreamFactory, fileManager) { _dataStore = dataStore; @@ -92,7 +96,7 @@ protected override void ParseLine(string keyword, string data) private void PushMaterial(string materialName) { _currentMaterial = new Material(materialName); - _dataStore.Materials.Add(_currentMaterial); + _dataStore.AddMaterial(_currentMaterial); } private static Vector3 ParseVec3(string data) diff --git a/ObjLoader/Loaders/ObjLoader/ObjLoader.cs b/ObjLoader/Loaders/ObjLoader/ObjLoader.cs index cfcd5f0..a79ac5b 100644 --- a/ObjLoader/Loaders/ObjLoader/ObjLoader.cs +++ b/ObjLoader/Loaders/ObjLoader/ObjLoader.cs @@ -1,8 +1,11 @@ using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.ObjLoader.Loader.Loaders; using SharpEngine.Core.ObjLoader.TypeParsers; + using Silk.NET.OpenGL; + using System.Collections.Generic; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader { @@ -21,7 +24,9 @@ public class ObjLoader : LoaderBase /// /// The path to the OBJ file. /// The data store to populate during parsing. - public ObjLoader(string path, DataStore dataStore) + /// Represents a factory for creating file streams. + /// Represents a manager for handling file operations. + public ObjLoader(string path, DataStore dataStore, IFileStreamFactory fileStreamFactory, IFileManager fileManager) : base(fileStreamFactory, fileManager) { _path = path; _dataStore = dataStore; diff --git a/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs b/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs index 5a7d90c..b99400c 100644 --- a/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs +++ b/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs @@ -1,21 +1,42 @@ +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; + using Silk.NET.OpenGL; using System; using System.Collections.Generic; using System.IO; - -using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.Components.Properties.Meshes; -using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; -using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader { + /// + /// Represents a testable wrapper for creating stream file related objects. + /// + public class FileManager : IFileManager + { + /// + public StreamReader StreamReader(string path) + => new(path); + } + /// /// Handles loading 3D models. /// public static class ObjLoaderFactory { + private static readonly FileSystem _fileSystem; + private static readonly FileManager _fileManager; + + static ObjLoaderFactory() + { + _fileSystem = new FileSystem(); + _fileManager = new FileManager(); + } + const string FbxExtension = ".fbx"; const string ObjExtension = ".obj"; @@ -56,13 +77,13 @@ private static List LoadObjMeshes(GL gl, string path) var textureParser = new TextureParser(dataStore); var vertexParser = new VertexParser(dataStore); - var materialLibraryLoader = new MaterialLibraryLoader(dataStore); + var materialLibraryLoader = new MaterialLibraryLoader(dataStore, _fileSystem.FileStream, _fileManager); var materialLoader = new MaterialLibraryLoaderFacade(materialLibraryLoader, path); var materialLibraryParser = new MaterialLibraryParser(materialLoader); var useMaterialParser = new UseMaterialParser(dataStore); - var loader = new ObjLoader(path, dataStore) + var loader = new ObjLoader(path, dataStore, _fileSystem.FileStream, _fileManager) .SetupTypeParsers(faceParser, groupParser, normalParser, textureParser, vertexParser, materialLibraryParser, useMaterialParser); return loader.Load(gl); diff --git a/ObjLoader/SharpEngine.Core.ObjLoader.csproj b/ObjLoader/SharpEngine.Core.ObjLoader.csproj index 7a52e96..04ffde4 100644 --- a/ObjLoader/SharpEngine.Core.ObjLoader.csproj +++ b/ObjLoader/SharpEngine.Core.ObjLoader.csproj @@ -7,6 +7,7 @@ + diff --git a/SharpEngine.Core.ArchitectureTests/Class1.cs b/SharpEngine.Core.ArchitectureTests/Class1.cs new file mode 100644 index 0000000..77198f6 --- /dev/null +++ b/SharpEngine.Core.ArchitectureTests/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Core.ArchitectureTests; + +public class Class1 +{ + +} diff --git a/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj b/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/SharpEngine.Core/Audio/Audio.cs b/SharpEngine.Core/Audio/Audio.cs index 7d9d171..7d38c4d 100644 --- a/SharpEngine.Core/Audio/Audio.cs +++ b/SharpEngine.Core/Audio/Audio.cs @@ -8,6 +8,8 @@ namespace SharpEngine.Core.Audio; /// public static class Audio { + private static AudioPlayerBase? audioPlayer = null; + /// /// Plays an audio file if it is in the WAV format. /// @@ -18,11 +20,34 @@ public static void Play(string filePath) switch (Path.GetExtension(filePath)) { case ".wav": - new WavPlayer().Play(filePath); + audioPlayer = new WavPlayer(); + audioPlayer.Play(filePath); + break; + + case ".mp3": + audioPlayer = new Mp3Player(); + audioPlayer.Play(filePath); break; default: throw new NotSupportedException("Unsupported file format."); } } + + /// + public static void Stop() + { + audioPlayer?.Stop(); + audioPlayer = null; + } + + /// + /// Pauses the currently playing audio, if any. + /// + public static void Pause() => audioPlayer?.Pause(); + + /// + /// Resumes the currently paused audio, if any. + /// + public static void Resume() => audioPlayer?.Resume(); } diff --git a/SharpEngine.Core/Audio/AudioBuffer.cs b/SharpEngine.Core/Audio/AudioBuffer.cs index 506f426..6041946 100644 --- a/SharpEngine.Core/Audio/AudioBuffer.cs +++ b/SharpEngine.Core/Audio/AudioBuffer.cs @@ -29,7 +29,7 @@ public AudioBuffer(AL al) /// /// The data to be loaded into the buffer. /// Details about the data. - public void LoadData(ReadOnlySpan data, WavData wavData) + public void LoadData(ReadOnlySpan data, AudioData wavData) { unsafe { diff --git a/SharpEngine.Core/Audio/WavData.cs b/SharpEngine.Core/Audio/AudioData.cs similarity index 90% rename from SharpEngine.Core/Audio/WavData.cs rename to SharpEngine.Core/Audio/AudioData.cs index 80ac937..fa81d42 100644 --- a/SharpEngine.Core/Audio/WavData.cs +++ b/SharpEngine.Core/Audio/AudioData.cs @@ -3,9 +3,9 @@ namespace SharpEngine.Core.Audio; /// -/// Represents audio data in WAV format. +/// Represents the data in a audio file. /// -public class WavData +public class AudioData { /// Gets or sets the number of channels used in the data. public short NumChannels { get; set; } diff --git a/SharpEngine.Core/Audio/AudioDevice.cs b/SharpEngine.Core/Audio/AudioDevice.cs index aa0560d..d5429bc 100644 --- a/SharpEngine.Core/Audio/AudioDevice.cs +++ b/SharpEngine.Core/Audio/AudioDevice.cs @@ -8,6 +8,14 @@ namespace SharpEngine.Core.Audio; /// public unsafe class AudioDevice : IDisposable { + // Shared single AudioDevice instance to ensure a single OpenAL context is used + private static readonly AudioDevice _shared = new AudioDevice(); + + /// + /// Gets the shared audio device instance used by the application. + /// + public static AudioDevice Shared => _shared; + /// Gets the OpenAL context. public AL Al { get; } diff --git a/SharpEngine.Core/Audio/AudioPlayerBase.cs b/SharpEngine.Core/Audio/AudioPlayerBase.cs index 5778672..8df590a 100644 --- a/SharpEngine.Core/Audio/AudioPlayerBase.cs +++ b/SharpEngine.Core/Audio/AudioPlayerBase.cs @@ -1,4 +1,5 @@ using SharpEngine.Core.Scenes; +using Silk.NET.OpenAL; using System; using System.IO; @@ -17,7 +18,7 @@ public abstract class AudioPlayerBase : SceneNode public AudioProperties AudioProperties { get; set; } /// Contains the audio data of the media file. - protected readonly WavData Data; + protected readonly AudioData Data; /// Represents a container for the . protected readonly AudioBuffer AudioBuffer; @@ -30,9 +31,10 @@ public abstract class AudioPlayerBase : SceneNode /// protected AudioPlayerBase() { - Data = new WavData(); + Data = new AudioData(); - var audioDevice = new AudioDevice(); + // Use the shared audio device so multiple players share the same OpenAL context + var audioDevice = AudioDevice.Shared; AudioBuffer = new AudioBuffer(audioDevice.Al); AudioSource = new AudioSource(audioDevice.Al); @@ -61,7 +63,27 @@ protected virtual void ValidateFile(string filePath) } /// - /// Stops the audio playback of the associated AudioSource. + /// Stops the audio playback of the associated . /// - public virtual void Stop() => AudioSource.Stop(); + public virtual void Stop() + { + // Stop playback but do not delete the source here. Deleting the source + // in AudioSource.Stop caused subsequent players to generate new sources + // while buffers still referenced old sources, leading to overlapping audio. + AudioSource.Stop(); + + // Detach buffer from source so it can be reused safely + var al = AudioDevice.Shared.Al; + al.SetSourceProperty(AudioSource.Get(), SourceInteger.Buffer, 0); + } + + /// + /// Pauses the audio playback of the associated . + /// + public virtual void Pause() => AudioSource.Pause(); + + /// + /// Resumes the audio playback of the associated . + /// + public virtual void Resume() => AudioSource.Resume(); } diff --git a/SharpEngine.Core/Audio/AudioSource.cs b/SharpEngine.Core/Audio/AudioSource.cs index d0a3669..6c11cd4 100644 --- a/SharpEngine.Core/Audio/AudioSource.cs +++ b/SharpEngine.Core/Audio/AudioSource.cs @@ -42,6 +42,29 @@ public void Play(AudioBuffer buffer) public void Stop() { _al.SourceStop(_source); + } + + /// + /// Pauses playback of the source. + /// + public void Pause() + { + _al.SourcePause(_source); + } + + /// + /// Resumes playback of the source if it was paused. + /// + public void Resume() + { + _al.SourcePlay(_source); + } + + /// + /// Deletes the underlying OpenAL source. Call when the source will no longer be used. + /// + public void Dispose() + { _al.DeleteSource(_source); } } \ No newline at end of file diff --git a/SharpEngine.Core/Audio/Mp3Player.cs b/SharpEngine.Core/Audio/Mp3Player.cs new file mode 100644 index 0000000..1fe5ff3 --- /dev/null +++ b/SharpEngine.Core/Audio/Mp3Player.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Buffers.Binary; +using System.IO; +using NLayer; +using Silk.NET.OpenAL; + +namespace SharpEngine.Core.Audio; + +internal class Mp3Player : AudioPlayerBase +{ + private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + + /// + protected override string FileExtension => AudioFileExtensions.Mp3Extension; + + public Mp3Player() + { + } + + /// + /// Thrown if the audio data contains more than 2 channels. + /// Thrown if the file path is invalid. + /// Thrown if the file is not found. + public override void Play(string filePath) + { + ValidateFile(filePath); + + using var fs = File.OpenRead(filePath); + using var mpeg = new MpegFile(fs); + + int sampleRate = mpeg.SampleRate; + int channels = mpeg.Channels; + + // Decode to float samples and convert to 16-bit PCM + using var pcmStream = new MemoryStream(); + var floatBuffer = new float[4096]; + int samplesRead; + while ((samplesRead = mpeg.ReadSamples(floatBuffer, 0, floatBuffer.Length)) > 0) + { + // samplesRead is number of float samples (interleaved channels) + var bytes = new byte[samplesRead * 2]; + for (int i = 0; i < samplesRead; i++) + { + float f = floatBuffer[i]; + // clamp and convert + int intVal = (int)MathF.Round(f * 32767f); + if (intVal > short.MaxValue) intVal = short.MaxValue; + else if (intVal < short.MinValue) intVal = short.MinValue; + short s = (short)intVal; + bytes[i * 2] = (byte)(s & 0xFF); + bytes[i * 2 + 1] = (byte)((s >> 8) & 0xFF); + } + + pcmStream.Write(bytes, 0, bytes.Length); + } + + var pcmData = pcmStream.ToArray(); + + // Fill WavData + Data.NumChannels = (short)channels; + Data.SampleRate = sampleRate; + Data.BitsPerSample = 16; + Data.Format = channels switch + { + 1 => BufferFormat.Mono16, + 2 => BufferFormat.Stereo16, + _ => throw new InvalidOperationException($"Can't play audio with {channels} channels.") + }; + + AudioBuffer.LoadData(pcmData, Data); + AudioSource.Play(AudioBuffer); + } +} \ No newline at end of file diff --git a/SharpEngine.Core/Audio/WavPlayer.cs b/SharpEngine.Core/Audio/WavPlayer.cs index 7869f28..33401aa 100644 --- a/SharpEngine.Core/Audio/WavPlayer.cs +++ b/SharpEngine.Core/Audio/WavPlayer.cs @@ -15,7 +15,7 @@ public class WavPlayer : AudioPlayerBase private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); /// - protected override string FileExtension => ".wav"; + protected override string FileExtension => AudioFileExtensions.WavExtension; private static class WavConstants { @@ -90,7 +90,7 @@ private static bool CheckHeader(ReadOnlySpan file, ref int index, ReadOnly return true; } - private static void ParseFormat(ReadOnlySpan formatChunk, WavData wavData) + private static void ParseFormat(ReadOnlySpan formatChunk, AudioData wavData) { int index = 0; var audioFormat = BinaryPrimitives.ReadInt16LittleEndian(formatChunk.Slice(index, WavConstants.AudioFormatLength)); diff --git a/SharpEngine.Core/Components/ObjLoader/DataStore/IVertexDataStore.cs b/SharpEngine.Core/Components/ObjLoader/DataStore/IVertexDataStore.cs deleted file mode 100644 index e69de29..0000000 diff --git a/SharpEngine.Core/Extensions/AssemblyExtensions.cs b/SharpEngine.Core/Extensions/AssemblyExtensions.cs index 39a3315..2501343 100644 --- a/SharpEngine.Core/Extensions/AssemblyExtensions.cs +++ b/SharpEngine.Core/Extensions/AssemblyExtensions.cs @@ -4,7 +4,7 @@ namespace SharpEngine.Core.Extensions; /// -/// Contains extension methods for the class, providing additional functionality related to assembly metadata and versioning. +/// Contains extension methods for the . /// public static class AssemblyExtensions { @@ -13,6 +13,7 @@ public static class AssemblyExtensions /// /// The assembly to get the version for. /// The version of the assembly. + /// Thrown when the assembly version is null. public static Version GetVersion(this Assembly assembly) => assembly.GetName().Version!; } diff --git a/SharpEngine.Core/Handlers/WindowHandler.cs b/SharpEngine.Core/Handlers/WindowHandler.cs index ef2d1ec..d82c773 100644 --- a/SharpEngine.Core/Handlers/WindowHandler.cs +++ b/SharpEngine.Core/Handlers/WindowHandler.cs @@ -200,8 +200,10 @@ private static Windowing.Window CreateWindow(WindowOptions options) private static void EnqueueWindow(WindowOptions options) { var window = CreateWindow(options); - foreach (var mouse in window!.Input!.Mice) - mouse.Click += Mouse_Click; + // Subscribe to the window's high-level mouse button event instead of + // attaching to the low-level Input.Mice click handlers. This centralizes + // input handling inside Window and avoids duplicate wiring. + window.OnButtonMouseDown += (mouse, button) => Mouse_Click(mouse, (Silk.NET.Input.MouseButton)button, mouse.Position); _inputContexts.Add(window.Input); _windows.Add(window); @@ -217,13 +219,13 @@ private void RegisterWindow(SilkWindow window, bool isMain = false) if (window is null) return; - if (window.Input is not null) - { - foreach (var mouse in window.Input.Mice) - mouse.Click += Mouse_Click; + // Prefer the higher-level event on the Window class for mouse button + // notifications rather than wiring Input.Mice click events directly. + if (window is Windowing.Window w) + w.OnButtonMouseDown += (mouse, button) => Mouse_Click(mouse, (Silk.NET.Input.MouseButton)button, mouse.Position); + if (window.Input is not null) _inputContexts.Add(window.Input); - } _windows.Add(window); diff --git a/SharpEngine.Core/Renderers/ParticleRenderer.cs b/SharpEngine.Core/Renderers/ParticleRenderer.cs index 53f6d93..e34e2e3 100644 --- a/SharpEngine.Core/Renderers/ParticleRenderer.cs +++ b/SharpEngine.Core/Renderers/ParticleRenderer.cs @@ -1,4 +1,7 @@ -using SharpEngine.Core.Interfaces; +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Scenes; using System; using System.Threading.Tasks; @@ -17,9 +20,27 @@ public ParticleRenderer(ISettings settings) : base(settings) { } + /// + /// Initializes a new instance of . + /// + public ParticleRenderer(CameraView camera, ISettings settings, Scene scene) + : this(camera, settings, scene, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) + { + } + + /// + /// Initializes a new instance of . + /// + public ParticleRenderer(CameraView camera, ISettings settings, Scene scene, ILogger logger) : base(settings) + { + // _scene = scene; + // _camera = camera; + // _logger = logger; + } + /// public override RenderFlags RenderFlag => RenderFlags.All; /// - public override Task Render() => throw new NotImplementedException(); + public override Task Render() => Task.CompletedTask; // => throw new NotImplementedException(); } diff --git a/SharpEngine.Core/Scenes/Scene.cs b/SharpEngine.Core/Scenes/Scene.cs index ff4db26..6ab4c91 100644 --- a/SharpEngine.Core/Scenes/Scene.cs +++ b/SharpEngine.Core/Scenes/Scene.cs @@ -1,10 +1,12 @@ using Microsoft.Extensions.Logging; using SharpEngine.Core.Entities.Properties; +using SharpEngine.Telemetry; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace SharpEngine.Core.Scenes; @@ -14,7 +16,7 @@ namespace SharpEngine.Core.Scenes; /// public class Scene { - private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + private static readonly ILogger Logger = LoggingExtensions.CreateLogger(); /// The file extension by which saved scenes are associated with. public const string SceneFileExtension = "sharpscene"; @@ -22,6 +24,7 @@ public class Scene private string? _fileFullPath; /// Gets or sets whether the scene has unsaved changes. + [JsonIgnore] public bool HasUnsavedChanges { get; set; } /// Gets or sets the name of the scene file. @@ -214,21 +217,29 @@ public void SetFileFullPath(string sceneFile) public bool HasSaveFile() => _fileFullPath is not null; + /// + /// Saves the scene. + /// + public void SaveScene() + => SaveSceneAsync().GetAwaiter().GetResult(); + /// /// Saves the scene. /// /// A representing an asynchronous operation. - public async Task SaveScene() - => await SaveScene(_fileFullPath!); + public async Task SaveSceneAsync() + => await SaveSceneAsync(_fileFullPath!); /// /// Saves the scene to the given . /// /// The name of the file to be saved. /// A representing an asynchronous operation. - public async Task SaveScene(string fileName) + public async Task SaveSceneAsync(string fileName) { var json = JsonSerializer.Serialize(this); await System.IO.File.WriteAllTextAsync(fileName, json); + + HasUnsavedChanges = false; } } \ No newline at end of file diff --git a/SharpEngine.Core/SharpEngine.Core.csproj b/SharpEngine.Core/SharpEngine.Core.csproj index 04fb6cb..8760f7b 100644 --- a/SharpEngine.Core/SharpEngine.Core.csproj +++ b/SharpEngine.Core/SharpEngine.Core.csproj @@ -18,6 +18,7 @@ true README.md + @@ -26,6 +27,7 @@ + diff --git a/SharpEngine.Core/Windowing/SilkWindow.cs b/SharpEngine.Core/Windowing/SilkWindow.cs index 1f5e1dd..c7c9fd3 100644 --- a/SharpEngine.Core/Windowing/SilkWindow.cs +++ b/SharpEngine.Core/Windowing/SilkWindow.cs @@ -75,6 +75,14 @@ public Vector2D Size set => CurrentWindow.Size = value; } + /// Gets the width of the window in pixels. + /// Use to change the window size. + public int Width => Size.X; + + /// Gets the height of the window in pixels. + /// Use to change the window size. + public int Height => Size.Y; + /// public string Title { diff --git a/SharpEngine.Core/Windowing/Window.cs b/SharpEngine.Core/Windowing/Window.cs index 903f3df..15afa84 100644 --- a/SharpEngine.Core/Windowing/Window.cs +++ b/SharpEngine.Core/Windowing/Window.cs @@ -92,14 +92,29 @@ public static GL SharedGL public GL GetGL() => _gl; private void SetGL(GL gl) => _gl = gl; + /// + /// Initializes a new instance of . + /// + /// The game instance. + public Window(Game game) + : this(game.Scene, game.Camera.Settings, logger: null, renderers: null) { } + + /// + /// Initializes a new instance of . + /// + /// The scene to be rendered. + /// The settings for the window. + public Window(Scene scene, IViewSettings settings) + : this(scene, settings, logger: null, renderers: null) { } + /// /// Initializes a new instance of . /// /// The camera the window should render from. /// Contains the game scene. /// The settings for the window. - public Window(CameraView camera, Scene scene, IViewSettings settings) : - this(camera, scene, settings, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) { } + public Window(CameraView camera, Scene scene, IViewSettings settings) + : this(camera, scene, settings, logger: null, renderers: null) { } /// /// Initializes a new instance of . @@ -107,9 +122,9 @@ public Window(CameraView camera, Scene scene, IViewSettings settings) : /// The camera the window should render from. /// Contains the game scene. /// The settings for the window. - /// The logger for the window. + /// The logger for the window. When , a default logger will be used. /// The renderers for the window. - public Window(CameraView camera, Scene scene, IViewSettings settings, ILogger logger, IEnumerable? renderers = null) + public Window(CameraView camera, Scene scene, IViewSettings settings, ILogger? logger = null, IEnumerable? renderers = null) { // TODO: // Should the developer need to call for a window initialization? @@ -119,31 +134,14 @@ public Window(CameraView camera, Scene scene, IViewSettings settings, ILogger builder.AddConsole()).CreateLogger(); CheckEngineVersion(); - // NOTE: Window initialization is intentionally not performed automatically - // here. Call InitializeWindow() explicitly when ready to create the underlying - // native window and load resources. See the TODO in the project for reasoning. - } - - private void CheckEngineVersion() - { - var project = new Project(); - var currentAssemlyVersion = typeof(Window).Assembly.GetVersion(); - - if (currentAssemlyVersion != project.EngineVersion) - _logger.LogWarning("The current engine version ({CurrentVersion}) does not match the project engine version ({ProjectVersion}). This may lead to unexpected behavior.", currentAssemlyVersion, project.EngineVersion); + // NOTE: Window initialization is intentionally not performed automatically here. + // Call InitializeWindow() explicitly when ready to create the underlying native window and load resources. } - /// - /// Initializes a new instance of . - /// - /// The game instance. - public Window(Game game) - : this(game.Scene, game.Camera.Settings, LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger()) { } - /// /// Initializes a new instance of . /// @@ -152,18 +150,27 @@ public Window(Game game) /// /// Contains the game scene. /// The settings for the window. - /// The logger for the window. + /// The logger for the window. When , a default logger will be used. /// The renderers for the window. - public Window(Scene scene, IViewSettings settings, ILogger logger, IEnumerable? renderers = null) + public Window(Scene scene, IViewSettings settings, ILogger? logger = null, IEnumerable? renderers = null) { Scene = scene; Settings = settings; Camera = new(Vector3.One, settings); _registeredRenderers = renderers?.ToArray() ?? []; - _logger = logger; + _logger = logger ?? LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + + // NOTE: Window initialization is intentionally not performed automatically here. + // Call InitializeWindow() explicitly when ready to create the underlying native window and load resources. + } - // Initialization is deferred. Call Initialize() when you want the - // native window to be created and resources to be loaded. + private void CheckEngineVersion() + { + var project = new ProjectDto(); + var currentAssemlyVersion = typeof(Window).Assembly.GetVersion(); + + if (currentAssemlyVersion != project.EngineVersion) + _logger.LogWarning("The current engine version ({CurrentVersion}) does not match the project engine version ({ProjectVersion}). This may lead to unexpected behavior.", currentAssemlyVersion, project.EngineVersion); } /// @@ -180,6 +187,11 @@ public override void Initialize() CurrentWindow.Closing += OnClosing; _windowInitialized = true; + + // Ensure the underlying native window is created and initialized. + // SilkWindow.Initialize calls CurrentWindow.Initialize(); call it here to + // complete the initialization sequence (create GL context, input, etc.). + base.Initialize(); } /// @@ -262,7 +274,14 @@ protected void RenderFrame(Frame frame) _gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); ToggleWireFrame(Settings.UseWireFrame); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during pre-rendering."); + } + try + { UseShaders(); var activeRenderers = _renderers.Where(renderer => Settings.RendererFlags.HasFlag(renderer.RenderFlag)) @@ -271,14 +290,28 @@ protected void RenderFrame(Frame frame) foreach (var renderer in activeRenderers) renderer.Render().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during rendering."); + } + try + { AfterRender(frame); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during post render."); + } + try + { _imGuiController?.Render(); } catch (Exception ex) { - _logger.LogInformation("{Message}", ex.Message); + _logger.LogError(ex, "An error occurred while rendering ImGui interface."); } } diff --git a/SharpEngine.slnx b/SharpEngine.slnx index 4cfa5e7..39b30b0 100644 --- a/SharpEngine.slnx +++ b/SharpEngine.slnx @@ -34,9 +34,23 @@ + + + + + + + + + + + + + + @@ -47,9 +61,6 @@ - - - diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props index 3ad3602..3b85263 100644 --- a/Tests/Directory.Build.props +++ b/Tests/Directory.Build.props @@ -4,7 +4,6 @@ False - - + \ No newline at end of file diff --git a/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs index be3028c..987b111 100644 --- a/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs @@ -1,16 +1,19 @@ using System.Collections.Generic; using System.Linq; -using NUnit.Framework; using FluentAssertions; +using Xunit; + using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties; using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; using System.Diagnostics.CodeAnalysis; +using Moq; +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Tests.Loaders { - [TestFixture] public class MaterialLibraryLoaderTests { private readonly MaterialLibrarySpy _materialLibrarySpy; @@ -24,10 +27,10 @@ public class MaterialLibraryLoaderTests public MaterialLibraryLoaderTests() { _materialLibrarySpy = new MaterialLibrarySpy(); - _materialLibraryLoader = new MaterialLibraryLoader(null!); + _materialLibraryLoader = new MaterialLibraryLoader(_materialLibrarySpy, Mock.Of(), Mock.Of()); } - [Test] + [Fact] public void Adds_correct_materials() { LoadMaterial(); @@ -37,7 +40,7 @@ public void Adds_correct_materials() _secondMaterial.Name.Should().BeEquivalentTo("second_material"); } - [Test] + [Fact] public void Sets_correct_ambient_color() { LoadMaterial(); @@ -47,7 +50,7 @@ public void Sets_correct_ambient_color() _firstMaterial.AmbientColor.Z.Should().BeApproximately(3, Epsilon); } - [Test] + [Fact] public void Sets_correct_diffuse_color() { LoadMaterial(); @@ -57,7 +60,7 @@ public void Sets_correct_diffuse_color() _firstMaterial.DiffuseColor.Z.Should().BeApproximately(0.0314f, Epsilon); } - [Test] + [Fact] public void Sets_correct_specular() { LoadMaterial(); @@ -69,7 +72,7 @@ public void Sets_correct_specular() _firstMaterial.SpecularCoefficient.Should().BeApproximately(32, Epsilon); } - [Test] + [Fact] public void Sets_correct_transparency() { LoadMaterial(); @@ -77,7 +80,7 @@ public void Sets_correct_transparency() _firstMaterial.Transparency.Should().BeApproximately(0.5f, Epsilon); } - [Test] + [Fact] public void Sets_correct_illumination_model() { LoadMaterial(); @@ -85,7 +88,7 @@ public void Sets_correct_illumination_model() _firstMaterial.IlluminationModel.Should().Be(2); } - [Test] + [Fact] public void Sets_correct_texure_maps() { LoadMaterial(); @@ -126,7 +129,7 @@ disp lenna_disp.tga decal lenna_stencil.tga newmtl second_material"; - private class MaterialLibrarySpy : IMaterialLibrary + private class MaterialLibrarySpy : IMaterialDataStore { public List Materials { get; private set; } @@ -135,10 +138,12 @@ public MaterialLibrarySpy() Materials = new List(); } - public void Push(Material material) + public void AddMaterial(Material material) { Materials.Add(material); } + + public void SetMaterial(string materialName) => throw new System.NotImplementedException(); } } } \ No newline at end of file diff --git a/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs b/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs index 56e9765..9cba209 100644 --- a/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs @@ -1,69 +1,59 @@ using System.IO; using System.Text; using System.Linq; -using NUnit.Framework; + using FluentAssertions; +using Xunit; using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.ObjLoader.Loader.TypeParsers; using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; using CoreObjLoader = SharpEngine.Core.ObjLoader.Loaders.ObjLoader.ObjLoader; +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using Moq; +using System.IO.Abstractions; namespace SharpEngine.Core.ObjLoader.Tests.Loaders { - [TestFixture] public class ObjLoaderTests { - private class MaterialStreamProviderSpy : IMaterialStreamProvider - { - public string RequestedMaterialFilePath { get; private set; } - public Stream StreamToReturn { get; set; } - - public Stream Open(string materialFilePath) - { - RequestedMaterialFilePath = materialFilePath; - return StreamToReturn; - } - } - - private CoreObjLoader _loader; + private readonly CoreObjLoader _loader; private Mesh _loadResult; - private DataStore _textureDataStore; - private FaceParser _faceParser; - private GroupParser _groupParser; - private NormalParser _normalParser; - private TextureParser _textureParser; - private VertexParser _vertexParser; - private MaterialLibraryLoader _materialLibraryLoader; - private MaterialLibraryParser _materialLibraryParser; - private UseMaterialParser _useMaterialParser; - private MaterialLibraryLoaderFacade _materialLibraryLoaderFacade; - private MaterialStreamProviderSpy _materialStreamProviderSpy; - - [SetUp] - public void SetUp() + private readonly DataStore _textureDataStore; + private readonly FaceParser _faceParser; + private readonly GroupParser _groupParser; + private readonly NormalParser _normalParser; + private readonly TextureParser _textureParser; + private readonly VertexParser _vertexParser; + private readonly MaterialLibraryLoader _materialLibraryLoader; + private readonly MaterialLibraryParser _materialLibraryParser; + private readonly UseMaterialParser _useMaterialParser; + private readonly MaterialLibraryLoaderFacade _materialLibraryLoaderFacade; + + public ObjLoaderTests() { _textureDataStore = new DataStore(); + var fileStreamFactory = Mock.Of(); + var fileManager = Mock.Of(); + _faceParser = new FaceParser(_textureDataStore); _groupParser = new GroupParser(_textureDataStore); _normalParser = new NormalParser(_textureDataStore); _textureParser = new TextureParser(_textureDataStore); _vertexParser = new VertexParser(_textureDataStore); - _materialStreamProviderSpy = new MaterialStreamProviderSpy(); - _materialStreamProviderSpy.StreamToReturn = CreateMemoryStreamFromString(MaterialLibraryString); - _materialLibraryLoader = new MaterialLibraryLoader(_textureDataStore); + _materialLibraryLoader = new MaterialLibraryLoader(_textureDataStore, fileStreamFactory, fileManager); _materialLibraryLoaderFacade = new MaterialLibraryLoaderFacade(_materialLibraryLoader, ""); _materialLibraryParser = new MaterialLibraryParser(_materialLibraryLoaderFacade); _useMaterialParser = new UseMaterialParser(_textureDataStore); - _loader = new CoreObjLoader("", _textureDataStore); + _loader = new CoreObjLoader("", _textureDataStore, fileStreamFactory, fileManager); } - [Test] + [Fact] public void Loads_object_and_material_correctly() { Load(); @@ -73,8 +63,6 @@ public void Loads_object_and_material_correctly() _loadResult.Normals.Should().HaveCount(8); _loadResult.Materials.Should().HaveCount(1); - _materialStreamProviderSpy.RequestedMaterialFilePath.Should().BeEquivalentTo("cube.mtl"); - _loadResult.Groups.Should().HaveCount(1); var group = _loadResult.Groups.First(); @@ -82,15 +70,9 @@ public void Loads_object_and_material_correctly() group.Material!.Name.Should().BeEquivalentTo("cube_material"); } - [Test] + [Fact] public void Loads_object_correctly_when_material_is_not_found() { - _materialStreamProviderSpy.StreamToReturn = null!; - var materialLibraryLoaderFacade = new MaterialLibraryLoaderFacade(_materialLibraryLoader, ""); - var materialLibraryParser = new MaterialLibraryParser(_materialLibraryLoaderFacade); - - _loader = new CoreObjLoader("", _textureDataStore); - Load(); _loadResult.Vertices.Should().HaveCount(8); @@ -107,17 +89,9 @@ public void Loads_object_correctly_when_material_is_not_found() private void Load() { - var objectStream = CreateMemoryStreamFromString(ObjectFileString); - _loadResult = _loader.Load(null!).First(); } - private Stream CreateMemoryStreamFromString(string str) - { - var data = Encoding.ASCII.GetBytes(str); - return new MemoryStream(data); - } - private const string ObjectFileString = "#Comment\r\n" + "mtllib\tcube.mtl\r\n" + @@ -185,8 +159,4 @@ map_bump lenna_bump.tga disp lenna_disp.tga decal lenna_stencil.tga"; } - - internal interface IMaterialStreamProvider - { - } } \ No newline at end of file diff --git a/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj b/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj index 6a619d7..3a8519e 100644 --- a/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj +++ b/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj @@ -6,10 +6,11 @@ - - - - + + + + + diff --git a/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs index df5b03a..05636a0 100644 --- a/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; @@ -7,21 +7,18 @@ namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class FaceParserTests { - private FaceGroupSpy _faceGroupSpy; - private FaceParser _faceParser; + private readonly FaceGroupSpy _faceGroupSpy; + private readonly FaceParser _faceParser; - [SetUp] - public void SetUp() + public FaceParserTests() { _faceGroupSpy = new FaceGroupSpy(); - _faceParser = new FaceParser(_faceGroupSpy); } - [Test] + [Fact] public void CanParse_returns_true_on_face_line() { const string groupKeyword = "f"; @@ -30,7 +27,7 @@ public void CanParse_returns_true_on_face_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_normal_line() { const string invalidKeyword = "vt"; @@ -39,7 +36,7 @@ public void CanParse_returns_false_on_non_normal_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_normal_line_correctly_1() { const string faceLine = "1 2 3"; @@ -60,7 +57,7 @@ public void Parses_normal_line_correctly_1() parsedFace[2].NormalIndex.Should().Be(0); } - [Test] + [Fact] public void Parses_normal_line_correctly_2() { const string faceLine = "3/1 4/2 5/3"; @@ -83,7 +80,7 @@ public void Parses_normal_line_correctly_2() parsedFace[2].NormalIndex.Should().Be(0); } - [Test] + [Fact] public void Parses_normal_line_correctly_3() { const string faceLine = "6/4/1 3/5/3 7/6/5"; @@ -106,7 +103,7 @@ public void Parses_normal_line_correctly_3() parsedFace[2].NormalIndex.Should().Be(5); } - [Test] + [Fact] public void Parses_normal_line_correctly_4() { const string faceLine = "6//1 3//3 7//5"; diff --git a/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs index 061dd2d..1e38c77 100644 --- a/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs @@ -1,12 +1,11 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.ObjLoader.Loader.TypeParsers; namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class GroupParserTests { private readonly GroupDataStoreMock _groupDataStoreMock; @@ -18,7 +17,7 @@ public GroupParserTests() _groupParser = new GroupParser(_groupDataStoreMock); } - [Test] + [Fact] public void CanParse_returns_true_on_normal_line() { const string groupKeyword = "g"; @@ -27,7 +26,7 @@ public void CanParse_returns_true_on_normal_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_normal_line() { const string invalidKeyword = "vt"; @@ -36,7 +35,7 @@ public void CanParse_returns_false_on_non_normal_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_normal_line_correctly() { const string normalLine = "test group"; diff --git a/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs index 3c974a6..e1771b6 100644 --- a/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs @@ -1,11 +1,11 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; + using SharpEngine.Core.ObjLoader.Loader.TypeParsers; using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class MaterialLibraryParserTests { private readonly MaterialLibraryLoaderFacadeSpy _materialLibraryLoaderFacadeSpy; @@ -17,7 +17,7 @@ public MaterialLibraryParserTests() _parser = new MaterialLibraryParser(_materialLibraryLoaderFacadeSpy); } - [Test] + [Fact] public void CanParse_returns_true_on_mtlib_line() { const string groupKeyword = "mtllib"; @@ -26,7 +26,7 @@ public void CanParse_returns_true_on_mtlib_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_mtlib_line() { const string invalidKeyword = "vt"; @@ -35,7 +35,7 @@ public void CanParse_returns_false_on_non_mtlib_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_mtlib_line_correctly() { const string faceLine = "cube.mtl"; diff --git a/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs index f047eb5..15bce84 100644 --- a/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; @@ -7,7 +7,6 @@ namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class NormalParserTests { private readonly NormalParser _normalParser; @@ -19,7 +18,7 @@ public NormalParserTests() _normalParser = new NormalParser(_normalDataStoreMock); } - [Test] + [Fact] public void CanParse_returns_true_on_normal_line() { const string normalLine = "vn"; @@ -28,7 +27,7 @@ public void CanParse_returns_true_on_normal_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_normal_line() { const string invalidNormal = "vt"; @@ -37,7 +36,7 @@ public void CanParse_returns_false_on_non_normal_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_normal_line_correctly() { const string normalLine = "1.000000 2.000000 -1.000000"; diff --git a/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs index 4b9ffd9..36402df 100644 --- a/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; @@ -7,7 +7,6 @@ namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class TextureParserTests { private readonly TextureDataStoreMock _textureDataStoreMock; @@ -20,7 +19,7 @@ public TextureParserTests() _textureParser = new TextureParser(_textureDataStoreMock); } - [Test] + [Fact] public void CanParse_returns_true_on_normal_line() { const string textureKeyword = "vt"; @@ -29,7 +28,7 @@ public void CanParse_returns_true_on_normal_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_normal_line() { const string invalidKeyword = "vn"; @@ -38,7 +37,7 @@ public void CanParse_returns_false_on_non_normal_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_normal_line_correctly() { const string textureLine = "0.500 -1.352"; diff --git a/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs index 29cbaa8..5b72637 100644 --- a/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs @@ -1,10 +1,13 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; + using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.Components.Properties; + +using System; namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class UseMaterialParserTests { private readonly ElementGroupSpy _elementGroupSpy; @@ -16,7 +19,7 @@ public UseMaterialParserTests() _parser = new UseMaterialParser(_elementGroupSpy); } - [Test] + [Fact] public void CanParse_returns_true_on_usemtl_line() { const string groupKeyword = "usemtl"; @@ -25,7 +28,7 @@ public void CanParse_returns_true_on_usemtl_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void CanParse_returns_false_on_non_usemtl_line() { const string invalidKeyword = "vt"; @@ -34,7 +37,7 @@ public void CanParse_returns_false_on_non_usemtl_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void Parses_usemtl_line_correctly_1() { const string useMtlLine = "materialName"; @@ -47,6 +50,9 @@ private class ElementGroupSpy : IMaterialDataStore { public string MaterialName { get; private set; } + public void AddMaterial(Material currentMaterial) + => throw new NotImplementedException(); + public void SetMaterial(string materialName) { MaterialName = materialName; diff --git a/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs index 8009a66..dfa5af7 100644 --- a/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs @@ -1,5 +1,5 @@ using FluentAssertions; -using NUnit.Framework; +using Xunit; using SharpEngine.Core.Components.ObjLoader.DataStore; using SharpEngine.Core.Components.Properties.Meshes.MeshData; @@ -7,7 +7,6 @@ namespace SharpEngine.Core.ObjLoader.Tests.TypeParsers { - [TestFixture] public class VertexParserTests { private readonly VertexDataStoreMock _vertexDataStoreMock; @@ -19,7 +18,7 @@ public VertexParserTests() _vertexParser = new VertexParser(_vertexDataStoreMock); } - [Test] + [Fact] public void CanParse_returns_false_on_non_vertex_line() { const string invalidVertex = "vt"; @@ -28,7 +27,7 @@ public void CanParse_returns_false_on_non_vertex_line() canParse.Should().BeFalse(); } - [Test] + [Fact] public void CanParse_returns_true_on_vertex_line() { const string vertexLine = "v"; @@ -37,7 +36,7 @@ public void CanParse_returns_true_on_vertex_line() canParse.Should().BeTrue(); } - [Test] + [Fact] public void Parses_vertex_line_correctly() { const string vertexLine = "0.123 0.234 0.345"; diff --git a/Tests/SharpEngine.Core.Components.Tests/Class1.cs b/Tests/SharpEngine.Core.Components.Tests/Class1.cs new file mode 100644 index 0000000..cab4a8c --- /dev/null +++ b/Tests/SharpEngine.Core.Components.Tests/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Core.Components.Tests; + +public class Class1 +{ + +} diff --git a/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj b/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj new file mode 100644 index 0000000..a15a71c --- /dev/null +++ b/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/Tests/SharpEngine.Core.Numerics.Tests/Class1.cs b/Tests/SharpEngine.Core.Numerics.Tests/Class1.cs new file mode 100644 index 0000000..580dc2a --- /dev/null +++ b/Tests/SharpEngine.Core.Numerics.Tests/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Core.Numerics.Tests; + +public class Class1 +{ + +} diff --git a/Tests/SharpEngine.Core.Numerics.Tests/SharpEngine.Core.Numerics.Tests.csproj b/Tests/SharpEngine.Core.Numerics.Tests/SharpEngine.Core.Numerics.Tests.csproj new file mode 100644 index 0000000..547dedb --- /dev/null +++ b/Tests/SharpEngine.Core.Numerics.Tests/SharpEngine.Core.Numerics.Tests.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/Tests/SharpEngine.Core.Tests/Class1.cs b/Tests/SharpEngine.Core.Tests/Class1.cs deleted file mode 100644 index ff34b98..0000000 --- a/Tests/SharpEngine.Core.Tests/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SharpEngine.Core.Tests; - -public class Class1 -{ - -} diff --git a/Tests/SharpEngine.Core.Tests/Extensions/AssemblyExtensionsTests.cs b/Tests/SharpEngine.Core.Tests/Extensions/AssemblyExtensionsTests.cs new file mode 100644 index 0000000..a58a131 --- /dev/null +++ b/Tests/SharpEngine.Core.Tests/Extensions/AssemblyExtensionsTests.cs @@ -0,0 +1,40 @@ +using SharpEngine.Core.Extensions; +using System.Reflection; +using Xunit; + +namespace SharpEngine.Core.Tests.Extensions; + +public class AssemblyExtensionsTests +{ + private class AssemblyStub : Assembly + { + public Version Version { get; } + public AssemblyStub(Version version) + { + Version = version; + } + + public override AssemblyName GetName() + { + return new AssemblyName("TestAssembly") + { + Version = Version + }; + } + } + + [Fact] + public void GetVersion_Returns_Correct_Version() + { + var assemblyVersion = new Version(1, 2, 3, 4); + + var assembly = new AssemblyStub(assemblyVersion); + var version = assembly.GetVersion(); + + Assert.NotNull(version); + Assert.Equal(1, version.Major); + Assert.Equal(2, version.Minor); + Assert.Equal(3, version.Build); + Assert.Equal(4, version.Revision); + } +} diff --git a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj index bbcf245..a3a2555 100644 --- a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj +++ b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj @@ -6,4 +6,29 @@ enable + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj b/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj index fa71b7a..bbcf245 100644 --- a/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj +++ b/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj @@ -1,7 +1,7 @@  - net8.0 + $(DotNetTargetFramework) enable enable From 77907ce94acf9e9b5a69b9fa485e97e84587d44a Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 19:41:42 +0300 Subject: [PATCH 05/10] #67 Shader Improvements (#136) * #109 Add initial unit tests * #109 Audio player updates Added pause, resume and loading new audios on top of an existing player * #109 Fix windowing * #109 Update scene calls * #67 Shader improvements (WIP) * #67 TEmp * #67 ShaderProgram inheritance * #67 Progress * #67 Remove duplicates and cleanup * #67 Solution builds --------- Co-authored-by: Antti Veikkolainen --- Examples/MinecraftClone/Block/BlockBase.cs | 2 +- Examples/MinecraftClone/Minecraft.cs | 24 +- .../Tutorial 4.1 - Model Loading/Program.cs | 2 +- .../MaterialLoader/MaterialLibraryLoader.cs | 18 +- .../Properties/Material.cs | 119 +++--- .../Properties/Meshes/Mesh.cs | 37 ++ .../Properties/Meshes/Model.cs | 101 +++-- .../Properties/Shaders/IShader.cs | 50 +++ .../Properties/Shaders/Shader.cs | 41 +- .../Properties/Shaders/ShaderExtensions.cs | 202 ++-------- .../Properties/Shaders/ShaderProgram.cs | 238 ++++++++++++ .../Properties/Transform2D.cs | 5 +- .../SharpEngine.Core.Components.csproj | 7 + SharpEngine.Core/Entities/GameObject.cs | 165 ++------ .../Entities/Lights/DirectionalLight.cs | 2 +- SharpEngine.Core/Entities/Lights/Light.cs | 2 +- .../Entities/Lights/PointLight.cs | 7 +- SharpEngine.Core/Entities/Lights/SpotLight.cs | 5 +- SharpEngine.Core/Entities/UI/MeshRenderer.cs | 25 ++ SharpEngine.Core/Entities/UI/UIElement.cs | 152 ++------ SharpEngine.Core/Primitives/Plane.cs | 38 +- .../Primitives/PrimitiveFactory.cs | 5 +- SharpEngine.Core/Renderers/Renderer.cs | 13 +- .../Rendering/ParameterDescriptor.cs | 18 + SharpEngine.Core/Rendering/ShaderParameter.cs | 37 ++ .../Rendering/ShaderParameterBinder.cs | 188 +++++++++ .../Rendering/ShaderParameterType.cs | 14 + .../Rendering/ShaderUpdateFrequency.cs | 9 + SharpEngine.Core/Scenes/Scene.cs | 4 +- SharpEngine.Core/Shaders/DefaultShader.cs | 10 + SharpEngine.Core/Shaders/LampShader.cs | 20 +- SharpEngine.Core/Shaders/LightingShader.cs | 16 +- SharpEngine.Core/Shaders/ShaderBase.cs | 35 -- .../Shaders/ShaderParameterAttribute.cs | 19 + SharpEngine.Core/Shaders/ShaderService.cs | 7 +- SharpEngine.Core/Shaders/UIShader.cs | 17 +- SharpEngine.Core/Windowing/SilkWindow.cs | 2 +- SharpEngine.Core/Windowing/Window.cs | 6 +- .../Windowing/WindowExtensions.cs | 12 + .../Loaders/MaterialLibraryLoaderTests.cs | 4 +- .../Meshes/ModelTests.cs | 98 +++++ .../SharpEngine.Core.Components.Tests.csproj | 11 +- .../Shaders/ShaderParameterBinderTests.cs | 360 ++++++++++++++++++ .../SharpEngine.Core.Tests.csproj | 3 +- 44 files changed, 1402 insertions(+), 748 deletions(-) create mode 100644 SharpEngine.Core.Components/Properties/Shaders/IShader.cs create mode 100644 SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs create mode 100644 SharpEngine.Core/Entities/UI/MeshRenderer.cs create mode 100644 SharpEngine.Core/Rendering/ParameterDescriptor.cs create mode 100644 SharpEngine.Core/Rendering/ShaderParameter.cs create mode 100644 SharpEngine.Core/Rendering/ShaderParameterBinder.cs create mode 100644 SharpEngine.Core/Rendering/ShaderParameterType.cs create mode 100644 SharpEngine.Core/Rendering/ShaderUpdateFrequency.cs create mode 100644 SharpEngine.Core/Shaders/DefaultShader.cs delete mode 100644 SharpEngine.Core/Shaders/ShaderBase.cs create mode 100644 SharpEngine.Core/Shaders/ShaderParameterAttribute.cs create mode 100644 SharpEngine.Core/Windowing/WindowExtensions.cs create mode 100644 Tests/SharpEngine.Core.Components.Tests/Meshes/ModelTests.cs create mode 100644 Tests/SharpEngine.Core.Tests/Shaders/ShaderParameterBinderTests.cs diff --git a/Examples/MinecraftClone/Block/BlockBase.cs b/Examples/MinecraftClone/Block/BlockBase.cs index 1c62d38..e10a295 100644 --- a/Examples/MinecraftClone/Block/BlockBase.cs +++ b/Examples/MinecraftClone/Block/BlockBase.cs @@ -42,7 +42,7 @@ protected BlockBase(Vector3 position, string name, string diffuseMapFile, string private void Initialize(Vector3 position, string name, string diffuseMapFile, string specularMapFile, string vertShaderFile, string fragShaderFile) { - var cube = PrimitiveFactory.Create(PrimitiveType.Cube, position, diffuseMapFile, specularMapFile, vertShaderFile, fragShaderFile); + var cube = PrimitiveFactory.Create(PrimitiveType.Cube, (SharpEngine.Core.Numerics.Vector3)position, diffuseMapFile, specularMapFile, vertShaderFile, fragShaderFile); AssignProperties(cube, name); } diff --git a/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs index 1b347e2..6819f78 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -75,21 +75,21 @@ public override void Initialize() _lightsNode = _scene.Root.AddChild("lights"); _blocksNode = _scene.Root.AddChild("blocks"); - var gridLayout = new GridLayout(); + // var gridLayout = new GridLayout(); // TODO: #89 Fix UI renderer - _uiElem = new UIElement("uiElement"); + _uiElem = new UIElement(Window.GetGL(), "uiElement"); _scene.UIElements.Add(_uiElem); - var uiElem2 = new UIElement("uiElement"); + var uiElem2 = new UIElement(Window.GetGL(), "uiElement"); uiElem2.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(0.2f, 0.2f); uiElem2.Transform.Position = new SharpEngine.Core.Numerics.Vector2(30, 0); - gridLayout.AddChild(_uiElem, uiElem2); + // gridLayout.AddChild(_uiElem, uiElem2); _scene.UIElements.Add(_uiElem); _scene.UIElements.Add(uiElem2); - _scene.UIElements.Add(gridLayout); + // _scene.UIElements.Add(gridLayout); InitializeWorld(); } @@ -172,17 +172,17 @@ private void InitializeLights() _lightsNode.AddChild(new DirectionalLight()); _lightsNode.AddChild( - new PointLight(new Vector3(0.7f, 0.2f, 2.0f), 0), - new PointLight(new Vector3(2.3f, -3.3f, -4.0f), 1), - new PointLight(new Vector3(-4.0f, 2.0f, -12.0f), 2), - new PointLight(new Vector3(0.0f, 0.0f, -3.0f), 3) + new PointLight(new SharpEngine.Core.Numerics.Vector3(0.7f, 0.2f, 2.0f), 0), + new PointLight(new SharpEngine.Core.Numerics.Vector3(2.3f, -3.3f, -4.0f), 1), + new PointLight(new SharpEngine.Core.Numerics.Vector3(-4.0f, 2.0f, -12.0f), 2), + new PointLight(new SharpEngine.Core.Numerics.Vector3(0.0f, 0.0f, -3.0f), 3) ); _lightsNode.AddChild(new SpotLight() { - Ambient = new Vector3(0.0f, 0.0f, 0.0f), - Diffuse = new Vector3(1.0f, 1.0f, 1.0f), - Specular = new Vector3(1.0f, 1.0f, 1.0f), + Ambient = new SharpEngine.Core.Numerics.Vector3(0.0f, 0.0f, 0.0f), + Diffuse = new SharpEngine.Core.Numerics.Vector3(1.0f, 1.0f, 1.0f), + Specular = new SharpEngine.Core.Numerics.Vector3(1.0f, 1.0f, 1.0f), }); } diff --git a/Examples/Tutorial 4.1 - Model Loading/Program.cs b/Examples/Tutorial 4.1 - Model Loading/Program.cs index 742d903..a3e0cad 100644 --- a/Examples/Tutorial 4.1 - Model Loading/Program.cs +++ b/Examples/Tutorial 4.1 - Model Loading/Program.cs @@ -82,7 +82,7 @@ private static void OnLoad() Gl = GL.GetApi(window); - Shader = new Shader(Gl, PathExtensions.GetAssemblyPath("shader2.vert"), Default.LightShader, "test").Initialize(); + Shader = new Shader(Gl, PathExtensions.GetAssemblyPath("shader2.vert"), Default.LightShader, "test"); Texture = new Texture(Gl, "silk.png"); var model = ObjLoaderFactory.Load(Gl, "Untitled2.obj"); diff --git a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs index e2dcb91..9861031 100644 --- a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs +++ b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs @@ -54,20 +54,20 @@ public MaterialLibraryLoader(IMaterialDataStore dataStore, IFileStreamFactory fi AddParseAction("illum", i => _currentMaterial.IlluminationModel = i.ParseInvariantInt()); - AddParseAction("map_Ka", m => _currentMaterial.AmbientTextureMap = m); - AddParseAction("map_Kd", m => _currentMaterial.DiffuseTextureMap = m); + AddParseAction("map_Ka", m => _currentMaterial.AmbientTextureMap = new TextureDto(m)); + AddParseAction("map_Kd", m => _currentMaterial.DiffuseMap = new TextureDto(m)); - AddParseAction("map_Ks", m => _currentMaterial.SpecularTextureMap = m); - AddParseAction("map_Ns", m => _currentMaterial.SpecularHighlightTextureMap = m); + AddParseAction("map_Ks", m => _currentMaterial.SpecularMap = new TextureDto(m)); + AddParseAction("map_Ns", m => _currentMaterial.SpecularHighlightTextureMap = new TextureDto(m)); - AddParseAction("map_d", m => _currentMaterial.AlphaTextureMap = m); + AddParseAction("map_d", m => _currentMaterial.AlphaTextureMap = new TextureDto(m)); - AddParseAction("map_bump", m => _currentMaterial.BumpMap = m); - AddParseAction("bump", m => _currentMaterial.BumpMap = m); + AddParseAction("map_bump", m => _currentMaterial.BumpMap = new TextureDto(m)); + AddParseAction("bump", m => _currentMaterial.BumpMap = new TextureDto(m)); - AddParseAction("disp", m => _currentMaterial.DisplacementMap = m); + AddParseAction("disp", m => _currentMaterial.DisplacementMap = new TextureDto(m)); - AddParseAction("decal", m => _currentMaterial.StencilDecalMap = m); + AddParseAction("decal", m => _currentMaterial.StencilDecalMap = new TextureDto(m)); } private void AddParseAction(string key, Action action) diff --git a/SharpEngine.Core.Components/Properties/Material.cs b/SharpEngine.Core.Components/Properties/Material.cs index 5d01401..3028aa2 100644 --- a/SharpEngine.Core.Components/Properties/Material.cs +++ b/SharpEngine.Core.Components/Properties/Material.cs @@ -5,58 +5,68 @@ namespace SharpEngine.Core.Components.Properties; +public record TextureDto +{ + public TextureDto(string path, Texture? texture = null) + { + Path = path; + Texture = texture; + } + + public string Path { get; set; } + public Texture? Texture { get; set; } +} + +public enum TextureUnitIndex : int +{ + /// The texture unit for the diffuse map. + DIFFUSE_UNIT = 0, + + /// The texture unit for the specular map. + SPECULAR_UNIT = 1 +} + /// /// Represents the material rendered onto a game object. /// -public class Material : ICloneable, IEquatable +public class Material { + /// + /// Initializes a new instance of . + /// + /// The name assigned to the new material. + public Material(string materialName) + : this(materialName, diffuseMap: null, specularMap: null) { } + /// /// Initializes a new instance of . /// /// The name assigned to the new material. /// The diffuse map texture of the material. /// The specular map texture of the material. Defaults to the diffuse map if not provided. - public Material(string materialName, Texture? diffuseMap = null, Texture? specularMap = null) : this(materialName) + public Material(string materialName, Texture? diffuseMap = null, Texture? specularMap = null) { - DiffuseMap = diffuseMap; - SpecularMap = specularMap ?? diffuseMap; + DiffuseMap = new TextureDto(diffuseMap?.Path ?? string.Empty, diffuseMap); + SpecularMap = new TextureDto(specularMap?.Path ?? string.Empty, specularMap); Specular = new(0.5f, 0.5f, 0.5f); Shininess = 32.0f; - } - /// - /// Initializes a new instance of . - /// - /// The name assigned to the new material. - public Material(string materialName) - { Name = materialName; } + // Resolve dependency issue with GL to fix this. + // public Shader Shader { get; init; } + public Shader Shader { get; set; } + /// Gets or sets the name of the material. public string Name { get; set; } /// Gets or sets the diffuse map texture. - public Texture? DiffuseMap { get; set; } - - /// Gets or sets the path to the diffuse texture map. - public string? DiffuseTextureMap { get; set; } + public TextureDto? DiffuseMap { get; set; } /// Gets or sets the specular map texture. - public Texture? SpecularMap { get; set; } - - /// Gets or sets the path to the specular texture map. - public string? SpecularTextureMap { get; set; } - - /// Gets a value indicating whether the material uses a specular map. - public bool UseSpecularMap => SpecularMap is not null && SpecularMap?.Handle != DiffuseMap?.Handle; - - /// The texture unit for the diffuse map. - public const int DIFFUSE_UNIT = 0; - - /// The texture unit for the specular map. - public const int SPECULAR_UNIT = 1; + public TextureDto? SpecularMap { get; set; } /// Gets or sets the specular color of the material. public Vector3 Specular { get; set; } @@ -83,22 +93,22 @@ public Material(string materialName) public int IlluminationModel { get; set; } /// Gets or sets the path to the ambient texture map. - public string? AmbientTextureMap { get; set; } + public TextureDto? AmbientTextureMap { get; set; } /// Gets or sets the path to the specular highlight texture map. - public string? SpecularHighlightTextureMap { get; set; } + public TextureDto? SpecularHighlightTextureMap { get; set; } /// Gets or sets the path to the bump map. - public string? BumpMap { get; set; } + public TextureDto? BumpMap { get; set; } /// Gets or sets the path to the displacement map. - public string? DisplacementMap { get; set; } + public TextureDto? DisplacementMap { get; set; } /// Gets or sets the path to the stencil decal map. - public string? StencilDecalMap { get; set; } + public TextureDto? StencilDecalMap { get; set; } /// Gets or sets the path to the alpha texture map. - public string? AlphaTextureMap { get; set; } + public TextureDto? AlphaTextureMap { get; set; } /// /// Sets the uniform values for the material in the specified shader. @@ -110,48 +120,19 @@ public void SetUniformValues(Shader shader) if (DiffuseMap is not null) { - DiffuseMap.Use(TextureUnit.Texture0); - shader.SetInt("material.diffuse", DIFFUSE_UNIT); + DiffuseMap.Texture?.Use(TextureUnit.Texture0); + shader.SetTextureUnit("material.diffuse", TextureUnitIndex.DIFFUSE_UNIT); } if (SpecularMap is not null) { - SpecularMap.Use(TextureUnit.Texture1); - shader.SetInt("material.specular", SPECULAR_UNIT); - shader.SetFloat("material.shininess", Shininess); + SpecularMap.Texture?.Use(TextureUnit.Texture1); + shader.SetTextureUnit("material.specular", TextureUnitIndex.SPECULAR_UNIT); } else - { - shader.SetInt("material.specular", DIFFUSE_UNIT); - shader.SetFloat("material.shininess", 0); - } - } - - /// - public object Clone() - => MemberwiseClone(); - - /// - public override bool Equals(object? obj) - { - if (obj is null) - return false; + shader.SetTextureUnit("material.specular", TextureUnitIndex.DIFFUSE_UNIT); - if (ReferenceEquals(this, obj)) - return true; + shader.SetFloat("material.shininess", Shininess); - if (GetType() != obj.GetType()) - return false; - - return true; - } - - /// - public bool Equals(Material? other) => Equals(other); - - /// - public override int GetHashCode() - { - throw new NotImplementedException(); } } diff --git a/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs b/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs index bf63131..2d5a8c5 100644 --- a/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs +++ b/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs @@ -131,6 +131,20 @@ public Mesh(GL gl, float[] vertices, uint[] indices, List textures) SetupMesh(); } + /// + /// Initializes a new instance of the class with the specified OpenGL context, interleaved vertices, and indices. + /// + /// The OpenGL context. + /// The interleaved vertex data (position, normal, uv per vertex). + /// The indices of the mesh. + public Mesh(GL gl, float[] vertices, uint[] indices) + { + GL = gl; + Vertices = vertices; + Indices = indices; + SetupMesh(); + } + /// /// Initializes a new instance of the class with the specified OpenGL context. /// @@ -141,6 +155,21 @@ public Mesh(GL gl) SetupMesh(); } + /// + /// Disposes the existing GPU buffers and reinitializes them using the current and data. + /// + /// + /// Call this method after updating or on a mesh that was already + /// set up, for example after in-place processing via . + /// + public void ReinitializeGpuBuffers() + { + VAO.Dispose(); + VBO.Dispose(); + EBO.Dispose(); + SetupMesh(); + } + /// /// Allocates the required memory for the mesh and sets up the Vertex Array Object (VAO), Vertex Buffer Object (VBO), and Element Buffer Object (EBO). /// @@ -166,6 +195,14 @@ public void SetupMesh() public void Bind() => VAO.Bind(); + public void Draw() + { + if (Indices.Length > 0) + GL.DrawElements(PrimitiveType.Triangles, (uint)Indices.Length, DrawElementsType.UnsignedInt, []); + else + GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)(Vertices.Length / (VertexData.VerticesSize + VertexData.NormalsSize + VertexData.TexCoordsSize))); + } + /// protected virtual void Dispose(bool disposing) { diff --git a/SharpEngine.Core.Components/Properties/Meshes/Model.cs b/SharpEngine.Core.Components/Properties/Meshes/Model.cs index e5ba6b5..360b321 100644 --- a/SharpEngine.Core.Components/Properties/Meshes/Model.cs +++ b/SharpEngine.Core.Components/Properties/Meshes/Model.cs @@ -60,54 +60,43 @@ public void Dispose() } /// - /// Produces a GPU-ready Mesh from the provided mesh by cloning when already processed or by expanding vertices and constructing indices, materials, and textures. + /// Processes the provided mesh in place, updating its vertex data, materials, textures, and GPU buffers so it is ready for rendering. /// - /// Source mesh to process. Must not be null. + /// The mesh to process. Must not be null. /// - /// A Mesh prepared for rendering: either a cloned processed mesh if the input already contains vertex buffer - /// data, or a newly constructed Mesh with expanded vertices, generated index buffer, and associated textures - /// and materials. + /// The same instance with updated , , + /// , , and reinitialized GPU buffers. + /// When the mesh already carries GPU-ready vertex data ( is non-empty) its + /// materials and textures are refreshed in place; otherwise vertices are expanded from face groups and all + /// data properties are computed and assigned. /// - /// Thrown if the input mesh is null. + /// Thrown if is null. public Mesh ProcessMesh(Mesh mesh) { ArgumentNullException.ThrowIfNull(mesh); - // TODO: Instead of cloning the object, we should just update the values. if (mesh.Vertices.Length > 0) - return CloneProcessedMesh(mesh); + { + var existingMaterials = mesh.Materials.Select(CloneMaterial).ToList(); + var existingTextures = (existingMaterials.Count > 0 ? BuildTextures(existingMaterials) : CloneTextures(mesh.Textures)).ToList(); + mesh.Materials = existingMaterials; + mesh.Textures = existingTextures; + mesh.ReinitializeGpuBuffers(); + return mesh; + } var vertices = ExpandVertices(mesh); var indices = Enumerable.Range(0, vertices.Count).Select(index => (uint)index).ToArray(); var materials = BuildMaterials(mesh).ToList(); var textures = BuildTextures(materials).ToList(); - // TODO: Instead of cloning the object, we should just update the values. - return new Mesh(_gl, BuildVertices(vertices), indices, textures) - { - Name = mesh.Name, - Vertices2 = vertices, - Normals2 = mesh.Normals2, - TextureCoordinates2 = mesh.TextureCoordinates2, - Groups = mesh.Groups, - Materials = materials, - }; - } - - private Mesh CloneProcessedMesh(Mesh template) - { - var materials = template.Materials.Select(CloneMaterial).ToList(); - var textures = materials.Count > 0 ? BuildTextures(materials) : CloneTextures(template.Textures); - - return new Mesh(_gl, template.Vertices, template.Indices, textures.ToList()) - { - Name = template.Name, - Vertices2 = template.Vertices2, - Normals2 = template.Normals2, - TextureCoordinates2 = template.TextureCoordinates2, - Groups = template.Groups, - Materials = materials, - }; + mesh.Vertices = BuildVertices(vertices); + mesh.Indices = indices; + mesh.Textures = textures; + mesh.Vertices2 = vertices; + mesh.Materials = materials; + mesh.ReinitializeGpuBuffers(); + return mesh; } private static List ExpandVertices(Mesh mesh) @@ -187,14 +176,14 @@ private IEnumerable BuildMaterials(Mesh mesh) private Material? CreateRuntimeMaterial(Material definition) { - if (string.IsNullOrWhiteSpace(definition.DiffuseTextureMap)) + if (string.IsNullOrWhiteSpace(definition.DiffuseMap?.Path)) return null; - var diffuseTexture = new EngineTexture(_gl, ResolveAssetPath(definition.DiffuseTextureMap), EngineTextureType.Diffuse); + var diffuseTexture = new EngineTexture(_gl, ResolveAssetPath(definition.DiffuseMap.Path), EngineTextureType.Diffuse); EngineTexture? specularTexture = null; - if (!string.IsNullOrWhiteSpace(definition.SpecularTextureMap)) - specularTexture = new EngineTexture(_gl, ResolveAssetPath(definition.SpecularTextureMap), EngineTextureType.Specular); + if (!string.IsNullOrWhiteSpace(definition.SpecularMap?.Path)) + specularTexture = new EngineTexture(_gl, ResolveAssetPath(definition.SpecularMap.Path), EngineTextureType.Specular); return new Material(definition.Name, diffuseTexture, specularTexture) { @@ -205,8 +194,8 @@ private IEnumerable BuildMaterials(Mesh mesh) SpecularCoefficient = definition.SpecularCoefficient, Transparency = definition.Transparency, IlluminationModel = definition.IlluminationModel, - DiffuseTextureMap = definition.DiffuseTextureMap, - SpecularTextureMap = definition.SpecularTextureMap, + DiffuseMap = definition.DiffuseMap, + SpecularMap = definition.SpecularMap, AmbientTextureMap = definition.AmbientTextureMap, SpecularHighlightTextureMap = definition.SpecularHighlightTextureMap, AlphaTextureMap = definition.AlphaTextureMap, @@ -222,12 +211,12 @@ private IEnumerable CloneTextures(IReadOnlyList te private Material CloneMaterial(Material material) { EngineTexture? diffuseTexture = null; - if (material.DiffuseMap != null) - diffuseTexture = new EngineTexture(_gl, material.DiffuseMap.Path, material.DiffuseMap.Type); - + if (material.DiffuseMap?.Texture is not null) + diffuseTexture = new EngineTexture(_gl, material.DiffuseMap.Path, material.DiffuseMap.Texture.Type); + EngineTexture? specularTexture = null; - if (material.UseSpecularMap) - specularTexture = new EngineTexture(_gl, material!.SpecularMap!.Path, material.SpecularMap.Type); + if (material.SpecularMap?.Texture is not null) + specularTexture = new EngineTexture(_gl, material.SpecularMap.Path, material.SpecularMap.Texture.Type); return new Material(material.Name, diffuseTexture, specularTexture) { @@ -238,8 +227,8 @@ private Material CloneMaterial(Material material) SpecularCoefficient = material.SpecularCoefficient, Transparency = material.Transparency, IlluminationModel = material.IlluminationModel, - DiffuseTextureMap = material.DiffuseTextureMap, - SpecularTextureMap = material.SpecularTextureMap, + DiffuseMap = material.DiffuseMap, + SpecularMap = material.SpecularMap, AmbientTextureMap = material.AmbientTextureMap, SpecularHighlightTextureMap = material.SpecularHighlightTextureMap, AlphaTextureMap = material.AlphaTextureMap, @@ -260,11 +249,11 @@ private static EngineTexture[] GetMaterialEngineTextures(Material material) ArgumentNullException.ThrowIfNull(material); var materials = new List(); - if (material.UseSpecularMap) - materials.Add(material.SpecularMap!); - - if (material.DiffuseMap != null) - materials.Add(material.DiffuseMap); + if (material.SpecularMap?.Texture is not null) + materials.Add(material.SpecularMap.Texture!); + + if (material.DiffuseMap?.Texture is not null) + materials.Add(material.DiffuseMap.Texture!); return [.. materials]; } @@ -280,7 +269,13 @@ private string ResolveAssetPath(string assetPath) assetPath : System.IO.Path.Combine(directory, assetPath); } - private static float[] BuildVertices(IEnumerable vertexCollection) + /// + /// Converts a collection of objects into an interleaved float array + /// (position XYZ, normal XYZ, texture-coordinate UV per vertex) suitable for uploading to a GPU vertex buffer. + /// + /// The vertices to convert. + /// A flat array with eight elements per vertex. + internal static float[] BuildVertices(IEnumerable vertexCollection) { var vertices = new List(); diff --git a/SharpEngine.Core.Components/Properties/Shaders/IShader.cs b/SharpEngine.Core.Components/Properties/Shaders/IShader.cs new file mode 100644 index 0000000..3c34378 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Shaders/IShader.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace SharpEngine.Core.Shaders; + +/// +/// Defines the contract for setting shader uniform parameters and querying their locations. +/// +public interface IShader +{ + /// Gets the uniform name-to-location map cached at shader initialization. + IReadOnlyDictionary UniformLocations { get; } + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetInt(string name, int data); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetFloat(string name, float data); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// Whether to transpose the matrix before uploading. Defaults to . + /// if the uniform was found and set; otherwise . + bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetVector2(string name, Vector2 data); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetVector3(string name, Vector3 data); + + /// Sets a uniform on this shader. + /// The name of the uniform. + /// The value to set. + /// if the uniform was found and set; otherwise . + bool SetVector4(string name, Vector4 data); +} diff --git a/SharpEngine.Core.Components/Properties/Shaders/Shader.cs b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs index 06e8322..8bee87e 100644 --- a/SharpEngine.Core.Components/Properties/Shaders/Shader.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs @@ -7,11 +7,8 @@ namespace SharpEngine.Core.Shaders; /// /// Represents a shader program. /// -public partial class Shader : IDisposable +public partial class Shader : ShaderProgram, IShader, IDisposable { - /// Gets the handle to the shader program. - public uint Handle { get; private set; } - /// Gets or sets the identifying name of the shader. public string Name { get; set; } @@ -21,12 +18,6 @@ public partial class Shader : IDisposable /// Gets or sets the path to the fragment shader file. public string FragPath { get; set; } - private readonly GL _gl; - private readonly ILogger _logger; - - private Dictionary _uniformLocations = []; - private bool disposedValue; - /// /// Initializes a new instance of . /// @@ -38,13 +29,10 @@ public partial class Shader : IDisposable /// The vertex shader full path. /// The fragment shader full path. /// The identifier name of the shader. - public Shader(GL gl, string vertPath, string fragPath, string name) + public Shader(GL gl, string vertPath, string fragPath, string name) : base(gl) { - _gl = gl; Name = name; - _logger = LoggingExtensions.CreateLogger(); - // There are several different types of shaders, but the only two you need for basic rendering are the vertex and fragment shaders. // The vertex shader is responsible for moving around vertices, and uploading that data to the fragment shader. // The vertex shader won't be too important here, but they'll be more important later. @@ -59,30 +47,7 @@ public Shader(GL gl, string vertPath, string fragPath, string name) VertPath = vertPath; FragPath = fragPath; - } - /// - protected virtual void Dispose(bool disposing) - { - if (disposedValue) - return; - - if (disposing) - { - // TODO: Collect handles in a separate container with proper access to the shared GL context. - //_gl.DeleteProgram(Handle); - Handle = 0; - _uniformLocations.Clear(); - } - - disposedValue = true; - } - - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + Initialize(vertPath, fragPath); } } diff --git a/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs index f9fb638..eaa5390 100644 --- a/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using SharpEngine.Core.Components.Properties; using Silk.NET.OpenGL; using System.Numerics; using System.Text.RegularExpressions; @@ -10,177 +11,6 @@ namespace SharpEngine.Core.Shaders; /// public partial class Shader { - /// - /// Initializes and compiles vertex and fragment shaders into a shader program. - /// - /// The current instance after initialization. - public Shader Initialize() - { - // Load and compile shader - if (!LoadShader(ShaderType.VertexShader, VertPath, out uint vertexShader)) - { - _logger.LogError("Unable to load vertex shader."); - return this; - } - - if (!LoadShader(ShaderType.FragmentShader, FragPath, out uint fragmentShader)) - { - _logger.LogError("Unable to load fragment shader."); - return this; - } - - // These two shaders must then be merged into a shader program, which can then be used by OpenGL. - // To do this, create a program... - - Handle = _gl.CreateProgram(); - - // Attach both shaders... - _gl.AttachShader(Handle, vertexShader); - _gl.AttachShader(Handle, fragmentShader); - - // And then link them together. - bool shaderLinked = LinkProgram(Handle); - - // When the shader program is linked, it no longer needs the individual shaders attached to it; the compiled code is copied into the shader program. - // Detach them, and then delete them. - _gl.DetachShader(Handle, vertexShader); - _gl.DetachShader(Handle, fragmentShader); - _gl.DeleteShader(fragmentShader); - _gl.DeleteShader(vertexShader); - - if (!shaderLinked) - { - _logger.LogInformation("Unable to link shader program."); - return this; - } - - // The shader is now ready to go, but first, we're going to cache all the shader uniform locations. - // Querying this from the shader is very slow, so we do it once on initialization and reuse those values - // later. - SetUniformLocations(); - - return this; - } - - private bool LoadShader(ShaderType shaderType, string shaderPath, out uint shader) - { - if (!File.Exists(shaderPath)) - { - _logger.LogInformation("Shader file not found: {Path}", shaderPath); - - shader = 0; - return false; - } - - string shaderSource = File.ReadAllText(shaderPath); - shaderSource = ProcessIncludes(shaderSource, Path.GetDirectoryName(shaderPath)!); - - // GL.CreateShader will create an empty shader (obviously). The ShaderType enum denotes which type of shader will be created. - shader = _gl.CreateShader(shaderType); - _gl.ShaderSource(shader, shaderSource); - - if (!CompileShader(shader)) - { - _logger.LogInformation("Unable to load {Type} shader from '{Path}'.", shaderType, shaderPath); - return false; - } - - return true; - } - - private void SetUniformLocations() - { - // First, we have to get the number of active uniforms in the shader. - _gl.GetProgram(Handle, GLEnum.ActiveUniforms, out var numberOfUniforms); - - Dictionary uniformLocations = []; - - // Loop over all the uniforms, - for (uint i = 0; i < numberOfUniforms; i++) - { - // get the name of this uniform, - var key = _gl.GetActiveUniform(Handle, i, out _, out _); - - // get the location, - var location = _gl.GetUniformLocation(Handle, key); - - // and then add it to the dictionary. - uniformLocations.Add(key, location); - } - - _uniformLocations = uniformLocations; - } - - private static string ProcessIncludes(string shaderCode, string directory) - { - const string includePattern = @"#include\s+""(.+?)"""; - return Regex.Replace(shaderCode, includePattern, match => - { - string includePath = Path.Combine(directory, match.Groups[1].Value); - string includeCode = File.ReadAllText(includePath); - return ProcessIncludes(includeCode, Path.GetDirectoryName(includePath)!); - }, RegexOptions.NonBacktracking); - } - - private bool CompileShader(uint shader) - { - // Try to compile the shader - _gl.CompileShader(shader); - - // Check for compilation errors - _gl.GetShader(shader, GLEnum.CompileStatus, out var statusCode); - if (statusCode != (int)GLEnum.True) - { - // We can use `GL.GetShaderInfoLog(shader)` to get information about the error. - var infoLog = _gl.GetShaderInfoLog(shader); - _logger.LogError("Error occurred whilst compiling Shader({Shader}).\n\n{Log}", shader, infoLog); - - return false; - } - - return true; - } - - private bool LinkProgram(uint program) - { - _gl.LinkProgram(program); - _gl.GetProgram(program, GLEnum.LinkStatus, out var statusCode); - - if (statusCode != (int)GLEnum.True) - { - string infoLog = _gl.GetProgramInfoLog(program); - _logger.LogError("Error occurred whilst linking Program({Program}): {Info}", program, infoLog); - - return false; - } - - return true; - } - - /// - /// Enables the shader program. - /// - public void Use() - => _gl.UseProgram(Handle); - - /// - /// Checks if the shader attribute exists within the current shader. - /// - /// The name of the attribute that's being looked for. - /// Outputs the location of the attribute in the shader if found; otherwise -1. - /// If the attribute exists, ; otherwise, . - public bool TryGetAttribLocation(string attribName, out int location) - { - location = _gl.GetAttribLocation(Handle, attribName); - if (location == ShaderAttributes.AttributeLocationNotFound) - { - _logger.LogWarning("Attribute '{Attribute}' not found in shader program.", attribName); - return false; - } - - return true; - } - // Uniform setters // Uniforms are variables that can be set by user code, instead of reading them from the VBO. // You use VBOs for vertex-related data, and uniforms for almost everything else. @@ -198,8 +28,8 @@ private bool TrySetUniform(string uniformName, T data, Action setter) return false; } - // TODO: #95 The _gl.UseProgram should be not be called here. Rather the renderer should call it once before rendering. - _gl.UseProgram(Handle); + // TODO: #95 The GL.UseProgram should be not be called here. Rather the renderer should call it once before rendering. + GL.UseProgram(Handle); setter(location, data); return true; @@ -211,7 +41,10 @@ private bool TrySetUniform(string uniformName, T data, Action setter) /// The name of the uniform. /// The data to set. public bool SetInt(string name, int data) - => TrySetUniform(name, data, _gl.Uniform1); + => TrySetUniform(name, data, GL.Uniform1); + + public bool SetTextureUnit(string name, TextureUnitIndex textureUnit) + => TrySetUniform(name, (int)textureUnit, GL.Uniform1); /// /// Set a uniform float on this shader. @@ -219,7 +52,7 @@ public bool SetInt(string name, int data) /// The name of the uniform. /// The data to set. public bool SetFloat(string name, float data) - => TrySetUniform(name, data, _gl.Uniform1); + => TrySetUniform(name, data, GL.Uniform1); /// /// Set a uniform Matrix4 on this shader @@ -233,7 +66,15 @@ public bool SetFloat(string name, float data) /// /// public bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true) - => TrySetUniform(name, data, (uniform, d) => _gl.UniformMatrix4(uniform, transpose, d.ToSpan())); + => TrySetUniform(name, data, (uniform, d) => GL.UniformMatrix4(uniform, transpose, d.ToSpan())); + + /// + /// Set a uniform Vector2 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetVector2(string name, SharpEngine.Core.Numerics.Vector2 data) + => TrySetUniform(name, (Vector2)data, GL.Uniform2); /// /// Set a uniform Vector2 on this shader. @@ -241,7 +82,7 @@ public bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true) /// The name of the uniform. /// The data to set. public bool SetVector2(string name, Vector2 data) - => TrySetUniform(name, data, _gl.Uniform2); + => TrySetUniform(name, data, GL.Uniform2); /// /// Set a uniform Vector3 on this shader. @@ -249,7 +90,10 @@ public bool SetVector2(string name, Vector2 data) /// The name of the uniform. /// The data to set. public bool SetVector3(string name, Vector3 data) - => TrySetUniform(name, data, _gl.Uniform3); + => TrySetUniform(name, data, GL.Uniform3); + + public bool SetVector3(string name, SharpEngine.Core.Numerics.Vector3 data) + => TrySetUniform(name, (Vector3)data, GL.Uniform3); /// /// Set a uniform Vector3 on this shader. @@ -257,5 +101,5 @@ public bool SetVector3(string name, Vector3 data) /// The name of the uniform. /// The data to set. public bool SetVector4(string name, Vector4 data) - => TrySetUniform(name, data, _gl.Uniform4); + => TrySetUniform(name, data, GL.Uniform4); } diff --git a/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs new file mode 100644 index 0000000..2249dee --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderProgram.cs @@ -0,0 +1,238 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; +using Silk.NET.OpenGL; +using System.Text.RegularExpressions; + +namespace SharpEngine.Core.Shaders; + +public abstract class ShaderProgram : IDisposable +{ + protected readonly ILogger _logger; + + protected readonly GL GL; + + protected Dictionary _uniformLocations = []; + private bool disposedValue; + + /// Gets the handle to the shader program. + public uint Handle { get; protected set; } + + /// Gets or sets the vertex array object. + public uint Vao { get; set; } + + /// Gets the uniform name-to-location map cached at shader initialization. + public IReadOnlyDictionary UniformLocations => _uniformLocations; + + /// + /// Sets the attributes for the shader. + /// + /// + /// if the attributes were set successfully; otherwise . + /// + public virtual bool SetAttributes(GL gl) => true; + + protected ShaderProgram(GL gl) + { + GL = gl; + _logger = LoggingExtensions.CreateLogger(); + } + + /// + /// Initializes and compiles vertex and fragment shaders into a shader program. + /// + protected void Initialize(string vertPath, string fragPath) + { + // Load and compile shader + if (!LoadShader(ShaderType.VertexShader, vertPath, out uint vertexShader)) + { + _logger.LogError("Unable to load vertex shader."); + return; + } + + if (!LoadShader(ShaderType.FragmentShader, fragPath, out uint fragmentShader)) + { + _logger.LogError("Unable to load fragment shader."); + return; + } + // These two shaders must then be merged into a shader program, which can then be used by OpenGL. + // To do this, create a program... + + Handle = GL.CreateProgram(); + + // Attach both shaders... + GL.AttachShader(Handle, vertexShader); + GL.AttachShader(Handle, fragmentShader); + + // And then link them together. + bool shaderLinked = LinkProgram(Handle); + + // When the shader program is linked, it no longer needs the individual shaders attached to it; the compiled code is copied into the shader program. + // Detach them, and then delete them. + GL.DetachShader(Handle, vertexShader); + GL.DetachShader(Handle, fragmentShader); + GL.DeleteShader(fragmentShader); + GL.DeleteShader(vertexShader); + + if (!shaderLinked) + { + _logger.LogInformation("Unable to link shader program."); + return; + } + + // The shader is now ready to go, but first, we're going to cache all the shader uniform locations. + // Querying this from the shader is very slow, so we do it once on initialization and reuse those values + // later. + SetUniformLocations(Handle); + + Vao = GL.GenVertexArray(); + GL.BindVertexArray(Vao); + + SetAttributes(GL); + + return; + } + + private bool LoadShader(ShaderType shaderType, string shaderPath, out uint shaderProgram) + { + if (!File.Exists(shaderPath)) + { + _logger.LogInformation("Shader file not found: {Path}", shaderPath); + + shaderProgram = 0; + return false; + } + + string shaderSource = File.ReadAllText(shaderPath); + shaderSource = ProcessIncludes(shaderSource, Path.GetDirectoryName(shaderPath)!); + + // GL.CreateShader will create an empty shader (obviously). The ShaderType enum denotes which type of shader will be created. + shaderProgram = GL.CreateShader(shaderType); + GL.ShaderSource(shaderProgram, shaderSource); + + if (!CompileShader(shaderProgram)) + { + _logger.LogInformation("Unable to load {Type} shader from '{Path}'.", shaderType, shaderPath); + return false; + } + + return true; + } + + private void SetUniformLocations(uint shaderProgramHandle) + { + // First, we have to get the number of active uniforms in the shader. + GL.GetProgram(shaderProgramHandle, GLEnum.ActiveUniforms, out var numberOfUniforms); + + Dictionary uniformLocations = []; + + // Loop over all the uniforms, + for (uint i = 0; i < numberOfUniforms; i++) + { + // get the name of this uniform, + var key = GL.GetActiveUniform(shaderProgramHandle, i, out _, out _); + + // get the location, + var location = GL.GetUniformLocation(shaderProgramHandle, key); + + // and then add it to the dictionary. + uniformLocations.Add(key, location); + } + + _uniformLocations = uniformLocations; + } + + private static string ProcessIncludes(string shaderCode, string directory) + { + const string includePattern = @"#include\s+""(.+?)"""; + return Regex.Replace(shaderCode, includePattern, match => + { + string includePath = Path.Combine(directory, match.Groups[1].Value); + string includeCode = File.ReadAllText(includePath); + return ProcessIncludes(includeCode, Path.GetDirectoryName(includePath)!); + }, RegexOptions.NonBacktracking); + } + + private bool CompileShader(uint shaderProgram) + { + // Try to compile the shader + GL.CompileShader(shaderProgram); + + // Check for compilation errors + GL.GetShader(shaderProgram, GLEnum.CompileStatus, out var statusCode); + if (statusCode != (int)GLEnum.True) + { + // We can use `GL.GetShaderInfoLog(shader)` to get information about the error. + var infoLog = GL.GetShaderInfoLog(shaderProgram); + _logger.LogError("Error occurred whilst compiling Shader({Shader}).\n\n{Log}", shaderProgram, infoLog); + + return false; + } + + return true; + } + + private bool LinkProgram(uint program) + { + GL.LinkProgram(program); + GL.GetProgram(program, GLEnum.LinkStatus, out var statusCode); + + if (statusCode != (int)GLEnum.True) + { + string infoLog = GL.GetProgramInfoLog(program); + _logger.LogError("Error occurred whilst linking Program({Program}): {Info}", program, infoLog); + + return false; + } + + return true; + } + + /// + /// Enables the shader program. + /// + public void Use() + => GL.UseProgram(Handle); + + /// + /// Checks if the shader attribute exists within the current shader. + /// + /// The name of the attribute that's being looked for. + /// Outputs the location of the attribute in the shader if found; otherwise -1. + /// If the attribute exists, ; otherwise, . + public bool TryGetAttribLocation(string attribName, out int location) + { + location = GL.GetAttribLocation(Handle, attribName); + if (location == ShaderAttributes.AttributeLocationNotFound) + { + _logger.LogWarning("Attribute '{Attribute}' not found in shader program.", attribName); + return false; + } + + return true; + } + + /// + protected virtual void Dispose(bool disposing) + { + if (disposedValue) + return; + + if (disposing) + { + // TODO: Collect handles in a separate container with proper access to the shared GL context. + //_gl.DeleteProgram(Handle); + Handle = 0; + _uniformLocations.Clear(); + } + + disposedValue = true; + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} diff --git a/SharpEngine.Core.Components/Properties/Transform2D.cs b/SharpEngine.Core.Components/Properties/Transform2D.cs index 8a44d56..0809d3e 100644 --- a/SharpEngine.Core.Components/Properties/Transform2D.cs +++ b/SharpEngine.Core.Components/Properties/Transform2D.cs @@ -19,8 +19,11 @@ public class Transform2D : ITransform public Vector2 Scale { get; set; } = new(1, 1); /// - /// Gets or sets the rotation of the game object. + /// Gets or sets the quaternion rotation of the game object. /// + /// + /// To get the rotation angle in degrees, use . + /// public Quaternion Rotation { get; set; } = new() { Angle = 0 }; /// diff --git a/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj index f411581..99cc5f5 100644 --- a/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj +++ b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj @@ -14,6 +14,7 @@ + @@ -22,4 +23,10 @@ + + + <_Parameter1>SharpEngine.Core.Components.Tests + + + diff --git a/SharpEngine.Core/Entities/GameObject.cs b/SharpEngine.Core/Entities/GameObject.cs index 1b52fea..99b0a14 100644 --- a/SharpEngine.Core/Entities/GameObject.cs +++ b/SharpEngine.Core/Entities/GameObject.cs @@ -1,22 +1,22 @@ using SharpEngine.Core.Attributes; +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Entities.UI; using SharpEngine.Core.Entities.Views; -using SharpEngine.Core.Components.Properties; -using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Interfaces; using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; -using Shader = SharpEngine.Core.Shaders.Shader; -using EngineTexture = SharpEngine.Core.Components.Properties.Textures.Texture; - using Silk.NET.OpenGL; -using System.Threading.Tasks; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using EngineTexture = SharpEngine.Core.Components.Properties.Textures.Texture; +using Shader = SharpEngine.Core.Shaders.Shader; namespace SharpEngine.Core.Entities; @@ -25,55 +25,53 @@ namespace SharpEngine.Core.Entities; /// public class GameObject : EmptyNode, IRenderable { + // TODO: Remove these two. + protected record struct TempShaderDataContainer(string shaderVertPath, string shaderFragPath, string shaderName); + protected readonly TempShaderDataContainer _tempShaderData; + private readonly object _modelCacheLock = new(); private readonly Dictionary _modelByShareGroup = []; - private readonly string _shaderVertPath; - private readonly string _shaderFragPath; - private readonly string _shaderName; - /// /// Initializes a new instance of the . /// - public GameObject() : base(string.Empty) - { - BoundingBox = BoundingBox.CalculateBoundingBox(Transform); - - _shaderVertPath = _Resources.Default.VertexShader; - _shaderFragPath = _Resources.Default.FragmentShader; - _shaderName = "lighting"; - } + public GameObject() : this(model: null!) { } /// /// Initializes a new instance of the class with a model. /// /// The model. - public GameObject(Model model) : base(string.Empty) + public GameObject(Model model) : this(model, shader: null!) { - Model = model; - BoundingBox = BoundingBox.CalculateBoundingBox(Transform); - - _shaderVertPath = _Resources.Default.VertexShader; - _shaderFragPath = _Resources.Default.FragmentShader; - _shaderName = "lighting"; + // Shader = ShaderService.Instance.LoadShader(Window.SharedGL, _shaderVertPath, _shaderFragPath, _shaderName); } /// /// Initializes a new instance of the with specified textures and shaders. /// - /// The shader to be used by the game object. /// The model of the game object. - public GameObject(Shader shader, Model model) : base(string.Empty) + /// The shader to be used by the game object. + public GameObject(Model model, Shader shader) : base(string.Empty) { Model = model; BoundingBox = BoundingBox.CalculateBoundingBox(Transform); Shader = shader; - _shaderVertPath = shader.VertPath; - _shaderFragPath = shader.FragPath; - _shaderName = shader.Name; + if (shader is not null) + _tempShaderData = new TempShaderDataContainer(shader.VertPath, shader.FragPath, shader.Name); + else + _tempShaderData = new TempShaderDataContainer(_Resources.Default.VertexShader, _Resources.Default.FragmentShader, "lighting"); } + /// + /// Gets or sets the mesh renderer for this game object. + /// + /// + /// When set, takes precedence over the property during rendering. + /// TODO: #53 This should be coming from the entity component system. + /// + public MeshRenderer MeshRenderer { get; set; } + /// /// Gets or sets the shader of the game object. /// @@ -125,19 +123,13 @@ protected virtual void SetShaderUniforms(CameraView camera) public override Task Render(CameraView camera, Window window) { // TODO: This needs to removed later once fixed. - if (Model is null || Model.Meshes is null) + if (Model is null || Model.Meshes is null || !Model.Meshes.Any()) return Task.CompletedTask; - var glInstance = window.GetGL(); - - // Ensure this instance is using a shader compiled for the correct context/share-group. - Shader = ShaderService.Instance.LoadShader(window, _shaderVertPath, _shaderFragPath, _shaderName); - - var modelToRender = GetOrCreateModelForWindow(window, glInstance); - if (modelToRender is null) - return Task.CompletedTask; + if (Shader is null) + Shader = ShaderService.Instance.LoadShader(window, _tempShaderData.shaderVertPath, _tempShaderData.shaderFragPath, _tempShaderData.shaderName); - foreach (var mesh in modelToRender.Meshes) + foreach (var mesh in Model.Meshes) { mesh.Bind(); @@ -150,100 +142,9 @@ public override Task Render(CameraView camera, Window window) foreach (var material in mesh.Materials) material.SetUniformValues(Shader); - if (mesh.Indices.Length > 0) - glInstance.DrawElements(PrimitiveType.Triangles, (uint)mesh.Indices.Length, DrawElementsType.UnsignedInt, []); - else - glInstance.DrawArrays(PrimitiveType.Triangles, 0, (uint)(mesh.Vertices.Length / (VertexData.VerticesSize + VertexData.NormalsSize + VertexData.TexCoordsSize))); + mesh.Draw(); } return Task.CompletedTask; } - - private static object GetShareGroupKey(Window window) - => (object?)window.SharedContext ?? (object?)window.GLContext ?? window; - - private Model? GetOrCreateModelForWindow(Window window, GL gl) - { - if (Model is null) - return null; - - var shareGroupKey = GetShareGroupKey(window); - - lock (_modelCacheLock) - { - if (_modelByShareGroup.TryGetValue(shareGroupKey, out var cachedModel)) - return cachedModel; - - // If this model was created using the same GL instance, we can reuse it for this share group. - // Otherwise, build a copy of the model bound to the GL instance for this window. - var canReuseModel = Model.Meshes.Count > 0 && ReferenceEquals(Model.Meshes[0].GL, gl); - - var modelForWindow = canReuseModel ? Model : CloneModel(gl, Model); - _modelByShareGroup[shareGroupKey] = modelForWindow; - - return modelForWindow; - } - } - - private static Model CloneModel(GL gl, Model template) - { - // Create an empty model and populate it with meshes recreated against the provided GL instance. - // This allows rendering on windows that don't share the original context. - var clone = new Model(gl, string.Empty); - - foreach (var mesh in template.Meshes) - clone.Meshes.Add(CloneMesh(gl, mesh)); - - return clone; - } - - private static Mesh CloneMesh(GL gl, Mesh template) - { - var textures = template.Textures is null ? - new List() : - [.. template.Textures.Select(t => new EngineTexture(gl, t.Path, t.Type))]; - - var clone = new Mesh(gl, template.Vertices, template.Indices, textures) - { - Name = template.Name, - }; - - if (template.Materials is { Count: > 0 }) - clone.Materials = [.. template.Materials.Select(m => CloneMaterial(gl, m))]; - - return clone; - } - - private static Material CloneMaterial(GL gl, Material template) - { - ArgumentNullException.ThrowIfNull(template); - var diffuse = new EngineTexture(gl, template.DiffuseMap!.Path, template.DiffuseMap!.Type); - - EngineTexture? specular = null; - if (template.UseSpecularMap) - specular = new EngineTexture(gl, template.SpecularMap!.Path, template.SpecularMap!.Type); - - var clone = new Material(template.Name, diffuse, specular) - { - Name = template.Name, - DiffuseTextureMap = template.DiffuseTextureMap, - SpecularTextureMap = template.SpecularTextureMap, - Specular = template.Specular, - Shininess = template.Shininess, - AmbientColor = template.AmbientColor, - DiffuseColor = template.DiffuseColor, - SpecularColor = template.SpecularColor, - SpecularCoefficient = template.SpecularCoefficient, - Transparency = template.Transparency, - IlluminationModel = template.IlluminationModel, - AmbientTextureMap = template.AmbientTextureMap, - SpecularHighlightTextureMap = template.SpecularHighlightTextureMap, - BumpMap = template.BumpMap, - DisplacementMap = template.DisplacementMap, - StencilDecalMap = template.StencilDecalMap, - AlphaTextureMap = template.AlphaTextureMap, - }; - - return clone; - } } diff --git a/SharpEngine.Core/Entities/Lights/DirectionalLight.cs b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs index b37be9a..11f4d68 100644 --- a/SharpEngine.Core/Entities/Lights/DirectionalLight.cs +++ b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs @@ -1,9 +1,9 @@ using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; -using System.Numerics; using System.Threading.Tasks; namespace SharpEngine.Core.Entities.Lights; diff --git a/SharpEngine.Core/Entities/Lights/Light.cs b/SharpEngine.Core/Entities/Lights/Light.cs index dfd2585..d56422d 100644 --- a/SharpEngine.Core/Entities/Lights/Light.cs +++ b/SharpEngine.Core/Entities/Lights/Light.cs @@ -1,4 +1,4 @@ -using System.Numerics; +using SharpEngine.Core.Numerics; namespace SharpEngine.Core.Entities.Lights; diff --git a/SharpEngine.Core/Entities/Lights/PointLight.cs b/SharpEngine.Core/Entities/Lights/PointLight.cs index 0b6ef21..0dec2fc 100644 --- a/SharpEngine.Core/Entities/Lights/PointLight.cs +++ b/SharpEngine.Core/Entities/Lights/PointLight.cs @@ -1,10 +1,9 @@ -using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; using System; -using System.Numerics; using System.Threading.Tasks; namespace SharpEngine.Core.Entities.Lights; @@ -59,7 +58,7 @@ protected override void SetShaderUniforms(CameraView camera) if (Shader == null) throw new NullReferenceException(nameof(Shader)); - Shader.SetVector3($"pointLights[{_index}].position", (Vector3)Transform.Position); + Shader.SetVector3($"pointLights[{_index}].position", Transform.Position); Shader.SetVector3($"pointLights[{_index}].ambient", Ambient); Shader.SetVector3($"pointLights[{_index}].diffuse", Diffuse); Shader.SetVector3($"pointLights[{_index}].specular", Specular); @@ -73,7 +72,7 @@ protected override void SetShaderUniforms(CameraView camera) /// public override Task Render(CameraView camera, Window window) { - Shader = ShaderService.Instance.LoadShader(window, Default.VertexShader, Default.FragmentShader, "lighting"); + Shader = ShaderService.Instance.LoadShader(window, _tempShaderData.shaderVertPath, _tempShaderData.shaderFragPath, _tempShaderData.shaderName); SetShaderUniforms(camera); return Task.CompletedTask; diff --git a/SharpEngine.Core/Entities/Lights/SpotLight.cs b/SharpEngine.Core/Entities/Lights/SpotLight.cs index 1da0b41..9c98412 100644 --- a/SharpEngine.Core/Entities/Lights/SpotLight.cs +++ b/SharpEngine.Core/Entities/Lights/SpotLight.cs @@ -1,4 +1,3 @@ -using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; @@ -82,9 +81,9 @@ protected override void SetShaderUniforms(CameraView camera) /// public override Task Render(CameraView camera, Window window) { - Shader = ShaderService.Instance.LoadShader(window, Default.VertexShader, Default.FragmentShader, "lighting"); + Shader = ShaderService.Instance.LoadShader(window, _tempShaderData.shaderVertPath, _tempShaderData.shaderFragPath, _tempShaderData.shaderName); SetShaderUniforms(camera); - + return Task.CompletedTask; } } diff --git a/SharpEngine.Core/Entities/UI/MeshRenderer.cs b/SharpEngine.Core/Entities/UI/MeshRenderer.cs new file mode 100644 index 0000000..7995074 --- /dev/null +++ b/SharpEngine.Core/Entities/UI/MeshRenderer.cs @@ -0,0 +1,25 @@ +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Entities.Properties.Meshes; + +namespace SharpEngine.Core.Entities.UI; + +public class MeshRenderer +{ + public MeshRenderer(Mesh mesh, Material material) + { + Mesh = mesh; + Material = material; + } + + // TODO: This should be the correct structure for the MeshRenderer, but for now we will keep these separate + public MeshRenderer(Model model, Material material) + { + Model = model; + Material = material; + } + + public Model Model { get; } + public Mesh Mesh { get; } + public Material Material { get; } +} \ No newline at end of file diff --git a/SharpEngine.Core/Entities/UI/UIElement.cs b/SharpEngine.Core/Entities/UI/UIElement.cs index 50d498a..305ba60 100644 --- a/SharpEngine.Core/Entities/UI/UIElement.cs +++ b/SharpEngine.Core/Entities/UI/UIElement.cs @@ -2,18 +2,20 @@ using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Rendering; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; using SharpEngine.Core.Textures; using SharpEngine.Core.Windowing; using SharpEngine.Core._Resources; +using SharpEngine.Telemetry; using Vector2 = SharpEngine.Core.Numerics.Vector2; using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; using Silk.NET.OpenGL; -using System.Collections.Generic; using System.Numerics; using System.Threading.Tasks; +using SharpEngine.Core.Components.Properties; namespace SharpEngine.Core.Entities.UI; @@ -22,140 +24,62 @@ namespace SharpEngine.Core.Entities.UI; /// public class UIElement : EmptyNode, IRenderable { - /// - /// Initializes a new instance of . - /// - public UIElement() : this("UIElement") { } - - /// - /// Initializes a new instance of . - /// - /// The name of the UI element. - public UIElement(string name) : base(name) - { - // TODO: #5 Support custom meshes? - Mesh = MeshService.Instance.LoadMesh(nameof(Primitives.Plane), Primitives.Plane.Mesh); - } + // TODO: #53 This should be coming from the entity component system + public MeshRenderer MeshRenderer { get; set; } - private readonly UIShader _uiShader = new(); - private readonly Texture _texture = TextureService.Instance.LoadTexture(Default.DebugTexture); + private readonly ShaderParameterBinder _paramBinder; /// Gets or sets the width of the ui element. + [ShaderParameter("width", ShaderParameterType.Float)] public float Width { get; set; } = 10; /// Gets or sets the height of the ui element. + [ShaderParameter("height", ShaderParameterType.Float)] public float Height { get; set; } = 10; - /// Gets or sets the mesh of the UI element. - public Mesh Mesh { get; set; } + [ShaderParameter("orthoMatrix", ShaderParameterType.Mat4)] + private Matrix4x4 OrthoMatrix = Matrix4x4.CreateOrthographicOffCenter(-1, 1, -1, 1, -1, 1); /// - /// Gets the most recently used VAO for this element. + /// Initializes a new instance of . /// - /// - /// UIElement maintains VAOs per OpenGL context. This property is updated on render. - /// - public uint VAO { get; private set; } - - private sealed record SharedBuffers(uint Vbo, uint Ebo, int IndexCount); - private sealed record ContextState(uint Vao); - - private readonly object _gpuLock = new(); - private readonly Dictionary _sharedBuffersByShareGroup = []; - private readonly Dictionary _contextStateByContext = []; - - private static object GetShareGroupKey(Window window) - => (object?)window.SharedContext ?? (object?)window.GLContext ?? (object)window; - - private static object GetContextKey(Window window) - => (object?)window.GLContext ?? (object)window; - - private SharedBuffers EnsureSharedBuffers(GL gl, Window window) - { - var shareGroupKey = GetShareGroupKey(window); - - lock (_gpuLock) - { - if (_sharedBuffersByShareGroup.TryGetValue(shareGroupKey, out var buffers)) - return buffers; - - // Buffers are shareable across contexts in the same share group. - var vbo = gl.GenBuffer(); - gl.BindBuffer(GLEnum.ArrayBuffer, vbo); - gl.BufferData(GLEnum.ArrayBuffer, Mesh.GetVertices(), GLEnum.StaticDraw); - - var ebo = gl.GenBuffer(); - gl.BindBuffer(GLEnum.ElementArrayBuffer, ebo); - gl.BufferData(GLEnum.ElementArrayBuffer, Mesh.Indices, GLEnum.StaticDraw); + public UIElement(GL gl) : this(gl, "UIElement") { } - buffers = new SharedBuffers(vbo, ebo, Mesh.Indices.Length); - _sharedBuffersByShareGroup[shareGroupKey] = buffers; - - return buffers; - } - } - - private ContextState EnsureContextState(GL gl, Window window, SharedBuffers sharedBuffers) + /// + /// Initializes a new instance of . + /// + /// The name of the UI element. + public UIElement(GL gl, string name) : base(name) { - var contextKey = GetContextKey(window); - - lock (_gpuLock) - { - if (_contextStateByContext.TryGetValue(contextKey, out var state)) - return state; - - // VAOs are generally not shared between contexts. - var vao = gl.GenVertexArray(); - gl.BindVertexArray(vao); - - gl.BindBuffer(GLEnum.ArrayBuffer, sharedBuffers.Vbo); - gl.BindBuffer(GLEnum.ElementArrayBuffer, sharedBuffers.Ebo); - - _uiShader.EnsureInitialized(window); - _uiShader.Shader!.Use(); - _uiShader.SetAttributes(gl); - - state = new ContextState(vao); - _contextStateByContext[contextKey] = state; + // TODO: #5 Support custom meshes? + var mesh = MeshService.Instance.LoadMesh(nameof(Primitives.Plane), Primitives.Plane.Mesh); + var debugTexture = TextureService.Instance.LoadTexture(Default.DebugTexture); + var material = new Material("defaultUiMaterial", debugTexture) { Shader = new UIShader(gl) }; + MeshRenderer = new MeshRenderer(mesh, material); - return state; - } + _paramBinder = new ShaderParameterBinder(LoggingExtensions.CreateLogger()); + _paramBinder.Bind(this, material.Shader); } - Matrix4x4 OrthoMatrix = Matrix4x4.CreateOrthographicOffCenter(-1, 1, -1, 1, -1, 1); - /// /// Render the UI element. /// + /// The camera view. + /// The window where the UI element is rendered. public override Task Render(CameraView camera, Window window) { - var gl = window.GetGL(); - - _uiShader.EnsureInitialized(window); - - var sharedBuffers = EnsureSharedBuffers(gl, window); - var contextState = EnsureContextState(gl, window, sharedBuffers); - - _uiShader.Shader!.Use(); - gl.BindVertexArray(contextState.Vao); - _texture.Use(TextureUnit.Texture0); - - VAO = contextState.Vao; - - // TODO: #75 These should come from somewhere else. - const float screenWidth = 1280; - const float screenHeight = 720; - - _uiShader.Shader!.SetFloat("width", Width); - _uiShader.Shader!.SetFloat("height", Height); - _uiShader.Shader!.SetVector2("screenSize", new System.Numerics.Vector2(screenWidth, screenHeight)); - _uiShader.Shader!.SetVector2("position", (System.Numerics.Vector2)Transform.Position); - _uiShader.Shader!.SetFloat("rotation", Math.DegreesToRadians(Transform.Rotation.Angle)); - _uiShader.Shader!.SetInt("texture1", 0); - _uiShader.Shader!.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); - _uiShader.Shader!.SetMatrix4("orthoMatrix", OrthoMatrix); // Pass the orthographic matrix to the shader - - gl.DrawElements(PrimitiveType.Triangles, (uint)sharedBuffers.IndexCount, DrawElementsType.UnsignedInt, []); + MeshRenderer.Mesh.Bind(); + MeshRenderer.Material.Shader.Use(); + MeshRenderer.Material.DiffuseMap!.Texture!.Use(TextureUnit.Texture0); + + _paramBinder.Apply(this, MeshRenderer.Material.Shader); + MeshRenderer.Material.Shader.SetVector2("screenSize", (System.Numerics.Vector2)window.Size); + MeshRenderer.Material.Shader.SetVector2("position", (System.Numerics.Vector2)Transform.Position); + MeshRenderer.Material.Shader.SetFloat("rotation", Math.DegreesToRadians(Transform.Rotation.Angle)); + MeshRenderer.Material.Shader.SetInt("texture1", 0); + MeshRenderer.Material.Shader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); + + MeshRenderer.Mesh.Draw(); return Task.CompletedTask; } diff --git a/SharpEngine.Core/Primitives/Plane.cs b/SharpEngine.Core/Primitives/Plane.cs index 854ae7a..3eae8b1 100644 --- a/SharpEngine.Core/Primitives/Plane.cs +++ b/SharpEngine.Core/Primitives/Plane.cs @@ -9,33 +9,17 @@ namespace SharpEngine.Core.Primitives; public static class Plane { /// The plane mesh. - public static Mesh Mesh { get; } = new(Window.SharedGL) - { - Vertices = + /// + /// Vertices are stored in interleaved format: position (x, y, z), normal (nx, ny, nz), UV (u, v). + /// + public static Mesh Mesh { get; } = new(Window.SharedGL, + vertices: [ - 1f, 1f, 0.0f, // top right - 1f, -1f, 0.0f, // bottom right - -1f, -1f, 0.0f, // bottom left - -1f, 1f, 0.0f, // top left + // pos.x, pos.y, pos.z, norm.x, norm.y, norm.z, uv.u, uv.v + 1f, 1f, 0.0f, 0f, 0f, 1f, 1.0f, 0.0f, // top right + 1f, -1f, 0.0f, 0f, 0f, 1f, 1.0f, 0.0f, // bottom right + -1f, -1f, 0.0f, 0f, 0f, 1f, 0.0f, 1.0f, // bottom left + -1f, 1f, 0.0f, 0f, 0f, 1f, 0.0f, 1.0f, // top left ], - Normals = - [ - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - 0.0f, 0.0f, 1.0f, - ], - TextureCoordinates = - [ - 1.0f, 0.0f, - 1.0f, 0.0f, - 0.0f, 1.0f, - 0.0f, 1.0f, - ], - Indices = - [ - 0, 1, 3, // first triangle - 1, 2, 3, // second triangle - ] - }; + indices: [0u, 1u, 3u, 1u, 2u, 3u]); } diff --git a/SharpEngine.Core/Primitives/PrimitiveFactory.cs b/SharpEngine.Core/Primitives/PrimitiveFactory.cs index 1148e59..60777f2 100644 --- a/SharpEngine.Core/Primitives/PrimitiveFactory.cs +++ b/SharpEngine.Core/Primitives/PrimitiveFactory.cs @@ -1,9 +1,8 @@ using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Entities; using SharpEngine.Core.Entities.Properties; - +using SharpEngine.Core.Numerics; using System; -using System.Numerics; namespace SharpEngine.Core.Primitives; @@ -42,7 +41,7 @@ public static GameObject Create(PrimitiveType primitiveType, Vector3 position, s return new GameObject() { Model = model, - Transform = new Transform((Numerics.Vector3)position), + Transform = new Transform(position), }; } diff --git a/SharpEngine.Core/Renderers/Renderer.cs b/SharpEngine.Core/Renderers/Renderer.cs index 873a359..55c0046 100644 --- a/SharpEngine.Core/Renderers/Renderer.cs +++ b/SharpEngine.Core/Renderers/Renderer.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using SharpEngine.Core.Entities; using SharpEngine.Core.Entities.Lights; using SharpEngine.Core.Entities.Properties; @@ -6,10 +7,8 @@ using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; - -using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; using Silk.NET.OpenGL; - using System; using System.Numerics; using System.Threading.Tasks; @@ -54,6 +53,12 @@ public Renderer(CameraView camera, ISettings settings, Scene scene, ILogger + /// Initializes a new instance of . + /// + public Renderer(CameraView camera, ISettings settings, Scene scene) + : this(camera, settings, scene, LoggingExtensions.CreateLogger()) { } + /// protected override void OnWindowAttached(Window window) { @@ -79,7 +84,7 @@ public override Task Render() _gl.Enable(EnableCap.Blend); _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - _camera.SetShaderUniforms(_lightingShader.Shader!); + _camera.SetShaderUniforms(_lightingShader); _gl.BindVertexArray(_lightingShader.Vao); var lightRenderTasks = _scene.IterateAsync(_scene.Root.Children, RenderLight); diff --git a/SharpEngine.Core/Rendering/ParameterDescriptor.cs b/SharpEngine.Core/Rendering/ParameterDescriptor.cs new file mode 100644 index 0000000..1964741 --- /dev/null +++ b/SharpEngine.Core/Rendering/ParameterDescriptor.cs @@ -0,0 +1,18 @@ +using System; + +namespace SharpEngine.Core.Rendering +{ + public sealed class ParameterDescriptor + { + public string Name { get; } + public ShaderParameterType Type { get; } + public ShaderUpdateFrequency Frequency { get; } + + public ParameterDescriptor(string name, ShaderParameterType type, ShaderUpdateFrequency frequency = ShaderUpdateFrequency.PerObject) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Type = type; + Frequency = frequency; + } + } +} diff --git a/SharpEngine.Core/Rendering/ShaderParameter.cs b/SharpEngine.Core/Rendering/ShaderParameter.cs new file mode 100644 index 0000000..6ee9682 --- /dev/null +++ b/SharpEngine.Core/Rendering/ShaderParameter.cs @@ -0,0 +1,37 @@ +using System; + +namespace SharpEngine.Core.Rendering +{ + public sealed class ParameterInstance + { + public ParameterDescriptor Descriptor { get; } + + // Boxed value. Use typed accessors/helpers in ShaderProgram to avoid frequent casts. + private object _value; + public object Value => _value; + + public bool Dirty { get; private set; } + + // Used to avoid re-uploading per-frame params multiple times per frame + public int LastUploadedFrame { get; internal set; } = -1; + + public ParameterInstance(ParameterDescriptor descriptor, object initialValue = null) + { + Descriptor = descriptor ?? throw new ArgumentNullException(nameof(descriptor)); + _value = initialValue; + Dirty = true; // not uploaded yet + } + + public bool SetValue(object newValue, bool forceDirty = false) + { + if (!forceDirty && Equals(_value, newValue)) + return false; + + _value = newValue; + Dirty = true; + return true; + } + + public void ClearDirty() => Dirty = false; + } +} diff --git a/SharpEngine.Core/Rendering/ShaderParameterBinder.cs b/SharpEngine.Core/Rendering/ShaderParameterBinder.cs new file mode 100644 index 0000000..a2551cd --- /dev/null +++ b/SharpEngine.Core/Rendering/ShaderParameterBinder.cs @@ -0,0 +1,188 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Shaders; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Numerics; +using System.Reflection; + +namespace SharpEngine.Core.Rendering; + +/// +/// Binds C# properties and fields decorated with to their +/// corresponding GLSL uniforms, validating coverage at bind time and dispatching values at render time. +/// +/// +/// Call once when initializing an object against a shader to validate the +/// attribute-to-uniform mapping. Then call every frame (or whenever values change) +/// to upload the current property values to the shader without writing individual Set* calls. +/// Uniforms that have no matching attribute are reported at Debug level; attributes that +/// reference a non-existent uniform are reported at Warning level. +/// +public class ShaderParameterBinder +{ + private readonly ILogger _logger; + + private readonly record struct MemberBinding( + MemberInfo Member, + ShaderParameterAttribute Attribute, + ShaderParameterType ResolvedType); + + // Reflection metadata is expensive; cache per source type across all instances. + private static readonly ConcurrentDictionary _memberCache = new(); + + private MemberBinding[] _bindings = []; + + /// + /// Initializes a new instance of . + /// + /// Logger used to report validation results and dispatch warnings. + public ShaderParameterBinder(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Scans for -decorated members, + /// validates them against the shader's active uniforms, and caches the resolved bindings for + /// use by . + /// + /// The object whose type is inspected for shader parameter attributes. + /// The shader whose are validated against. + public void Bind(object source, IShader shader) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(shader); + + var type = source.GetType(); + var discovered = _memberCache.GetOrAdd(type, DiscoverMembers); + + var parameterNames = new HashSet(discovered.Length, StringComparer.Ordinal); + foreach (var (_, attr) in discovered) + parameterNames.Add(attr.Name); + + // Uniforms with no matching [ShaderParameter] attribute — developer may be setting them manually. + foreach (var uniformName in shader.UniformLocations.Keys) + { + if (!parameterNames.Contains(uniformName)) + _logger.LogDebug("Shader uniform '{Uniform}' has no [ShaderParameter] mapping on '{Type}'.", uniformName, type.Name); + } + + var resolved = new List(discovered.Length); + foreach (var (member, attr) in discovered) + { + if (!shader.UniformLocations.ContainsKey(attr.Name)) + { + _logger.LogWarning( + "[ShaderParameter(\"{Name}\")] on '{Member}' of '{Type}' has no matching uniform in the shader.", + attr.Name, member.Name, type.Name); + continue; + } + + var resolvedType = attr.Type != ShaderParameterType.Unknown ? attr.Type : InferType(member); + if (resolvedType == ShaderParameterType.Unknown) + { + _logger.LogWarning( + "Cannot determine ParameterType for [ShaderParameter(\"{Name}\")] on '{Member}'. Specify the type explicitly.", + attr.Name, member.Name); + continue; + } + + resolved.Add(new MemberBinding(member, attr, resolvedType)); + } + + _bindings = [.. resolved]; + } + + /// + /// Reads the current values of all bound members from and uploads + /// them to using the appropriate Set* method. + /// + /// The object from which property/field values are read. + /// The shader that receives the uniform values. + public void Apply(object source, IShader shader) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(shader); + + foreach (var binding in _bindings) + { + var value = GetMemberValue(binding.Member, source); + if (value is null) + continue; + + var uploaded = binding.ResolvedType switch + { + ShaderParameterType.Int when value is int i => shader.SetInt(binding.Attribute.Name, i), + ShaderParameterType.Float when value is float f => shader.SetFloat(binding.Attribute.Name, f), + ShaderParameterType.Vec2 when value is Vector2 v2 => shader.SetVector2(binding.Attribute.Name, v2), + ShaderParameterType.Vec3 when value is Vector3 v3 => shader.SetVector3(binding.Attribute.Name, v3), + ShaderParameterType.Vec4 when value is Vector4 v4 => shader.SetVector4(binding.Attribute.Name, v4), + ShaderParameterType.Mat4 when value is Matrix4x4 m => shader.SetMatrix4(binding.Attribute.Name, m), + ShaderParameterType.Texture when value is int slot => shader.SetInt(binding.Attribute.Name, slot), + _ => LogTypeMismatch(binding, value) + }; + + _ = uploaded; // result intentionally unused; Set* implementations log their own failures + } + } + + private bool LogTypeMismatch(MemberBinding binding, object value) + { + _logger.LogWarning("Value type '{ValueType}' is incompatible with ParameterType '{ParamType}' for uniform '{Name}'.", + value.GetType().Name, binding.ResolvedType, binding.Attribute.Name); + + return false; + } + + private static (MemberInfo Member, ShaderParameterAttribute Attribute)[] DiscoverMembers(Type type) + { + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + var results = new List<(MemberInfo, ShaderParameterAttribute)>(); + + foreach (var prop in type.GetProperties(flags)) + { + var attr = prop.GetCustomAttribute(); + if (attr is not null) + results.Add((prop, attr)); + } + + foreach (var field in type.GetFields(flags)) + { + var attr = field.GetCustomAttribute(); + if (attr is not null) + results.Add((field, attr)); + } + + return [.. results]; + } + + private static object? GetMemberValue(MemberInfo member, object source) + => member switch + { + PropertyInfo prop => prop.GetValue(source), + FieldInfo field => field.GetValue(source), + _ => null + }; + + private static ShaderParameterType InferType(MemberInfo member) + { + var memberType = member switch + { + PropertyInfo prop => prop.PropertyType, + FieldInfo field => field.FieldType, + _ => null + }; + + return memberType switch + { + not null when memberType == typeof(int) => ShaderParameterType.Int, + not null when memberType == typeof(float) => ShaderParameterType.Float, + not null when memberType == typeof(Vector2) => ShaderParameterType.Vec2, + not null when memberType == typeof(Vector3) => ShaderParameterType.Vec3, + not null when memberType == typeof(Vector4) => ShaderParameterType.Vec4, + not null when memberType == typeof(Matrix4x4) => ShaderParameterType.Mat4, + _ => ShaderParameterType.Unknown + }; + } +} diff --git a/SharpEngine.Core/Rendering/ShaderParameterType.cs b/SharpEngine.Core/Rendering/ShaderParameterType.cs new file mode 100644 index 0000000..1ede3b8 --- /dev/null +++ b/SharpEngine.Core/Rendering/ShaderParameterType.cs @@ -0,0 +1,14 @@ +namespace SharpEngine.Core.Rendering +{ + public enum ShaderParameterType + { + Int, + Float, + Vec2, + Vec3, + Vec4, + Mat4, + Texture, + Unknown + } +} diff --git a/SharpEngine.Core/Rendering/ShaderUpdateFrequency.cs b/SharpEngine.Core/Rendering/ShaderUpdateFrequency.cs new file mode 100644 index 0000000..311164b --- /dev/null +++ b/SharpEngine.Core/Rendering/ShaderUpdateFrequency.cs @@ -0,0 +1,9 @@ +namespace SharpEngine.Core.Rendering +{ + public enum ShaderUpdateFrequency + { + Static, + PerObject, + PerFrame + } +} diff --git a/SharpEngine.Core/Scenes/Scene.cs b/SharpEngine.Core/Scenes/Scene.cs index 6ab4c91..670ff38 100644 --- a/SharpEngine.Core/Scenes/Scene.cs +++ b/SharpEngine.Core/Scenes/Scene.cs @@ -63,11 +63,13 @@ public Scene(string sceneFile) /// Adds an empty node to the scene root. /// /// The name of the new empty node. - public void AddNode(string name) + public SceneNode AddNode(string name) { var node = new EmptyNode(name); Nodes.Add(node); Root.Children.Add(node); + + return node; } /// diff --git a/SharpEngine.Core/Shaders/DefaultShader.cs b/SharpEngine.Core/Shaders/DefaultShader.cs new file mode 100644 index 0000000..1338e87 --- /dev/null +++ b/SharpEngine.Core/Shaders/DefaultShader.cs @@ -0,0 +1,10 @@ +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +public class DefaultShader : Shader +{ + public DefaultShader(GL gl) : base(gl, _Resources.Default.VertexShader, _Resources.Default.FragmentShader, "default") { } + + // public override bool SetAttributes() => true; +} diff --git a/SharpEngine.Core/Shaders/LampShader.cs b/SharpEngine.Core/Shaders/LampShader.cs index f7ce4e1..b95f1a2 100644 --- a/SharpEngine.Core/Shaders/LampShader.cs +++ b/SharpEngine.Core/Shaders/LampShader.cs @@ -5,24 +5,14 @@ namespace SharpEngine.Core.Shaders; -internal class LampShader : ShaderBase +internal class LampShader : Shader { - private readonly GL _gl; - /// /// Initializes a new instance of using the provided OpenGL context. /// /// The OpenGL context to use for shader and VAO creation. - public LampShader(GL gl) + public LampShader(GL gl) : base(gl, Default.VertexShader, Default.LightShader, nameof(LampShader)) { - _gl = gl; - - Shader = ShaderService.Instance.LoadShader(_gl, Default.VertexShader, Default.LightShader, "lamp"); - - Vao = _gl.GenVertexArray(); - _gl.BindVertexArray(Vao); - - SetAttributes(_gl); } /// @@ -31,12 +21,12 @@ public override bool SetAttributes(GL gl) if (!base.SetAttributes(gl)) return false; - if (!Shader!.TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) + if (!TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) return false; var positionLocationUint = (uint)positionLocation; - _gl.EnableVertexAttribArray(positionLocationUint); - _gl.VertexAttribPointer(positionLocationUint, 3, VertexAttribPointerType.Float, false, VertexData.Stride, 0); + GL.EnableVertexAttribArray(positionLocationUint); + GL.VertexAttribPointer(positionLocationUint, 3, VertexAttribPointerType.Float, false, VertexData.Stride, 0); return true; } diff --git a/SharpEngine.Core/Shaders/LightingShader.cs b/SharpEngine.Core/Shaders/LightingShader.cs index 55b5e5e..4681f5b 100644 --- a/SharpEngine.Core/Shaders/LightingShader.cs +++ b/SharpEngine.Core/Shaders/LightingShader.cs @@ -4,25 +4,13 @@ namespace SharpEngine.Core.Shaders; -internal class LightingShader : ShaderBase +internal class LightingShader : Shader { - private readonly GL _gl; - /// /// Initializes a new instance of . /// /// The OpenGL context used to create and configure the shader program. - public LightingShader(GL gl) - { - _gl = gl; - - Shader = ShaderService.Instance.LoadShader(_gl, Default.VertexShader, Default.FragmentShader, "lighting"); - - Vao = _gl.GenVertexArray(); - _gl.BindVertexArray(Vao); - - SetAttributes(_gl); - } + public LightingShader(GL gl) : base(gl, Default.VertexShader, Default.FragmentShader, nameof(LightingShader)) { } /// public override bool SetAttributes(GL gl) diff --git a/SharpEngine.Core/Shaders/ShaderBase.cs b/SharpEngine.Core/Shaders/ShaderBase.cs deleted file mode 100644 index 492217a..0000000 --- a/SharpEngine.Core/Shaders/ShaderBase.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Extensions.Logging; -using Silk.NET.OpenGL; - -namespace SharpEngine.Core.Shaders; - -/// -/// Represents the base class for all shaders. -/// -public abstract class ShaderBase -{ - private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); - - /// Gets the shader. - public Shader? Shader { get; protected set; } - - /// Gets or sets the vertex array object. - public uint Vao { get; set; } - - /// - /// Sets the attributes for the shader. - /// - /// - /// if the attributes were set successfully; otherwise . - /// - public virtual bool SetAttributes(GL gl) - { - if (Shader is null) - { - Logger.LogError("Unable to set shader attributes, shader not found."); - return false; - } - - return true; - } -} diff --git a/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs new file mode 100644 index 0000000..8b925b0 --- /dev/null +++ b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs @@ -0,0 +1,19 @@ +using SharpEngine.Core.Rendering; +using System; + +namespace SharpEngine.Core.Shaders; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)] +public sealed class ShaderParameterAttribute : Attribute +{ + public string Name { get; } + public ShaderParameterType Type { get; } + public ShaderUpdateFrequency Frequency { get; } + + public ShaderParameterAttribute(string name, ShaderParameterType type = ShaderParameterType.Unknown, ShaderUpdateFrequency frequency = ShaderUpdateFrequency.PerObject) + { + Name = name; + Type = type; + Frequency = frequency; + } +} \ No newline at end of file diff --git a/SharpEngine.Core/Shaders/ShaderService.cs b/SharpEngine.Core/Shaders/ShaderService.cs index 74fee61..5832856 100644 --- a/SharpEngine.Core/Shaders/ShaderService.cs +++ b/SharpEngine.Core/Shaders/ShaderService.cs @@ -73,7 +73,7 @@ public Shader GetByName(string name) /// A shader with the given name. /// Thrown when either the vertex or fragment shader is not found. public Shader LoadShader(Window window, string vertPath, string fragPath, string name) - => LoadShader(window.GetGL(), GetShareGroupKey(window), vertPath, fragPath, name); + => LoadShader(window.GetGL(), window.GetShareGroupKey(), vertPath, fragPath, name); /// /// Loads a shader from the specified vertex and fragment paths.
@@ -121,14 +121,11 @@ public Shader LoadShader(GL gl, object shareGroupKey, string vertPath, string fr // Create a new shader instance and add it to the cache. // Shader program objects are shareable across contexts *only* when those contexts share. - var shader = new Shader(gl, vertPath, fragPath, name).Initialize(); + var shader = new Shader(gl, vertPath, fragPath, name); _shaderCache[cacheKey] = shader; HasShadersToLoad = true; return shader; } - - private static object GetShareGroupKey(Window window) - => (object?)window.SharedContext ?? (object?)window.GLContext ?? (object)window; } diff --git a/SharpEngine.Core/Shaders/UIShader.cs b/SharpEngine.Core/Shaders/UIShader.cs index 50e80a6..3a4cb2d 100644 --- a/SharpEngine.Core/Shaders/UIShader.cs +++ b/SharpEngine.Core/Shaders/UIShader.cs @@ -13,22 +13,9 @@ namespace SharpEngine.Core.Shaders; /// This shader is responsible for rendering 2D UI components on the screen, such as buttons, panels, and other interface elements. /// It is designed to work with the specific vertex and fragment shaders defined for UI rendering in the game engine. /// -internal class UIShader : ShaderBase +internal class UIShader : Shader { - /// - /// Ensures that the shader is initialized. - /// - /// - /// This method should be called before using the shader to ensure that it is properly loaded and ready for use. - /// - /// The window where the shader will be used. - public void EnsureInitialized(Window window) - { - if (Shader is not null) - return; - - Shader = ShaderService.Instance.LoadShader(window, Default.UIVertexShader, Default.UIFragmentShader, nameof(UIShader)); - } + public UIShader(GL gl) : base(gl, Default.UIVertexShader, Default.UIFragmentShader, nameof(UIShader)) { } /// public override bool SetAttributes(GL gl) diff --git a/SharpEngine.Core/Windowing/SilkWindow.cs b/SharpEngine.Core/Windowing/SilkWindow.cs index c7c9fd3..2c5cc08 100644 --- a/SharpEngine.Core/Windowing/SilkWindow.cs +++ b/SharpEngine.Core/Windowing/SilkWindow.cs @@ -369,4 +369,4 @@ protected virtual void PreRender(Frame frame) { } /// Handles closing the application. ///
public virtual void OnClosing() { } -} +} \ No newline at end of file diff --git a/SharpEngine.Core/Windowing/Window.cs b/SharpEngine.Core/Windowing/Window.cs index 15afa84..fca661d 100644 --- a/SharpEngine.Core/Windowing/Window.cs +++ b/SharpEngine.Core/Windowing/Window.cs @@ -260,10 +260,8 @@ public override void OnLoad() /// Contains information about the previous frame. protected void RenderFrame(Frame frame) { - while (!_initialized) - { - // Wait for the window to be initialized. - } + if (!_initialized) + throw new InvalidOperationException("Window has not been initialized. Call Initialize() before rendering."); try { diff --git a/SharpEngine.Core/Windowing/WindowExtensions.cs b/SharpEngine.Core/Windowing/WindowExtensions.cs new file mode 100644 index 0000000..ccb5b1b --- /dev/null +++ b/SharpEngine.Core/Windowing/WindowExtensions.cs @@ -0,0 +1,12 @@ +namespace SharpEngine.Core.Windowing; + +public static class WindowExtensions +{ + /// + /// Gets a key representing the share group for the window. + /// + /// The window to get the share group key for. + /// A key representing the share group for the window. + public static object GetShareGroupKey(this Window window) + => (object?)window.SharedContext ?? (object?)window.GLContext ?? window; +} \ No newline at end of file diff --git a/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs index 987b111..6e8cf13 100644 --- a/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs @@ -94,8 +94,8 @@ public void Sets_correct_texure_maps() LoadMaterial(); _firstMaterial.AmbientTextureMap.Should().BeEquivalentTo("lenna1.tga"); - _firstMaterial.DiffuseTextureMap.Should().BeEquivalentTo("lenna2.tga"); - _firstMaterial.SpecularTextureMap.Should().BeEquivalentTo("lenna3.tga"); + _firstMaterial.DiffuseMap.Should().BeEquivalentTo("lenna2.tga"); + _firstMaterial.SpecularMap.Should().BeEquivalentTo("lenna3.tga"); _firstMaterial.SpecularHighlightTextureMap.Should().BeEquivalentTo("lenna_spec.tga"); _firstMaterial.AlphaTextureMap.Should().BeEquivalentTo("lenna_alpha.tga"); _firstMaterial.BumpMap.Should().BeEquivalentTo("lenna_bump.tga"); diff --git a/Tests/SharpEngine.Core.Components.Tests/Meshes/ModelTests.cs b/Tests/SharpEngine.Core.Components.Tests/Meshes/ModelTests.cs new file mode 100644 index 0000000..a9bdcae --- /dev/null +++ b/Tests/SharpEngine.Core.Components.Tests/Meshes/ModelTests.cs @@ -0,0 +1,98 @@ +using FluentAssertions; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Components.Properties.Meshes.MeshData; +using System.Numerics; +using Xunit; + +namespace SharpEngine.Core.Components.Tests.Meshes; + +/// +/// Unit tests for covering the pure, GL-independent methods. +/// +/// +/// Tests that require a real OpenGL context (e.g., ) need an +/// integration test environment with a live windowing / GL context and are therefore not +/// covered here. +/// +public class ModelTests +{ + // ------------------------------------------------------------------------- + // BuildVertices + // ------------------------------------------------------------------------- + + [Fact] + public void BuildVertices_EmptyCollection_ReturnsEmptyArray() + { + var result = Model.BuildVertices([]); + + result.Should().BeEmpty(); + } + + [Fact] + public void BuildVertices_SingleVertex_ReturnsEightFloatsInCorrectOrder() + { + var vertex = new Vertex + { + Position = new Vector3(1f, 2f, 3f), + Normal = new Vector3(0f, 1f, 0f), + TexCoords = new Vector2(0.5f, 0.25f) + }; + + var result = Model.BuildVertices([vertex]); + + result.Should().HaveCount(8); + result.Should().ContainInOrder( + 1f, 2f, 3f, // Position + 0f, 1f, 0f, // Normal + 0.5f, 0.25f); // TexCoords + } + + [Fact] + public void BuildVertices_MultipleVertices_ProducesContiguousFloatLayout() + { + var v1 = new Vertex { Position = new Vector3(1f, 0f, 0f), Normal = new Vector3(0f, 0f, 1f), TexCoords = new Vector2(0f, 0f) }; + var v2 = new Vertex { Position = new Vector3(0f, 1f, 0f), Normal = new Vector3(0f, 0f, 1f), TexCoords = new Vector2(1f, 1f) }; + + var result = Model.BuildVertices([v1, v2]); + + result.Should().HaveCount(16); + + // First vertex + result[0].Should().Be(1f); + result[1].Should().Be(0f); + result[2].Should().Be(0f); + result[3].Should().Be(0f); + result[4].Should().Be(0f); + result[5].Should().Be(1f); + result[6].Should().Be(0f); + result[7].Should().Be(0f); + + // Second vertex + result[8].Should().Be(0f); + result[9].Should().Be(1f); + result[10].Should().Be(0f); + result[11].Should().Be(0f); + result[12].Should().Be(0f); + result[13].Should().Be(1f); + result[14].Should().Be(1f); + result[15].Should().Be(1f); + } + + [Fact] + public void BuildVertices_VertexWithNegativeCoordinates_PreservesSignedValues() + { + var vertex = new Vertex + { + Position = new Vector3(-0.5f, -0.5f, -0.5f), + Normal = new Vector3(-1f, 0f, 0f), + TexCoords = new Vector2(0f, 1f) + }; + + var result = Model.BuildVertices([vertex]); + + result.Should().ContainInOrder( + -0.5f, -0.5f, -0.5f, + -1f, 0f, 0f, + 0f, 1f); + } +} diff --git a/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj b/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj index a15a71c..1a08f02 100644 --- a/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj +++ b/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj @@ -1,11 +1,18 @@ - + - net10.0 + $(DotNetTargetFramework) enable enable + + + + + + + diff --git a/Tests/SharpEngine.Core.Tests/Shaders/ShaderParameterBinderTests.cs b/Tests/SharpEngine.Core.Tests/Shaders/ShaderParameterBinderTests.cs new file mode 100644 index 0000000..cb88507 --- /dev/null +++ b/Tests/SharpEngine.Core.Tests/Shaders/ShaderParameterBinderTests.cs @@ -0,0 +1,360 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using SharpEngine.Core.Rendering; +using SharpEngine.Core.Shaders; +using System.Collections.Generic; +using System.Numerics; +using Xunit; + +namespace SharpEngine.Core.Tests.Shaders; + +public class ShaderParameterBinderTests +{ + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + private static ShaderParameterBinder CreateBinder() + => new(NullLogger.Instance); + + private static Mock CreateShaderMock(params string[] uniformNames) + { + var dict = new Dictionary(uniformNames.Length); + for (int i = 0; i < uniformNames.Length; i++) + dict[uniformNames[i]] = i; + + var mock = new Mock(); + mock.Setup(s => s.UniformLocations).Returns(dict); + mock.Setup(s => s.SetFloat(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetInt(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetVector2(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetVector3(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetVector4(It.IsAny(), It.IsAny())).Returns(true); + mock.Setup(s => s.SetMatrix4(It.IsAny(), It.IsAny(), It.IsAny())).Returns(true); + return mock; + } + + // --------------------------------------------------------------------------- + // Test objects + // --------------------------------------------------------------------------- + + private class FloatSource + { + [ShaderParameter("myFloat", ShaderParameterType.Float)] + public float Value { get; set; } = 3.14f; + } + + private class IntSource + { + [ShaderParameter("myInt", ShaderParameterType.Int)] + public int Value { get; set; } = 7; + } + + private class Vec2Source + { + [ShaderParameter("myVec2", ShaderParameterType.Vec2)] + public Vector2 Value { get; set; } = new(1f, 2f); + } + + private class Vec3Source + { + [ShaderParameter("myVec3", ShaderParameterType.Vec3)] + public Vector3 Value { get; set; } = new(1f, 2f, 3f); + } + + private class Vec4Source + { + [ShaderParameter("myVec4", ShaderParameterType.Vec4)] + public Vector4 Value { get; set; } = new(1f, 2f, 3f, 4f); + } + + private class Mat4Source + { + [ShaderParameter("myMat4", ShaderParameterType.Mat4)] + public Matrix4x4 Value { get; set; } = Matrix4x4.Identity; + } + + private class TextureSource + { + [ShaderParameter("tex", ShaderParameterType.Texture)] + public int Slot { get; set; } = 0; + } + + private class PrivateFieldSource + { + [ShaderParameter("fieldUniform", ShaderParameterType.Float)] +#pragma warning disable CS0414 + private float _field = 9.9f; +#pragma warning restore CS0414 + } + + private class InferredFloatSource + { + // No explicit ParameterType — should be inferred from C# float + [ShaderParameter("inferredFloat")] + public float Value { get; set; } = 1.5f; + } + + private class InferredMat4Source + { + // No explicit ParameterType — should be inferred from Matrix4x4 + [ShaderParameter("inferredMat4")] + public Matrix4x4 Value { get; set; } = Matrix4x4.Identity; + } + + private class MismatchSource + { + // Attribute says Vec2 but C# type is float — Apply should not call any Set* + [ShaderParameter("mismatch", ShaderParameterType.Vec2)] + public float Value { get; set; } = 1f; + } + + private class UnmappedAttributeSource + { + [ShaderParameter("doesNotExistInShader", ShaderParameterType.Float)] + public float Value { get; set; } = 1f; + } + + private class MultiPropertySource + { + [ShaderParameter("width", ShaderParameterType.Float)] + public float Width { get; set; } = 100f; + + [ShaderParameter("height", ShaderParameterType.Float)] + public float Height { get; set; } = 200f; + } + + // --------------------------------------------------------------------------- + // Bind – validation + // --------------------------------------------------------------------------- + + [Fact] + public void Bind_AttributeWithNoMatchingUniform_DoesNotAddBinding() + { + var binder = CreateBinder(); + var shader = CreateShaderMock("otherUniform"); // no "doesNotExistInShader" + var source = new UnmappedAttributeSource(); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void Bind_NullSource_ThrowsArgumentNullException() + { + var binder = CreateBinder(); + var shader = CreateShaderMock(); + var act = () => binder.Bind(null!, shader.Object); + act.Should().Throw(); + } + + [Fact] + public void Bind_NullShader_ThrowsArgumentNullException() + { + var binder = CreateBinder(); + var act = () => binder.Bind(new FloatSource(), null!); + act.Should().Throw(); + } + + // --------------------------------------------------------------------------- + // Apply – correct dispatch per ParameterType + // --------------------------------------------------------------------------- + + [Fact] + public void Apply_Float_CallsSetFloat() + { + var binder = CreateBinder(); + var source = new FloatSource { Value = 3.14f }; + var shader = CreateShaderMock("myFloat"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("myFloat", 3.14f), Times.Once); + } + + [Fact] + public void Apply_Int_CallsSetInt() + { + var binder = CreateBinder(); + var source = new IntSource { Value = 7 }; + var shader = CreateShaderMock("myInt"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetInt("myInt", 7), Times.Once); + } + + [Fact] + public void Apply_Vec2_CallsSetVector2() + { + var binder = CreateBinder(); + var source = new Vec2Source { Value = new Vector2(1f, 2f) }; + var shader = CreateShaderMock("myVec2"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetVector2("myVec2", new Vector2(1f, 2f)), Times.Once); + } + + [Fact] + public void Apply_Vec3_CallsSetVector3() + { + var binder = CreateBinder(); + var source = new Vec3Source { Value = new Vector3(1f, 2f, 3f) }; + var shader = CreateShaderMock("myVec3"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetVector3("myVec3", new Vector3(1f, 2f, 3f)), Times.Once); + } + + [Fact] + public void Apply_Vec4_CallsSetVector4() + { + var binder = CreateBinder(); + var source = new Vec4Source { Value = new Vector4(1f, 2f, 3f, 4f) }; + var shader = CreateShaderMock("myVec4"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetVector4("myVec4", new Vector4(1f, 2f, 3f, 4f)), Times.Once); + } + + [Fact] + public void Apply_Mat4_CallsSetMatrix4() + { + var binder = CreateBinder(); + var source = new Mat4Source { Value = Matrix4x4.Identity }; + var shader = CreateShaderMock("myMat4"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetMatrix4("myMat4", Matrix4x4.Identity, true), Times.Once); + } + + [Fact] + public void Apply_Texture_CallsSetInt() + { + var binder = CreateBinder(); + var source = new TextureSource { Slot = 2 }; + var shader = CreateShaderMock("tex"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetInt("tex", 2), Times.Once); + } + + [Fact] + public void Apply_PrivateField_IsDispatched() + { + var binder = CreateBinder(); + var source = new PrivateFieldSource(); + var shader = CreateShaderMock("fieldUniform"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("fieldUniform", 9.9f), Times.Once); + } + + [Fact] + public void Apply_TypeMismatch_DoesNotCallAnySetMethod() + { + var binder = CreateBinder(); + var source = new MismatchSource { Value = 1f }; + var shader = CreateShaderMock("mismatch"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat(It.IsAny(), It.IsAny()), Times.Never); + shader.Verify(s => s.SetVector2(It.IsAny(), It.IsAny()), Times.Never); + } + + // --------------------------------------------------------------------------- + // Apply – type inference (ParameterType.Unknown) + // --------------------------------------------------------------------------- + + [Fact] + public void Apply_InferredFloat_CallsSetFloat() + { + var binder = CreateBinder(); + var source = new InferredFloatSource { Value = 1.5f }; + var shader = CreateShaderMock("inferredFloat"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("inferredFloat", 1.5f), Times.Once); + } + + [Fact] + public void Apply_InferredMat4_CallsSetMatrix4() + { + var binder = CreateBinder(); + var source = new InferredMat4Source { Value = Matrix4x4.Identity }; + var shader = CreateShaderMock("inferredMat4"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetMatrix4("inferredMat4", Matrix4x4.Identity, true), Times.Once); + } + + // --------------------------------------------------------------------------- + // Apply – multiple properties + // --------------------------------------------------------------------------- + + [Fact] + public void Apply_MultipleProperties_EachDispatchedSeparately() + { + var binder = CreateBinder(); + var source = new MultiPropertySource { Width = 100f, Height = 200f }; + var shader = CreateShaderMock("width", "height"); + + binder.Bind(source, shader.Object); + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("width", 100f), Times.Once); + shader.Verify(s => s.SetFloat("height", 200f), Times.Once); + } + + [Fact] + public void Apply_BeforeBind_DoesNotCallAnySetMethod() + { + var binder = CreateBinder(); + var source = new FloatSource(); + var shader = CreateShaderMock("myFloat"); + + // Apply without calling Bind first — _bindings is empty, nothing should be dispatched + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public void Apply_ReflectsCurrentPropertyValues() + { + var binder = CreateBinder(); + var source = new FloatSource { Value = 1f }; + var shader = CreateShaderMock("myFloat"); + + binder.Bind(source, shader.Object); + + source.Value = 42f; + binder.Apply(source, shader.Object); + + shader.Verify(s => s.SetFloat("myFloat", 42f), Times.Once); + shader.Verify(s => s.SetFloat("myFloat", 1f), Times.Never); + } +} diff --git a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj index a3a2555..397a4c5 100644 --- a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj +++ b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj @@ -1,4 +1,4 @@ - + $(DotNetTargetFramework) @@ -21,7 +21,6 @@ - From 4a935735845174359293e70e802a23afc24333a7 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 19:51:08 +0300 Subject: [PATCH 06/10] #134 Use Perlin Noise (#137) * #134 Minecraft terrain logic * #87 Use SharpEngine Numerics --------- Co-authored-by: Antti Veikkolainen --- Examples/MinecraftClone/Inventory.cs | 21 +- Examples/MinecraftClone/Minecraft.cs | 179 ++----- .../{ => Terrain}/Block/BlockBase.cs | 9 +- .../{ => Terrain/Block}/BlockFactory.cs | 28 +- .../MinecraftClone/Terrain/Block/BlockId.cs | 26 + .../{ => Terrain}/Block/Dirt.cs | 9 +- .../MinecraftClone/Terrain/Block/Grass.cs | 21 + Examples/MinecraftClone/Terrain/Block/Iron.cs | 17 + Examples/MinecraftClone/Terrain/Block/Leaf.cs | 17 + .../{ => Terrain}/Block/Stone.cs | 15 +- Examples/MinecraftClone/Terrain/Block/Wood.cs | 17 + Examples/MinecraftClone/Terrain/ChunkData.cs | 57 +++ .../Terrain/ChunkGenerationContext.cs | 27 + .../Terrain/IChunkGenerationLayer.cs | 6 + .../Terrain/Layers/BaseTerrainPass.cs | 59 +++ .../MinecraftClone/Terrain/Layers/CavePass.cs | 54 ++ .../Terrain/Layers/HeightMapPass.cs | 26 + .../Terrain/TerrainGenerator.cs | 158 ++++++ .../Terrain/TerrainGenerator_Old.cs | 145 ++++++ .../Terrain/terrain_layering_pipeline.md | 478 ++++++++++++++++++ SharpEngine.Core/Abstractions/IGame.cs | 6 +- SharpEngine.Core/Entities/Views/CameraView.cs | 6 +- SharpEngine.Core/Entities/Views/View.cs | 3 +- SharpEngine.Core/Ray.cs | 2 +- SharpEngine.Core/Windowing/Window.cs | 4 +- 25 files changed, 1207 insertions(+), 183 deletions(-) rename Examples/MinecraftClone/{ => Terrain}/Block/BlockBase.cs (93%) rename Examples/MinecraftClone/{ => Terrain/Block}/BlockFactory.cs (54%) create mode 100644 Examples/MinecraftClone/Terrain/Block/BlockId.cs rename Examples/MinecraftClone/{ => Terrain}/Block/Dirt.cs (83%) create mode 100644 Examples/MinecraftClone/Terrain/Block/Grass.cs create mode 100644 Examples/MinecraftClone/Terrain/Block/Iron.cs create mode 100644 Examples/MinecraftClone/Terrain/Block/Leaf.cs rename Examples/MinecraftClone/{ => Terrain}/Block/Stone.cs (50%) create mode 100644 Examples/MinecraftClone/Terrain/Block/Wood.cs create mode 100644 Examples/MinecraftClone/Terrain/ChunkData.cs create mode 100644 Examples/MinecraftClone/Terrain/ChunkGenerationContext.cs create mode 100644 Examples/MinecraftClone/Terrain/IChunkGenerationLayer.cs create mode 100644 Examples/MinecraftClone/Terrain/Layers/BaseTerrainPass.cs create mode 100644 Examples/MinecraftClone/Terrain/Layers/CavePass.cs create mode 100644 Examples/MinecraftClone/Terrain/Layers/HeightMapPass.cs create mode 100644 Examples/MinecraftClone/Terrain/TerrainGenerator.cs create mode 100644 Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs create mode 100644 Examples/MinecraftClone/Terrain/terrain_layering_pipeline.md diff --git a/Examples/MinecraftClone/Inventory.cs b/Examples/MinecraftClone/Inventory.cs index ed8b7eb..b195560 100644 --- a/Examples/MinecraftClone/Inventory.cs +++ b/Examples/MinecraftClone/Inventory.cs @@ -1,4 +1,5 @@ -using System; +using Minecraft.Terrain.Block; +using System; using System.Linq; namespace Minecraft; @@ -31,7 +32,7 @@ public class Inventory public InventoryItem GetActiveSlotItems(int slotNumber) => Toolbar[slotNumber].Items; - /*public void AddItem(BlockType type) + /*public void AddItem(BlockId type) { var item = Items.FirstOrDefault(x => x.Type == type); if (item is not null) @@ -43,10 +44,10 @@ public InventoryItem GetActiveSlotItems(int slotNumber) /// /// Adds an item to the toolbar. /// - /// The type of the block to add to the toolbar. - public void AddToolbarItem(BlockType blockType) + /// The type of the block to add to the toolbar. + public void AddToolbarItem(BlockId BlockId) { - var slot = Toolbar.FirstOrDefault(i => i.Items.Type == blockType); + var slot = Toolbar.FirstOrDefault(i => i.Items.Type == BlockId); if (slot is not null) { slot.Items.Amount += 1; @@ -57,18 +58,18 @@ public void AddToolbarItem(BlockType blockType) return; } - if (SelectedSlot.Items.Type == BlockType.None) + if (SelectedSlot.Items.Type == BlockId.Air) { - SelectedSlot.Items = new InventoryItem { Type = blockType, Amount = 1 }; + SelectedSlot.Items = new InventoryItem { Type = BlockId, Amount = 1 }; Toolbar[SelectedSlotIndex] = SelectedSlot; return; } for (int i = 0; i < Toolbar.Length; i++) { - if (Toolbar[i].Items.Type == BlockType.None) + if (Toolbar[i].Items.Type == BlockId.Air) { - Toolbar[i].Items = new InventoryItem { Type = blockType, Amount = 1 }; + Toolbar[i].Items = new InventoryItem { Type = BlockId, Amount = 1 }; return; } } @@ -130,7 +131,7 @@ public class InventoryItem /// /// Gets or sets the type of the block in the inventory. /// - public BlockType Type { get; set; } = BlockType.None; + public BlockId Type { get; set; } = BlockId.Air; /// Gets or sets the amount of the block. public int Amount { get; set; } diff --git a/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs index 6819f78..cb637fd 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -1,24 +1,17 @@ using ImGuiNET; - -using Minecraft.Block; using Microsoft.Extensions.Logging; - +using Minecraft.Terrain.Block; +using Minecraft.Terrain.Layers; using SharpEngine.Core; using SharpEngine.Core.Entities; -using SharpEngine.Core.Entities.Lights; -using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Entities.UI; -using SharpEngine.Core.Entities.UI.Layouts; using SharpEngine.Core.Enums; using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using SharpEngine.Core.Windowing; -using SharpEngine.Core.ObjLoader.Loaders.ObjLoader; - using Silk.NET.Input; - using System; -using System.Numerics; namespace Minecraft; @@ -32,14 +25,14 @@ public class Minecraft : Game private readonly Scene _scene; private readonly ILogger _logger; - private SceneNode _lightsNode; - private SceneNode _blocksNode; - private Input _input; private readonly Inventory _inventory; private UIElement _uiElem; + private readonly Terrain.TerrainGenerator_New _terrain; + private readonly SceneNode _blocksNode; + /// /// Gets the main window. /// @@ -60,6 +53,17 @@ public Minecraft(Scene scene, ISettings settings, ILogger logger) _inventory = new Inventory(); + _blocksNode = _scene.AddNode("Blocks"); + _terrain = new Terrain.TerrainGenerator_New(_scene, _blocksNode, + [ + new HeightMapPass(), + new BaseTerrainPass(), + new CavePass(), + // new OrePass(), + // new WaterPass(), + // new SurfaceDecorationPass(), + // new FoliagePass() + ]); } /// @@ -72,26 +76,9 @@ public override void Initialize() _input = new Input(Camera); _inventory.Initialize(); - _lightsNode = _scene.Root.AddChild("lights"); - _blocksNode = _scene.Root.AddChild("blocks"); - - // var gridLayout = new GridLayout(); + _terrain.InitializeWorld(); - // TODO: #89 Fix UI renderer - _uiElem = new UIElement(Window.GetGL(), "uiElement"); - _scene.UIElements.Add(_uiElem); - - var uiElem2 = new UIElement(Window.GetGL(), "uiElement"); - uiElem2.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(0.2f, 0.2f); - uiElem2.Transform.Position = new SharpEngine.Core.Numerics.Vector2(30, 0); - - // gridLayout.AddChild(_uiElem, uiElem2); - _scene.UIElements.Add(_uiElem); - _scene.UIElements.Add(uiElem2); - - // _scene.UIElements.Add(gridLayout); - - InitializeWorld(); + InitializeUI(); } catch (Exception ex) { @@ -99,6 +86,25 @@ public override void Initialize() } } + private void InitializeUI() + { + // var gridLayout = new GridLayout(); + + // TODO: #89 Fix UI renderer + _uiElem = new UIElement(Window.GetGL(), "uiElement"); + _scene.UIElements.Add(_uiElem); + + var uiElem2 = new UIElement(Window.GetGL(), "uiElement"); + uiElem2.Transform.Scale = new 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); + } + /// /// Handles rendering after the frame is drawn. /// @@ -133,97 +139,20 @@ public void OnAfterRender(Frame frame) ImGui.SliderFloat("rotation", ref rotation, 0, 360); ImGui.Text($"UI Element width: {_uiElem.Width}"); - ImGui.SliderFloat("width", ref width, 0, 100); + ImGui.SliderFloat("width", ref width, 0, Window.Width); ImGui.Text($"UI Element height: {_uiElem.Height}"); - ImGui.SliderFloat("height", ref height, 0, 100); + ImGui.SliderFloat("height", ref height, 0, Window.Height); ImGui.End(); - _uiElem.Transform.Position = new SharpEngine.Core.Numerics.Vector2(x, y); - _uiElem.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(sx, sy); + _uiElem.Transform.Position = new Vector2(x, y); + _uiElem.Transform.Scale = new 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.GetGL(), @"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() - { - _lightsNode.AddChild(new DirectionalLight()); - - _lightsNode.AddChild( - new PointLight(new SharpEngine.Core.Numerics.Vector3(0.7f, 0.2f, 2.0f), 0), - new PointLight(new SharpEngine.Core.Numerics.Vector3(2.3f, -3.3f, -4.0f), 1), - new PointLight(new SharpEngine.Core.Numerics.Vector3(-4.0f, 2.0f, -12.0f), 2), - new PointLight(new SharpEngine.Core.Numerics.Vector3(0.0f, 0.0f, -3.0f), 3) - ); - - _lightsNode.AddChild(new SpotLight() - { - Ambient = new SharpEngine.Core.Numerics.Vector3(0.0f, 0.0f, 0.0f), - Diffuse = new SharpEngine.Core.Numerics.Vector3(1.0f, 1.0f, 1.0f), - Specular = new SharpEngine.Core.Numerics.Vector3(1.0f, 1.0f, 1.0f), - }); - } - - private void InitializeChunks() - { - // TODO: #88 Generate chunks when player moves - - // TODO: #87 Generate chunks using 3d Perlin noise - - const int chunkSize = 16; - const int numChunks = 1; - // const int numChunks = 3; - - for (int i = 0; i < numChunks; i++) - { - var chunkPos = new Vector3(i * chunkSize, 0, 0); - GenerateChunk(chunkSize, chunkPos); - } - } - - private void GenerateChunk(int chunkSize, Vector3 chunkPos) - { - for (int x = 0; x < chunkSize; x++) - { - for (int z = 0; z < chunkSize; z++) - { - var blockPos = chunkPos + new Vector3(x, 0, z); - - var dirt = new Dirt(blockPos, $"Dirt ({x}{z})"); - _blocksNode.AddChild(dirt); - - for (int y = 1; y < chunkSize; y++) - { - blockPos.Y = -y; - var stone = new Stone(blockPos, $"Dirt ({x}{z}.{y})"); - _blocksNode.AddChild(stone); - } - } - } - } - /// public override void Update(double deltaTime, IInputContext input) => _input.HandleKeyboard(input.Keyboards[0], (float)deltaTime); @@ -232,7 +161,7 @@ public override void Update(double deltaTime, IInputContext input) /// public override void HandleKeyboard(IKeyboard input, double deltaTime) { - for (int i = 0; i <= 9; i++) + for (int i = 1; i <= 9; i++) { if (input.IsKeyPressed(Key.Number0 + i)) { @@ -262,13 +191,13 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) { if (button == MouseButton.Right) { - if (_inventory.SelectedSlot.Items.Type != BlockType.None && _inventory.SelectedSlot.Items.Amount > 0) + if (_inventory.SelectedSlot.Items.Type != BlockId.Air && _inventory.SelectedSlot.Items.Amount > 0) { PlaceBlock(); _inventory.SelectedSlot.Items.Amount -= 1; if (_inventory.SelectedSlot.Items.Amount < 0) - _inventory.SelectedSlot.Items.Type = BlockType.None; + _inventory.SelectedSlot.Items.Type = BlockId.Air; } else { @@ -278,26 +207,26 @@ public override void HandleMouseDown(IMouse mouse, MouseButton button) if (button == MouseButton.Left) { - var destroyedBlockType = DestroyBlock(); - if (destroyedBlockType != BlockType.None) + var destroyedBlockId = DestroyBlock(); + if (destroyedBlockId != BlockId.Air) { // 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. - _logger.LogInformation("Block destroyed: {DestroyedBlockType}.", destroyedBlockType); - _inventory.AddToolbarItem(destroyedBlockType); + _logger.LogInformation("Block destroyed: {DestroyedBlockId}.", destroyedBlockId); + _inventory.AddToolbarItem(destroyedBlockId); } } } - private BlockType DestroyBlock() + private BlockId DestroyBlock() { - if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out Vector3 _, allowedTypes: typeof(BlockBase))) - return BlockType.None; + if (!Camera.IsInView(_scene, out GameObject? intersectingObject, out _, allowedTypes: typeof(BlockBase))) + return BlockId.Air; var block = (BlockBase)intersectingObject!; _blocksNode.RemoveChild(intersectingObject!); - return block.BlockType; + return block.BlockId; } private void PlaceBlock() @@ -318,7 +247,7 @@ private void PlaceBlock() private static Vector3 GetNewBlockPosition(Vector3 hitPosition, GameObject intersectingObject) { Vector3 normal = Ray.GetClosestFaceNormal(hitPosition, intersectingObject); - return (System.Numerics.Vector3)intersectingObject.Transform.Position + (normal * (System.Numerics.Vector3)intersectingObject.Transform.Scale); + return intersectingObject.Transform.Position + (normal * intersectingObject.Transform.Scale); } /// diff --git a/Examples/MinecraftClone/Block/BlockBase.cs b/Examples/MinecraftClone/Terrain/Block/BlockBase.cs similarity index 93% rename from Examples/MinecraftClone/Block/BlockBase.cs rename to Examples/MinecraftClone/Terrain/Block/BlockBase.cs index e10a295..7461f0e 100644 --- a/Examples/MinecraftClone/Block/BlockBase.cs +++ b/Examples/MinecraftClone/Terrain/Block/BlockBase.cs @@ -1,9 +1,8 @@ using SharpEngine.Core.Entities; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Primitives; -using System.Numerics; - -namespace Minecraft.Block; +namespace Minecraft.Terrain.Block; /// /// Represents a block in the game. @@ -13,10 +12,10 @@ public class BlockBase : GameObject /// /// Gets the block type. /// - public virtual BlockType BlockType { get; } + public virtual BlockId BlockId { get; } /// Gets a value indicating whether the block is solid. - public virtual bool IsSolid { get; } + public virtual bool IsSolid { get; } = true; /// Gets or sets the size of the block in the inventory. public int SizeInInventory { get; set; } = 1; diff --git a/Examples/MinecraftClone/BlockFactory.cs b/Examples/MinecraftClone/Terrain/Block/BlockFactory.cs similarity index 54% rename from Examples/MinecraftClone/BlockFactory.cs rename to Examples/MinecraftClone/Terrain/Block/BlockFactory.cs index 7991855..44d1deb 100644 --- a/Examples/MinecraftClone/BlockFactory.cs +++ b/Examples/MinecraftClone/Terrain/Block/BlockFactory.cs @@ -1,9 +1,7 @@ -using Minecraft.Block; - +using SharpEngine.Core.Numerics; using System; -using System.Numerics; -namespace Minecraft +namespace Minecraft.Terrain.Block { /// /// Used to create a block objects. @@ -18,27 +16,13 @@ public static class BlockFactory /// The name of the block to be created. /// The newly created block. /// - public static BlockBase CreateBlock(BlockType type, Vector3 position, string name) + public static BlockBase CreateBlock(BlockId type, Vector3 position, string name) => type switch { - BlockType.Dirt => new Dirt(position, name), - BlockType.Stone => new Stone(position, name), + BlockId.Dirt => new Dirt(position, name), + BlockId.Stone => new Stone(position, name), + BlockId.Grass => new Grass(position, name), _ => throw new ArgumentException("Invalid block type", nameof(type)), }; } - - /// - /// Represents the type of block. - /// - public enum BlockType - { - /// Represents no block type. - None, - - /// Represents a dirt block. - Dirt, - - /// Represents a stone block. - Stone - } } diff --git a/Examples/MinecraftClone/Terrain/Block/BlockId.cs b/Examples/MinecraftClone/Terrain/Block/BlockId.cs new file mode 100644 index 0000000..34b428a --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/BlockId.cs @@ -0,0 +1,26 @@ +namespace Minecraft.Terrain.Block; + +/// +/// Represents the type of block. +/// +public enum BlockId : ushort +{ + Air = 0, + + Grass = 1, + Dirt = 2, + Stone = 3, + + Water = 10, + + CoalOre = 20, + IronOre = 21, + CopperOre = 22, + + Wood = 50, + + TallGrass = 100, + Flower = 101, + Log = 102, + Leaves = 103 +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Block/Dirt.cs b/Examples/MinecraftClone/Terrain/Block/Dirt.cs similarity index 83% rename from Examples/MinecraftClone/Block/Dirt.cs rename to Examples/MinecraftClone/Terrain/Block/Dirt.cs index 183d5d8..d76622e 100644 --- a/Examples/MinecraftClone/Block/Dirt.cs +++ b/Examples/MinecraftClone/Terrain/Block/Dirt.cs @@ -1,8 +1,8 @@ using SharpEngine.Core._Resources; using SharpEngine.Core.Extensions; -using System.Numerics; +using SharpEngine.Core.Numerics; -namespace Minecraft.Block; +namespace Minecraft.Terrain.Block; /// /// Represents a dirt block. @@ -20,8 +20,5 @@ public Dirt(Vector3 position, string name) : base(position, name, PathExtensions Default.FragmentShader) { } /// - public override BlockType BlockType => BlockType.Dirt; - - /// - public override bool IsSolid => true; + public override BlockId BlockId => BlockId.Dirt; } diff --git a/Examples/MinecraftClone/Terrain/Block/Grass.cs b/Examples/MinecraftClone/Terrain/Block/Grass.cs new file mode 100644 index 0000000..8fb3faa --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Grass.cs @@ -0,0 +1,21 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; + +namespace Minecraft.Terrain.Block; + +internal class Grass : BlockBase +{ + /// + /// Initializes a new block. + /// + /// The position where the block should be initialized. + /// The name of the block to initialize. + public Grass(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources\\grass.jpg"), + PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"), + Default.VertexShader, + Default.FragmentShader) { } + + /// + public override BlockId BlockId => BlockId.Grass; +} diff --git a/Examples/MinecraftClone/Terrain/Block/Iron.cs b/Examples/MinecraftClone/Terrain/Block/Iron.cs new file mode 100644 index 0000000..431bf46 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Iron.cs @@ -0,0 +1,17 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; + +namespace Minecraft.Terrain.Block; + +internal class Iron : BlockBase +{ + public Iron(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Default.VertexShader, Default.FragmentShader) { } + + private static string DiffuseMap() => PathExtensions.GetAssemblyPath("Resources\\container2.png"); + private static string SpecularMap() => PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"); + + /// + public override BlockId BlockId => BlockId.IronOre; +} diff --git a/Examples/MinecraftClone/Terrain/Block/Leaf.cs b/Examples/MinecraftClone/Terrain/Block/Leaf.cs new file mode 100644 index 0000000..b047da2 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Leaf.cs @@ -0,0 +1,17 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; + +namespace Minecraft.Terrain.Block; + +internal class Leaf : BlockBase +{ + public Leaf(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Default.VertexShader, Default.FragmentShader) { } + + private static string DiffuseMap() => PathExtensions.GetAssemblyPath("Resources\\container2.png"); + private static string SpecularMap() => PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"); + + /// + public override BlockId BlockId => BlockId.Leaves; +} diff --git a/Examples/MinecraftClone/Block/Stone.cs b/Examples/MinecraftClone/Terrain/Block/Stone.cs similarity index 50% rename from Examples/MinecraftClone/Block/Stone.cs rename to Examples/MinecraftClone/Terrain/Block/Stone.cs index 13352d3..dcd3b3f 100644 --- a/Examples/MinecraftClone/Block/Stone.cs +++ b/Examples/MinecraftClone/Terrain/Block/Stone.cs @@ -1,8 +1,8 @@ using SharpEngine.Core._Resources; using SharpEngine.Core.Extensions; -using System.Numerics; +using SharpEngine.Core.Numerics; -namespace Minecraft.Block; +namespace Minecraft.Terrain.Block; internal class Stone : BlockBase { @@ -14,13 +14,14 @@ internal class Stone : BlockBase /// /// 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"), - Default.VertexShader, - Default.FragmentShader) { } + public Stone(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Default.VertexShader, Default.FragmentShader) { } + + private static string DiffuseMap() => PathExtensions.GetAssemblyPath("Resources\\container2.png"); + private static string SpecularMap() => PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"); /// - public override BlockType BlockType => BlockType.Stone; + public override BlockId BlockId => BlockId.Stone; /// public override bool IsSolid => true; diff --git a/Examples/MinecraftClone/Terrain/Block/Wood.cs b/Examples/MinecraftClone/Terrain/Block/Wood.cs new file mode 100644 index 0000000..f58ff58 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Wood.cs @@ -0,0 +1,17 @@ +using SharpEngine.Core._Resources; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; + +namespace Minecraft.Terrain.Block; + +internal class Wood : BlockBase +{ + public Wood(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Default.VertexShader, Default.FragmentShader) { } + + private static string DiffuseMap() => PathExtensions.GetAssemblyPath("Resources\\container2.png"); + private static string SpecularMap() => PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"); + + /// + public override BlockId BlockId => BlockId.Wood; +} diff --git a/Examples/MinecraftClone/Terrain/ChunkData.cs b/Examples/MinecraftClone/Terrain/ChunkData.cs new file mode 100644 index 0000000..63d27c6 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/ChunkData.cs @@ -0,0 +1,57 @@ +using Minecraft.Terrain.Block; + +namespace Minecraft.Terrain; + +public sealed class ChunkData +{ + private readonly BlockId[,,] _blocks; + private readonly int[,] _heightMap; + + public ChunkData(int size, int height) + { + Size = size; + Height = height; + + _blocks = new BlockId[size, height, size]; + _heightMap = new int[size, size]; + } + + public int Size { get; } + + public int Height { get; } + + public BlockId GetBlock(int x, int y, int z) + { + return _blocks[x, y, z]; + } + + public void SetBlock(int x, int y, int z, BlockId block) + { + _blocks[x, y, z] = block; + } + + public int GetHeight(int x, int z) + { + return _heightMap[x, z]; + } + + public void SetHeight(int x, int z, int height) + { + _heightMap[x, z] = height; + } + + public bool IsInside(int x, int y, int z) + { + return x >= 0 && + y >= 0 && + z >= 0 && + x < Size && + y < Height && + z < Size; + } + + public bool IsAir(int x, int y, int z) + { + return GetBlock(x, y, z) == BlockId.Air; + } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/ChunkGenerationContext.cs b/Examples/MinecraftClone/Terrain/ChunkGenerationContext.cs new file mode 100644 index 0000000..6f19da9 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/ChunkGenerationContext.cs @@ -0,0 +1,27 @@ +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Numerics.Noise; + +namespace Minecraft.Terrain; + +public sealed class ChunkGenerationContext +{ + public required int Seed { get; init; } + + public required Vector3 ChunkPosition { get; init; } + + public required int ChunkSize { get; init; } + + public required int MinY { get; init; } + + public required int MaxY { get; init; } + + public required int WaterLevel { get; init; } + + public required INoiseGenerator TerrainNoise { get; init; } + + public required INoiseGenerator CaveNoise { get; init; } + + public required INoiseGenerator OreNoise { get; init; } + + public required INoiseGenerator FoliageNoise { get; init; } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/IChunkGenerationLayer.cs b/Examples/MinecraftClone/Terrain/IChunkGenerationLayer.cs new file mode 100644 index 0000000..5980bbd --- /dev/null +++ b/Examples/MinecraftClone/Terrain/IChunkGenerationLayer.cs @@ -0,0 +1,6 @@ +namespace Minecraft.Terrain; + +public interface IChunkGenerationLayer +{ + void Generate(ChunkGenerationContext context, ChunkData chunk); +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/Layers/BaseTerrainPass.cs b/Examples/MinecraftClone/Terrain/Layers/BaseTerrainPass.cs new file mode 100644 index 0000000..4ad29f1 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Layers/BaseTerrainPass.cs @@ -0,0 +1,59 @@ +using Minecraft.Terrain; +using Minecraft.Terrain.Block; +using System; + +namespace Minecraft.Terrain.Layers; + +public sealed class BaseTerrainPass : IChunkGenerationLayer +{ + private const int DirtDepth = 3; + + public void Generate(ChunkGenerationContext context, ChunkData chunk) + { + for (int x = 0; x < chunk.Size; x++) + { + for (int z = 0; z < chunk.Size; z++) + { + int surfaceWorldY = chunk.GetHeight(x, z); + int surfaceLocalY = WorldYToLocalY(context, surfaceWorldY); + + GenerateColumn(chunk, x, z, surfaceLocalY); + } + } + } + + private static void GenerateColumn(ChunkData chunk, int x, int z, int surfaceLocalY) + { + for (int localY = 0; localY < chunk.Height; localY++) + { + BlockId block = GetBlockForLayer(localY, surfaceLocalY); + + chunk.SetBlock(x, localY, z, block); + } + } + + private static BlockId GetBlockForLayer(int localY, int surfaceLocalY) + { + if (localY > surfaceLocalY) + return BlockId.Air; + + if (localY == 0) + return BlockId.Stone; + + int depthFromSurface = surfaceLocalY - localY; + + return depthFromSurface switch + { + 0 => BlockId.Grass, + + > 0 and <= DirtDepth => BlockId.Dirt, + + _ => BlockId.Stone + }; + } + + private static int WorldYToLocalY(ChunkGenerationContext context, int worldY) + { + return worldY - context.MinY; + } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/Layers/CavePass.cs b/Examples/MinecraftClone/Terrain/Layers/CavePass.cs new file mode 100644 index 0000000..6239107 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Layers/CavePass.cs @@ -0,0 +1,54 @@ +using Minecraft.Terrain.Block; + +namespace Minecraft.Terrain.Layers; + +public sealed class CavePass : IChunkGenerationLayer +{ + private const float CaveThreshold = 0.68f; + + public void Generate(ChunkGenerationContext context, ChunkData chunk) + { + for (int x = 0; x < chunk.Size; x++) + { + for (int y = 0; y < chunk.Height; y++) + { + for (int z = 0; z < chunk.Size; z++) + { + TryCarveBlock(context, chunk, x, y, z); + } + } + } + } + + private static void TryCarveBlock( + ChunkGenerationContext context, + ChunkData chunk, + int localX, + int localY, + int localZ) + { + if (localY == 0) + return; + + BlockId block = chunk.GetBlock(localX, localY, localZ); + + if (block != BlockId.Stone) + return; + + float worldX = context.ChunkPosition.X + localX; + float worldY = LocalYToWorldY(context, localY); + float worldZ = context.ChunkPosition.Z + localZ; + + float caveNoise = context.CaveNoise.Sample3D(worldX, worldY, worldZ); + + if (caveNoise <= CaveThreshold) + return; + + chunk.SetBlock(localX, localY, localZ, BlockId.Air); + } + + private static int LocalYToWorldY(ChunkGenerationContext context, int localY) + { + return context.MinY + localY; + } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/Layers/HeightMapPass.cs b/Examples/MinecraftClone/Terrain/Layers/HeightMapPass.cs new file mode 100644 index 0000000..cffdedc --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Layers/HeightMapPass.cs @@ -0,0 +1,26 @@ +namespace Minecraft.Terrain.Layers; + +internal class HeightMapPass : IChunkGenerationLayer +{ + public void Generate(ChunkGenerationContext context, ChunkData chunk) + { + for (int x = 0; x < chunk.Size; x++) + { + for (int z = 0; z < chunk.Size; z++) + { + float worldX = context.ChunkPosition.X + x; + float worldZ = context.ChunkPosition.Z + z; + + float noise = context.TerrainNoise.Sample2D(worldX, worldZ); + + int minSurfaceY = 0; + int maxSurfaceY = 16; + + int surfaceY = minSurfaceY + + (int)(noise * (maxSurfaceY - minSurfaceY)); + + chunk.SetHeight(x, z, surfaceY); + } + } + } +} diff --git a/Examples/MinecraftClone/Terrain/TerrainGenerator.cs b/Examples/MinecraftClone/Terrain/TerrainGenerator.cs new file mode 100644 index 0000000..b71e3e9 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/TerrainGenerator.cs @@ -0,0 +1,158 @@ +using Minecraft.Terrain.Block; +using SharpEngine.Core.Entities; +using SharpEngine.Core.Entities.Lights; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Numerics.Noise; +using SharpEngine.Core.Scenes; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Minecraft.Terrain; + +public sealed class TerrainGenerator_New +{ + private readonly INoiseGenerator _terrainNoise = new PerlinNoiseGenerator(seed: 1234) + { + Scale = 32f, + Octaves = 5, + Persistence = 0.45f, + Lacunarity = 2f + }; + + private readonly INoiseGenerator _caveNoise = new PerlinNoiseGenerator(seed: 5678) + { + Scale = 18f, + Octaves = 3, + Persistence = 0.5f, + Lacunarity = 2f + }; + + // TODO: Tweak + private readonly INoiseGenerator _oreNoise = new PerlinNoiseGenerator(seed: 1234) + { + Scale = 32f, + Octaves = 5, + Persistence = 0.45f, + Lacunarity = 2f + }; + + // TODO: Tweak + private readonly INoiseGenerator _foliageNoise = new PerlinNoiseGenerator(seed: 5678) + { + Scale = 18f, + Octaves = 3, + Persistence = 0.5f, + Lacunarity = 2f + }; + + private readonly IReadOnlyList _layers; + + private readonly Scene _scene; + + private SceneNode _lightsNode; + private SceneNode _blocksNode; + + internal TerrainGenerator_New(Scene scene, SceneNode blocksNode, IEnumerable? layers = null) + { + _scene = scene; + _lightsNode = _scene.AddNode("Lights"); + _blocksNode = blocksNode; + _layers = layers?.ToArray() ?? Array.Empty(); + } + + internal void InitializeWorld() + { + InitializeLights(); + InitializeChunks(); + } + + private void InitializeLights() + { + _lightsNode.AddChild(new DirectionalLight()); + + _lightsNode.AddChild( + new PointLight(new Vector3(0.7f, 0.2f, 2.0f), 0), + new PointLight(new Vector3(2.3f, -3.3f, -4.0f), 1), + new PointLight(new Vector3(-4.0f, 2.0f, -12.0f), 2), + new PointLight(new Vector3(0.0f, 0.0f, -3.0f), 3) + ); + + _lightsNode.AddChild(new SpotLight() + { + Ambient = new Vector3(0.0f, 0.0f, 0.0f), + Diffuse = new Vector3(1.0f, 1.0f, 1.0f), + Specular = new Vector3(1.0f, 1.0f, 1.0f), + }); + } + + private void InitializeChunks() + { + const int chunkSize = 16; + const int numChunks = 5; + + for (int chunkZ = 0; chunkZ < numChunks; chunkZ++) + { + for (int chunkX = 0; chunkX < numChunks; chunkX++) + { + var chunkPos = new Vector3(chunkX * chunkSize, 0, chunkZ * chunkSize); + + var context = new ChunkGenerationContext + { + Seed = 1234, + ChunkPosition = chunkPos, + ChunkSize = chunkSize, + MinY = -16, + MaxY = 16, + WaterLevel = 0, + + TerrainNoise = _terrainNoise, + CaveNoise = _caveNoise, + OreNoise = _oreNoise, + FoliageNoise = _foliageNoise + }; + + ChunkData chunk = GenerateChunk(context); + + CreateBlocksFromChunkData(chunk, chunkPos); + } + } + } + + public ChunkData GenerateChunk(ChunkGenerationContext context) + { + int height = context.MaxY - context.MinY + 1; + + var chunk = new ChunkData(context.ChunkSize, height); + + foreach (IChunkGenerationLayer layer in _layers) + { + layer.Generate(context, chunk); + } + + return chunk; + } + + private void CreateBlocksFromChunkData(ChunkData chunk, Vector3 chunkPos) + { + for (int x = 0; x < chunk.Size; x++) + { + for (int y = 0; y < chunk.Height; y++) + { + for (int z = 0; z < chunk.Size; z++) + { + BlockId block = chunk.GetBlock(x, y, z); + + if (block == BlockId.Air) + continue; + + var blockPos = chunkPos + new Vector3(x, y, z); + + var blockObject = BlockFactory.CreateBlock(block, blockPos, $"Block_{blockPos.X}_{blockPos.Y}_{blockPos.Z}"); + + _blocksNode.AddChild(blockObject); + } + } + } + } +} \ No newline at end of file diff --git a/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs b/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs new file mode 100644 index 0000000..84ee4f0 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs @@ -0,0 +1,145 @@ +using Minecraft.Terrain.Block; +using SharpEngine.Core.Entities.Lights; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Numerics.Noise; +using SharpEngine.Core.Scenes; + +namespace Minecraft.Terrain; + +internal class TerrainGenerator_Old +{ + private readonly INoiseGenerator _terrainNoise = new PerlinNoiseGenerator(seed: 1234) + { + Scale = 32f, + Octaves = 5, + Persistence = 0.45f, + Lacunarity = 2f + }; + + private readonly INoiseGenerator _caveNoise = new PerlinNoiseGenerator(seed: 5678) + { + Scale = 18f, + Octaves = 3, + Persistence = 0.5f, + Lacunarity = 2f + }; + + private readonly Scene _scene; + + private SceneNode _lightsNode; + private SceneNode _blocksNode; + + internal TerrainGenerator_Old(Scene scene, SceneNode blocksNode) + { + _scene = scene; + _lightsNode = _scene.AddNode("Lights"); + _blocksNode = blocksNode; + } + + internal void InitializeWorld() + { + InitializeLights(); + InitializeChunks(); + } + + private void InitializeLights() + { + _lightsNode.AddChild(new DirectionalLight()); + + _lightsNode.AddChild( + new PointLight(new Vector3(0.7f, 0.2f, 2.0f), 0), + new PointLight(new Vector3(2.3f, -3.3f, -4.0f), 1), + new PointLight(new Vector3(-4.0f, 2.0f, -12.0f), 2), + new PointLight(new Vector3(0.0f, 0.0f, -3.0f), 3) + ); + + _lightsNode.AddChild(new SpotLight() + { + Ambient = new Vector3(0.0f, 0.0f, 0.0f), + Diffuse = new Vector3(1.0f, 1.0f, 1.0f), + Specular = new Vector3(1.0f, 1.0f, 1.0f), + }); + } + + private void InitializeChunks() + { + // TODO: #88 Generate chunks when player moves + + // TODO: #87 Generate chunks using 3d Perlin noise + + const int chunkSize = 16; + const int numChunks = 1; + // const int numChunks = 3; + + for (int i = 0; i < numChunks; i++) + { + var chunkPos = new Vector3(i * chunkSize, 0, 0); + GenerateChunk(chunkSize, chunkPos); + } + } + + private void GenerateChunk(int chunkSize, Vector3 chunkPos) + { + const int minTerrainHeight = 4; + const int maxTerrainHeight = 16; + + const int dirtDepth = 3; + + const int lowestY = -16; + + const float caveThreshold = 0.68f; + + for (int x = 0; x < chunkSize; x++) + { + for (int z = 0; z < chunkSize; z++) + { + float worldX = chunkPos.X + x; + float worldZ = chunkPos.Z + z; + + float terrainNoise = _terrainNoise.Sample2D(worldX, worldZ); + + int columnHeight = minTerrainHeight + + (int)(terrainNoise * (maxTerrainHeight - minTerrainHeight)); + + int surfaceY = (int)chunkPos.Y + columnHeight - 1; + int bottomY = (int)chunkPos.Y + lowestY; + + for (int worldY = surfaceY; worldY >= bottomY; worldY--) + { + var blockPos = new Vector3(worldX, worldY, worldZ); + + int depthFromSurface = surfaceY - worldY; + + if (depthFromSurface == 0) + { + var grass = new Grass(blockPos, $"Grass ({worldX}, {worldY}, {worldZ})"); + _blocksNode.AddChild(grass); + + continue; + } + + if (depthFromSurface <= dirtDepth) + { + var dirt = new Dirt(blockPos, $"Dirt ({worldX}, {worldY}, {worldZ})"); + _blocksNode.AddChild(dirt); + + continue; + } + + bool isLowestLevel = worldY == bottomY; + + if (!isLowestLevel) + { + float caveNoise = _caveNoise.Sample3D(worldX, worldY, worldZ); + + if (caveNoise > caveThreshold) + continue; + } + + var stone = new Stone(blockPos, $"Stone ({worldX}, {worldY}, {worldZ})"); + _blocksNode.AddChild(stone); + } + } + } + } +} diff --git a/Examples/MinecraftClone/Terrain/terrain_layering_pipeline.md b/Examples/MinecraftClone/Terrain/terrain_layering_pipeline.md new file mode 100644 index 0000000..a971e31 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/terrain_layering_pipeline.md @@ -0,0 +1,478 @@ +# Terrain Layering and Generation Pipeline + +This document describes a scalable terrain layering approach for chunk-based world generation. + +The goal is to avoid placing every terrain rule directly inside one large block loop. Instead, terrain generation should be treated as a sequence of independent generation passes. + +Each pass has one responsibility and modifies lightweight chunk/block data before the final renderable blocks or meshes are created. + +--- + +## Core Idea + +Terrain generation should be split into two main concerns: + +```text +Terrain shape + Determines where terrain exists. + +Terrain materials + Determines what each block should be. +``` + +For example: + +```text +Shape: + - Column height + - Cave positions + - Terrain slope + - Sea level + - Mountain regions + +Materials: + - Grass + - Dirt + - Stone + - Ore + - Water + - Foliage +``` + +This makes terrain easier to extend later with features such as ores, trees, flowers, water, snow, sand, or biome-specific surfaces. + +--- + +## Recommended Generation Pipeline + +```mermaid +flowchart TD + A[Chunk Coordinates] --> B[Height Map Pass] + + B --> C[Base Terrain Pass] + C --> C1[Grass Surface Layer] + C --> C2[Dirt Subsurface Layer] + C --> C3[Stone Underground Layer] + C --> C4[Bottom Stone / Bedrock Layer] + + C1 --> D[Chunk Block Data] + C2 --> D + C3 --> D + C4 --> D + + D --> E[Cave Pass] + E --> E1[Carve Only Stone] + E1 --> F[Ore Pass] + + F --> F1[Replace Stone With Ore] + F1 --> G[Water Pass] + + G --> G1[Fill Empty Blocks Below Water Level] + G1 --> H[Surface Decoration Pass] + + H --> H1[Grass Tufts] + H --> H2[Flowers] + H --> H3[Rocks] + + H1 --> I[Foliage Pass] + H2 --> I + H3 --> I + + I --> I1[Trees] + I --> I2[Bushes] + I --> I3[Large Plants] + + I1 --> J[Finalize Chunk] + I2 --> J + I3 --> J + + J --> K[Mesh / GameObject Creation] +``` + +--- + +## Layer Responsibilities + +### 1. Height Map Pass + +The height map pass decides the surface height for each `x/z` column. + +It should answer: + +```text +How tall is the terrain at this world position? +``` + +Typical inputs: + +- World seed +- World `x/z` position +- 2D noise +- Biome data +- Terrain settings + +Example output: + +```text +Column at x=10, z=4 has surfaceY = 12 +``` + +The height map pass should not decide whether a block is grass, dirt, stone, ore, or water. It only defines the terrain shape. + +--- + +### 2. Base Terrain Pass + +The base terrain pass creates the main solid terrain layers. + +Recommended rule: + +```text +Highest solid block = Grass +3 blocks below surface = Dirt +Everything below that = Stone +Lowest level = Stone or Bedrock +``` + +Conceptually: + +```text +surfaceY Grass +surfaceY - 1 Dirt +surfaceY - 2 Dirt +surfaceY - 3 Dirt +surfaceY - 4 Stone +surfaceY - 5 Stone +... +bottomY Stone / Bedrock +``` + +This pass should create predictable base terrain before caves, ores, water, or decorations are added. + +--- + +### 3. Cave Pass + +The cave pass removes underground blocks based on 3D noise. + +Important rule: + +```text +Caves should only carve stone. +``` + +This prevents caves from cutting holes through grass and dirt surface layers unless that is intentionally desired. + +Recommended cave rules: + +```text +If block is Stone +And block is not the lowest layer +And cave noise is above threshold +Then replace block with Air +``` + +The lowest layer should usually remain solid to prevent chunks from having holes through the bottom. + +--- + +### 4. Ore Pass + +The ore pass replaces some stone blocks with ore blocks. + +Important rule: + +```text +Ores should replace stone, not grass or dirt. +``` + +Example rules: + +```text +Coal: + Allowed in stone + Common + Wide Y range + +Iron: + Allowed in stone + Medium rarity + Mid/deep Y range + +Rare ore: + Allowed in stone + Low rarity + Deep Y range +``` + +Ore generation can use: + +- 3D noise +- Random chance +- Y-level ranges +- Vein size rules +- Biome-specific modifiers + +--- + +### 5. Water Pass + +The water pass fills empty spaces below a configured water level. + +Recommended rule: + +```text +If block is Air +And y is below or equal to waterLevel +Then place Water +``` + +This should usually happen after caves are carved, because caves below sea level may become flooded. + +Water generation may also consider: + +- Oceans +- Lakes +- Rivers +- Underground water +- Biome rules + +--- + +### 6. Surface Decoration Pass + +The surface decoration pass places small objects on top of terrain. + +Examples: + +- Grass tufts +- Flowers +- Pebbles +- Mushrooms +- Snow layers +- Fallen sticks + +Recommended rule: + +```text +If block is Grass +And space above is Air +And decoration noise/random allows it +Then place decoration above the grass block +``` + +Surface decorations should usually not replace the terrain block itself. They usually occupy the air block above the surface. + +--- + +### 7. Foliage Pass + +The foliage pass handles larger vegetation. + +Examples: + +- Trees +- Bushes +- Large plants +- Cacti +- Reeds + +Tree placement usually needs more checks than small decorations: + +```text +If block is Grass +And enough vertical space exists +And enough horizontal space exists +And tree noise/random allows it +Then place tree +``` + +Large foliage should be generated late because it may occupy several blocks. + +--- + +## Recommended Pass Order + +A practical order is: + +```text +1. Height Map Pass +2. Base Terrain Pass +3. Cave Pass +4. Bottom Layer Pass +5. Ore Pass +6. Water Pass +7. Surface Decoration Pass +8. Foliage Pass +9. Final Mesh / GameObject Creation +``` + +A more abstract version: + +```text +Shape +Material +Carving +Replacement +Fluid +Decoration +Finalization +``` + +--- + +## Why Use Passes? + +Using generation passes keeps each system focused. + +Instead of one huge method containing every rule: + +```text +GenerateChunk() + handles grass + handles dirt + handles stone + handles caves + handles ores + handles water + handles trees + handles flowers +``` + +Prefer: + +```text +HeightMapPass +BaseTerrainPass +CavePass +OrePass +WaterPass +DecorationPass +FoliagePass +``` + +This makes the world generator easier to extend, debug, test, and tune. + +--- + +## Recommended Data Flow + +```mermaid +flowchart LR + A[World Seed] --> B[Generation Settings] + B --> C[Chunk Data] + C --> D[Generation Passes] + D --> E[Block IDs] + E --> F[Chunk Mesh] + E --> G[Optional GameObjects] +``` + +The generator should ideally operate on lightweight block data first. + +For example: + +```text +BlockId.Grass +BlockId.Dirt +BlockId.Stone +BlockId.Air +BlockId.Water +BlockId.IronOre +``` + +After generation is complete, the engine can convert the chunk data into: + +- A mesh +- Renderable chunk objects +- Physics colliders +- Optional `GameObject` instances for interactive blocks + +This is usually better than creating one full `GameObject` for every block during terrain generation. + +--- + +## Mental Model: Masks + +A useful way to think about terrain features is through masks. + +A mask defines where something is allowed to exist. + +Examples: + +```text +Grass mask: + Only the highest solid block in a column + +Dirt mask: + Only the first 3 blocks below grass + +Stone mask: + Everything below dirt + +Cave mask: + Only stone blocks, excluding the bottom layer + +Ore mask: + Only stone blocks within allowed Y ranges + +Water mask: + Empty space below water level + +Tree mask: + Grass surface, enough empty space above, suitable random/noise value +``` + +This avoids messy conditionals and makes each terrain feature easier to reason about. + +--- + +## Suggested Future Interfaces + +Eventually, the terrain generator could be modeled around generation passes: + +```csharp +public interface IChunkGenerationPass +{ + void Generate(ChunkGenerationContext context, ChunkData chunk); +} +``` + +Example passes: + +```csharp +public sealed class HeightMapPass : IChunkGenerationPass; +public sealed class BaseTerrainPass : IChunkGenerationPass; +public sealed class CavePass : IChunkGenerationPass; +public sealed class OrePass : IChunkGenerationPass; +public sealed class WaterPass : IChunkGenerationPass; +public sealed class FoliagePass : IChunkGenerationPass; +``` + +The chunk generator then becomes a pipeline: + +```csharp +foreach (IChunkGenerationPass pass in _passes) +{ + pass.Generate(context, chunk); +} +``` + +--- + +## Summary + +Use a terrain generation pipeline where each layer or feature is handled by a dedicated pass. + +Recommended structure: + +```text +Height map decides shape. +Base terrain creates grass, dirt and stone. +Caves carve only stone. +Ores replace stone. +Water fills empty air below water level. +Decorations and foliage are placed last. +Final chunk data is converted into renderable meshes or objects. +``` + +This keeps the system flexible enough to support future terrain features such as foliage, water, ores, biomes, snow, rivers, and structures. diff --git a/SharpEngine.Core/Abstractions/IGame.cs b/SharpEngine.Core/Abstractions/IGame.cs index 0fad3ee..73662b9 100644 --- a/SharpEngine.Core/Abstractions/IGame.cs +++ b/SharpEngine.Core/Abstractions/IGame.cs @@ -1,7 +1,9 @@ using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Enums; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; + using Silk.NET.Input; namespace SharpEngine.Core.Interfaces; @@ -21,7 +23,7 @@ public abstract class Game public Game() { Scene = new Scene(); - Camera = new(new System.Numerics.Vector3(0), new DefaultViewSettings()); + Camera = new(new Vector3(0), new DefaultViewSettings()); } /// @@ -33,7 +35,7 @@ public Game() public Game(Scene scene) { Scene = scene; - Camera = new(new System.Numerics.Vector3(0), new DefaultViewSettings()); + Camera = new(new Vector3(0), new DefaultViewSettings()); } /// diff --git a/SharpEngine.Core/Entities/Views/CameraView.cs b/SharpEngine.Core/Entities/Views/CameraView.cs index 16ea0fb..df06475 100644 --- a/SharpEngine.Core/Entities/Views/CameraView.cs +++ b/SharpEngine.Core/Entities/Views/CameraView.cs @@ -1,7 +1,9 @@ using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Shaders; -using System.Numerics; + using Plane = System.Numerics.Plane; +using Matrix4x4 = System.Numerics.Matrix4x4; namespace SharpEngine.Core.Entities.Views; @@ -103,7 +105,7 @@ public float Fov /// /// The view matrix. public override Matrix4x4 GetViewMatrix() - => Matrix4x4.CreateLookAt(Position, Position + _front, _up); + => Matrix4x4.CreateLookAt((System.Numerics.Vector3)Position, (System.Numerics.Vector3)(Position + _front), (System.Numerics.Vector3)_up); /// /// Gets the projection matrix of the camera. diff --git a/SharpEngine.Core/Entities/Views/View.cs b/SharpEngine.Core/Entities/Views/View.cs index b037f4e..aac388b 100644 --- a/SharpEngine.Core/Entities/Views/View.cs +++ b/SharpEngine.Core/Entities/Views/View.cs @@ -1,9 +1,10 @@ using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; using System; -using System.Numerics; +using Matrix4x4 = System.Numerics.Matrix4x4; namespace SharpEngine.Core.Entities.Views; diff --git a/SharpEngine.Core/Ray.cs b/SharpEngine.Core/Ray.cs index 942346b..1084cfd 100644 --- a/SharpEngine.Core/Ray.cs +++ b/SharpEngine.Core/Ray.cs @@ -1,10 +1,10 @@ using SharpEngine.Core.Entities; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; namespace SharpEngine.Core; diff --git a/SharpEngine.Core/Windowing/Window.cs b/SharpEngine.Core/Windowing/Window.cs index fca661d..5b9e734 100644 --- a/SharpEngine.Core/Windowing/Window.cs +++ b/SharpEngine.Core/Windowing/Window.cs @@ -22,7 +22,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Numerics; +using SharpEngine.Core.Numerics; namespace SharpEngine.Core.Windowing; @@ -364,7 +364,7 @@ protected void OnUpdateFrame(Frame frame) var mouse = Input?.Mice[0]; if (mouse is not null) { - Camera.UpdateMousePosition(mouse.Position); + Camera.UpdateMousePosition((Vector2)mouse.Position); OnHandleMouse?.Invoke(mouse); } From 86ee4d9a9c53269c48a39d78ab1596babee892e6 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 28 Jun 2026 20:58:56 +0300 Subject: [PATCH 07/10] #2 Obj Loader improvements (#138) * #2 Object loader working (WIP) Some execution paths still can be improved * #2 Add unhandled object and smoothing group parses --------- Co-authored-by: Antti Veikkolainen --- .../Tutorial 4.1 - Model Loading/Program.cs | 9 +-- ObjLoader/DataStore.cs | 33 ++++++++++- ObjLoader/IObjectNameDataStore.cs | 13 +++++ ObjLoader/ISmoothingGroupDataStore.cs | 18 ++++++ ObjLoader/Loaders/LoaderBase.cs | 32 +++++++++-- .../MaterialLoader/MaterialLibraryLoader.cs | 5 +- ObjLoader/Loaders/ObjLoader/FileManager.cs | 15 +++++ ObjLoader/Loaders/ObjLoader/ObjLoader.cs | 29 ++++++++-- .../Loaders/ObjLoader/ObjLoaderFactory.cs | 55 ++++++++++++------- ObjLoader/TypeParsers/FaceParser.cs | 15 +++-- ObjLoader/TypeParsers/GroupParser.cs | 7 ++- ObjLoader/TypeParsers/ITypeParser.cs | 5 +- .../TypeParsers/MaterialLibraryParser.cs | 6 +- ObjLoader/TypeParsers/NormalParser.cs | 4 +- ObjLoader/TypeParsers/ObjectNameParser.cs | 33 +++++++++++ ObjLoader/TypeParsers/SmoothingGroupParser.cs | 45 +++++++++++++++ ObjLoader/TypeParsers/TextureParser.cs | 4 +- ObjLoader/TypeParsers/TypeParserBase.cs | 2 +- ObjLoader/TypeParsers/UseMaterialParser.cs | 9 ++- ObjLoader/TypeParsers/VertexParser.cs | 4 +- .../Properties/Meshes/Model.cs | 10 +++- SharpEngine.Core/Abstractions/ISettings.cs | 11 +++- .../Views/Settings/DefaultViewSettings.cs | 6 +- .../Entities/Views/Settings/ViewSettings.cs | 6 +- 24 files changed, 314 insertions(+), 62 deletions(-) create mode 100644 ObjLoader/IObjectNameDataStore.cs create mode 100644 ObjLoader/ISmoothingGroupDataStore.cs create mode 100644 ObjLoader/Loaders/ObjLoader/FileManager.cs create mode 100644 ObjLoader/TypeParsers/ObjectNameParser.cs create mode 100644 ObjLoader/TypeParsers/SmoothingGroupParser.cs diff --git a/Examples/Tutorial 4.1 - Model Loading/Program.cs b/Examples/Tutorial 4.1 - Model Loading/Program.cs index a3e0cad..8f974e5 100644 --- a/Examples/Tutorial 4.1 - Model Loading/Program.cs +++ b/Examples/Tutorial 4.1 - Model Loading/Program.cs @@ -85,10 +85,11 @@ private static void OnLoad() Shader = new Shader(Gl, PathExtensions.GetAssemblyPath("shader2.vert"), Default.LightShader, "test"); Texture = new Texture(Gl, "silk.png"); - var model = ObjLoaderFactory.Load(Gl, "Untitled2.obj"); - - _models.Add(model); - _models.Add(model); + if (ObjLoaderFactory.Load(Gl, "D:\\Untitled2.obj", out var model)) + { + _models.Add(model!); + _models.Add(model!); + } } private static void OnUpdate(double deltaTime) diff --git a/ObjLoader/DataStore.cs b/ObjLoader/DataStore.cs index ec82bc1..1840908 100644 --- a/ObjLoader/DataStore.cs +++ b/ObjLoader/DataStore.cs @@ -9,14 +9,23 @@ namespace SharpEngine.Core.ObjLoader { /// - /// Stores parsed OBJ file data such as vertices, normals, texture coordinates, materials and groups. - /// This type is used by the OBJ loader type parsers to collect geometry and material information. + /// Represents the smoothing group information for a face group. /// + /// Determines whether smoothing is enabled for the face group. + /// The ID of the smoothing group, if applicable. + public readonly record struct SmoothingGroup(bool Enabled, int? GroupId); + + /// /// Stores parsed OBJ file data such as vertices, normals, texture coordinates, materials and groups. /// This type is used by the OBJ loader type parsers to collect geometry and material information. /// - public class DataStore : IGroupDataStore, IFaceGroup, ITextureDataStore, INormalDataStore, IVertexDataStore, IMaterialDataStore + /// + /// TODO: This is probably not implemented corretly. + /// We are not handling things on a per-object basis and we are not handling smoothing groups correctly. + /// We should probably have a more complex data structure that can handle multiple objects, each with their own groups, materials, and smoothing groups. + /// + public class DataStore : IGroupDataStore, IFaceGroup, ITextureDataStore, INormalDataStore, IVertexDataStore, IMaterialDataStore, ISmoothingGroupDataStore, IObjectNameDataStore { private Group _currentGroup = null!; @@ -35,6 +44,12 @@ public class DataStore : IGroupDataStore, IFaceGroup, ITextureDataStore, INormal /// Gets the list of groups present in the file. public List Groups { get; } = []; + /// Gets the list of smoothing groups defined in the file. + public List SmoothingGroups { get; } = []; + + /// Gets the name of the object defined in the file, if any. + public string ObjectName { get; private set; } = string.Empty; + /// /// Initializes a new instance of and creates a default group. /// @@ -83,5 +98,17 @@ public void AddVertex(Vertex vertex) /// public void AddMaterial(Material currentMaterial) => Materials.Add(currentMaterial); + + /// + public void SetSmoothingGroup(int groupNumber) + => SmoothingGroups.Add(new SmoothingGroup(true, groupNumber)); + + /// + public void SetSmoothingGroupOff() + => SmoothingGroups.Add(new SmoothingGroup(false, null)); + + /// + public void SetObjectName(string name) + => ObjectName = name; } } \ No newline at end of file diff --git a/ObjLoader/IObjectNameDataStore.cs b/ObjLoader/IObjectNameDataStore.cs new file mode 100644 index 0000000..44806a0 --- /dev/null +++ b/ObjLoader/IObjectNameDataStore.cs @@ -0,0 +1,13 @@ +namespace SharpEngine.Core.ObjLoader; + +/// +/// Holds the name of the object being parsed, allowing it to be set during parsing and retrieved later. +/// +public interface IObjectNameDataStore +{ + /// + /// Sets the name of the object being parsed. + /// + /// The name of the object. + void SetObjectName(string name); +} \ No newline at end of file diff --git a/ObjLoader/ISmoothingGroupDataStore.cs b/ObjLoader/ISmoothingGroupDataStore.cs new file mode 100644 index 0000000..8afc13c --- /dev/null +++ b/ObjLoader/ISmoothingGroupDataStore.cs @@ -0,0 +1,18 @@ +namespace SharpEngine.Core.ObjLoader; + +/// +/// Represents a data store for smoothing group information in an OBJ file, allowing the storage of smoothing group lines for later processing. +/// +public interface ISmoothingGroupDataStore +{ + /// + /// Sets the current smoothing group to the specified group number. + /// + /// The smoothing group number. + void SetSmoothingGroup(int groupNumber); + + /// + /// Turns off the current smoothing group. + /// + void SetSmoothingGroupOff(); +} \ No newline at end of file diff --git a/ObjLoader/Loaders/LoaderBase.cs b/ObjLoader/Loaders/LoaderBase.cs index 5253b8f..206c6d5 100644 --- a/ObjLoader/Loaders/LoaderBase.cs +++ b/ObjLoader/Loaders/LoaderBase.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; using System.IO; using System.IO.Abstractions; @@ -11,6 +13,8 @@ public abstract class LoaderBase private readonly IFileStreamFactory _fileStreamFactory; private readonly IFileManager _fileManager; + private readonly ILogger _logger = LoggingExtensions.CreateLogger(); + /// /// Initializes a new instance of the . /// @@ -27,8 +31,15 @@ protected LoaderBase(IFileStreamFactory fileStreamFactory, IFileManager fileMana /// I/O exceptions (for example FileNotFoundException, UnauthorizedAccessException, IOException) are propagated to the caller. /// /// The path of the file to open and parse. - public void ParseFile(string path) + /// if the file was parsed successfully; otherwise, . + public bool ParseFile(string path) { + if (!_fileStreamFactory.FileSystem.File.Exists(path)) + { + _logger.LogError("File not found: {Path}", path); + return false; + } + using var fileStream = _fileStreamFactory.FileSystem.FileStream.New(path, FileMode.Open, FileAccess.Read); using var lineStreamReader = _fileManager.StreamReader(path); @@ -37,20 +48,28 @@ public void ParseFile(string path) var currentLine = lineStreamReader.ReadLine(); if (!string.IsNullOrWhiteSpace(currentLine)) - ParseLine(currentLine); + { + if (!ParseLine(currentLine)) + { + _logger.LogError("Failed to parse line: {Line}", currentLine); + // return false; + } + } } + + return true; } - private void ParseLine(string currentLine) + private bool ParseLine(string currentLine) { if (currentLine[0] == '#') - return; + return true; var fields = currentLine.Trim().Split(null, 2); var keyword = fields[0].Trim(); var data = fields[1].Trim(); - ParseLine(keyword, data); + return ParseLine(keyword, data); } /// @@ -58,6 +77,7 @@ private void ParseLine(string currentLine) /// /// Keyword that identifies the line type or command. /// Data associated with the keyword; may be empty or contain parameters or raw text to interpret. - protected abstract void ParseLine(string keyword, string data); + /// if the line was parsed successfully; otherwise, . + protected abstract bool ParseLine(string keyword, string data); } } \ No newline at end of file diff --git a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs index 9861031..b45b5a0 100644 --- a/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs +++ b/ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs @@ -74,17 +74,18 @@ private void AddParseAction(string key, Action action) => _parseActionDictionary.Add(key.ToLowerInvariant(), action); /// - protected override void ParseLine(string keyword, string data) + protected override bool ParseLine(string keyword, string data) { var parseAction = GetKeywordAction(keyword, out bool found); if (!found) { _unrecognizedLines.Add(keyword + " " + data); - return; + return false; } parseAction!(data); + return true; } private Action? GetKeywordAction(string keyword, out bool found) diff --git a/ObjLoader/Loaders/ObjLoader/FileManager.cs b/ObjLoader/Loaders/ObjLoader/FileManager.cs new file mode 100644 index 0000000..0e44ce3 --- /dev/null +++ b/ObjLoader/Loaders/ObjLoader/FileManager.cs @@ -0,0 +1,15 @@ +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using System.IO; + +namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader +{ + /// + /// Represents a testable wrapper for creating stream file related objects. + /// + public class FileManager : IFileManager + { + /// + public StreamReader StreamReader(string path) + => new(path); + } +} \ No newline at end of file diff --git a/ObjLoader/Loaders/ObjLoader/ObjLoader.cs b/ObjLoader/Loaders/ObjLoader/ObjLoader.cs index a79ac5b..fbb9b0f 100644 --- a/ObjLoader/Loaders/ObjLoader/ObjLoader.cs +++ b/ObjLoader/Loaders/ObjLoader/ObjLoader.cs @@ -1,11 +1,13 @@ -using SharpEngine.Core.Entities.Properties.Meshes; +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.ObjLoader.Loader.Loaders; using SharpEngine.Core.ObjLoader.TypeParsers; - +using SharpEngine.Telemetry; using Silk.NET.OpenGL; using System.Collections.Generic; using System.IO.Abstractions; +using System.Linq; namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader { @@ -19,6 +21,8 @@ public class ObjLoader : LoaderBase private readonly List _typeParsers = []; private readonly List _unrecognizedLines = []; + private readonly ILogger _logger = LoggingExtensions.CreateLogger(); + /// /// Initializes a new instance of with the specified file path and data store. /// @@ -46,16 +50,25 @@ public ObjLoader SetupTypeParsers(params ITypeParser[] parsers) } /// - protected override void ParseLine(string keyword, string data) + protected override bool ParseLine(string keyword, string data) { foreach (var typeParser in _typeParsers) + { if (typeParser.CanParse(keyword)) { typeParser.Parse(data); - return; + return true; } + else + { + _logger.LogDebug("Type parser {Parser} cannot parse keyword: {Keyword}", typeParser.GetType().Name, keyword); + } + } _unrecognizedLines.Add(keyword + " " + data); + _logger.LogError("Unrecognized line in OBJ file: {Line}", keyword + " " + data); + + return false; } /// @@ -64,12 +77,18 @@ protected override void ParseLine(string keyword, string data) /// The OpenGL context used to construct GPU resources. public List Load(GL gl) { - ParseFile(_path); + if (!ParseFile(_path)) + { + _logger.LogError("Failed to parse OBJ file: {Path}", _path); + return []; + } + // TODO: Handle multiple meshes and objects in the OBJ file. Currently, this implementation assumes a single mesh. return [ new(gl) { + Name = _dataStore.ObjectName, Vertices2 = _dataStore.Vertices, TextureCoordinates2 = _dataStore.Textures, Normals2 = _dataStore.Normals, diff --git a/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs b/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs index b99400c..20b7d20 100644 --- a/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs +++ b/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs @@ -1,9 +1,10 @@ +using Microsoft.Extensions.Logging; using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.ObjLoader.Loader.Loaders; using SharpEngine.Core.ObjLoader.Loader.TypeParsers; using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; - +using SharpEngine.Core.ObjLoader.TypeParsers; +using SharpEngine.Telemetry; using Silk.NET.OpenGL; using System; @@ -13,16 +14,6 @@ namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader { - /// - /// Represents a testable wrapper for creating stream file related objects. - /// - public class FileManager : IFileManager - { - /// - public StreamReader StreamReader(string path) - => new(path); - } - /// /// Handles loading 3D models. /// @@ -31,6 +22,8 @@ public static class ObjLoaderFactory private static readonly FileSystem _fileSystem; private static readonly FileManager _fileManager; + private readonly static ILogger _logger = LoggingExtensions.CreateLogger(typeof(ObjLoaderFactory)); + static ObjLoaderFactory() { _fileSystem = new FileSystem(); @@ -45,15 +38,33 @@ static ObjLoaderFactory() /// /// 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. /// 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 Load(GL gl, string path) - => Path.GetExtension(path) switch + /// Thrown when no meshes are loaded. + public static bool Load(GL gl, string path, out Model? model) + { + try + { + string fileExtension = Path.GetExtension(path); + model = fileExtension switch + { + FbxExtension => LoadFbx("", path), + ObjExtension => LoadObj(gl, path), + _ => throw new NotSupportedException($"The file extension {fileExtension} is not a supported mesh file.") + }; + + _logger.LogDebug("Successfully loaded model '{name}' from path: {path}.", model.Name, path); + return true; + } + catch (Exception ex) { - FbxExtension => LoadFbx("", path), - ObjExtension => LoadObj(gl, path), - _ => throw new NotSupportedException($"The file extension {Path.GetExtension(path)} is not a supported mesh file.") - }; + _logger.LogWarning(ex, "Failed to load model from path: {path}.", path); + + model = null; + return false; + } + } // TODO: #3 Load fbx mesh from file private static Model LoadFbx(string identifier, string meshFilePath) @@ -64,6 +75,10 @@ private static Model LoadFbx(string identifier, string meshFilePath) private static Model LoadObj(GL gl, string path) { var meshes = LoadObjMeshes(gl, path); + + if (meshes.Count == 0) + throw new InvalidOperationException("The .obj file contains no meshes."); + return new Model(gl, path, meshes); } @@ -76,6 +91,8 @@ private static List LoadObjMeshes(GL gl, string path) var normalParser = new NormalParser(dataStore); var textureParser = new TextureParser(dataStore); var vertexParser = new VertexParser(dataStore); + var smoothingGroupParser = new SmoothingGroupParser(dataStore); + var objectNameParser = new ObjectNameParser(dataStore); var materialLibraryLoader = new MaterialLibraryLoader(dataStore, _fileSystem.FileStream, _fileManager); @@ -84,7 +101,7 @@ private static List LoadObjMeshes(GL gl, string path) var useMaterialParser = new UseMaterialParser(dataStore); var loader = new ObjLoader(path, dataStore, _fileSystem.FileStream, _fileManager) - .SetupTypeParsers(faceParser, groupParser, normalParser, textureParser, vertexParser, materialLibraryParser, useMaterialParser); + .SetupTypeParsers(faceParser, groupParser, normalParser, textureParser, vertexParser, materialLibraryParser, useMaterialParser, smoothingGroupParser, objectNameParser); return loader.Load(gl); } diff --git a/ObjLoader/TypeParsers/FaceParser.cs b/ObjLoader/TypeParsers/FaceParser.cs index 0df5e68..83581d5 100644 --- a/ObjLoader/TypeParsers/FaceParser.cs +++ b/ObjLoader/TypeParsers/FaceParser.cs @@ -27,7 +27,7 @@ public FaceParser(IFaceGroup faceGroup) protected override string Keyword => "f"; /// - public override void Parse(string line) + public override bool Parse(string line) { string[] vertices = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); @@ -35,19 +35,22 @@ public override void Parse(string line) foreach (string vertexString in vertices) { - var faceVertex = ParseFaceVertex(vertexString); - face.AddVertex(faceVertex); + if (TryParseFaceVertex(vertexString, out FaceVertex faceVertex)) + { + face.AddVertex(faceVertex); + } } _faceGroup.AddFace(face); + return true; } - private static FaceVertex ParseFaceVertex(string vertexString) + private static bool TryParseFaceVertex(string vertexString, out FaceVertex faceVertex) { string[] fields = vertexString.Split('/', StringSplitOptions.None); int vertexIndex = fields[0].ParseInvariantInt(); - var faceVertex = new FaceVertex(vertexIndex, 0, 0); + faceVertex = new FaceVertex(vertexIndex, 0, 0); if (fields.Length > 1) { @@ -61,7 +64,7 @@ private static FaceVertex ParseFaceVertex(string vertexString) faceVertex.NormalIndex = normalIndex; } - return faceVertex; + return true; } } } \ No newline at end of file diff --git a/ObjLoader/TypeParsers/GroupParser.cs b/ObjLoader/TypeParsers/GroupParser.cs index e5dae69..0934906 100644 --- a/ObjLoader/TypeParsers/GroupParser.cs +++ b/ObjLoader/TypeParsers/GroupParser.cs @@ -23,7 +23,10 @@ public GroupParser(IGroupDataStore dataStore) protected override string Keyword => "g"; /// - public override void Parse(string line) - => _dataStore.PushGroup(line); + public override bool Parse(string line) + { + _dataStore.PushGroup(line); + return true; + } } } \ No newline at end of file diff --git a/ObjLoader/TypeParsers/ITypeParser.cs b/ObjLoader/TypeParsers/ITypeParser.cs index 1b041ba..3fd685b 100644 --- a/ObjLoader/TypeParsers/ITypeParser.cs +++ b/ObjLoader/TypeParsers/ITypeParser.cs @@ -9,13 +9,14 @@ public interface ITypeParser /// Determines whether the specified keyword can be parsed. /// /// The keyword to check. - /// True if the keyword can be parsed; otherwise, false. + /// if the keyword can be parsed; otherwise, . bool CanParse(string keyword); /// /// Parses a single input line and updates the object's state accordingly. /// /// The input line to parse. - void Parse(string line); + /// if the line was successfully parsed; otherwise, . + bool Parse(string line); } } \ No newline at end of file diff --git a/ObjLoader/TypeParsers/MaterialLibraryParser.cs b/ObjLoader/TypeParsers/MaterialLibraryParser.cs index f622cc8..7be0889 100644 --- a/ObjLoader/TypeParsers/MaterialLibraryParser.cs +++ b/ObjLoader/TypeParsers/MaterialLibraryParser.cs @@ -23,6 +23,10 @@ public MaterialLibraryParser(IMaterialLibraryLoaderFacade facade) protected override string Keyword => "mtllib"; /// - public override void Parse(string line) => _libraryLoaderFacade.Load(line); + public override bool Parse(string line) + { + _libraryLoaderFacade.Load(line); + return true; + } } } \ No newline at end of file diff --git a/ObjLoader/TypeParsers/NormalParser.cs b/ObjLoader/TypeParsers/NormalParser.cs index af7974b..111800b 100644 --- a/ObjLoader/TypeParsers/NormalParser.cs +++ b/ObjLoader/TypeParsers/NormalParser.cs @@ -25,7 +25,7 @@ public NormalParser(INormalDataStore dataStore) protected override string Keyword => "vn"; /// - public override void Parse(string line) + public override bool Parse(string line) { string[] parts = line.Split(' '); @@ -35,6 +35,8 @@ public override void Parse(string line) var normal = new Normal(x, y, z); _dataStore.AddNormal(normal); + + return true; } } } \ No newline at end of file diff --git a/ObjLoader/TypeParsers/ObjectNameParser.cs b/ObjLoader/TypeParsers/ObjectNameParser.cs new file mode 100644 index 0000000..13d0d9f --- /dev/null +++ b/ObjLoader/TypeParsers/ObjectNameParser.cs @@ -0,0 +1,33 @@ +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using System; + +namespace SharpEngine.Core.ObjLoader.TypeParsers; + +/// +/// Parses object name lines `o object_name` and handles the object name accordingly. +/// +public class ObjectNameParser : TypeParserBase, ITypeParser +{ + private IObjectNameDataStore _dataStore; + + /// + /// Initializes a new instance of . + /// + /// The data store to store the parsed object name. + public ObjectNameParser(IObjectNameDataStore dataStore) + { + _dataStore = dataStore; + } + + /// + protected override string Keyword => "o"; + + /// + /// Thrown when the line format is invalid. + public override bool Parse(string data) + { + _dataStore.SetObjectName(data); + + return true; + } +} diff --git a/ObjLoader/TypeParsers/SmoothingGroupParser.cs b/ObjLoader/TypeParsers/SmoothingGroupParser.cs new file mode 100644 index 0000000..693bcea --- /dev/null +++ b/ObjLoader/TypeParsers/SmoothingGroupParser.cs @@ -0,0 +1,45 @@ +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using System; + +namespace SharpEngine.Core.ObjLoader.TypeParsers; + +/// +/// Parses smoothing group definitions ("s") and populates the current group's smoothing group information. +/// +public class SmoothingGroupParser : TypeParserBase, ITypeParser +{ + private ISmoothingGroupDataStore _dataStore; + + /// + /// Initializes a new instance of . + /// + /// The data store to store the parsed smoothing group information. + public SmoothingGroupParser(ISmoothingGroupDataStore dataStore) + { + _dataStore = dataStore; + } + + /// + protected override string Keyword => "s"; + + /// + /// + /// The line is expected to be in the format "group_number" or "off" so the s at the beginning is not included in the data string passed to this method. + /// + /// Thrown when the line format is invalid or the smoothing group number is not a valid integer. + public override bool Parse(string data) + { + if (data.Equals("off", StringComparison.OrdinalIgnoreCase)) + { + _dataStore.SetSmoothingGroupOff(); + return true; + } + + // Parse the smoothing group number + if (!int.TryParse(data, out int groupNumber)) + throw new FormatException($"Invalid smoothing group identifier: {data}"); + + _dataStore.SetSmoothingGroup(groupNumber); + return true; + } +} diff --git a/ObjLoader/TypeParsers/TextureParser.cs b/ObjLoader/TypeParsers/TextureParser.cs index 43a2e8e..df5a0e3 100644 --- a/ObjLoader/TypeParsers/TextureParser.cs +++ b/ObjLoader/TypeParsers/TextureParser.cs @@ -25,7 +25,7 @@ public TextureParser(ITextureDataStore dataStore) protected override string Keyword => "vt"; /// - public override void Parse(string line) + public override bool Parse(string line) { string[] parts = line.Split(' '); @@ -34,6 +34,8 @@ public override void Parse(string line) var texture = new TextureCoordinate(x, y); _dataStore.AddTexture(texture); + + return true; } } } \ No newline at end of file diff --git a/ObjLoader/TypeParsers/TypeParserBase.cs b/ObjLoader/TypeParsers/TypeParserBase.cs index 7410790..30ac334 100644 --- a/ObjLoader/TypeParsers/TypeParserBase.cs +++ b/ObjLoader/TypeParsers/TypeParserBase.cs @@ -17,6 +17,6 @@ public abstract class TypeParserBase : ITypeParser public bool CanParse(string keyword) => keyword.EqualsOrdinalIgnoreCase(Keyword); /// - public abstract void Parse(string line); + public abstract bool Parse(string line); } } \ No newline at end of file diff --git a/ObjLoader/TypeParsers/UseMaterialParser.cs b/ObjLoader/TypeParsers/UseMaterialParser.cs index aa4d6eb..76d6f5d 100644 --- a/ObjLoader/TypeParsers/UseMaterialParser.cs +++ b/ObjLoader/TypeParsers/UseMaterialParser.cs @@ -1,5 +1,4 @@ -using SharpEngine.Core.Components.ObjLoader.DataStore; -using SharpEngine.Core.ObjLoader.TypeParsers; +using SharpEngine.Core.ObjLoader.TypeParsers; namespace SharpEngine.Core.ObjLoader.Loader.TypeParsers { @@ -23,6 +22,10 @@ public UseMaterialParser(IMaterialDataStore dataStore) protected override string Keyword => "usemtl"; /// - public override void Parse(string line) => _dataStore.SetMaterial(line); + public override bool Parse(string line) + { + _dataStore.SetMaterial(line); + return true; + } } } \ No newline at end of file diff --git a/ObjLoader/TypeParsers/VertexParser.cs b/ObjLoader/TypeParsers/VertexParser.cs index 94159b3..fadb733 100644 --- a/ObjLoader/TypeParsers/VertexParser.cs +++ b/ObjLoader/TypeParsers/VertexParser.cs @@ -27,7 +27,7 @@ public VertexParser(IVertexDataStore dataStore) protected override string Keyword => "v"; /// - public override void Parse(string line) + public override bool Parse(string line) { string[] parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); @@ -41,6 +41,8 @@ public override void Parse(string line) }; _dataStore.AddVertex(vertex); + + return true; } } } \ No newline at end of file diff --git a/SharpEngine.Core.Components/Properties/Meshes/Model.cs b/SharpEngine.Core.Components/Properties/Meshes/Model.cs index 360b321..2947ec0 100644 --- a/SharpEngine.Core.Components/Properties/Meshes/Model.cs +++ b/SharpEngine.Core.Components/Properties/Meshes/Model.cs @@ -23,12 +23,14 @@ public class Model : IDisposable /// Gets the list of meshes contained within the model. public List Meshes { get; set; } = []; + public string Name { get; } + /// /// Initializes a new instance of the Model class with the specified OpenGL context and model file path. /// /// The GL context used for rendering and resource management. /// The file path of the model asset. - public Model(GL gl, string path) + public Model(GL gl, string path) : this(gl, path, Enumerable.Empty()) { _gl = gl; Path = path; @@ -47,7 +49,11 @@ public Model(GL gl, string path, IEnumerable meshes) { _gl = gl; Path = path; - Meshes = [.. meshes.Select(ProcessMesh)]; + + if (meshes.Any()) + Meshes = meshes.Select(ProcessMesh).ToList(); + + Name = meshes.FirstOrDefault()?.Name ?? System.IO.Path.GetFileNameWithoutExtension(path) ?? "UnnamedModel"; } /// diff --git a/SharpEngine.Core/Abstractions/ISettings.cs b/SharpEngine.Core/Abstractions/ISettings.cs index 6800463..d0984b2 100644 --- a/SharpEngine.Core/Abstractions/ISettings.cs +++ b/SharpEngine.Core/Abstractions/ISettings.cs @@ -1,4 +1,5 @@ -using SharpEngine.Core.Renderers; +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Renderers; using Silk.NET.Input; namespace SharpEngine.Core.Interfaces; @@ -26,6 +27,11 @@ public interface ISettings /// Gets or sets the secondary mouse button. public MouseButton SecondaryButton { get; set; } + + /// + /// Gets or sets the log level for the game engine. + /// + public LogLevel LogLevel { get; set; } } /// @@ -45,4 +51,7 @@ public class DefaultSettings : ISettings /// public MouseButton SecondaryButton { get; set; } = MouseButton.Right; + + /// + public LogLevel LogLevel { get; set; } = LogLevel.Information; } diff --git a/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs b/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs index 03d03f7..dbeef01 100644 --- a/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs +++ b/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs @@ -1,4 +1,5 @@ -using SharpEngine.Core.Renderers; +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Renderers; using Silk.NET.Input; using Silk.NET.Maths; @@ -48,4 +49,7 @@ public DefaultViewSettings() /// public MouseButton SecondaryButton { get; set; } = MouseButton.Right; + + /// + public LogLevel LogLevel { get; set; } = LogLevel.Information; } diff --git a/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs b/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs index 09c65cb..013f31b 100644 --- a/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs +++ b/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs @@ -1,4 +1,5 @@ -using SharpEngine.Core.Renderers; +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Renderers; using Silk.NET.Input; using Silk.NET.Windowing; @@ -52,4 +53,7 @@ public ViewSettings() /// public MouseButton SecondaryButton { get; set; } = MouseButton.Right; + + /// + public LogLevel LogLevel { get; set; } = LogLevel.Information; } From 4c6d964f5ff5d39ab2210d96de13a93fcd59f4e1 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Mon, 29 Jun 2026 20:36:58 +0300 Subject: [PATCH 08/10] Cleanup settings --- .../Abstractions/DefaultSettings.cs | 27 +++++++++++++++++++ SharpEngine.Core/Abstractions/ISettings.cs | 24 +---------------- 2 files changed, 28 insertions(+), 23 deletions(-) create mode 100644 SharpEngine.Core/Abstractions/DefaultSettings.cs diff --git a/SharpEngine.Core/Abstractions/DefaultSettings.cs b/SharpEngine.Core/Abstractions/DefaultSettings.cs new file mode 100644 index 0000000..9347b4e --- /dev/null +++ b/SharpEngine.Core/Abstractions/DefaultSettings.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Renderers; +using Silk.NET.Input; + +namespace SharpEngine.Core.Interfaces; + +/// +public class DefaultSettings : ISettings +{ + /// + public bool UseWireFrame { get; set; } + + /// + public bool PrintFrameRate { get; set; } + + /// + public RenderFlags RendererFlags { get; set; } = RenderFlags.All; + + /// + public MouseButton PrimaryButton { get; set; } = MouseButton.Left; + + /// + public MouseButton SecondaryButton { get; set; } = MouseButton.Right; + + /// + public LogLevel LogLevel { get; set; } = LogLevel.Information; +} diff --git a/SharpEngine.Core/Abstractions/ISettings.cs b/SharpEngine.Core/Abstractions/ISettings.cs index d0984b2..cc8fba4 100644 --- a/SharpEngine.Core/Abstractions/ISettings.cs +++ b/SharpEngine.Core/Abstractions/ISettings.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using SharpEngine.Core.Renderers; using Silk.NET.Input; @@ -33,25 +33,3 @@ public interface ISettings /// public LogLevel LogLevel { get; set; } } - -/// -public class DefaultSettings : ISettings -{ - /// - public bool UseWireFrame { get; set; } - - /// - public bool PrintFrameRate { get; set; } - - /// - public RenderFlags RendererFlags { get; set; } = RenderFlags.All; - - /// - public MouseButton PrimaryButton { get; set; } = MouseButton.Left; - - /// - public MouseButton SecondaryButton { get; set; } = MouseButton.Right; - - /// - public LogLevel LogLevel { get; set; } = LogLevel.Information; -} From 02f633c55e0e0927f8de8d433bbe6892e392f121 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Mon, 29 Jun 2026 20:37:10 +0300 Subject: [PATCH 09/10] Add architecture tests project --- .../SharpEngine.Core.ArchitectureTests}/Class1.cs | 0 .../SharpEngine.Core.ArchitectureTests.csproj | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {SharpEngine.Core.ArchitectureTests => Tests/SharpEngine.Core.ArchitectureTests}/Class1.cs (100%) rename {SharpEngine.Core.ArchitectureTests => Tests/SharpEngine.Core.ArchitectureTests}/SharpEngine.Core.ArchitectureTests.csproj (100%) diff --git a/SharpEngine.Core.ArchitectureTests/Class1.cs b/Tests/SharpEngine.Core.ArchitectureTests/Class1.cs similarity index 100% rename from SharpEngine.Core.ArchitectureTests/Class1.cs rename to Tests/SharpEngine.Core.ArchitectureTests/Class1.cs diff --git a/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj b/Tests/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj similarity index 100% rename from SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj rename to Tests/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj From 651c4b2feb3ed33a090337b766aa8728c4ce84c7 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Tue, 30 Jun 2026 01:23:47 +0300 Subject: [PATCH 10/10] Add link to docs (#139) --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 0d657e1..aae9d34 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,9 @@ ![Issues](https://img.shields.io/github/issues/Av3boy/SharpEngine) ![PRs](https://img.shields.io/github/issues-pr/Av3boy/SharpEngine) --> + +[Documentation](https://docs.sharpengine.net/) + ## About