From 29cb24b9d1ab0dd02f71427b795adf2d502c992e Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Mon, 5 Jan 2026 12:56:32 +0200 Subject: [PATCH 01/14] #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/14] #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/14] #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/14] #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/14] #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/14] #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/14] #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/14] 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/14] 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/14] 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 From d107b45d0aca554ad98a63e728107ddb2c96475c Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Mon, 10 Aug 2026 22:52:29 +0300 Subject: [PATCH 11/14] Add mermaid diagrams for engine execution flows Note, these are work in progress and subject to change --- docs/engine_loop.mermaid | 97 +++++++++++++++++++++++++++++++ docs/engine_shutdown_flow.mermaid | 67 +++++++++++++++++++++ docs/engine_startup_flow.mermaid | 97 +++++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 docs/engine_loop.mermaid create mode 100644 docs/engine_shutdown_flow.mermaid create mode 100644 docs/engine_startup_flow.mermaid diff --git a/docs/engine_loop.mermaid b/docs/engine_loop.mermaid new file mode 100644 index 0000000..b8a630c --- /dev/null +++ b/docs/engine_loop.mermaid @@ -0,0 +1,97 @@ +flowchart TD + + A{Engine Running?} + + A -->|No| Z[Shutdown] + + A -->|Yes| B[Begin Engine Iteration] + + %% ========================================================= + %% ENGINE COMMANDS + %% ========================================================= + + B --> C[Process Engine Command Queue] + + C --> C1[Window Commands] + C1 --> C2[Asset Commands] + C2 --> C3[Scene Commands] + C3 --> C4[Other Cross-Thread Commands] + + %% ========================================================= + %% WINDOW MANAGEMENT + %% ========================================================= + + C4 --> D[WindowHandler.ProcessPending] + + D --> D1{Pending Window Command?} + + D1 -->|Add| DA[Dequeue AddWindow] + DA --> DA1[Create Silk.NET Window] + DA1 --> DA2[Register Window Callbacks] + DA2 --> DA3[Start Window Loop] + DA3 --> D1 + + D1 -->|Remove| DR[Dequeue CloseWindow] + DR --> DR1[Request Silk Window Close] + DR1 --> D1 + + D1 -->|None| E[Update Engine Timing] + + %% ========================================================= + %% FIXED SIMULATION + %% ========================================================= + + E --> F{Fixed Update Required?} + + F -->|Yes| F1[Run ECS Fixed Systems] + F1 --> F2[Character Controllers] + F2 --> F3[Physics Pre-Step] + F3 --> F4[Physics Simulation] + F4 --> F5[Collision Detection / Resolution] + F5 --> F6[Dispatch Collision Events] + F6 --> F7[Apply Deferred ECS Changes] + F7 --> F8[Consume Fixed Time] + F8 --> F + + %% ========================================================= + %% VARIABLE UPDATE + %% ========================================================= + + F -->|No| G[Consume Input Snapshot] + + G --> G1[Run Gameplay ECS Systems] + G1 --> G2[AI / Navigation] + G2 --> G3[State Machines] + G3 --> G4[Animation Systems] + G4 --> G5[Transform Systems] + G5 --> G6[Audio Systems] + G6 --> G7[Apply Deferred ECS Changes] + + %% ========================================================= + %% RENDER SNAPSHOT + %% ========================================================= + + G7 --> H[Build Render Snapshot] + + H --> H1[Copy World Transforms] + H1 --> H2[Collect Mesh / Material Handles] + H2 --> H3[Collect Bone Matrices] + H3 --> H4[Collect Lights] + H4 --> H5[Collect Camera Data] + H5 --> H6[Collect Renderable ECS State] + + H6 --> I[Publish Render Snapshot] + + %% ========================================================= + %% ASYNC COMPLETION + %% ========================================================= + + I --> J[Process Completed Async Work] + + J --> J1[Asset Load Completions] + J1 --> J2[Background Job Completions] + J2 --> J3[Deferred Resource Cleanup] + + J3 --> K[End Engine Iteration] + + K --> A \ No newline at end of file diff --git a/docs/engine_shutdown_flow.mermaid b/docs/engine_shutdown_flow.mermaid new file mode 100644 index 0000000..3863c35 --- /dev/null +++ b/docs/engine_shutdown_flow.mermaid @@ -0,0 +1,67 @@ +flowchart TD + + A[Shutdown] --> B[Begin Engine Shutdown] + + B --> B1[Set Engine Running = false] + B1 --> B2[Stop Accepting New Engine Commands] + B2 --> B3[Stop Simulation] + + %% ========================================================= + %% WINDOWS + %% ========================================================= + + B3 --> C[Request All Windows Close] + + C --> C1[WindowHandler.StopAcceptingWindows] + C1 --> C2[Signal Silk.NET Windows To Close] + C2 --> C3[Window Loops Exit] + C3 --> C4[Dispose Per-Window Renderer Resources] + C4 --> C5[Dispose Silk.NET Windows] + + %% ========================================================= + %% ECS / SCENE + %% ========================================================= + + C5 --> D[Unload Active Scene Async] + + D --> D1[Run OnDisable] + D1 --> D2[Run OnDestroy] + D2 --> D3[Destroy Entities] + D3 --> D4[Apply Final ECS Structural Changes] + + %% ========================================================= + %% SYSTEM SHUTDOWN + %% ========================================================= + + D4 --> E[Shutdown Runtime Systems Async] + + E --> E1[PhysicsSystem.ShutdownAsync] + E --> E2[AnimationSystem.ShutdownAsync] + E --> E3[AudioSystem.ShutdownAsync] + E --> E4[AssetSystem.ShutdownAsync] + + E1 --> EB[await Task.WhenAll] + E2 --> EB + E3 --> EB + E4 --> EB + + %% ========================================================= + %% CORE SHUTDOWN + %% ========================================================= + + EB --> F[Shutdown Core Systems] + + F --> F1[ECS.ShutdownAsync] + F1 --> F2[Drain Job System] + F2 --> F3[JobSystem.ShutdownAsync] + F3 --> F4[WindowHandler.ShutdownAsync] + F4 --> F5[FileSystem.ShutdownAsync] + + %% ========================================================= + %% FINAL CLEANUP + %% ========================================================= + + F5 --> G[Dispose Dependency Injection Container] + G --> G1[Flush Logs Async] + G1 --> G2[Shutdown Logging] + G2 --> H[Process Exit] \ No newline at end of file diff --git a/docs/engine_startup_flow.mermaid b/docs/engine_startup_flow.mermaid new file mode 100644 index 0000000..b8a630c --- /dev/null +++ b/docs/engine_startup_flow.mermaid @@ -0,0 +1,97 @@ +flowchart TD + + A{Engine Running?} + + A -->|No| Z[Shutdown] + + A -->|Yes| B[Begin Engine Iteration] + + %% ========================================================= + %% ENGINE COMMANDS + %% ========================================================= + + B --> C[Process Engine Command Queue] + + C --> C1[Window Commands] + C1 --> C2[Asset Commands] + C2 --> C3[Scene Commands] + C3 --> C4[Other Cross-Thread Commands] + + %% ========================================================= + %% WINDOW MANAGEMENT + %% ========================================================= + + C4 --> D[WindowHandler.ProcessPending] + + D --> D1{Pending Window Command?} + + D1 -->|Add| DA[Dequeue AddWindow] + DA --> DA1[Create Silk.NET Window] + DA1 --> DA2[Register Window Callbacks] + DA2 --> DA3[Start Window Loop] + DA3 --> D1 + + D1 -->|Remove| DR[Dequeue CloseWindow] + DR --> DR1[Request Silk Window Close] + DR1 --> D1 + + D1 -->|None| E[Update Engine Timing] + + %% ========================================================= + %% FIXED SIMULATION + %% ========================================================= + + E --> F{Fixed Update Required?} + + F -->|Yes| F1[Run ECS Fixed Systems] + F1 --> F2[Character Controllers] + F2 --> F3[Physics Pre-Step] + F3 --> F4[Physics Simulation] + F4 --> F5[Collision Detection / Resolution] + F5 --> F6[Dispatch Collision Events] + F6 --> F7[Apply Deferred ECS Changes] + F7 --> F8[Consume Fixed Time] + F8 --> F + + %% ========================================================= + %% VARIABLE UPDATE + %% ========================================================= + + F -->|No| G[Consume Input Snapshot] + + G --> G1[Run Gameplay ECS Systems] + G1 --> G2[AI / Navigation] + G2 --> G3[State Machines] + G3 --> G4[Animation Systems] + G4 --> G5[Transform Systems] + G5 --> G6[Audio Systems] + G6 --> G7[Apply Deferred ECS Changes] + + %% ========================================================= + %% RENDER SNAPSHOT + %% ========================================================= + + G7 --> H[Build Render Snapshot] + + H --> H1[Copy World Transforms] + H1 --> H2[Collect Mesh / Material Handles] + H2 --> H3[Collect Bone Matrices] + H3 --> H4[Collect Lights] + H4 --> H5[Collect Camera Data] + H5 --> H6[Collect Renderable ECS State] + + H6 --> I[Publish Render Snapshot] + + %% ========================================================= + %% ASYNC COMPLETION + %% ========================================================= + + I --> J[Process Completed Async Work] + + J --> J1[Asset Load Completions] + J1 --> J2[Background Job Completions] + J2 --> J3[Deferred Resource Cleanup] + + J3 --> K[End Engine Iteration] + + K --> A \ No newline at end of file From c9c68a362d72d782c2ded6700095deadc6e0b202 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Fri, 28 Aug 2026 18:47:22 +0300 Subject: [PATCH 12/14] Added support for dark and light theme logo (#143) * Added support for dark and light theme logo * Add white logo --- README.md | 6 +++++- Resources/icon.svg | 13 ++++++++++++- Resources/icon_white.svg | 12 ++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 Resources/icon_white.svg diff --git a/README.md b/README.md index aae9d34..0b0344a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@

- + + + + The SharpEngine logo. + SharpEngine

diff --git a/Resources/icon.svg b/Resources/icon.svg index 44da28f..7562dc3 100644 --- a/Resources/icon.svg +++ b/Resources/icon.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/Resources/icon_white.svg b/Resources/icon_white.svg new file mode 100644 index 0000000..a95a85f --- /dev/null +++ b/Resources/icon_white.svg @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file From 00c11823c25cf76e1532812f6589919df07b6c74 Mon Sep 17 00:00:00 2001 From: Antti Veikkolainen Date: Sun, 20 Sep 2026 15:19:52 +0300 Subject: [PATCH 13/14] #21 Structural changes (#142) * #21 Input and windowing improvements * #21 Fix window load and input (wip) * #21 Fix UI rendering to use pixel * #21 UI alignment works (WIP) * #21 Rearrange logic into the grid container (WIP) Doesn't work quite yet * #21 Temp * Temp * Placeholder commit Everything after this point needs to have a structure and proper planning. It's been a wild west until now * Temp We need to start figuring out the shared repo and how the "main" repo will build as open source * Structural consistency cleanup * Add Copilot MCP config Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add copilot instruction file * Update docs * Structural updates * Fix renderers * Add mcp for sonarcloud * Project improvements * Fixes & Cleanup * #21 Fix project reference paths --------- Co-authored-by: Antti Veikkolainen Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .copilot/mcp-config.json | 27 + .copilot/mcp.yml | 39 ++ .editorconfig | 277 +++++++++- .github/copilot-instructions.md | 43 +- Examples/MinecraftClone/Minecraft.cs | 50 +- Examples/MinecraftClone/Minecraft.csproj | 3 +- Examples/MinecraftClone/Program.cs | 38 +- Examples/MinecraftClone/Resources/Slot.png | Bin 0 -> 609 bytes .../Terrain/TerrainGenerator.cs | 3 +- .../Terrain/TerrainGenerator_Old.cs | 3 +- Examples/MultipleWindows/Program.cs | 10 +- .../Tutorial 4.1 - Model Loading.csproj | 3 +- README.md | 14 +- .../Properties/Material.cs | 5 +- .../App.cs | 59 +-- .../AppBuilder.cs | 0 .../EngineRegistrationBuilder.cs | 36 ++ .../EngineServiceCollectionExtensions.cs | 25 + .../HandlerRegistrationBuilder.cs | 37 ++ ...harpEngine.Core.DependencyInjection.csproj | 18 + .../WindowServiceCollectionExtensions.cs | 80 +++ .../DataStore.cs | 0 .../IMaterialDataStore.cs | 0 .../IObjectNameDataStore.cs | 0 .../ISmoothingGroupDataStore.cs | 0 .../Loaders/IFileManager.cs | 0 .../Loaders/LoaderBase.cs | 0 .../IMaterialLibraryLoaderFacade.cs | 0 .../MaterialLoader/MaterialLibraryLoader.cs | 0 .../MaterialLibraryLoaderFacade.cs | 0 .../Loaders/ObjLoader/FileManager.cs | 0 .../Loaders/ObjLoader/ObjLoader.cs | 0 .../Loaders/ObjLoader/ObjLoaderFactory.cs | 0 .../SharpEngine.Core.ObjLoader.csproj | 0 .../TypeParsers/FaceParser.cs | 0 .../TypeParsers/GroupParser.cs | 0 .../TypeParsers/ITypeParser.cs | 0 .../TypeParsers/MaterialLibraryParser.cs | 0 .../TypeParsers/NormalParser.cs | 0 .../TypeParsers/ObjectNameParser.cs | 0 .../TypeParsers/SmoothingGroupParser.cs | 0 .../TypeParsers/TextureParser.cs | 0 .../TypeParsers/TypeParserBase.cs | 0 .../TypeParsers/UseMaterialParser.cs | 0 .../TypeParsers/VertexParser.cs | 0 .../Debug.cs | 0 SharpEngine.Core/Abstractions/IGame.cs | 13 +- SharpEngine.Core/Architecture.cd | 476 ++++++++++++++++++ .../ServiceCollectionExtensions.cs | 46 -- SharpEngine.Core/Engine.cs | 69 ++- SharpEngine.Core/EngineServiceManager.cs | 20 +- SharpEngine.Core/Entities/GameObject.cs | 94 ++-- .../Entities/Interfaces/IClickable.cs | 27 + .../Entities/Interfaces/IComponent.cs | 34 ++ .../Entities/UI/DraggableUIElement.cs | 15 + .../Entities/UI/Layouts/FlexLayout.cs | 8 +- .../Entities/UI/Layouts/GridLayout.cs | 108 +++- .../Entities/UI/Layouts/LayoutBase.cs | 26 +- SharpEngine.Core/Entities/UI/MeshRenderer.cs | 3 +- SharpEngine.Core/Entities/UI/UIElement.cs | 63 +-- SharpEngine.Core/Handlers/EngineHandler.cs | 27 +- SharpEngine.Core/Handlers/WindowHandler.cs | 160 +----- SharpEngine.Core/InputManager.cs | 183 +++++++ SharpEngine.Core/Ray.cs | 2 +- SharpEngine.Core/RendererManager.cs | 60 +++ SharpEngine.Core/Renderers/Renderer.cs | 30 +- SharpEngine.Core/Renderers/UIRenderer.cs | 134 ++++- SharpEngine.Core/Scenes/EmptyNode.cs | 23 + SharpEngine.Core/Scenes/Scene.cs | 213 +------- SharpEngine.Core/Scenes/SceneExtensions.cs | 85 ++++ SharpEngine.Core/Scenes/SceneNode.cs | 175 +++++-- SharpEngine.Core/ShaderManager.cs | 30 ++ .../Rendering/ParameterDescriptor.cs | 2 +- .../Rendering/ShaderParameter.cs | 2 +- .../Rendering/ShaderParameterBinder.cs | 7 +- .../Rendering/ShaderParameterType.cs | 2 +- .../Rendering/ShaderUpdateFrequency.cs | 2 +- .../Shaders/ShaderParameterAttribute.cs | 2 +- SharpEngine.Core/SharpEngine.Core.csproj | 10 +- SharpEngine.Core/Windowing/SilkWindow.cs | 40 +- SharpEngine.Core/Windowing/Window.cs | 248 +++------ .../_Resources/Shaders/uiShader.vert | 37 +- SharpEngine.Dto/Class1.cs | 6 + SharpEngine.Dto/SharpEngine.Core.Dto.csproj | 9 + SharpEngine.slnx | 18 +- .../SharpEngine.Core.ObjLoader.Tests.csproj | 1 + .../Class1.cs | 6 - .../GameObjectTests.cs | 28 ++ .../SharpEngine.Core.ArchitectureTests.csproj | 9 + .../Shaders/ShaderParameterBinderTests.cs | 2 +- imgui.ini | 10 + 91 files changed, 2376 insertions(+), 959 deletions(-) create mode 100644 .copilot/mcp-config.json create mode 100644 .copilot/mcp.yml create mode 100644 Examples/MinecraftClone/Resources/Slot.png rename {SharpEngine.Core/DependencyInjection => SharpEngine.Core.DependencyInjection}/App.cs (52%) rename {SharpEngine.Core/DependencyInjection => SharpEngine.Core.DependencyInjection}/AppBuilder.cs (100%) create mode 100644 SharpEngine.Core.DependencyInjection/EngineRegistrationBuilder.cs create mode 100644 SharpEngine.Core.DependencyInjection/EngineServiceCollectionExtensions.cs create mode 100644 SharpEngine.Core.DependencyInjection/HandlerRegistrationBuilder.cs create mode 100644 SharpEngine.Core.DependencyInjection/SharpEngine.Core.DependencyInjection.csproj create mode 100644 SharpEngine.Core.DependencyInjection/WindowServiceCollectionExtensions.cs rename {ObjLoader => SharpEngine.Core.ObjLoader}/DataStore.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/IMaterialDataStore.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/IObjectNameDataStore.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/ISmoothingGroupDataStore.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/Loaders/IFileManager.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/Loaders/LoaderBase.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/Loaders/MaterialLoader/MaterialLibraryLoader.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/Loaders/ObjLoader/FileManager.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/Loaders/ObjLoader/ObjLoader.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/Loaders/ObjLoader/ObjLoaderFactory.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/SharpEngine.Core.ObjLoader.csproj (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/FaceParser.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/GroupParser.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/ITypeParser.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/MaterialLibraryParser.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/NormalParser.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/ObjectNameParser.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/SmoothingGroupParser.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/TextureParser.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/TypeParserBase.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/UseMaterialParser.cs (100%) rename {ObjLoader => SharpEngine.Core.ObjLoader}/TypeParsers/VertexParser.cs (100%) rename {SharpEngine.Shared => SharpEngine.Core.Shared}/Debug.cs (100%) create mode 100644 SharpEngine.Core/Architecture.cd delete mode 100644 SharpEngine.Core/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 SharpEngine.Core/Entities/Interfaces/IClickable.cs create mode 100644 SharpEngine.Core/Entities/Interfaces/IComponent.cs create mode 100644 SharpEngine.Core/Entities/UI/DraggableUIElement.cs create mode 100644 SharpEngine.Core/InputManager.cs create mode 100644 SharpEngine.Core/RendererManager.cs create mode 100644 SharpEngine.Core/Scenes/EmptyNode.cs create mode 100644 SharpEngine.Core/Scenes/SceneExtensions.cs create mode 100644 SharpEngine.Core/ShaderManager.cs rename SharpEngine.Core/{ => Shaders}/Rendering/ParameterDescriptor.cs (91%) rename SharpEngine.Core/{ => Shaders}/Rendering/ShaderParameter.cs (96%) rename SharpEngine.Core/{ => Shaders}/Rendering/ShaderParameterBinder.cs (97%) rename SharpEngine.Core/{ => Shaders}/Rendering/ShaderParameterType.cs (78%) rename SharpEngine.Core/{ => Shaders}/Rendering/ShaderUpdateFrequency.cs (70%) create mode 100644 SharpEngine.Dto/Class1.cs create mode 100644 SharpEngine.Dto/SharpEngine.Core.Dto.csproj delete mode 100644 Tests/SharpEngine.Core.ArchitectureTests/Class1.cs create mode 100644 Tests/SharpEngine.Core.ArchitectureTests/GameObjectTests.cs create mode 100644 imgui.ini diff --git a/.copilot/mcp-config.json b/.copilot/mcp-config.json new file mode 100644 index 0000000..bf9f939 --- /dev/null +++ b/.copilot/mcp-config.json @@ -0,0 +1,27 @@ +{ + "mcpServers": { + "sonarqube": { + "command": "docker", + "args": [ + "run", + "--init", + "--pull=always", + "-i", + "--rm", + "-e", + "SONARQUBE_TOKEN", + "-e", + "SONARQUBE_ORG", + "sonarsource/sonarqube-mcp" + ], + "env": { + "SONARQUBE_TOKEN": "", + "SONARQUBE_ORG": "" + }, + "type": "local", + "tools": [ + "*" + ] + } + } +} \ No newline at end of file diff --git a/.copilot/mcp.yml b/.copilot/mcp.yml new file mode 100644 index 0000000..df1852d --- /dev/null +++ b/.copilot/mcp.yml @@ -0,0 +1,39 @@ +# Copilot MCP configuration for Av3boy/SharpEngine +# Purpose: tell Copilot which repository resources to index and surface as context + +version: 1 +name: SharpEngine Copilot MCP +repository: Av3boy/SharpEngine + +# Which repo sources to include for context (issues, project boards, PRs, discussions, code, workflows) +contexts: + - type: github + include: + - issues + - projects + - pull_requests + - discussions + - labels + - milestones + - commits + - workflows + + - type: files + include_paths: + - README.md + - docs/** + - .github/** + - src/** + +settings: + indexing: + max_documents: 1000 + refresh: weekly + visibility: + include_private: false + +# Notes: +# - This file is a lightweight starting point. Adjust `include` and `include_paths` to match +# which repo artifacts you want the Copilot agent to surface (e.g., add tests/** or design/**). +# - No secrets or tokens are stored here; GitHub will use repository permissions when Copilot +# fetches the requested resources. diff --git a/.editorconfig b/.editorconfig index efb06f1..5d8e474 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,275 @@ -[*.cs] +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true -# IDE0290: Use primary constructor +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Code Actions #### + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:error +dotnet_style_qualification_for_field = false:error +dotnet_style_qualification_for_method = false:error +dotnet_style_qualification_for_property = false:error + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:suggestion +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_prefer_system_hash_code = true +dotnet_style_coalesce_expression = true:warning +dotnet_style_collection_initializer = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true:warning +dotnet_style_object_initializer = true:warning +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = false +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_non_hidden_explicit_cast_in_source = true +dotnet_style_prefer_simplified_boolean_expressions = true:warning +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:warning + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = false:error +dotnet_style_allow_statement_immediately_after_block_experimental = false:error + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = true + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:warning +csharp_style_expression_bodied_constructors = false:error +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = true:warning +csharp_style_expression_bodied_methods = true:warning +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = when_on_single_line:warning + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_pattern_matching_over_is_with_cast_check = true:warning +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true:warning +csharp_style_prefer_pattern_matching = true:warning +csharp_style_prefer_switch_expression = true:warning + +# Null-checking preferences +csharp_style_conditional_delegate_call = false:warning + +# Modifier preferences +csharp_prefer_static_anonymous_function = true:warning +csharp_prefer_static_local_function = true:warning +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,closed,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true:warning +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = false +csharp_prefer_simple_using_statement = true:warning +csharp_prefer_system_threading_lock = true +csharp_style_namespace_declarations = file_scoped:error +csharp_style_prefer_labeled_jump_statements = true +csharp_style_prefer_method_group_conversion = true csharp_style_prefer_primary_constructors = false +csharp_style_prefer_simple_property_accessors = true +csharp_style_prefer_top_level_statements = false:warning + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:warning +csharp_style_deconstructed_variable_declaration = true:silent +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +csharp_style_inlined_variable_declaration = true:warning +csharp_style_prefer_implicitly_typed_lambda_expression = true +csharp_style_prefer_index_operator = true:silent +csharp_style_prefer_local_over_anonymous_function = true:warning +csharp_style_prefer_null_check_over_type_check = true:warning +csharp_style_prefer_range_operator = true:silent +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_unbound_generic_type_in_nameof = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true:silent +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:warning + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:warning + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:error +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false:warning +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:error +csharp_style_allow_embedded_statements_on_same_line_experimental = false:error + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = no_change +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.consts_should_be_consts.severity = suggestion +dotnet_naming_rule.consts_should_be_consts.symbols = consts +dotnet_naming_rule.consts_should_be_consts.style = consts + +dotnet_naming_rule.privates_and_internals_should_be_privates.severity = suggestion +dotnet_naming_rule.privates_and_internals_should_be_privates.symbols = privates_and_internals +dotnet_naming_rule.privates_and_internals_should_be_privates.style = privates + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.privates_and_internals.applicable_kinds = property, field, event, delegate +dotnet_naming_symbols.privates_and_internals.applicable_accessibilities = internal, private +dotnet_naming_symbols.privates_and_internals.required_modifiers = readonly, static + +dotnet_naming_symbols.consts.applicable_kinds = +dotnet_naming_symbols.consts.applicable_accessibilities = * +dotnet_naming_symbols.consts.required_modifiers = const + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.privates.required_prefix = _ +dotnet_naming_style.privates.required_suffix = +dotnet_naming_style.privates.word_separator = +dotnet_naming_style.privates.capitalization = camel_case -# IDE0130: Namespace does not match folder structure, Justification = "Either one is fine, depending on the sittuation you might want to use block type but file scoped is default." -dotnet_diagnostic.IDE0130.severity = silent -csharp_style_namespace_declarations = file_scoped:silent +dotnet_naming_style.consts.required_prefix = +dotnet_naming_style.consts.required_suffix = +dotnet_naming_style.consts.word_separator = _ +dotnet_naming_style.consts.capitalization = all_upper diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1318621..b332ad9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,17 +1,36 @@ -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. +Copilot/Agent Guidance (canonical: .github/copilot-instructions.md) -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. +Canonical location +- This file is the authoritative Copilot/automation guidance for this repository. Keep only this file as the canonical Copilot instruction source (remove any duplicate copies at repository root or elsewhere after review and approval). -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. +Commit and automation policy +- Agents MUST NOT create, amend, or push commits without explicit human approval. Present diffs and allow the user to explicitly approve (e.g., "Approve commit") before any commit is made. +- Include the required Co-authored-by trailer in commits made after explicit approval. -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. +Coding style and change guidance +- When making changes to the codebase, follow existing styles, naming, formatting, and conventions used in the repository. When in doubt, examine the dominant pattern in the affected area and mimic it, or ask the user for guidance. +- Update XML documentation for any new or changed public members. +- Ensure changes are unit testable. Add or update unit tests to cover new behavior and run existing tests locally when possible. +- Favor composition over inheritance, and follow SOLID principles for maintainability and testability. +- When referencing system libraries (System.IO, System.Net.Http, etc.), prefer abstractions or interfaces to enable easier testing and mocking. -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. +- Exception handling rule: do not use empty catch blocks. Every catch block must handle the exception appropriately; at minimum it must log the caught exception (including exception type and message). Swallowing exceptions silently is forbidden. Prefer catching specific exception types and rethrowing or wrapping as needed. For library code, include context in the log message to aid debugging. -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 +Issue and repository inquiries +- When a user asks about an issue, the agent should check the repository's GitHub Issues list first and reference any relevant issues. +- If the agent cannot access GitHub Issues (network restrictions, lack of permissions, or API limits), clearly inform the user that Issues could not be checked and that the agent will fall back to local code inspection only after notifying the user. + +External access and transparency +- If any external service (GitHub, CI, package registries) cannot be reached from the execution environment, explicitly state which checks or steps were skipped and why. + +Safety and secrets +- Never create or commit secrets, credentials, or private tokens. If code seems to include secrets, raise an alert and ask the user how to proceed. + +PR / branch guidance (recommended) +- Use descriptive PR titles and reference related issue numbers. If this repository has branch naming conventions, follow them; otherwise, ask the user for preferred branch naming. + +What to present to the user +- For any set of changes: present a concise diff or list of changed files, a short explanation of the change's purpose, and which tests were run (or which could not be run due to limitations). +- Ask for explicit approval before making commits. + +If you have additional preferences (tone, level of detail, or where this file should live), provide guidance and this file will be adjusted accordingly. \ No newline at end of file diff --git a/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs index cb637fd..39bdbc2 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -4,7 +4,9 @@ using Minecraft.Terrain.Layers; using SharpEngine.Core; using SharpEngine.Core.Entities; +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; @@ -33,15 +35,6 @@ public class Minecraft : Game private readonly Terrain.TerrainGenerator_New _terrain; private readonly SceneNode _blocksNode; - /// - /// Gets the main window. - /// - public static Window Window - { - get => field ?? throw new InvalidOperationException("The game window has not been assigned yet."); - set; - } - /// /// Initializes a new instance of the . /// @@ -53,7 +46,7 @@ public Minecraft(Scene scene, ISettings settings, ILogger logger) _inventory = new Inventory(); - _blocksNode = _scene.AddNode("Blocks"); + _blocksNode = _scene.Root.AddChild("Blocks"); _terrain = new Terrain.TerrainGenerator_New(_scene, _blocksNode, [ new HeightMapPass(), @@ -88,28 +81,41 @@ public override void Initialize() private void InitializeUI() { - // var gridLayout = new GridLayout(); + var numElements = 10; + const float elementSpacing = 50; + const float elementSize = 50; - // TODO: #89 Fix UI renderer - _uiElem = new UIElement(Window.GetGL(), "uiElement"); - _scene.UIElements.Add(_uiElem); + var gridLayout = new GridLayout( + new Vector2(Window.Left, Window.Bottom), + new Vector2(Window.Right, Window.Bottom)) + { + Rows = 1, + Columns = (uint)numElements, + Spacing = new Vector2(elementSpacing, elementSpacing) + }; - var uiElem2 = new UIElement(Window.GetGL(), "uiElement"); - uiElem2.Transform.Scale = new Vector2(0.2f, 0.2f); - uiElem2.Transform.Position = new Vector2(30, 0); + for (int i = 0; i < numElements; i++) + { + var uiElem = new UIElement("uiElement " + i) + { + Width = elementSize, + Height = elementSize + }; - // gridLayout.AddChild(_uiElem, uiElem2); - _scene.UIElements.Add(_uiElem); - _scene.UIElements.Add(uiElem2); + _uiElem = uiElem; - // _scene.UIElements.Add(gridLayout); + gridLayout.AddChild(_uiElem); + } + + // Attach the layout into the scene graph so traversal finds the UI elements + _scene.Root.AddChild(gridLayout); } /// /// Handles rendering after the frame is drawn. /// /// Information about the frame. - public void OnAfterRender(Frame frame) + public override void OnAfterRender(Frame frame) { var x = _uiElem.Transform.Position.X; var y = _uiElem.Transform.Position.Y; diff --git a/Examples/MinecraftClone/Minecraft.csproj b/Examples/MinecraftClone/Minecraft.csproj index 447f24c..389d0a7 100644 --- a/Examples/MinecraftClone/Minecraft.csproj +++ b/Examples/MinecraftClone/Minecraft.csproj @@ -7,7 +7,8 @@ - + + diff --git a/Examples/MinecraftClone/Program.cs b/Examples/MinecraftClone/Program.cs index 97df2f9..b9f80fc 100644 --- a/Examples/MinecraftClone/Program.cs +++ b/Examples/MinecraftClone/Program.cs @@ -1,12 +1,14 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; - +using SharpEngine.Core; using SharpEngine.Core.DependencyInjection; using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Handlers; using SharpEngine.Core.Interfaces; using SharpEngine.Core.Renderers; using SharpEngine.Core.Scenes; using SharpEngine.Core.Windowing; +using System; namespace Minecraft; @@ -34,36 +36,14 @@ private static void ConfigureServices(IServiceCollection services) }); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(serviceProvider => serviceProvider.GetRequiredService().Camera); + 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); + services.AddEngine(engine => + { + engine.AddHandler().AddWindow(); + }); } } diff --git a/Examples/MinecraftClone/Resources/Slot.png b/Examples/MinecraftClone/Resources/Slot.png new file mode 100644 index 0000000000000000000000000000000000000000..20fa1479dabf36ba543fb3cae35c32de865fe1a8 GIT binary patch literal 609 zcmeAS@N?(olHy`uVBq!ia0vp^@gU5>1|<7@q`xvSFll(YIEGZrd3*O_;jI7>*NdyU zoUArj7qD-%_Fxax;>crIm!Ytv$$|Zy!V>0k0j~q16PgvJn3I(Hzt+@!n`Qa=%U<>i zAHJ??;QiQJb$tHWwE5d^zwMQCm)e!PYE{)7AGQ5o|HMx%3rvf1Jrc5-dHu$Va$DSZ zuNOKlEzO$sY_{FYV%tZ&UwkF}mtU5zvbij2Z(RRs1+#Nx%Cqpf?pkj>()vnkt{beW z-a7Ynj_lF%fAjyy^QOkE{%FOgwRY;v?v5Xe7WOQax?WfyRll0~Y0i}&i_fL!{yDR! zwgWGBW_KW>Myag?%mIk$EusT z@>r?NoMZdB-W}F`8P$8eFd)-?l6b!6_blt{H?6w&PyX|VCwJwt(m5}B_D`;1ZY%oM z`Tu#+pSgGTGaD({Wy_m<`Mkt7L-X~H1(&K${{7z|ox5b=ZMW)>)uCT5SlhL^*DT(@ Ye8T1YZnlCyz@)?A>FVdQ&MBb@0H10j;Q#;t literal 0 HcmV?d00001 diff --git a/Examples/MinecraftClone/Terrain/TerrainGenerator.cs b/Examples/MinecraftClone/Terrain/TerrainGenerator.cs index b71e3e9..c9d37b5 100644 --- a/Examples/MinecraftClone/Terrain/TerrainGenerator.cs +++ b/Examples/MinecraftClone/Terrain/TerrainGenerator.cs @@ -1,6 +1,7 @@ using Minecraft.Terrain.Block; using SharpEngine.Core.Entities; using SharpEngine.Core.Entities.Lights; +using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Numerics; using SharpEngine.Core.Numerics.Noise; using SharpEngine.Core.Scenes; @@ -56,7 +57,7 @@ public sealed class TerrainGenerator_New internal TerrainGenerator_New(Scene scene, SceneNode blocksNode, IEnumerable? layers = null) { _scene = scene; - _lightsNode = _scene.AddNode("Lights"); + _lightsNode = _scene.Root.AddChild("Lights"); _blocksNode = blocksNode; _layers = layers?.ToArray() ?? Array.Empty(); } diff --git a/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs b/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs index 84ee4f0..31ec3ad 100644 --- a/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs +++ b/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs @@ -1,5 +1,6 @@ using Minecraft.Terrain.Block; using SharpEngine.Core.Entities.Lights; +using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Numerics; using SharpEngine.Core.Numerics.Noise; using SharpEngine.Core.Scenes; @@ -32,7 +33,7 @@ internal class TerrainGenerator_Old internal TerrainGenerator_Old(Scene scene, SceneNode blocksNode) { _scene = scene; - _lightsNode = _scene.AddNode("Lights"); + _lightsNode = _scene.Root.AddChild("Lights"); _blocksNode = blocksNode; } diff --git a/Examples/MultipleWindows/Program.cs b/Examples/MultipleWindows/Program.cs index b1dfc16..943d2f4 100644 --- a/Examples/MultipleWindows/Program.cs +++ b/Examples/MultipleWindows/Program.cs @@ -34,7 +34,7 @@ public static partial class Program public static async Task Main(string[] _) { Core.Handlers.WindowHandler windowHandler = new(); - windowHandler.Start(); + windowHandler.Start(_cancellationTokenSource); Console.WriteLine("end"); Console.ReadLine(); @@ -108,7 +108,7 @@ private static Window CreateWindow() } }; - var window = new Window(new(), options, _windowLogger); + var window = new Window(new(), options); window.Initialize(); return window; @@ -119,10 +119,10 @@ private static void EnqueueWindow() var window = CreateWindow(); // Prefer the higher-level Window event to avoid wiring low-level input handlers. - window.OnButtonMouseDown += (mouse, button) => Mouse_Click(mouse, (MouseButton)button, mouse.Position); + window.InputManager.OnButtonMouseDown += (mouse, button) => Mouse_Click(mouse, (MouseButton)button, mouse.Position); - if (window.Input is not null) - _inputContexts.Add(window.Input); + if (window.InputManager.Context is not null) + _inputContexts.Add(window.InputManager.Context); _windows.Add(window); } diff --git a/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 index bbd8dbb..04f525f 100644 --- a/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 @@ -7,8 +7,9 @@ - + + diff --git a/README.md b/README.md index 0b0344a..8f9cdfe 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ [![NuGet Version](https://img.shields.io/nuget/v/SharpEngine.Core.NET)](https://www.nuget.org/packages/SharpEngine.Core.NET) ![NuGet Downloads](https://img.shields.io/nuget/dt/PACKAGE_NAME) +[![Quality gate status](https://sonarcloud.io/api/project_badges/measure?project=Av3boy_SharpEngine&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=Av3boy_SharpEngine) + + true + + + + + + + + + + + diff --git a/SharpEngine.Core.Components/IComponent.cs b/SharpEngine.Core.Components/IComponent.cs deleted file mode 100644 index 5a744ba..0000000 --- a/SharpEngine.Core.Components/IComponent.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace SharpEngine.Core.Components; - -// TODO: #53 Entity Component System - -/// -/// 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/Properties/Textures/TextureRuntime.cs b/SharpEngine.Core.Components/Properties/Textures/TextureRuntime.cs new file mode 100644 index 0000000..a04c593 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Textures/TextureRuntime.cs @@ -0,0 +1,36 @@ +using Silk.NET.OpenGL; +using System.IO; + +namespace SharpEngine.Core.Components.Properties.Textures; + +public partial class Texture +{ + /// + /// Creates a texture from raw RGBA bytes. + /// + public Texture(GL gl, byte[] rgbaData, int width, int height, string path = "", TextureType type = TextureType.Diffuse) + { + _gl = gl; + Path = path; + Type = type; + Handle = _gl.GenTexture(); + + Use(); + + // Flip the rows because System.Drawing bitmaps are top-left origin while OpenGL expects bottom-left. + var flipped = new byte[rgbaData.Length]; + int rowBytes = width * 4; + for (int y = 0; y < height; y++) + { + int srcRow = y * rowBytes; + int dstRow = (height - 1 - y) * rowBytes; + System.Buffer.BlockCopy(rgbaData, srcRow, flipped, dstRow, rowBytes); + } + + // Upload the pixel data (flipped vertically) + _gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba, (uint)width, (uint)height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, flipped); + + SetParameters(); + _gl.GenerateMipmap(GLEnum.Texture2D); + } +} diff --git a/SharpEngine.Core.Components/Properties/Transform2D.cs b/SharpEngine.Core.Components/Properties/Transform2D.cs index 0809d3e..48c9960 100644 --- a/SharpEngine.Core.Components/Properties/Transform2D.cs +++ b/SharpEngine.Core.Components/Properties/Transform2D.cs @@ -30,8 +30,8 @@ public class Transform2D : ITransform /// Gets the transformation of the game object as a model matrix. ///
public Matrix4x4 ModelMatrix => Matrix4x4.CreateScale(new Vector3(Scale.X, Scale.Y, 0)) * - // Matrix4x4.CreateRotationZ(Math.DegreesToRadians(Rotation.Angle)) * - Matrix4x4.CreateRotationZ(0) * + Matrix4x4.CreateRotationZ(Math.DegreesToRadians(Rotation.Angle)) * + // Matrix4x4.CreateRotationZ(0) * // Matrix4x4.CreateTranslation(new Vector3(Position.X, Position.Y, 0)); Matrix4x4.CreateTranslation(new Vector3(0, 0, 0)); } \ No newline at end of file diff --git a/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj index 99cc5f5..77bb1c8 100644 --- a/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj +++ b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj @@ -29,4 +29,8 @@ + + + + diff --git a/SharpEngine.Core/_Resources/Default.cs b/SharpEngine.Core.Defaults/Defaults.cs similarity index 65% rename from SharpEngine.Core/_Resources/Default.cs rename to SharpEngine.Core.Defaults/Defaults.cs index 88fc100..09b291a 100644 --- a/SharpEngine.Core/_Resources/Default.cs +++ b/SharpEngine.Core.Defaults/Defaults.cs @@ -1,11 +1,11 @@ -using SharpEngine.Core.Extensions; +using SharpEngine.IO.Extensions; -namespace SharpEngine.Core._Resources; +namespace SharpEngine.Core.Defaults; /// /// Contains default resources used by the engine. /// -public static class Default +public static class Defaults { /// Gets the path to the debug texture. public static string DebugTexture => PathExtensions.GetAssemblyPath("_Resources\\Textures\\debug.JPG"); @@ -24,4 +24,15 @@ public static class Default /// Gets the path to the default fragment shader used for rendering UI elements. public static string UIFragmentShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\uiShader.frag"); + + /// Gets the path to the fragment shader used for SDF UI rendering. + public static string UIFragmentShaderSdf => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\ui_sdf.frag"); + + /// + /// Gets the default fonts directory where bundled or project fonts may be located. + /// + public static string FontsDirectory => PathExtensions.GetAssemblyPath("Assets\\Fonts"); + + /// Gets the default font family name used by TextElement when no font is specified. + public const string Font = "Segoe UI"; } diff --git a/SharpEngine.Core.Defaults/SharpEngine.Core.Defaults.csproj b/SharpEngine.Core.Defaults/SharpEngine.Core.Defaults.csproj new file mode 100644 index 0000000..75b3770 --- /dev/null +++ b/SharpEngine.Core.Defaults/SharpEngine.Core.Defaults.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/SharpEngine.Core.DependencyInjection/WindowServiceCollectionExtensions.cs b/SharpEngine.Core.DependencyInjection/WindowServiceCollectionExtensions.cs index 200c58f..6de501a 100644 --- a/SharpEngine.Core.DependencyInjection/WindowServiceCollectionExtensions.cs +++ b/SharpEngine.Core.DependencyInjection/WindowServiceCollectionExtensions.cs @@ -56,11 +56,11 @@ private static void ConfigureWindow(IServiceProvider serviceProvider, Window win var game = serviceProvider.GetRequiredService(); window.OnLoaded += game.Initialize; - window._inputManager.OnHandleMouse += game.HandleMouse; - window._inputManager.OnUpdate += game.Update; - window._inputManager.OnHandleKeyboard += game.HandleKeyboard; - window._inputManager.OnButtonMouseDown += game.HandleMouseDown; - window._inputManager.HandleMouseWheel += game.HandleMouseWheel; + window.InputManager.OnHandleMouse += game.HandleMouse; + window.InputManager.OnUpdate += game.Update; + window.InputManager.OnHandleKeyboard += game.HandleKeyboard; + window.InputManager.OnButtonMouseDown += game.HandleMouseDown; + window.InputManager.HandleMouseWheel += game.HandleMouseWheel; window.OnAfterRender += game.OnAfterRender; game.Window = window; diff --git a/SharpEngine.Core.Numerics/Math.cs b/SharpEngine.Core.Numerics/Math.cs new file mode 100644 index 0000000..8dcc435 --- /dev/null +++ b/SharpEngine.Core.Numerics/Math.cs @@ -0,0 +1,47 @@ +using System; +using System.Numerics; +using SharpEngine.Core.Numerics; + +namespace SharpEngine.Core; + +/// +/// Provides math-related functions. +/// +public static class Math +{ + /// + /// Converts the given to a representation. + /// + /// The matrix to be converted. + /// A ReadOnlySpan of floats representing the matrix. + public static ReadOnlySpan ToSpan(this Matrix4x4 matrix) + => new([ + matrix.M11, matrix.M12, matrix.M13, matrix.M14, + matrix.M21, matrix.M22, matrix.M23, matrix.M24, + matrix.M31, matrix.M32, matrix.M33, matrix.M34, + matrix.M41, matrix.M42, matrix.M43, matrix.M44 + ]); + + /// Gets the value of π / 2. + public static float PiOver2 => MathF.PI / 2; + + /// + /// Converts a value in to radians. + /// + /// The degrees to be converted. + /// The given in radians. + public static double DegreesToRadians(double degrees) + => System.Math.PI / 180 * degrees; + + /// + public static float DegreesToRadians(float degrees) + => MathF.PI / 180 * degrees; + + /// + /// Converts a value in to degrees. + /// + /// The radians to convert. + /// The given as degrees. + public static float RadiansToDegrees(float radians) + => radians * (180 / MathF.PI); +} diff --git a/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs index a961cd8..91ad8e7 100644 --- a/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs +++ b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs @@ -1,4 +1,5 @@ using MathEx = SharpEngine.Core.Numerics.MathExtensions; +using SysMath = System.Math; namespace SharpEngine.Core.Numerics.Noise; @@ -34,21 +35,21 @@ public float Scale public int Octaves { get => _octaves; - set => _octaves = Math.Max(1, value); + set => _octaves = SysMath.Max(1, value); } /// public float Persistence { get => _persistence; - set => _persistence = Math.Max(0f, value); + set => _persistence = SysMath.Max(0f, value); } /// public float Lacunarity { get => _lacunarity; - set => _lacunarity = Math.Max(0.0001f, value); + set => _lacunarity = SysMath.Max(0.0001f, value); } /// @@ -186,6 +187,6 @@ 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); + return SysMath.Clamp(result, 0f, 1f); } } \ No newline at end of file diff --git a/SharpEngine.Core.Numerics/Vector2.cs b/SharpEngine.Core.Numerics/Vector2.cs index 0e94630..89d9d6b 100644 --- a/SharpEngine.Core.Numerics/Vector2.cs +++ b/SharpEngine.Core.Numerics/Vector2.cs @@ -10,410 +10,409 @@ using Quaternion = System.Numerics.Quaternion; using Vector = System.Numerics.Vector; -namespace SharpEngine.Core.Numerics +namespace SharpEngine.Core.Numerics; + +/// +/// A structure encapsulating two single precision floating point values and provides hardware accelerated methods. +/// +public partial struct Vector2 : IEquatable, IFormattable { + #region Public Static Properties + + /// Returns the vector (0,0). + public static Vector2 Zero => new(); + + /// Returns the vector (1,1). + public static Vector2 One => new(1.0f, 1.0f); + + /// Returns the vector (1,0). + public static Vector2 UnitX => new(1.0f, 0.0f); + + /// Returns the vector (0,1). + public static Vector2 UnitY => new(0.0f, 1.0f); + + #endregion Public Static Properties + + #region Public instance methods + + /// + /// Returns the hash code for this instance. + /// + /// The hash code. + public override readonly int GetHashCode() + => HashCodeHelper.CombineHashCodes(X.GetHashCode(), Y.GetHashCode()); + /// - /// A structure encapsulating two single precision floating point values and provides hardware accelerated methods. + /// Returns a boolean indicating whether the given Object is equal to this Vector2 instance. /// - public partial struct Vector2 : IEquatable, IFormattable + /// The Object to compare against. + /// True if the Object is equal to this Vector2; False otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) { - #region Public Static Properties - - /// Returns the vector (0,0). - public static Vector2 Zero => new(); - - /// Returns the vector (1,1). - public static Vector2 One => new(1.0f, 1.0f); - - /// Returns the vector (1,0). - public static Vector2 UnitX => new(1.0f, 0.0f); - - /// Returns the vector (0,1). - public static Vector2 UnitY => new(0.0f, 1.0f); - - #endregion Public Static Properties - - #region Public instance methods - - /// - /// Returns the hash code for this instance. - /// - /// The hash code. - public override readonly int GetHashCode() - => HashCodeHelper.CombineHashCodes(X.GetHashCode(), Y.GetHashCode()); - - /// - /// Returns a boolean indicating whether the given Object is equal to this Vector2 instance. - /// - /// The Object to compare against. - /// True if the Object is equal to this Vector2; False otherwise. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override bool Equals(object? obj) - { - if (obj is not Vector2 vec2) - return false; + if (obj is not Vector2 vec2) + return false; - return Equals(vec2); - } + return Equals(vec2); + } - /// - /// Returns a String representing this Vector2 instance. - /// - /// The string representation. - public override readonly string ToString() => ToString("G", CultureInfo.CurrentCulture); - - /// - /// Returns a String representing this Vector2 instance, using the specified format to format individual elements. - /// - /// The format of individual elements. - /// The string representation. - public readonly string ToString(string format) => ToString(format, CultureInfo.CurrentCulture); - - /// - /// Returns a String representing this Vector2 instance, using the specified format to format individual elements - /// and the given IFormatProvider. - /// - /// The format of individual elements. - /// The format provider to use when formatting elements. - /// The string representation. - public readonly string ToString(string? format, IFormatProvider? formatProvider) - { - var sb = new StringBuilder(); - string separator = NumberFormatInfo.GetInstance(formatProvider).NumberGroupSeparator; - sb.Append('<'); - sb.Append(X.ToString(format, formatProvider)); - sb.Append(separator); - sb.Append(' '); - sb.Append(Y.ToString(format, formatProvider)); - sb.Append('>'); - return sb.ToString(); - } + /// + /// Returns a String representing this Vector2 instance. + /// + /// The string representation. + public override readonly string ToString() => ToString("G", CultureInfo.CurrentCulture); - /// - /// Returns the length of the vector. - /// - /// The vector's length. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly float Length() - { - if (Vector.IsHardwareAccelerated) - { - float ls = Vector2.Dot(this, this); - return (float)MathF.Sqrt(ls); - } - else - { - float ls = (X * X) + (Y * Y); - return MathF.Sqrt(ls); - } - } + /// + /// Returns a String representing this Vector2 instance, using the specified format to format individual elements. + /// + /// The format of individual elements. + /// The string representation. + public readonly string ToString(string format) => ToString(format, CultureInfo.CurrentCulture); - /// - /// Returns the length of the vector squared. This operation is cheaper than Length(). - /// - /// The vector's length squared. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly float LengthSquared() - { - if (Vector.IsHardwareAccelerated) - return Vector2.Dot(this, this); + /// + /// Returns a String representing this Vector2 instance, using the specified format to format individual elements + /// and the given IFormatProvider. + /// + /// The format of individual elements. + /// The format provider to use when formatting elements. + /// The string representation. + public readonly string ToString(string? format, IFormatProvider? formatProvider) + { + var sb = new StringBuilder(); + string separator = NumberFormatInfo.GetInstance(formatProvider).NumberGroupSeparator; + sb.Append('<'); + sb.Append(X.ToString(format, formatProvider)); + sb.Append(separator); + sb.Append(' '); + sb.Append(Y.ToString(format, formatProvider)); + sb.Append('>'); + return sb.ToString(); + } - else - return (X * X) + (Y * Y); - } - #endregion Public Instance Methods - - #region Public Static Methods - /// - /// Returns the Euclidean distance between the two given points. - /// - /// The first point. - /// The second point. - /// The distance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Distance(Vector2 value1, Vector2 value2) + /// + /// Returns the length of the vector. + /// + /// The vector's length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float Length() + { + if (Vector.IsHardwareAccelerated) { - if (Vector.IsHardwareAccelerated) - { - var difference = value1 - value2; - float ls = Vector2.Dot(difference, difference); - return (float)System.MathF.Sqrt(ls); - } - else - { - float dx = value1.X - value2.X; - float dy = value1.Y - value2.Y; - - float ls = (dx * dx) + (dy * dy); - - return MathF.Sqrt(ls); - } + float ls = Vector2.Dot(this, this); + return (float)MathF.Sqrt(ls); } - - /// - /// Returns the Euclidean distance squared between the two given points. - /// - /// The first point. - /// The second point. - /// The distance squared. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DistanceSquared(Vector2 value1, Vector2 value2) + else { - if (Vector.IsHardwareAccelerated) - { - var difference = value1 - value2; - return Vector2.Dot(difference, difference); - } - else - { - float dx = value1.X - value2.X; - float dy = value1.Y - value2.Y; - - return (dx * dx) + (dy * dy); - } + float ls = (X * X) + (Y * Y); + return MathF.Sqrt(ls); } + } - /// - /// Returns a vector with the same direction as the given vector, but with a length of 1. - /// - /// The vector to normalize. - /// The normalized vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Normalize(Vector2 value) - { - if (Vector.IsHardwareAccelerated) - { - float length = value.Length(); - return value / length; - } - else - { - float ls = (value.X * value.X) + (value.Y * value.Y); - float invNorm = 1.0f / MathF.Sqrt(ls); - - return new Vector2( - value.X * invNorm, - value.Y * invNorm); - } - } + /// + /// Returns the length of the vector squared. This operation is cheaper than Length(). + /// + /// The vector's length squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float LengthSquared() + { + if (Vector.IsHardwareAccelerated) + return Vector2.Dot(this, this); + + else + return (X * X) + (Y * Y); + } + #endregion Public Instance Methods - /// - /// Returns the reflection of a vector off a surface that has the specified normal. - /// - /// The source vector. - /// The normal of the surface being reflected off. - /// The reflected vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Reflect(Vector2 vector, Vector2 normal) + #region Public Static Methods + /// + /// Returns the Euclidean distance between the two given points. + /// + /// The first point. + /// The second point. + /// The distance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Distance(Vector2 value1, Vector2 value2) + { + if (Vector.IsHardwareAccelerated) { - if (Vector.IsHardwareAccelerated) - { - float dot = Vector2.Dot(vector, normal); - return vector - (2 * dot * normal); - } - else - { - float dot = (vector.X * normal.X) + (vector.Y * normal.Y); - - return new Vector2( - vector.X - (2.0f * dot * normal.X), - vector.Y - (2.0f * dot * normal.Y)); - } + var difference = value1 - value2; + float ls = Vector2.Dot(difference, difference); + return (float)System.MathF.Sqrt(ls); } - - /// - /// Restricts a vector between a min and max value. - /// - /// The source vector. - /// The minimum value. - /// The maximum value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Clamp(Vector2 value1, Vector2 min, Vector2 max) + else { - // This compare order is very important!!! - // We must follow HLSL behavior in the case user specified min value is bigger than max value. - float x = value1.X; - x = (x > max.X) ? max.X : x; - x = (x < min.X) ? min.X : x; + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; - float y = value1.Y; - y = (y > max.Y) ? max.Y : y; - y = (y < min.Y) ? min.Y : y; + float ls = (dx * dx) + (dy * dy); - return new Vector2(x, y); + return MathF.Sqrt(ls); } + } - /// - /// Linearly interpolates between two vectors based on the given weighting. - /// - /// The first source vector. - /// The second source vector. - /// Value between 0 and 1 indicating the weight of the second source vector. - /// The interpolated vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Lerp(Vector2 value1, Vector2 value2, float amount) + /// + /// Returns the Euclidean distance squared between the two given points. + /// + /// The first point. + /// The second point. + /// The distance squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DistanceSquared(Vector2 value1, Vector2 value2) + { + if (Vector.IsHardwareAccelerated) { - return new Vector2( - value1.X + ((value2.X - value1.X) * amount), - value1.Y + ((value2.Y - value1.Y) * amount)); + var difference = value1 - value2; + return Vector2.Dot(difference, difference); } - - /// - /// Transforms a vector by the given matrix. - /// - /// The source vector. - /// The transformation matrix. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Transform(Vector2 position, Matrix3x2 matrix) + else { - return new Vector2( - (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M31, - (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M32); + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + + return (dx * dx) + (dy * dy); } + } - /// - /// Transforms a vector by the given matrix. - /// - /// The source vector. - /// The transformation matrix. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Transform(Vector2 position, Matrix4x4 matrix) + /// + /// Returns a vector with the same direction as the given vector, but with a length of 1. + /// + /// The vector to normalize. + /// The normalized vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Normalize(Vector2 value) + { + if (Vector.IsHardwareAccelerated) { - return new Vector2( - (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M41, - (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M42); + float length = value.Length(); + return value / length; } - - /// - /// Transforms a vector normal by the given matrix. - /// - /// The source vector. - /// The transformation matrix. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 TransformNormal(Vector2 normal, Matrix3x2 matrix) + else { + float ls = (value.X * value.X) + (value.Y * value.Y); + float invNorm = 1.0f / MathF.Sqrt(ls); + return new Vector2( - (normal.X * matrix.M11) + (normal.Y * matrix.M21), - (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + value.X * invNorm, + value.Y * invNorm); } + } - /// - /// Transforms a vector normal by the given matrix. - /// - /// The source vector. - /// The transformation matrix. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 TransformNormal(Vector2 normal, Matrix4x4 matrix) + /// + /// Returns the reflection of a vector off a surface that has the specified normal. + /// + /// The source vector. + /// The normal of the surface being reflected off. + /// The reflected vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Reflect(Vector2 vector, Vector2 normal) + { + if (Vector.IsHardwareAccelerated) { - return new Vector2( - (normal.X * matrix.M11) + (normal.Y * matrix.M21), - (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + float dot = Vector2.Dot(vector, normal); + return vector - (2 * dot * normal); } - - /// - /// Transforms a vector by the given Quaternion rotation value. - /// - /// The source vector to be rotated. - /// The rotation to apply. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Transform(Vector2 value, Quaternion rotation) + else { - float x2 = rotation.X + rotation.X; - float y2 = rotation.Y + rotation.Y; - float z2 = rotation.Z + rotation.Z; - - float wz2 = rotation.W * z2; - float xx2 = rotation.X * x2; - float xy2 = rotation.X * y2; - float yy2 = rotation.Y * y2; - float zz2 = rotation.Z * z2; + float dot = (vector.X * normal.X) + (vector.Y * normal.Y); return new Vector2( - (value.X * (1.0f - yy2 - zz2)) + (value.Y * (xy2 - wz2)), - (value.X * (xy2 + wz2)) + (value.Y * (1.0f - xx2 - zz2))); + vector.X - (2.0f * dot * normal.X), + vector.Y - (2.0f * dot * normal.Y)); } - #endregion Public Static Methods - - #region Public operator methods - // all the below methods should be inlined as they are - // implemented over JIT intrinsics - - /// - /// Adds two vectors together. - /// - /// The first source vector. - /// The second source vector. - /// The summed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Add(Vector2 left, Vector2 right) => left + right; - - /// - /// Subtracts the second vector from the first. - /// - /// The first source vector. - /// The second source vector. - /// The difference vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Subtract(Vector2 left, Vector2 right) => left - right; - - /// - /// Multiplies two vectors together. - /// - /// The first source vector. - /// The second source vector. - /// The product vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Multiply(Vector2 left, Vector2 right) => left * right; - - /// - /// Multiplies a vector by the given scalar. - /// - /// The source vector. - /// The scalar value. - /// The scaled vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Multiply(Vector2 left, float right) => left * right; - - /// - /// Multiplies a vector by the given scalar. - /// - /// The scalar value. - /// The source vector. - /// The scaled vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Multiply(float left, Vector2 right) => left * right; - - /// - /// Divides the first vector by the second. - /// - /// The first source vector. - /// The second source vector. - /// The vector resulting from the division. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Divide(Vector2 left, Vector2 right) => left / right; - - /// - /// Divides the vector by the given scalar. - /// - /// The source vector. - /// The scalar value. - /// The result of the division. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Divide(Vector2 left, float divisor) => left / divisor; - - /// - /// Negates a given vector. - /// - /// The source vector. - /// The negated vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Negate(Vector2 value) => -value; - - #endregion Public operator methods } + + /// + /// Restricts a vector between a min and max value. + /// + /// The source vector. + /// The minimum value. + /// The maximum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Clamp(Vector2 value1, Vector2 min, Vector2 max) + { + // This compare order is very important!!! + // We must follow HLSL behavior in the case user specified min value is bigger than max value. + float x = value1.X; + x = (x > max.X) ? max.X : x; + x = (x < min.X) ? min.X : x; + + float y = value1.Y; + y = (y > max.Y) ? max.Y : y; + y = (y < min.Y) ? min.Y : y; + + return new Vector2(x, y); + } + + /// + /// Linearly interpolates between two vectors based on the given weighting. + /// + /// The first source vector. + /// The second source vector. + /// Value between 0 and 1 indicating the weight of the second source vector. + /// The interpolated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Lerp(Vector2 value1, Vector2 value2, float amount) + { + return new Vector2( + value1.X + ((value2.X - value1.X) * amount), + value1.Y + ((value2.Y - value1.Y) * amount)); + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 position, Matrix3x2 matrix) + { + return new Vector2( + (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M31, + (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M32); + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 position, Matrix4x4 matrix) + { + return new Vector2( + (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M41, + (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M42); + } + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 TransformNormal(Vector2 normal, Matrix3x2 matrix) + { + return new Vector2( + (normal.X * matrix.M11) + (normal.Y * matrix.M21), + (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + } + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 TransformNormal(Vector2 normal, Matrix4x4 matrix) + { + return new Vector2( + (normal.X * matrix.M11) + (normal.Y * matrix.M21), + (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + } + + /// + /// Transforms a vector by the given Quaternion rotation value. + /// + /// The source vector to be rotated. + /// The rotation to apply. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 value, Quaternion rotation) + { + float x2 = rotation.X + rotation.X; + float y2 = rotation.Y + rotation.Y; + float z2 = rotation.Z + rotation.Z; + + float wz2 = rotation.W * z2; + float xx2 = rotation.X * x2; + float xy2 = rotation.X * y2; + float yy2 = rotation.Y * y2; + float zz2 = rotation.Z * z2; + + return new Vector2( + (value.X * (1.0f - yy2 - zz2)) + (value.Y * (xy2 - wz2)), + (value.X * (xy2 + wz2)) + (value.Y * (1.0f - xx2 - zz2))); + } + #endregion Public Static Methods + + #region Public operator methods + // all the below methods should be inlined as they are + // implemented over JIT intrinsics + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Add(Vector2 left, Vector2 right) => left + right; + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Subtract(Vector2 left, Vector2 right) => left - right; + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(Vector2 left, Vector2 right) => left * right; + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(Vector2 left, float right) => left * right; + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(float left, Vector2 right) => left * right; + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Divide(Vector2 left, Vector2 right) => left / right; + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Divide(Vector2 left, float divisor) => left / divisor; + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Negate(Vector2 value) => -value; + + #endregion Public operator methods } \ No newline at end of file diff --git a/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs b/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs index e747bab..a495966 100644 --- a/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs +++ b/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs @@ -2,307 +2,307 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Runtime.CompilerServices; +using SysMath = System.Math; -namespace SharpEngine.Core.Numerics +namespace SharpEngine.Core.Numerics; + +// This file contains the definitions for all of the JIT intrinsic methods and properties that are recognized by the current x64 JIT compiler. +// The implementation defined here is used in any circumstance where the JIT fails to recognize these members as intrinsic. +// The JIT recognizes these methods and properties by name and signature: if either is changed, the JIT will no longer recognize the member. +// Some methods declared here are not strictly intrinsic, but delegate to an intrinsic method. For example, only one overload of CopyTo() +// is actually recognized by the JIT, but both are here for simplicity. + +public partial struct Vector3 : IVector { - // This file contains the definitions for all of the JIT intrinsic methods and properties that are recognized by the current x64 JIT compiler. - // The implementation defined here is used in any circumstance where the JIT fails to recognize these members as intrinsic. - // The JIT recognizes these methods and properties by name and signature: if either is changed, the JIT will no longer recognize the member. - // Some methods declared here are not strictly intrinsic, but delegate to an intrinsic method. For example, only one overload of CopyTo() - // is actually recognized by the JIT, but both are here for simplicity. + /// + public float X { get; set; } + + /// + public float Y { get; set; } + + /// + public float Z { get; set; } + + #region Constructors + + /// + /// Constructs a vector whose elements are all the single specified value. + /// + /// The element to fill the vector with. + [JitIntrinsic] + public Vector3(float value) : this(value, value, value) { } + + /// + /// Constructs a Vector3 from the given Vector2 and a third value. + /// + /// The Vector to extract X and Y components from. + /// The Z component. + public Vector3(Vector2 value, float z) : this(value.X, value.Y, z) { } + + /// + /// Constructs a vector with the given individual elements. + /// + /// The X component. + /// The Y component. + /// The Z component. + [JitIntrinsic] + public Vector3(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } - public partial struct Vector3 : IVector + #endregion Constructors + + #region Public Instance Methods + + /// + /// Copies the contents of the vector into the given array. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void CopyTo(float[] array) => CopyTo(array, 0); + + /// + /// Copies the contents of the vector into the given array, starting from index. + /// + /// If array is null. + /// If array is multidimensional. + /// If index is greater than end of the array or index is less than zero. + /// If number of elements in source vector is greater than those available in destination array. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void CopyTo(float[] array, int index) { - /// - public float X { get; set; } - - /// - public float Y { get; set; } - - /// - public float Z { get; set; } - - #region Constructors - - /// - /// Constructs a vector whose elements are all the single specified value. - /// - /// The element to fill the vector with. - [JitIntrinsic] - public Vector3(float value) : this(value, value, value) { } - - /// - /// Constructs a Vector3 from the given Vector2 and a third value. - /// - /// The Vector to extract X and Y components from. - /// The Z component. - public Vector3(Vector2 value, float z) : this(value.X, value.Y, z) { } - - /// - /// Constructs a vector with the given individual elements. - /// - /// The X component. - /// The Y component. - /// The Z component. - [JitIntrinsic] - public Vector3(float x, float y, float z) - { - X = x; - Y = y; - Z = z; - } - - #endregion Constructors - - #region Public Instance Methods - - /// - /// Copies the contents of the vector into the given array. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void CopyTo(float[] array) => CopyTo(array, 0); - - /// - /// Copies the contents of the vector into the given array, starting from index. - /// - /// If array is null. - /// If array is multidimensional. - /// If index is greater than end of the array or index is less than zero. - /// If number of elements in source vector is greater than those available in destination array. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void CopyTo(float[] array, int index) - { - // Match the JIT's exception type here. For perf, a NullReference is thrown instead of an ArgumentNull. - ArgumentNullException.ThrowIfNull(array); - - if (index < 0 || index >= array.Length) - throw new ArgumentOutOfRangeException(nameof(index)); - - if ((array.Length - index) < 3) - throw new ArgumentException("", nameof(index)); - - array[index] = X; - array[index + 1] = Y; - array[index + 2] = Z; - } - - /// - /// Returns a boolean indicating whether the given Vector3 is equal to this Vector3 instance. - /// - /// The Vector3 to compare this instance to. - /// True if the other Vector3 is approximately equal to this instance; False otherwise. - [JitIntrinsic] - public readonly bool Equals(Vector3 other) - => MathF.Abs(X - other.X) < Float.Tolerance && - MathF.Abs(Y - other.Y) < Float.Tolerance && - MathF.Abs(Z - other.Z) < Float.Tolerance; - - #endregion Public Instance Methods - - #region Public Static Methods - - /// - /// Returns the dot product of two vectors. - /// - /// The first vector. - /// The second vector. - /// The dot product. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Dot(Vector3 vector1, Vector3 vector2) - => (vector1.X * vector2.X) + - (vector1.Y * vector2.Y) + - (vector1.Z * vector2.Z); - - /// - /// Returns a vector whose elements are the minimum of each of the pairs of elements in the two source vectors. - /// - /// The first source vector. - /// The second source vector. - /// The minimized vector. - [JitIntrinsic] - public static Vector3 Min(Vector3 value1, Vector3 value2) - => new((value1.X < value2.X) ? value1.X : value2.X, - (value1.Y < value2.Y) ? value1.Y : value2.Y, - (value1.Z < value2.Z) ? value1.Z : value2.Z); - - /// - /// Returns a vector whose elements are the maximum of each of the pairs of elements in the two source vectors. - /// - /// The first source vector. - /// The second source vector. - /// The maximized vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 Max(Vector3 value1, Vector3 value2) - => new((value1.X > value2.X) ? value1.X : value2.X, - (value1.Y > value2.Y) ? value1.Y : value2.Y, - (value1.Z > value2.Z) ? value1.Z : value2.Z); - - /// - /// Returns a vector whose elements are the absolute values of each of the source vector's elements. - /// - /// The source vector. - /// The absolute value vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 Abs(Vector3 value) - => new(MathF.Abs(value.X), - MathF.Abs(value.Y), - MathF.Abs(value.Z)); - - /// - /// Returns a vector whose elements are the square root of each of the source vector's elements. - /// - /// The source vector. - /// The square root vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 SquareRoot(Vector3 value) - => new(MathF.Sqrt(value.X), - MathF.Sqrt(value.Y), - MathF.Sqrt(value.Z)); - - #endregion Public Static Methods - - #region Public Static Operators - - /// - /// Adds two vectors together. - /// - /// The first source vector. - /// The second source vector. - /// The summed vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator +(Vector3 left, Vector3 right) - => new(left.X + right.X, - left.Y + right.Y, - left.Z + right.Z); - - /// - /// Subtracts the second vector from the first. - /// - /// The first source vector. - /// The second source vector. - /// The difference vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator -(Vector3 left, Vector3 right) - => new(left.X - right.X, - left.Y - right.Y, - left.Z - right.Z); - - /// - /// Multiplies two vectors together. - /// - /// The first source vector. - /// The second source vector. - /// The product vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator *(Vector3 left, Vector3 right) - => new(left.X * right.X, - left.Y * right.Y, - left.Z * right.Z); - - /// - /// Multiplies a vector by the given scalar. - /// - /// The source vector. - /// The scalar value. - /// The scaled vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator *(Vector3 left, float right) => left * new Vector3(right); - - /// - /// Multiplies a vector by the given scalar. - /// - /// The scalar value. - /// The source vector. - /// The scaled vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator *(float left, Vector3 right) => new Vector3(left) * right; - - /// - /// Divides the first vector by the second. - /// - /// The first source vector. - /// The second source vector. - /// The vector resulting from the division. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator /(Vector3 left, Vector3 right) - => new(left.X / right.X, - left.Y / right.Y, - left.Z / right.Z); - - /// - /// Divides the vector by the given scalar. - /// - /// The source vector. - /// The scalar value. - /// The result of the division. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator /(Vector3 value1, float value2) - { - float invDiv = 1.0f / value2; - - return new Vector3( - value1.X * invDiv, - value1.Y * invDiv, - value1.Z * invDiv); - } - - /// - /// Negates a given vector. - /// - /// The source vector. - /// The negated vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator -(Vector3 value) => Zero - value; - - /// - /// Returns a boolean indicating whether the two given vectors are equal. - /// - /// The first vector to compare. - /// The second vector to compare. - /// True if the vectors are equal; False otherwise. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(Vector3 left, Vector3 right) - => Math.Abs(left.X - right.X) < Float.Tolerance && - Math.Abs(left.Y - right.Y) < Float.Tolerance && - Math.Abs(left.Z - right.Z) < Float.Tolerance; - - /// - /// Returns a boolean indicating whether the two given vectors are not equal. - /// - /// The first vector to compare. - /// The second vector to compare. - /// True if the vectors are not equal; False if they are equal. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(Vector3 left, Vector3 right) - => !(left == right); - - /// - /// Converts the SharpEngine representation to a System.Numerics representation. - /// - /// The value to convert - public static explicit operator System.Numerics.Vector3(Vector3 value) - => new(value.X, value.Y, value.Z); - - /// - /// Converts the System.Numerics representation to a SharpEngine representation. - /// - /// The value to convert - public static explicit operator Vector3(System.Numerics.Vector3 value) - => new(value.X, value.Y, value.Z); - - #endregion Public Static Operators + // Match the JIT's exception type here. For perf, a NullReference is thrown instead of an ArgumentNull. + ArgumentNullException.ThrowIfNull(array); + + if (index < 0 || index >= array.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + if ((array.Length - index) < 3) + throw new ArgumentException("", nameof(index)); + + array[index] = X; + array[index + 1] = Y; + array[index + 2] = Z; } + + /// + /// Returns a boolean indicating whether the given Vector3 is equal to this Vector3 instance. + /// + /// The Vector3 to compare this instance to. + /// True if the other Vector3 is approximately equal to this instance; False otherwise. + [JitIntrinsic] + public readonly bool Equals(Vector3 other) + => MathF.Abs(X - other.X) < Float.Tolerance && + MathF.Abs(Y - other.Y) < Float.Tolerance && + MathF.Abs(Z - other.Z) < Float.Tolerance; + + #endregion Public Instance Methods + + #region Public Static Methods + + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Dot(Vector3 vector1, Vector3 vector2) + => (vector1.X * vector2.X) + + (vector1.Y * vector2.Y) + + (vector1.Z * vector2.Z); + + /// + /// Returns a vector whose elements are the minimum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The minimized vector. + [JitIntrinsic] + public static Vector3 Min(Vector3 value1, Vector3 value2) + => new((value1.X < value2.X) ? value1.X : value2.X, + (value1.Y < value2.Y) ? value1.Y : value2.Y, + (value1.Z < value2.Z) ? value1.Z : value2.Z); + + /// + /// Returns a vector whose elements are the maximum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The maximized vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Max(Vector3 value1, Vector3 value2) + => new((value1.X > value2.X) ? value1.X : value2.X, + (value1.Y > value2.Y) ? value1.Y : value2.Y, + (value1.Z > value2.Z) ? value1.Z : value2.Z); + + /// + /// Returns a vector whose elements are the absolute values of each of the source vector's elements. + /// + /// The source vector. + /// The absolute value vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Abs(Vector3 value) + => new(MathF.Abs(value.X), + MathF.Abs(value.Y), + MathF.Abs(value.Z)); + + /// + /// Returns a vector whose elements are the square root of each of the source vector's elements. + /// + /// The source vector. + /// The square root vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 SquareRoot(Vector3 value) + => new(MathF.Sqrt(value.X), + MathF.Sqrt(value.Y), + MathF.Sqrt(value.Z)); + + #endregion Public Static Methods + + #region Public Static Operators + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator +(Vector3 left, Vector3 right) + => new(left.X + right.X, + left.Y + right.Y, + left.Z + right.Z); + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator -(Vector3 left, Vector3 right) + => new(left.X - right.X, + left.Y - right.Y, + left.Z - right.Z); + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(Vector3 left, Vector3 right) + => new(left.X * right.X, + left.Y * right.Y, + left.Z * right.Z); + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(Vector3 left, float right) => left * new Vector3(right); + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(float left, Vector3 right) => new Vector3(left) * right; + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator /(Vector3 left, Vector3 right) + => new(left.X / right.X, + left.Y / right.Y, + left.Z / right.Z); + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator /(Vector3 value1, float value2) + { + float invDiv = 1.0f / value2; + + return new Vector3( + value1.X * invDiv, + value1.Y * invDiv, + value1.Z * invDiv); + } + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator -(Vector3 value) => Zero - value; + + /// + /// Returns a boolean indicating whether the two given vectors are equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are equal; False otherwise. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector3 left, Vector3 right) + => SysMath.Abs(left.X - right.X) < Float.Tolerance && + SysMath.Abs(left.Y - right.Y) < Float.Tolerance && + SysMath.Abs(left.Z - right.Z) < Float.Tolerance; + + /// + /// Returns a boolean indicating whether the two given vectors are not equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are not equal; False if they are equal. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector3 left, Vector3 right) + => !(left == right); + + /// + /// Converts the SharpEngine representation to a System.Numerics representation. + /// + /// The value to convert + public static explicit operator System.Numerics.Vector3(Vector3 value) + => new(value.X, value.Y, value.Z); + + /// + /// Converts the System.Numerics representation to a SharpEngine representation. + /// + /// The value to convert + public static explicit operator Vector3(System.Numerics.Vector3 value) + => new(value.X, value.Y, value.Z); + + #endregion Public Static Operators } \ No newline at end of file diff --git a/SharpEngine.Core.Physics/SharpEngine.Core.Physics.csproj b/SharpEngine.Core.Physics/SharpEngine.Core.Physics.csproj new file mode 100644 index 0000000..7cfa364 --- /dev/null +++ b/SharpEngine.Core.Physics/SharpEngine.Core.Physics.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/SharpEngine.Core/Entities/UI/DraggableUIElement.cs b/SharpEngine.Core.UI/Entities/DraggableUIElement.cs similarity index 86% rename from SharpEngine.Core/Entities/UI/DraggableUIElement.cs rename to SharpEngine.Core.UI/Entities/DraggableUIElement.cs index b86ee88..21e52dc 100644 --- a/SharpEngine.Core/Entities/UI/DraggableUIElement.cs +++ b/SharpEngine.Core.UI/Entities/DraggableUIElement.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Silk.NET.OpenGL; -namespace SharpEngine.Core.Entities.UI; +namespace SharpEngine.Core.UI.Entities; public class DraggableUIElement : UIElement { diff --git a/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs b/SharpEngine.Core.UI/Entities/Layouts/FlexLayout.cs similarity index 91% rename from SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs rename to SharpEngine.Core.UI/Entities/Layouts/FlexLayout.cs index 979227e..f042f62 100644 --- a/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs +++ b/SharpEngine.Core.UI/Entities/Layouts/FlexLayout.cs @@ -1,8 +1,9 @@ using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; +using SharpEngine.Core.UI.Entities; using System; -namespace SharpEngine.Core.Entities.UI.Layouts; +namespace SharpEngine.Core.UI.Entities.Layouts; /// /// Represents a layout that dynamically distributes items in a flex layout. diff --git a/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs b/SharpEngine.Core.UI/Entities/Layouts/GridLayout.cs similarity index 98% rename from SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs rename to SharpEngine.Core.UI/Entities/Layouts/GridLayout.cs index 1ef0d39..11443a7 100644 --- a/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs +++ b/SharpEngine.Core.UI/Entities/Layouts/GridLayout.cs @@ -1,9 +1,9 @@ using SharpEngine.Core.Scenes; -using SharpEngine.Core.Entities.UI; using SharpEngine.Core.Numerics; using System; +using SharpEngine.Core.UI.Entities; -namespace SharpEngine.Core.Entities.UI.Layouts; +namespace SharpEngine.Core.UI.Entities.Layouts; /// /// Represents a grid layout that automatically updates item transforms under the hood. diff --git a/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs b/SharpEngine.Core.UI/Entities/Layouts/LayoutBase.cs similarity index 93% rename from SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs rename to SharpEngine.Core.UI/Entities/Layouts/LayoutBase.cs index f1f3570..435dfdc 100644 --- a/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs +++ b/SharpEngine.Core.UI/Entities/Layouts/LayoutBase.cs @@ -1,9 +1,10 @@ using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; +using SharpEngine.Core.UI.Entities; using System.Collections.Generic; -namespace SharpEngine.Core.Entities.UI.Layouts; +namespace SharpEngine.Core.UI.Entities.Layouts; /// /// Represents the base class for all layout types. diff --git a/SharpEngine.Core/Entities/UI/UIElement.cs b/SharpEngine.Core.UI/Entities/UIElement.cs similarity index 96% rename from SharpEngine.Core/Entities/UI/UIElement.cs rename to SharpEngine.Core.UI/Entities/UIElement.cs index c3a5e3d..2b6f8ab 100644 --- a/SharpEngine.Core/Entities/UI/UIElement.cs +++ b/SharpEngine.Core.UI/Entities/UIElement.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.Entities.Views; @@ -10,11 +9,11 @@ using Silk.NET.OpenGL; using System.Numerics; -using System.Threading.Tasks; using SharpEngine.Core.Entities.Interfaces; using SharpEngine.Core.Shaders.Rendering; +using SharpEngine.Core.Entities; -namespace SharpEngine.Core.Entities.UI; +namespace SharpEngine.Core.UI.Entities; /// /// Represents a User Interface entity. diff --git a/SharpEngine.Core/Handlers/ParticleEmitter.cs b/SharpEngine.Core.UI/Handlers/ParticleEmitter.cs similarity index 96% rename from SharpEngine.Core/Handlers/ParticleEmitter.cs rename to SharpEngine.Core.UI/Handlers/ParticleEmitter.cs index 3d62aaf..cfb8d3a 100644 --- a/SharpEngine.Core/Handlers/ParticleEmitter.cs +++ b/SharpEngine.Core.UI/Handlers/ParticleEmitter.cs @@ -1,10 +1,5 @@ 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; diff --git a/SharpEngine.Core/Renderers/ParticleRenderer.cs b/SharpEngine.Core.UI/Renderers/ParticleRenderer.cs similarity index 94% rename from SharpEngine.Core/Renderers/ParticleRenderer.cs rename to SharpEngine.Core.UI/Renderers/ParticleRenderer.cs index e34e2e3..23baf3c 100644 --- a/SharpEngine.Core/Renderers/ParticleRenderer.cs +++ b/SharpEngine.Core.UI/Renderers/ParticleRenderer.cs @@ -1,11 +1,10 @@ using Microsoft.Extensions.Logging; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Renderers; using SharpEngine.Core.Scenes; -using System; -using System.Threading.Tasks; -namespace SharpEngine.Core.Renderers; +namespace SharpEngine.Core.UI.Renderers; /// /// Renders particle effects. diff --git a/SharpEngine.Core/Renderers/UIRenderer.cs b/SharpEngine.Core.UI/Renderers/UIRenderer.cs similarity index 97% rename from SharpEngine.Core/Renderers/UIRenderer.cs rename to SharpEngine.Core.UI/Renderers/UIRenderer.cs index e34b534..2843ea3 100644 --- a/SharpEngine.Core/Renderers/UIRenderer.cs +++ b/SharpEngine.Core.UI/Renderers/UIRenderer.cs @@ -1,4 +1,3 @@ -using SharpEngine.Core.Entities.UI; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; @@ -7,13 +6,11 @@ using Microsoft.Extensions.Logging; using Silk.NET.OpenGL; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using System.Linq; using SharpEngine.Telemetry; +using SharpEngine.Core.UI.Entities; +using SharpEngine.Core.Renderers; -namespace SharpEngine.Core.Renderers; +namespace SharpEngine.Core.UI.Renderers; /// /// Represents a renderer dedicated to drawing UI elements to the screen. diff --git a/SharpEngine.Core/Shaders/UIShader.cs b/SharpEngine.Core.UI/Shaders/UIShader.cs similarity index 89% rename from SharpEngine.Core/Shaders/UIShader.cs rename to SharpEngine.Core.UI/Shaders/UIShader.cs index 3a4cb2d..4e0c5ec 100644 --- a/SharpEngine.Core/Shaders/UIShader.cs +++ b/SharpEngine.Core.UI/Shaders/UIShader.cs @@ -1,6 +1,5 @@ using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.Windowing; -using SharpEngine.Core._Resources; using Silk.NET.OpenGL; @@ -15,7 +14,7 @@ namespace SharpEngine.Core.Shaders; /// internal class UIShader : Shader { - public UIShader(GL gl) : base(gl, Default.UIVertexShader, Default.UIFragmentShader, nameof(UIShader)) { } + public UIShader(GL gl) : base(gl, Defaults.Defaults.UIVertexShader, Defaults.Defaults.UIFragmentShader, nameof(UIShader)) { } /// public override bool SetAttributes(GL gl) diff --git a/SharpEngine.Core.UI/SharpEngine.Core.UI.csproj b/SharpEngine.Core.UI/SharpEngine.Core.UI.csproj new file mode 100644 index 0000000..7cfa364 --- /dev/null +++ b/SharpEngine.Core.UI/SharpEngine.Core.UI.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/SharpEngine.Core/Entities/UI/MeshRenderer.cs b/SharpEngine.Core/Entities/Components/MeshRenderer.cs similarity index 83% rename from SharpEngine.Core/Entities/UI/MeshRenderer.cs rename to SharpEngine.Core/Entities/Components/MeshRenderer.cs index 1a9f2b8..1849168 100644 --- a/SharpEngine.Core/Entities/UI/MeshRenderer.cs +++ b/SharpEngine.Core/Entities/Components/MeshRenderer.cs @@ -1,9 +1,10 @@ -using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Components; +using SharpEngine.Core.Components.Properties; using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Entities.Interfaces; using SharpEngine.Core.Entities.Properties.Meshes; -namespace SharpEngine.Core.Entities.UI; +namespace SharpEngine.Core.UI.Entities; public class MeshRenderer : IComponent { diff --git a/SharpEngine.Core/Entities/GameObject.cs b/SharpEngine.Core/Entities/GameObject.cs index 018b0e6..2a51a6a 100644 --- a/SharpEngine.Core/Entities/GameObject.cs +++ b/SharpEngine.Core/Entities/GameObject.cs @@ -1,25 +1,20 @@ using SharpEngine.Core.Attributes; -using SharpEngine.Core.Components.Properties; using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Entities.Interfaces; using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.Entities.UI; 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 Silk.NET.OpenGL; +using Shader = SharpEngine.Core.Shaders.Shader; + using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using SharpEngine.Core._Resources; -using SharpEngine.Core.Textures; -using EngineTexture = SharpEngine.Core.Components.Properties.Textures.Texture; -using Shader = SharpEngine.Core.Shaders.Shader; +using SharpEngine.Core.UI.Entities; namespace SharpEngine.Core.Entities; @@ -98,7 +93,7 @@ public GameObject(Model model, Shader shader) : base(string.Empty) 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"); + _tempShaderData = new TempShaderDataContainer(Defaults.Defaults.VertexShader, Defaults.Defaults.FragmentShader, "lighting"); } /// @@ -146,13 +141,3 @@ public override Task Render(CameraView camera, Window window) return Task.CompletedTask; } } - -public static class MaterialExtensions -{ - public static Material Default(Shader shader) - { - var debugTexture = TextureService.Instance.LoadTexture(SharpEngine.Core._Resources.Default.DebugTexture); - var material = new Material("defaultMaterial", debugTexture) { Shader = shader }; - return material; - } -} \ No newline at end of file diff --git a/SharpEngine.Core/Entities/Lights/DirectionalLight.cs b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs index 11f4d68..b4516cd 100644 --- a/SharpEngine.Core/Entities/Lights/DirectionalLight.cs +++ b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs @@ -1,4 +1,3 @@ -using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Numerics; using SharpEngine.Core.Shaders; @@ -30,7 +29,7 @@ public DirectionalLight() /// public override Task Render(CameraView camera, Window window) { - Shader = ShaderService.Instance.LoadShader(window, Default.VertexShader, Default.FragmentShader, "lighting"); + Shader = ShaderService.Instance.LoadShader(window, Defaults.Defaults.VertexShader, Defaults.Defaults.FragmentShader, "lighting"); Shader.SetVector3("dirLight.direction", Direction); Shader.SetVector3("dirLight.ambient", Ambient); Shader.SetVector3("dirLight.diffuse", Diffuse); diff --git a/SharpEngine.Core/Entities/MaterialExtensions.cs b/SharpEngine.Core/Entities/MaterialExtensions.cs new file mode 100644 index 0000000..b55fb97 --- /dev/null +++ b/SharpEngine.Core/Entities/MaterialExtensions.cs @@ -0,0 +1,15 @@ +using SharpEngine.Core.Components.Properties; +using SharpEngine.Core.Textures; +using Shader = SharpEngine.Core.Shaders.Shader; + +namespace SharpEngine.Core.Entities; + +public static class MaterialExtensions +{ + public static Material Default(Shader shader) + { + var debugTexture = TextureService.Instance.LoadTexture(Defaults.Defaults.DebugTexture); + var material = new Material("defaultMaterial", debugTexture) { Shader = shader }; + return material; + } +} \ No newline at end of file diff --git a/SharpEngine.Core/Extensions/PathExtensions.cs b/SharpEngine.Core/Extensions/PathExtensions.cs deleted file mode 100644 index 0de788f..0000000 --- a/SharpEngine.Core/Extensions/PathExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.IO; - -namespace SharpEngine.Core.Extensions; - -/// -/// Contains extensions for handling paths and files. -/// -public static class PathExtensions -{ - /// - /// Gets the path for the file in the executing assembly directory. - /// - /// The file whose path is being resolved. - /// The file in the currently executing assembly context. - public static string GetAssemblyPath(string file) - => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, file); -} diff --git a/SharpEngine.Core/InputManager.cs b/SharpEngine.Core/InputManager.cs index 3fa9c64..4065269 100644 --- a/SharpEngine.Core/InputManager.cs +++ b/SharpEngine.Core/InputManager.cs @@ -1,8 +1,10 @@ using Microsoft.Extensions.Logging; + using SharpEngine.Core.Enums; using SharpEngine.Core.Handlers; using SharpEngine.Core.Windowing; using SharpEngine.Telemetry; + using Silk.NET.Input; using System; using System.Numerics; @@ -134,7 +136,7 @@ public virtual void AssignInputEvents() /// The mouse that triggered the event. /// The mouse button that was clicked. /// The position of the mouse when the button was clicked. - public virtual void OnMouseClick(IMouse mouse, MouseButton button, Vector2 vector) { } + public virtual void OnMouseClick(IMouse mouse, MouseButton button, System.Numerics.Vector2 vector) { } /// /// Handles key down events, invoked when a key is pressed on the keyboard. diff --git a/SharpEngine.Core/Primitives/Cube.cs b/SharpEngine.Core/Primitives/Cube.cs index 76ac6ae..6a44032 100644 --- a/SharpEngine.Core/Primitives/Cube.cs +++ b/SharpEngine.Core/Primitives/Cube.cs @@ -1,4 +1,3 @@ -using SharpEngine.Core._Resources; using SharpEngine.Core.Components.Properties; using SharpEngine.Core.Components.Properties.Meshes; using SharpEngine.Core.Components.Properties.Textures; @@ -20,7 +19,7 @@ static Cube() if (_loaded) return; - var defaultTexture = TextureService.Instance.LoadTexture(Default.DebugTexture); + var defaultTexture = TextureService.Instance.LoadTexture(Defaults.Defaults.DebugTexture); var mesh = new Mesh(Window.SharedGL) { diff --git a/SharpEngine.Core/Primitives/Plane.cs b/SharpEngine.Core/Primitives/Plane.cs index 3eae8b1..0d4d771 100644 --- a/SharpEngine.Core/Primitives/Plane.cs +++ b/SharpEngine.Core/Primitives/Plane.cs @@ -16,9 +16,9 @@ public static class Plane vertices: [ // 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, 1.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, 0.0f, // bottom left -1f, 1f, 0.0f, 0f, 0f, 1f, 0.0f, 1.0f, // top left ], indices: [0u, 1u, 3u, 1u, 2u, 3u]); diff --git a/SharpEngine.Core/Primitives/PrimitiveFactory.cs b/SharpEngine.Core/Primitives/PrimitiveFactory.cs index 60777f2..a9f0975 100644 --- a/SharpEngine.Core/Primitives/PrimitiveFactory.cs +++ b/SharpEngine.Core/Primitives/PrimitiveFactory.cs @@ -17,7 +17,7 @@ public static class PrimitiveFactory /// The type of primitive to create. /// Where the game object should be placed. public static GameObject Create(PrimitiveType primitiveType, Vector3 position) - => Create(primitiveType, position, _Resources.Default.DebugTexture); + => Create(primitiveType, position, Defaults.Defaults.DebugTexture); /// /// The type of primitive to be created. diff --git a/SharpEngine.Core/Renderers/TextRenderer.cs b/SharpEngine.Core/Renderers/TextRenderer.cs deleted file mode 100644 index 15670c8..0000000 --- a/SharpEngine.Core/Renderers/TextRenderer.cs +++ /dev/null @@ -1,31 +0,0 @@ -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/SharpEngine.Core/Scenes/SceneNode.cs b/SharpEngine.Core/Scenes/SceneNode.cs index 550f6ee..0d1829e 100644 --- a/SharpEngine.Core/Scenes/SceneNode.cs +++ b/SharpEngine.Core/Scenes/SceneNode.cs @@ -104,7 +104,7 @@ private SceneNode AddChildInternal(SceneNode node) { node.Parent = this; // propagate scene reference so node knows which scene it belongs to - node.SetSceneRecursive(this.Scene); + node.SetSceneRecursive(Scene); Children.Add(node); // Ensure lifecycle hook runs for newly added nodes @@ -118,7 +118,7 @@ private SceneNode AddChildInternal(SceneNode node) } // Notify the scene that structure changed - this.Scene?.IncrementRevision(); + Scene?.IncrementRevision(); return node; } @@ -194,7 +194,7 @@ public virtual void OnDeleted() { } /// internal void SetSceneRecursive(Scene? scene) { - this.Scene = scene; + Scene = scene; foreach (var child in Children) child.SetSceneRecursive(scene); } diff --git a/SharpEngine.Core/Shaders/DefaultShader.cs b/SharpEngine.Core/Shaders/DefaultShader.cs index 1338e87..7509bc0 100644 --- a/SharpEngine.Core/Shaders/DefaultShader.cs +++ b/SharpEngine.Core/Shaders/DefaultShader.cs @@ -4,7 +4,7 @@ namespace SharpEngine.Core.Shaders; public class DefaultShader : Shader { - public DefaultShader(GL gl) : base(gl, _Resources.Default.VertexShader, _Resources.Default.FragmentShader, "default") { } + public DefaultShader(GL gl) : base(gl, Defaults.Defaults.VertexShader, Defaults.Defaults.FragmentShader, "default") { } // public override bool SetAttributes() => true; } diff --git a/SharpEngine.Core/Shaders/LampShader.cs b/SharpEngine.Core/Shaders/LampShader.cs index b95f1a2..ac1d006 100644 --- a/SharpEngine.Core/Shaders/LampShader.cs +++ b/SharpEngine.Core/Shaders/LampShader.cs @@ -1,5 +1,5 @@ -using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Defaults; using Silk.NET.OpenGL; @@ -11,7 +11,7 @@ internal class LampShader : Shader /// Initializes a new instance of using the provided OpenGL context. /// /// The OpenGL context to use for shader and VAO creation. - public LampShader(GL gl) : base(gl, Default.VertexShader, Default.LightShader, nameof(LampShader)) + public LampShader(GL gl) : base(gl, Defaults.Defaults.VertexShader, Defaults.Defaults.LightShader, nameof(LampShader)) { } diff --git a/SharpEngine.Core/Shaders/LightingShader.cs b/SharpEngine.Core/Shaders/LightingShader.cs index 4681f5b..e0de59f 100644 --- a/SharpEngine.Core/Shaders/LightingShader.cs +++ b/SharpEngine.Core/Shaders/LightingShader.cs @@ -1,5 +1,3 @@ -using SharpEngine.Core._Resources; - using Silk.NET.OpenGL; namespace SharpEngine.Core.Shaders; @@ -10,7 +8,7 @@ internal class LightingShader : Shader /// Initializes a new instance of . /// /// The OpenGL context used to create and configure the shader program. - public LightingShader(GL gl) : base(gl, Default.VertexShader, Default.FragmentShader, nameof(LightingShader)) { } + public LightingShader(GL gl) : base(gl, Defaults.Defaults.VertexShader, Defaults.Defaults.FragmentShader, nameof(LightingShader)) { } /// public override bool SetAttributes(GL gl) diff --git a/SharpEngine.Core/SharpEngine.Core.csproj b/SharpEngine.Core/SharpEngine.Core.csproj index c6f7fd2..d3ab898 100644 --- a/SharpEngine.Core/SharpEngine.Core.csproj +++ b/SharpEngine.Core/SharpEngine.Core.csproj @@ -1,6 +1,7 @@  + preview $(DotNetTargetFramework) Everything needed to build games using a purely C# game engine. @@ -12,38 +13,32 @@ True + $(AssemblyName).NET - - - true README.md - - - - - - - - - - + + + + + + @@ -59,6 +54,14 @@ PreserveNewest + + + + + + + + diff --git a/SharpEngine.Core/Windowing/Window.cs b/SharpEngine.Core/Windowing/Window.cs index f7f7e8b..7c903f6 100644 --- a/SharpEngine.Core/Windowing/Window.cs +++ b/SharpEngine.Core/Windowing/Window.cs @@ -1,7 +1,6 @@ using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Enums; -using SharpEngine.Core.Extensions; using SharpEngine.Core.Renderers; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; @@ -25,6 +24,7 @@ using System.Numerics; using SharpEngine.Telemetry; using Vector3 = SharpEngine.Core.Numerics.Vector3; +using SharpEngine.IO.Extensions; namespace SharpEngine.Core.Windowing; diff --git a/SharpEngine.Core/_Resources/Shaders/text-bitmap.frag b/SharpEngine.Core/_Resources/Shaders/text-bitmap.frag new file mode 100644 index 0000000..3b3459d --- /dev/null +++ b/SharpEngine.Core/_Resources/Shaders/text-bitmap.frag @@ -0,0 +1,11 @@ +#version 410 core +in vec2 vUv; +out vec4 FragColor; +uniform sampler2D uTexture; +uniform vec4 uColor; + +void main() +{ + vec4 tex = texture(uTexture, vUv); + FragColor = vec4(uColor.rgb * tex.rgb, uColor.a * tex.a); +} \ No newline at end of file diff --git a/SharpEngine.Core/_Resources/Shaders/text-bitmap.vert b/SharpEngine.Core/_Resources/Shaders/text-bitmap.vert new file mode 100644 index 0000000..0fa3f99 --- /dev/null +++ b/SharpEngine.Core/_Resources/Shaders/text-bitmap.vert @@ -0,0 +1,15 @@ +#version 410 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aUv; + +out vec2 vUv; + +uniform mat4 uModel; +uniform mat4 uViewProj; + +void main() +{ + vUv = aUv; + gl_Position = uViewProj * uModel * vec4(aPos, 1.0); +} \ No newline at end of file diff --git a/SharpEngine.Core/_Resources/Shaders/text-curve.frag b/SharpEngine.Core/_Resources/Shaders/text-curve.frag new file mode 100644 index 0000000..a285757 --- /dev/null +++ b/SharpEngine.Core/_Resources/Shaders/text-curve.frag @@ -0,0 +1,10 @@ +#version 410 core +in vec2 vUv; +out vec4 FragColor; +uniform vec4 uColor; + +// TODO: Placeholder analytic curve rendering fragment shader. Implement Loop-Blinn later. +void main() +{ + FragColor = uColor; +} \ No newline at end of file diff --git a/SharpEngine.Core/_Resources/Shaders/text-curve.vert b/SharpEngine.Core/_Resources/Shaders/text-curve.vert new file mode 100644 index 0000000..0fa3f99 --- /dev/null +++ b/SharpEngine.Core/_Resources/Shaders/text-curve.vert @@ -0,0 +1,15 @@ +#version 410 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aUv; + +out vec2 vUv; + +uniform mat4 uModel; +uniform mat4 uViewProj; + +void main() +{ + vUv = aUv; + gl_Position = uViewProj * uModel * vec4(aPos, 1.0); +} \ No newline at end of file diff --git a/SharpEngine.Core/_Resources/Shaders/text-sdf.frag b/SharpEngine.Core/_Resources/Shaders/text-sdf.frag new file mode 100644 index 0000000..84d561e --- /dev/null +++ b/SharpEngine.Core/_Resources/Shaders/text-sdf.frag @@ -0,0 +1,13 @@ +#version 410 core +in vec2 vUv; +out vec4 FragColor; +uniform sampler2D uTexture; +uniform vec4 uColor; + +void main() +{ + float sdf = texture(uTexture, vUv).r; // 0..1 with 0.5 as edge + float width = fwidth(sdf); + float alpha = smoothstep(0.5 - width, 0.5 + width, sdf); + FragColor = vec4(uColor.rgb, uColor.a * alpha); +} \ No newline at end of file diff --git a/SharpEngine.Core/_Resources/Shaders/text-sdf.vert b/SharpEngine.Core/_Resources/Shaders/text-sdf.vert new file mode 100644 index 0000000..0fa3f99 --- /dev/null +++ b/SharpEngine.Core/_Resources/Shaders/text-sdf.vert @@ -0,0 +1,15 @@ +#version 410 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aUv; + +out vec2 vUv; + +uniform mat4 uModel; +uniform mat4 uViewProj; + +void main() +{ + vUv = aUv; + gl_Position = uViewProj * uModel * vec4(aPos, 1.0); +} \ No newline at end of file diff --git a/SharpEngine.Core/_Resources/Shaders/ui_sdf.frag b/SharpEngine.Core/_Resources/Shaders/ui_sdf.frag new file mode 100644 index 0000000..2d62a1c --- /dev/null +++ b/SharpEngine.Core/_Resources/Shaders/ui_sdf.frag @@ -0,0 +1,15 @@ +#version 410 core + +in vec2 vUv; +out vec4 FragColor; + +uniform sampler2D uTexture; +uniform vec4 uColor; + +void main() +{ + float sdf = texture(uTexture, vUv).r; // assume 0..1 with 0.5 as edge + float width = fwidth(sdf); + float alpha = smoothstep(0.5 - width, 0.5 + width, sdf); + FragColor = vec4(uColor.rgb, uColor.a * alpha); +} \ No newline at end of file diff --git a/SharpEngine.Core/_Resources/Shaders/ui_sdf.vert b/SharpEngine.Core/_Resources/Shaders/ui_sdf.vert new file mode 100644 index 0000000..0fa3f99 --- /dev/null +++ b/SharpEngine.Core/_Resources/Shaders/ui_sdf.vert @@ -0,0 +1,15 @@ +#version 410 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aUv; + +out vec2 vUv; + +uniform mat4 uModel; +uniform mat4 uViewProj; + +void main() +{ + vUv = aUv; + gl_Position = uViewProj * uModel * vec4(aPos, 1.0); +} \ No newline at end of file diff --git a/SharpEngine.Text/BitMapExtensions.cs b/SharpEngine.Text/BitMapExtensions.cs new file mode 100644 index 0000000..8c27f14 --- /dev/null +++ b/SharpEngine.Text/BitMapExtensions.cs @@ -0,0 +1,161 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.Versioning; + +namespace SharpEngine.Core.Rendering; + +public enum Alpha +{ + Transparent = 0, + Opague = 255 +} + +/// +/// Windows-only text bitmap renderer. Isolated into its own file so the containing callers +/// can remain platform-agnostic and only call these APIs behind an OS check. +/// +[SupportedOSPlatform("windows")] +public static class BitMapExtensions +{ + // TODO: Apply the adapter pattern to the bitmaps generated here so that they become more testable. + + public static Bitmap RenderTextToBitmap(string text, string fontFamily, int fontSize, out int width, out int height) + { + if (string.IsNullOrEmpty(text)) + { + width = 1; + height = 1; + return new Bitmap(width, height); + } + + var font = new System.Drawing.Font(fontFamily, fontSize, FontStyle.Regular, GraphicsUnit.Pixel); + GetBitMapSize(font, text, out width, out height); + + var bmp = new Bitmap(width, height); + using (var g = Graphics.FromImage(bmp)) + { + g.Clear(Color.Transparent); + g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; + + using var brush = new SolidBrush(Color.FromArgb(255, Color.White)); + g.DrawString(text, font, brush, 0, 0); + g.Flush(); + } + + // Trim transparent borders to reduce texture size + try + { + var rect = new Rectangle(0, 0, bmp.Width, bmp.Height); + var data = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + int stride = System.Math.Abs(data.Stride); + int bytesLen = stride * bmp.Height; + var bytes = new byte[bytesLen]; + + System.Runtime.InteropServices.Marshal.Copy(data.Scan0, bytes, 0, bytesLen); + bmp.UnlockBits(data); + + int left = bmp.Width, top = bmp.Height, right = 0, bottom = 0; + for (int y = 0; y < bmp.Height; y++) + { + int rowStart = y * stride; + for (int x = 0; x < bmp.Width; x++) + { + int idx = rowStart + (x * 4); + byte a = bytes[idx + 3]; // ARGB layout when locked + + // threshold to ignore near-transparent antialiasing pixels + if (a <= 8) + continue; + + if (x < left) + left = x; + + if (x > right) + right = x; + + if (y < top) + top = y; + + if (y > bottom) + bottom = y; + } + } + + if (right < left || bottom < top) + { + // Fully transparent — return a 1x1 transparent bitmap + width = 1; + height = 1; + return new Bitmap(width, height); + } + + var crop = Rectangle.FromLTRB(left, top, right + 1, bottom + 1); + var cropped = bmp.Clone(crop, PixelFormat.Format32bppArgb); + + width = cropped.Width; + height = cropped.Height; + + bmp.Dispose(); + return cropped; + } + catch + { + // TODO: Should we just re-throw here instead and in "finally" dispose? + // On any failure during trim, return the original full bitmap. + width = bmp.Width; + height = bmp.Height; + return bmp; + } + } + + private static void GetBitMapSize(System.Drawing.Font font, string text, out int width, out int height) + { + using var tmp = new Bitmap(1, 1); + using var gtmp = Graphics.FromImage(tmp); + + var size = gtmp.MeasureString(text, font); + + width = System.Math.Max(1, (int)System.MathF.Ceiling(size.Width)); + height = System.Math.Max(1, (int)System.MathF.Ceiling(size.Height)); + } + + public static byte[] BitmapToRgba(Bitmap bmp) + { + var rect = new Rectangle(0, 0, bmp.Width, bmp.Height); + var data = bmp.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); + + try + { + var length = System.Math.Abs(data.Stride) * bmp.Height; + var bytes = new byte[length]; + System.Runtime.InteropServices.Marshal.Copy(data.Scan0, bytes, 0, length); + + // Convert ARGB -> RGBA + for (int i = 0; i < bytes.Length; i += 4) + { + var a = bytes[i + 3]; + var r = bytes[i + 2]; + var g = bytes[i + 1]; + var b = bytes[i + 0]; + + bytes[i + 0] = r; + bytes[i + 1] = g; + bytes[i + 2] = b; + bytes[i + 3] = a; + } + + // Return RGBA bytes (no vertical flip here — Texture runtime handles row orientation). + return bytes; + } + catch + { + // Let all exceptions bubble up so they can be logged or otherwise properly prosessed. + throw; + } + finally + { + // Once the operation succeeds or an exception has been catched, make sure that the data is unlocked. + bmp.UnlockBits(data); + } + } +} diff --git a/SharpEngine.Text/Entities/TextElement.cs b/SharpEngine.Text/Entities/TextElement.cs new file mode 100644 index 0000000..902ee83 --- /dev/null +++ b/SharpEngine.Text/Entities/TextElement.cs @@ -0,0 +1,247 @@ +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Windowing; + +using Microsoft.Extensions.Logging; +using Silk.NET.OpenGL; + +using System.Threading.Tasks; +using System.Linq; +using System.Collections.Generic; +using System.Numerics; +using System; +using SharpEngine.Core.Rendering; +using SharpEngine.Text.Fonts; +using SharpEngine.Text.Glyphs; +using SharpEngine.Text; +using SharpEngine.Core.UI.Entities; + +namespace SharpEngine.Core.Entities; + +/// +/// Represents a text element that can be placed in the scene. +/// Uses UIElement as a base so it can reuse the UI mesh/shader for textured quads. +/// This implementation draws the text into a bitmap and uploads it as a texture on initialization. +/// +public class TextElement : UIElement +{ + /// Gets or sets the text content. + public string Text { get; set; } = string.Empty; + + /// Gets or sets the font family name. + public string FontFamily { get; set; } = "Segoe UI"; + + /// Gets or sets the font size in points. + public float FontSize { get; set; } = 16.0f; + + /// Gets or sets the color of the text. + public Vector4 Color { get; set; } = new Vector4(1, 1, 1, 1); + + private bool _textureCreated = false; + + /// + /// When true, attempt to render the glyph as vector geometry (triangulated) rather than rasterizing to a bitmap. + /// + public bool UseVectorRendering { get; set; } = true; + + private readonly ILogger? _logger; + + /// + /// Initializes a new instance of . + /// + /// The text content. + public TextElement(string textContent) : this(textContent, "TextElement") { } + + /// + /// Initializes a new instance of . + /// + /// The text content. + /// The name of the text element. + public TextElement(string textContent, string name) : base(name) + { + Text = textContent; + } + + /// + public override void OnInitialized(GL gl) + { + base.OnInitialized(gl); + + try + { + if (_textureCreated) + return; + + if (UseVectorRendering) + { + RenderVector(gl); + } + + // Rasterization fallback: only use System.Drawing on Windows where it's supported. + if (!OperatingSystem.IsWindows()) + { + // On non-Windows platforms, skip the System.Drawing raster path. + // The vector path or other rendering approaches (SDF, external renderer) should be used instead. + return; + } + + using var bmp = BitMapExtensions.RenderTextToBitmap(Text, FontFamily, (int)FontSize, out var measuredWidth, out var measuredHeight); + if (bmp is null) + return; + + var rgba = BitMapExtensions.BitmapToRgba(bmp); + var key = GetTextTextureCacheKey(); + var tex = TextTextureCache.GetOrCreate(gl, key, rgba, bmp.Width, bmp.Height, path: $""); + + var meshRenderer = Components.OfType().FirstOrDefault(); + if (meshRenderer is not null) + { + meshRenderer.Material.DiffuseMap.Texture = tex; + meshRenderer.Material.DiffuseMap.Path = tex.Path; + } + + Width = measuredWidth; + Height = measuredHeight; + + _textureCreated = true; + } + catch (Exception ex) + { + _logger?.LogError(ex, "Error initializing text element: {ex}", ex); + } + } + + private void RenderVector(GL gl) + { + try + { + // Layout per-glyph using metrics extractor and triangulate each glyph into a single mesh. + float penX = 0f; + var allVerts = new List(); + var allIndices = new List(); + var vertCount = 0u; + + // Determine font file path if available + Font? fontFile = null; + if (FontManager.Instance.TryGetFont(FontFamily, out var f)) + fontFile = f; + + for (int i = 0; i < Text.Length; i++) + { + var ch = Text[i]; + var glyphKey = $"{fontFile?.FilePath ?? ""}|{FontFamily}|{ch}|{FontSize}"; + + if (!GlyphMeshCache.TryGet(glyphKey, out var cachedMesh, out var cachedAdvance, out var gwidth, out var gheight)) + { + var (glyph, advance) = GlyphMetricsExtractor.GetGlyphAndAdvance(fontFile?.FilePath, FontFamily, ch, FontSize); + var tris = glyph.Triangulate().ToList(); + if (tris.Count < 3) + continue; + + // Build vertices for this glyph and store as cache + var verts = new List(tris.Count * 8); + var indices = new List(tris.Count); + for (int t = 0; t < tris.Count; t++) + { + var v = tris[t]; + verts.Add(v.X); + verts.Add(v.Y); + verts.Add(0f); + + verts.Add(0f); + verts.Add(0f); + verts.Add(1f); + + verts.Add(0f); + verts.Add(0f); + + indices.Add((uint)t); + } + + var mesh = GlyphMeshCache.GetOrCreate(gl, glyphKey, verts.ToArray(), indices.ToArray(), advance, 0, 0); + cachedAdvance = advance; + } + + // Kerning with next char + float kern = 0f; + if (i + 1 < Text.Length) + { + kern = GlyphMetricsExtractor.GetKerning(fontFile?.FilePath, FontFamily, ch, Text[i + 1], FontSize); + } + + // Fetch cached mesh vertices by re-obtaining the mesh (we stored verts earlier in cache as mesh only) + if (GlyphMeshCache.TryGet(glyphKey, out var meshObj, out var adv, out var w, out var h)) + { + // Read mesh vertices by reflecting internal Mesh buffers is complex; instead, regenerate triangles for placement. + var (glyph2, advance2) = GlyphMetricsExtractor.GetGlyphAndAdvance(fontFile?.FilePath, FontFamily, ch, FontSize); + var tris2 = glyph2.Triangulate().ToList(); + for (int t = 0; t < tris2.Count; t++) + { + var vx = tris2[t].X + penX; + var vy = tris2[t].Y; + + allVerts.Add(vx); + allVerts.Add(vy); + allVerts.Add(0f); + + allVerts.Add(0f); + allVerts.Add(0f); + allVerts.Add(1f); + + allVerts.Add(0f); + allVerts.Add(0f); + + allIndices.Add(vertCount++); + } + + penX += adv + kern; + } + } + + // Evil Artifact Detector: if the total triangle count is excessive or font size is very large, fallback to bitmap/SDF path. + var triCount = allIndices.Count / 3; + if (triCount > 2000 || FontSize > 128) + { + // Abort vector path and let bitmap fallback handle this element for better performance and memory usage. + return; + } + + if (allVerts.Count >= 8 && allIndices.Count >= 3) + { + var mesh = new Properties.Meshes.Mesh(gl, allVerts.ToArray(), allIndices.ToArray()); + var white = new byte[] { 255, 255, 255, 255 }; + var whiteTex = new Components.Properties.Textures.Texture(gl, white, 1, 1, path: $""); + var mat = new Components.Properties.Material($"text-vector-{Name}", whiteTex); + + _ = Components.RemoveAll(c => c is MeshRenderer); + Components.Add(new MeshRenderer(mesh, mat)); + + // Width is penX + Width = penX; + Height = FontSize; + + _textureCreated = true; + return; + } + } + catch (Exception ex) + { + // On any failure, fall through to bitmap raster fallback + _logger?.LogError(ex, "Error initializing text element: {ex}", ex); + } + } + + private string GetTextTextureCacheKey() + { + var x = Color.X.ToString(System.Globalization.CultureInfo.InvariantCulture); + var y = Color.Y.ToString(System.Globalization.CultureInfo.InvariantCulture); + var z = Color.Z.ToString(System.Globalization.CultureInfo.InvariantCulture); + var w = Color.W.ToString(System.Globalization.CultureInfo.InvariantCulture); + + return $"{Text}|{FontFamily}|{FontSize}|{x}|{y}|{z}|{w}"; + } + + // Defer to UIElement.Render which draws the textured quad + /// + public override Task Render(CameraView camera, Window window) + => base.Render(camera, window); +} diff --git a/SharpEngine.Text/Fonts/Font.cs b/SharpEngine.Text/Fonts/Font.cs new file mode 100644 index 0000000..d789dfe --- /dev/null +++ b/SharpEngine.Text/Fonts/Font.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Logging; +using System; +using System.IO.Abstractions; + +namespace SharpEngine.Text.Fonts; + +/// +/// Lightweight representation of a font file. Detailed glyph parsing is handled by later milestones. +/// +public class Font +{ + /// Name of the font (derived from file name). + public string Name { get; } + + /// Full file path to the font file. + public string FilePath { get; } + + /// Primary family name parsed from the font file when available. + public string FamilyName { get; } + + /// + /// Raw font bytes. Loaded lazily on first access. + /// + public byte[] Data + { + get + { + field ??= _fileSystem.File.ReadAllBytes(FilePath); + return field; + } + } + + private readonly IFileSystem _fileSystem; + private readonly ILogger _logger; + + public Font(string filePath) : this(filePath, new FileSystem(), new LoggerFactory().CreateLogger()) { } + + public Font(string filePath, IFileSystem fileSystem, ILogger logger) + { + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentNullException(nameof(filePath)); + + Name = _fileSystem.Path.GetFileNameWithoutExtension(filePath); + FilePath = filePath; + + // TODO: Replace with a cross-platform font parsing library to extract the family name from the font file. + + // Attempt to extract family name using PrivateFontCollection so we can map by family + using var pfc = new System.Drawing.Text.PrivateFontCollection(); + pfc.AddFontFile(filePath); + + // TODO: Can there be multiple families in a single font file? If so, we may need to handle that differently. + if (pfc.Families.Length > 0) + FamilyName = pfc.Families[0].Name; + else + FamilyName = Name; + } +} diff --git a/SharpEngine.Text/Fonts/FontGlyphExtractor.cs b/SharpEngine.Text/Fonts/FontGlyphExtractor.cs new file mode 100644 index 0000000..dee75b8 --- /dev/null +++ b/SharpEngine.Text/Fonts/FontGlyphExtractor.cs @@ -0,0 +1,110 @@ +using SharpEngine.Text.Glyphs; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Text; +using System.Numerics; +using System.Runtime.Versioning; + +namespace SharpEngine.Text.Fonts; + +/// +/// Extracts glyph contours from a text string using GDI+ GraphicsPath. +/// +/// +/// This is a pragmatic approach for extracting vector outlines from installed fonts or local font files (via PrivateFontCollection). +/// It flattens curves to line segments and returns contours as lists of Vector2 points in pixel space. +/// +public class FontGlyphExtractor +{ + private readonly TypographyGlyphExtractor _typographyGlyphExtractor; + + /// + /// Initializes a new instance of . + /// + public FontGlyphExtractor(TypographyGlyphExtractor typographyGlyphExtractor) + { + _typographyGlyphExtractor = typographyGlyphExtractor; + } + + [SupportedOSPlatform("windows")] + public Glyph ExtractGlyph(string text, string fontFamily, float fontSize, float dpi = 96f) + { + if (string.IsNullOrEmpty(text)) + throw new ArgumentNullException(nameof(text)); + + // If a Typography-based extractor is available, attempt to use it first (managed integration via reflection). Fallback to PrivateFontCollection GDI+ approach when unavailable. + bool fontFound = FontManager.Instance.TryGetFont(fontFamily, out var font); + if (fontFound && font is not null) + { + if (_typographyGlyphExtractor.TryExtractGlyph(font.FilePath, text, fontSize, out var ttGlyph, out var _)) + return ttGlyph; + } + + var fontFamToUse = GetFontFamilyToUse(fontFamily, fontFound, font); + GetPathData(fontFamToUse, text, fontSize, dpi, out var pts, out var types); + + var glyph = new Glyph(text); + var currentPoints = new List(); + + for (int i = 0; i < pts.Length; i++) + { + var p = pts[i]; + var t = types[i]; + + var v = new Vector2(p.X, p.Y); + currentPoints.Add(v); + + // If the point type has CloseSubpath flag, finalize the contour + bool isClose = (t & (byte)PathPointType.CloseSubpath) != 0; + bool isLast = i == pts.Length - 1; + + if (!isClose && !isLast) + continue; + + if (currentPoints.Count >= 2) + { + // Convert polyline into GlyphSegments as lines + var segments = new List(); + for (int j = 0; j < currentPoints.Count - 1; j++) + { + segments.Add(new GlyphSegment(SegmentType.Line, currentPoints[j], currentPoints[j + 1])); + } + + glyph.AddContour(segments); + } + + currentPoints.Clear(); + } + + return glyph; + } + + [SupportedOSPlatform("windows")] + private static FontFamily GetFontFamilyToUse(string fontFamily, bool fontFound, Font fontFile) + { + if (fontFound) + { + using var pfc = new PrivateFontCollection(); + pfc.AddFontFile(fontFile.FilePath); + return pfc.Families.Length > 0 ? pfc.Families[0] : new FontFamily(fontFamily); + } + + return new FontFamily(fontFamily); + } + + [SupportedOSPlatform("windows")] + private static void GetPathData(FontFamily fam, string text, float fontSize, float dpi, out PointF[] pts, out byte[] types) + { + // Create a GraphicsPath with the text outlines + using var path = new GraphicsPath(); + path.AddString(text, fam, (int)FontStyle.Regular, fontSize * (dpi / 72f), PointF.Empty, StringFormat.GenericDefault); + + // Flatten the path to convert bezier curves to line segments + path.Flatten(); + + pts = path.PathPoints; + types = path.PathTypes; + } +} diff --git a/SharpEngine.Text/Fonts/FontManager.cs b/SharpEngine.Text/Fonts/FontManager.cs new file mode 100644 index 0000000..79d5fcf --- /dev/null +++ b/SharpEngine.Text/Fonts/FontManager.cs @@ -0,0 +1,108 @@ +using SharpEngine.Core.Defaults; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; +using System.Linq; + +namespace SharpEngine.Text.Fonts; + +/// +/// Simple font manager responsible for scanning a fonts directory and providing Font objects. +/// Detailed glyph parsing and metrics are handled by later milestones. +/// +public class FontManager +{ + // TODO: Replace through dependency injection. + public static FontManager Instance { get; } = new FontManager(); + private readonly Dictionary _fontCache = new(StringComparer.OrdinalIgnoreCase); + + private readonly IFileSystem _fileSystem; + + private FontManager(IFileSystem? fileSystem = null) + { + _fileSystem = fileSystem ?? new FileSystem(); + + // Attempt to pre-load fonts from default directory if present + try + { + if (_fileSystem.Directory.Exists(Defaults.FontsDirectory)) + LoadFontsFromDirectory(Defaults.FontsDirectory); + } + catch + { + // TODO: swallow errors for now; more explicit errors/diagnostics will be added in future milestones + } + } + + /// + /// Loads all font files found in the specified directory. + /// Supported extensions: .ttf, .otf + /// + /// Directory to scan for font files. + public void LoadFontsFromDirectory(string directory) + { + if (string.IsNullOrWhiteSpace(directory)) + throw new ArgumentNullException(nameof(directory)); + + if (!_fileSystem.Directory.Exists(directory)) + return; + + var files = _fileSystem.Directory.GetFiles(directory, "*.*", SearchOption.TopDirectoryOnly) + .Where(f => f.EndsWith(".ttf", StringComparison.OrdinalIgnoreCase) || f.EndsWith(".otf", StringComparison.OrdinalIgnoreCase)); + + foreach (var file in files) + { + try + { + var font = new Font(file); + _fontCache[font.Name] = font; + } + catch + { + // TODO: Handle invalid font files + } + } + } + + /// + /// Loads a single font file and returns the Font object. + /// + public Font LoadFont(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + throw new ArgumentNullException(nameof(filePath)); + + if (!_fileSystem.File.Exists(filePath)) + throw new FileNotFoundException("Font file not found", filePath); + + var font = new Font(filePath); + _fontCache[font.Name] = font; + return font; + } + + /// + /// Attempts to get a loaded font by name. + /// + public bool TryGetFont(string name, out Font? font) + { + if (string.IsNullOrWhiteSpace(name)) + { + font = null; + return false; + } + + // Try exact key first + if (_fontCache.TryGetValue(name, out font)) + return true; + + // Fallback: try match by family name + font = _fontCache.Values.FirstOrDefault(f => string.Equals(f.FamilyName, name, StringComparison.OrdinalIgnoreCase)); + return font != null; + } + + /// + /// Gets all available font names (file-based keys). + /// + public IReadOnlyCollection AvailableFonts => _fontCache.Keys.ToList().AsReadOnly(); +} diff --git a/SharpEngine.Text/GeometryUtils.cs b/SharpEngine.Text/GeometryUtils.cs new file mode 100644 index 0000000..12d7c72 --- /dev/null +++ b/SharpEngine.Text/GeometryUtils.cs @@ -0,0 +1,202 @@ +using SharpEngine.Text.Glyphs; +using System; +using System.Collections.Generic; +using System.Numerics; + +namespace SharpEngine.Text; + +/// +/// Geometry utilities for working with bezier curves and polygons used in glyph tessellation. +/// Provides evaluation, splitting and point-in-contour tests used by the tessellator. +/// +public static class GeometryUtils +{ + public static Vector2 EvaluateQuadratic(Vector2 p0, Vector2 p1, Vector2 p2, float t) + { + var u = 1 - t; + return new Vector2(u * u * p0.X + 2 * u * t * p1.X + t * t * p2.X, + u * u * p0.Y + 2 * u * t * p1.Y + t * t * p2.Y); + } + + public static Vector2 EvaluateCubic(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t) + { + var u = 1 - t; + var tt = t * t; + var uu = u * u; + var uuu = uu * u; + var ttt = tt * t; + + var x = uuu * p0.X + 3 * uu * t * p1.X + 3 * u * tt * p2.X + ttt * p3.X; + var y = uuu * p0.Y + 3 * uu * t * p1.Y + 3 * u * tt * p2.Y + ttt * p3.Y; + return new Vector2(x, y); + } + + /// + /// Splits a cubic bezier curve at parameter t into two cubic bezier curves. + /// Returns (left0,left1,left2,left3), (right0,right1,right2,right3) as two arrays of 4 points. + /// + public static (Vector2[] left, Vector2[] right) SplitCubic(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t) + { + var p01 = Lerp(p0, p1, t); + var p12 = Lerp(p1, p2, t); + var p23 = Lerp(p2, p3, t); + + var p012 = Lerp(p01, p12, t); + var p123 = Lerp(p12, p23, t); + + var p0123 = Lerp(p012, p123, t); + + var left = new[] { p0, p01, p012, p0123 }; + var right = new[] { p0123, p123, p23, p3 }; + return (left, right); + } + + public static (Vector2[] left, Vector2[] right) SplitQuadratic(Vector2 p0, Vector2 p1, Vector2 p2, float t) + { + var p01 = Lerp(p0, p1, t); + var p12 = Lerp(p1, p2, t); + var p012 = Lerp(p01, p12, t); + + var left = new[] { p0, p01, p012 }; + var right = new[] { p012, p12, p2 }; + return (left, right); + } + + private static Vector2 Lerp(Vector2 a, Vector2 b, float t) + { + return new Vector2(a.X + (b.X - a.X) * t, a.Y + (b.Y - a.Y) * t); + } + + /// + /// Determines whether the given point is inside the polygon defined by the provided points using the non-zero winding rule. + /// Points must define a closed polygon (first point not repeated). + /// + public static bool IsPointInPolygon(IList polygon, Vector2 point) + { + // Non-zero winding number algorithm + int windingNumber = 0; + int n = polygon.Count; + for (int i = 0; i < n; i++) + { + var a = polygon[i]; + var b = polygon[(i + 1) % n]; + + if (a.Y <= point.Y) + { + if (b.Y > point.Y && IsLeft(a, b, point) > 0) + windingNumber++; + } + else + { + if (b.Y <= point.Y && IsLeft(a, b, point) < 0) + windingNumber--; + } + } + + return windingNumber != 0; + } + + private static float IsLeft(Vector2 a, Vector2 b, Vector2 c) + { + return (b.X - a.X) * (c.Y - a.Y) - (c.X - a.X) * (b.Y - a.Y); + } + + /// + /// Flattens a glyph contour (which may contain bezier segments) into a polyline by adaptively subdividing curves. + /// maxDepth controls recursion depth, tolerance controls flatness. + /// + public static List FlattenContour(List segments, int maxDepth = 8, float tolerance = 0.25f) + { + var result = new List(); + + foreach (var seg in segments) + { + switch (seg.Type) + { + case SegmentType.Line: + // Points: [p0,p1] + result.Add(seg.Points[0]); + // ensure the final point is added by next segment or at end + break; + + case SegmentType.Quadratic: + // approximate quadratic with adaptive subdivision + SubdivideQuadratic(seg.Points[0], seg.Points[1], seg.Points[2], result, 0, maxDepth, tolerance); + break; + + case SegmentType.Cubic: + SubdivideCubic(seg.Points[0], seg.Points[1], seg.Points[2], seg.Points[3], result, 0, maxDepth, tolerance); + break; + } + } + + // add last point of last segment explicitly + if (segments.Count > 0) + { + var last = segments[segments.Count - 1]; + var pts = last.Points; + result.Add(pts[pts.Length - 1]); + } + + return result; + } + + private static void SubdivideQuadratic(Vector2 p0, Vector2 p1, Vector2 p2, List outPts, int depth, int maxDepth, float tol) + { + // flatness metric: distance from midpoint of control to midpoint of chord + var mid = EvaluateQuadratic(p0, p1, p2, 0.5f); + var chordMid = new Vector2((p0.X + p2.X) * 0.5f, (p0.Y + p2.Y) * 0.5f); + var dx = mid.X - chordMid.X; + var dy = mid.Y - chordMid.Y; + + if (depth >= maxDepth || (dx * dx + dy * dy) <= tol * tol) + { + outPts.Add(p0); + // final point p2 will be added by caller or later + return; + } + + var (left, right) = SplitQuadratic(p0, p1, p2, 0.5f); + SubdivideQuadratic(left[0], left[1], left[2], outPts, depth + 1, maxDepth, tol); + SubdivideQuadratic(right[0], right[1], right[2], outPts, depth + 1, maxDepth, tol); + } + + private static void SubdivideCubic(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, List outPts, int depth, int maxDepth, float tol) + { + // use distance from cubic midpoint to chord midpoint as flatness + var mid = EvaluateCubic(p0, p1, p2, p3, 0.5f); + var chordMid = new Vector2((p0.X + p3.X) * 0.5f, (p0.Y + p3.Y) * 0.5f); + var dx = mid.X - chordMid.X; + var dy = mid.Y - chordMid.Y; + + if (depth >= maxDepth || (dx * dx + dy * dy) <= tol * tol) + { + outPts.Add(p0); + return; + } + + var (left, right) = SplitCubic(p0, p1, p2, p3, 0.5f); + SubdivideCubic(left[0], left[1], left[2], left[3], outPts, depth + 1, maxDepth, tol); + SubdivideCubic(right[0], right[1], right[2], right[3], outPts, depth + 1, maxDepth, tol); + } + + /// + /// Determines whether the given point is inside the glyph (considering all contours and winding rule). + /// + public static bool IsPointInGlyph(Glyph glyph, Vector2 point) + { + // Apply non-zero winding rule across all contours + int winding = 0; + foreach (var contour in glyph.Contours) + { + var poly = FlattenContour(contour); + if (poly.Count < 3) + continue; + + if (IsPointInPolygon(poly, point)) + winding++; + } + + return winding != 0; + } +} diff --git a/SharpEngine.Text/Glyphs/Glyph.cs b/SharpEngine.Text/Glyphs/Glyph.cs new file mode 100644 index 0000000..850e7bc --- /dev/null +++ b/SharpEngine.Text/Glyphs/Glyph.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace SharpEngine.Text.Glyphs; + +/// +/// Represents a glyph outline composed of one or more contours. +/// +/// +/// Each contour is a sequence of segments (lines, quadratic or cubic beziers). +/// TODO: This class is intentionally lightweight: full TrueType parsing is implemented in later milestones. +/// +public class Glyph +{ + /// + /// Gets the name of the glyph, typically the character or string it represents. + /// + public string Name { get; } + + /// + /// Collection of contours. Each contour is a list of segments. + /// + public List> Contours { get; } = new(); + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the glyph. + /// Thrown when the name is . + public Glyph(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Adds a new contour to the glyph. + /// + /// The segments that make up the contour. + public void AddContour(IEnumerable segments) => Contours.Add([.. segments]); + + /// + /// Triangulates the glyph by flattening bezier contours and applying an ear-clipping triangulator per contour. + /// + /// + /// TODO: This is a simple implementation and does not correctly handle nested holes — outer contours should be provided in CCW order and holes in CW order for correct results. + /// + public IEnumerable Triangulate(int flattenMaxDepth = 8, float flattenTolerance = 0.25f) + { + // Use LibTessDotNet for robust triangulation with holes when available. + try + { + var tess = new LibTessDotNet.Tess(); + foreach (var contour in Contours) + { + var poly = GeometryUtils.FlattenContour(contour, flattenMaxDepth, flattenTolerance); + if (poly.Count < 3) + continue; + + var verts = new LibTessDotNet.ContourVertex[poly.Count]; + for (int i = 0; i < poly.Count; i++) + { + verts[i].Position = new LibTessDotNet.Vec3 { X = poly[i].X, Y = poly[i].Y, Z = 0 }; + } + + tess.AddContour(verts, LibTessDotNet.ContourOrientation.Original); + } + + tess.Tessellate(LibTessDotNet.WindingRule.NonZero, LibTessDotNet.ElementType.Polygons, 3); + + var result = new List(); + var vertices = tess.Vertices; + var elements = tess.Elements; + for (int i = 0; i < elements.Length; i += 3) + { + if (elements[i] == -1 || elements[i + 1] == -1 || elements[i + 2] == -1) + break; + + var a = vertices[elements[i]].Position; + var b = vertices[elements[i + 1]].Position; + var c = vertices[elements[i + 2]].Position; + + result.Add(new Vector2(a.X, a.Y)); + result.Add(new Vector2(b.X, b.Y)); + result.Add(new Vector2(c.X, c.Y)); + } + + return result; + } + catch + { + // fallback to simple ear-clipping implementation below + // TODO: Handle logging. + } + + var triangles = new List(); + + foreach (var contour in Contours) + { + var poly = GeometryUtils.FlattenContour(contour, flattenMaxDepth, flattenTolerance); + if (poly.Count < 3) + continue; + + var inds = TriangulatePolygon(poly); + for (int i = 0; i < inds.Count; i += 3) + { + triangles.Add(poly[inds[i]]); + triangles.Add(poly[inds[i + 1]]); + triangles.Add(poly[inds[i + 2]]); + } + } + + return triangles; + } + + // Simple ear-clipping triangulation. Returns a list of triangle vertex indices into the input polygon. + private static List TriangulatePolygon(List poly) + { + int n = poly.Count; + var result = new List(System.Math.Max(0, (n - 2) * 3)); + + if (n < 3) + return result; + + if (SignedArea(poly) < 0) + poly.Reverse(); + + Span prev = stackalloc int[n]; + Span next = stackalloc int[n]; + + for (int i = 0; i < n; i++) + { + prev[i] = i == 0 ? n - 1 : i - 1; + next[i] = i == n - 1 ? 0 : i + 1; + } + + int remaining = n; + int current = 0; + int guard = 0; + + while (remaining > 3 && guard < 10000) + { + bool earFound = false; + int start = current; + + do + { + int p = prev[current]; + int c = current; + int nx = next[current]; + + Vector2 a = poly[p]; + Vector2 b = poly[c]; + Vector2 triangleC = poly[nx]; + + if (IsConvex(a, b, triangleC)) + { + bool anyInside = false; + + int j = next[nx]; + + while (j != p) + { + if (PointInTriangle(poly[j], a, b, triangleC)) + { + anyInside = true; + break; + } + + j = next[j]; + } + + if (!anyInside) + { + result.Add(p); + result.Add(c); + result.Add(nx); + + // Remove c from the circular linked list. + next[p] = nx; + prev[nx] = p; + + current = nx; + remaining--; + + earFound = true; + break; + } + } + + current = next[current]; + + } while (current != start); + + if (!earFound) + break; + + guard++; + } + + if (remaining == 3) + { + int a = current; + int b = next[a]; + int c = next[b]; + + result.Add(a); + result.Add(b); + result.Add(c); + } + + return result; + } + + private static float SignedArea(List poly) + { + double area = 0.0; + + for (int i = 0; i < poly.Count; i++) + { + var p0 = poly[i]; + var p1 = poly[(i + 1) % poly.Count]; + area += (p0.X * p1.Y) - (p1.X * p0.Y); + } + + return (float)(area * 0.5); + } + + private static bool IsConvex(Vector2 a, Vector2 b, Vector2 c) + => (((b.X - a.X) * (c.Y - a.Y)) - ((b.Y - a.Y) * (c.X - a.X))) > 0; + + private static bool PointInTriangle(Vector2 p, Vector2 a, Vector2 b, Vector2 c) + { + // barycentric technique + var v0 = new Vector2(c.X - a.X, c.Y - a.Y); + var v1 = new Vector2(b.X - a.X, b.Y - a.Y); + var v2 = new Vector2(p.X - a.X, p.Y - a.Y); + + var dot00 = (v0.X * v0.X) + (v0.Y * v0.Y); + var dot01 = (v0.X * v1.X) + (v0.Y * v1.Y); + var dot02 = (v0.X * v2.X) + (v0.Y * v2.Y); + var dot11 = (v1.X * v1.X) + (v1.Y * v1.Y); + var dot12 = (v1.X * v2.X) + (v1.Y * v2.Y); + + var denom = (dot00 * dot11) - (dot01 * dot01); + if (System.Math.Abs(denom) < 1e-8) + return false; + + var u = ((dot11 * dot02) - (dot01 * dot12)) / denom; + var v = ((dot00 * dot12) - (dot01 * dot02)) / denom; + return u >= 0 && v >= 0 && u + v < 1; + } +} diff --git a/SharpEngine.Text/Glyphs/GlyphAtlas.cs b/SharpEngine.Text/Glyphs/GlyphAtlas.cs new file mode 100644 index 0000000..30e4c07 --- /dev/null +++ b/SharpEngine.Text/Glyphs/GlyphAtlas.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Numerics; + +namespace SharpEngine.Text.Glyphs; + +/// +/// Simple glyph atlas implementation. Packs bitmaps into a single texture and exposes UV rectangles per glyph. +/// For now uses a naive skyline packing algorithm and stores glyph bitmaps as byte[] grayscale rows. +/// SDF generation is provided as a simple naive distance transform for prototyping; future versions will use multi-channel SDF and optimized kernels. +/// +public class GlyphAtlas : IDisposable +{ + private readonly Dictionary _uvRects = new(); + private readonly Dictionary _glyphBitmaps = new(); + + public int Width { get; private set; } + public int Height { get; private set; } + + public GlyphAtlas(int width = 1024, int height = 1024) + { + Width = width; + Height = height; + } + + public void AddGlyphBitmap(string glyphName, int w, int h, byte[] pixels) + { + if (pixels is null) + throw new ArgumentNullException(nameof(pixels)); + + if (pixels.Length != w * h) + throw new ArgumentException("Pixel length mismatch"); + + _glyphBitmaps[glyphName] = pixels; + } + + /// + /// Packs the currently added glyph bitmaps into the atlas texture using a simple row-based packer. + /// + public void Pack() + { + // naive packing: place glyphs in rows until full + var x = 0; + var y = 0; + var rowHeight = 0; + + foreach (var kv in _glyphBitmaps) + { + var name = kv.Key; + // For prototype we assume glyph metadata contains width/height encoded in name as name|w|h + var parts = name.Split('|'); + var w = int.Parse(parts[1]); + var h = int.Parse(parts[2]); + + if (x + w > Width) + { + x = 0; + y += rowHeight; + rowHeight = 0; + } + + if (y + h > Height) + throw new InvalidOperationException("Atlas too small for glyph set"); + + _uvRects[kv.Key] = new Rectangle(x, y, w, h); + x += w; + rowHeight = System.Math.Max(rowHeight, h); + } + } + + public bool TryGetUV(string glyphName, out Rectangle rect) + { + return _uvRects.TryGetValue(glyphName, out rect); + } + + /// + /// Generates a simple (slow) SDF for the glyph bitmap using brute-force distance transform. + /// Returns float array with signed distances normalized to 0..1 map with 0.5 as zero distance. + /// + public static float[] GenerateSDF(int w, int h, byte[] bitmap, int spread = 16) + { + var sdf = new float[w * h]; + for (int y = 0; y < h; y++) + { + for (int x = 0; x < w; x++) + { + var idx = y * w + x; + var inside = bitmap[idx] > 127; + var bestDistSq = int.MaxValue; + + for (int yy = 0; yy < h; yy++) + { + for (int xx = 0; xx < w; xx++) + { + var idx2 = yy * w + xx; + var inside2 = bitmap[idx2] > 127; + if (inside == inside2) + continue; + + var dx = xx - x; + var dy = yy - y; + var dsq = dx * dx + dy * dy; + + if (dsq < bestDistSq) + bestDistSq = dsq; + } + } + + var dist = System.Math.Sqrt(bestDistSq); + // positive outside distances, negative inside distances + sdf[idx] = (float)(dist / spread / 2.0 + 0.5) * (inside ? -1 : 1); + } + } + + return sdf; + } + + public void Dispose() + { + _uvRects.Clear(); + _glyphBitmaps.Clear(); + } +} diff --git a/SharpEngine.Text/Glyphs/GlyphAtlasExtensions.cs b/SharpEngine.Text/Glyphs/GlyphAtlasExtensions.cs new file mode 100644 index 0000000..d537399 --- /dev/null +++ b/SharpEngine.Text/Glyphs/GlyphAtlasExtensions.cs @@ -0,0 +1,62 @@ +using Silk.NET.OpenGL; + +namespace SharpEngine.Text.Glyphs; + +public static class GlyphAtlasExtensions +{ + /// + /// Builds a runtime RGBA texture for the atlas and returns a Texture instance. + /// Glyph bitmaps are assumed to be single-channel grayscale stored in _glyphBitmaps entries. + /// + public static Core.Components.Properties.Textures.Texture BuildRuntimeTexture(this GlyphAtlas atlas, GL gl) + { + // Create RGBA buffer + var rgba = new byte[atlas.Width * atlas.Height * 4]; + + foreach (var kv in atlas.GetGlyphEntries()) + { + var key = kv.key; + var rect = kv.rect; + var pixels = kv.pixels; // grayscale length == rect.Width*rect.Height + + for (int y = 0; y < rect.Height; y++) + { + for (int x = 0; x < rect.Width; x++) + { + int srcIdx = y * rect.Width + x; + int dstIdx = ((rect.Y + y) * atlas.Width + (rect.X + x)) * 4; + byte v = pixels[srcIdx]; + rgba[dstIdx + 0] = v; + rgba[dstIdx + 1] = v; + rgba[dstIdx + 2] = v; + rgba[dstIdx + 3] = 255; + } + } + } + + var tex = new Core.Components.Properties.Textures.Texture(gl, rgba, atlas.Width, atlas.Height, path: ""); + return tex; + } + + private static System.Collections.Generic.IEnumerable<(string key, System.Drawing.Rectangle rect, byte[] pixels)> GetGlyphEntries(this GlyphAtlas atlas) + { + // Reflection-ish access to private fields (avoid changing GlyphAtlas public API in this iteration) + var t = atlas.GetType(); + var uvField = t.GetField("_uvRects", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var bmpField = t.GetField("_glyphBitmaps", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var uv = (System.Collections.IDictionary)uvField.GetValue(atlas); + var bmp = (System.Collections.IDictionary)bmpField.GetValue(atlas); + + var keys = new System.Collections.Generic.List(); + foreach (System.Collections.DictionaryEntry de in bmp) + keys.Add(de.Key); + + foreach (var k in keys) + { + var name = (string)k; + var rect = (System.Drawing.Rectangle)uv[name]; + var pixels = (byte[])bmp[name]; + yield return (name, rect, pixels); + } + } +} diff --git a/SharpEngine.Text/Glyphs/GlyphMeshCache.cs b/SharpEngine.Text/Glyphs/GlyphMeshCache.cs new file mode 100644 index 0000000..cb22496 --- /dev/null +++ b/SharpEngine.Text/Glyphs/GlyphMeshCache.cs @@ -0,0 +1,41 @@ +using SharpEngine.Core.Entities.Properties.Meshes; +using Silk.NET.OpenGL; +using System.Collections.Concurrent; + +namespace SharpEngine.Text.Glyphs; + +internal static class GlyphMeshCache +{ + private static readonly ConcurrentDictionary _cache = new(); + + public static bool TryGet(string key, out Mesh? mesh, out float advance, out float width, out float height) + { + if (_cache.TryGetValue(key, out var v)) + { + mesh = v.mesh; + advance = v.advance; + width = v.width; + height = v.height; + + return true; + } + + mesh = null; + advance = 0; + width = 0; + height = 0; + + return false; + } + + public static Mesh GetOrCreate(GL gl, string key, float[] verts, uint[] indices, float advance, float width, float height) + { + if (_cache.TryGetValue(key, out var v)) + return v.mesh; + + var mesh = new Mesh(gl, verts, indices); + _cache[key] = (mesh, advance, width, height); + + return mesh; + } +} diff --git a/SharpEngine.Text/Glyphs/GlyphMetricsExtractor.cs b/SharpEngine.Text/Glyphs/GlyphMetricsExtractor.cs new file mode 100644 index 0000000..46c7e1f --- /dev/null +++ b/SharpEngine.Text/Glyphs/GlyphMetricsExtractor.cs @@ -0,0 +1,156 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Text; +using System.Drawing.Drawing2D; +using System.Runtime.Versioning; +using System.Numerics; + +namespace SharpEngine.Text.Glyphs; + +/// +/// Pragmatic glyph extractor and metrics provider using GDI+ PrivateFontCollection. +/// Provides per-character outlines (flattened) and advance widths and pairwise kerning (measured). +/// This is not a full shaping engine (no HarfBuzz) but gives reasonable metrics for Latin scripts. +/// +internal static class GlyphMetricsExtractor +{ + private static readonly ConcurrentDictionary _glyphCache = new(); + private static readonly ConcurrentDictionary _kerningCache = new(); + + [SupportedOSPlatform("windows")] + public static (Glyph glyph, float advance) GetGlyphAndAdvance(string fontFilePath, string familyName, char ch, float fontSize, float dpi = 96f) + { + var key = $"{fontFilePath ?? ""}|{familyName}|{ch}|{fontSize}"; + if (_glyphCache.TryGetValue(key, out var v)) + return v; + + Glyph glyph; + float advance = 0f; + + using var bmp = new Bitmap(1, 1); + using var g = Graphics.FromImage(bmp); + g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; + + var fam = GetFontFamily(fontFilePath, familyName); + + using var sysFont = new System.Drawing.Font(fam, fontSize, FontStyle.Regular, GraphicsUnit.Pixel); + + // Build GraphicsPath outline for the single character + using var path = new GraphicsPath(); + path.AddString(ch.ToString(), fam, (int)FontStyle.Regular, fontSize * (dpi / 72f), PointF.Empty, StringFormat.GenericDefault); + path.Flatten(); + + glyph = new Glyph(ch.ToString()); + var pts = path.PathPoints; + var types = path.PathTypes; + + var currentPoints = new List(); + for (int i = 0; i < pts.Length; i++) + { + var p = pts[i]; + var t = types[i]; + + currentPoints.Add(new Vector2(p.X, p.Y)); + + bool isClose = (t & (byte)PathPointType.CloseSubpath) != 0; + bool isLast = i == pts.Length - 1; + + if (isClose || isLast) + { + if (currentPoints.Count >= 2) + { + var segments = new List(); + for (int j = 0; j < currentPoints.Count - 1; j++) + { + segments.Add(new GlyphSegment(SegmentType.Line, currentPoints[j], currentPoints[j + 1])); + } + + glyph.AddContour(segments); + } + + currentPoints.Clear(); + } + } + + // Measure advance using MeasureCharacterRanges + var layout = new RectangleF(0, 0, 1000, 1000); + var sf = new StringFormat(); + sf.SetMeasurableCharacterRanges([new CharacterRange(0, 1)]); + var ranges = g.MeasureCharacterRanges(ch.ToString(), sysFont, layout, sf); + if (ranges != null && ranges.Length > 0) + { + var rect = ranges[0].GetBounds(g); + advance = rect.Width; + } + else + { + var size = g.MeasureString(ch.ToString(), sysFont); + advance = size.Width; + } + + _glyphCache[key] = (glyph, advance); + return (glyph, advance); + } + + [SupportedOSPlatform("windows")] + private static FontFamily GetFontFamily(string fontFilePath, string familyName) + { + // Create font + FontFamily? fam = null; + if (!string.IsNullOrEmpty(fontFilePath) && System.IO.File.Exists(fontFilePath)) + { + try + { + var pfc = new PrivateFontCollection(); + pfc.AddFontFile(fontFilePath); + if (pfc.Families.Length > 0) + fam = pfc.Families[0]; + } + catch + { + fam = null; + } + } + + if (fam is null) + { + try + { + fam = new FontFamily(familyName); + } + catch + { + fam = FontFamily.GenericSansSerif; + } + } + + return fam ?? FontFamily.GenericSansSerif; + } + + [SupportedOSPlatform("windows")] + public static float GetKerning(string fontFilePath, string familyName, char left, char right, float fontSize) + { + var key = $"kern|{fontFilePath ?? ""}|{familyName}|{left}|{right}|{fontSize}"; + if (_kerningCache.TryGetValue(key, out var v)) + return v; + + using var bmp = new Bitmap(1,1); + using var g = Graphics.FromImage(bmp); + g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; + + FontFamily fam = GetFontFamily(fontFilePath, familyName); + + using var sysFont = new System.Drawing.Font(fam, fontSize, FontStyle.Regular, GraphicsUnit.Pixel); + + // Measure pair width + var layout = new RectangleF(0, 0, 2000, 2000); + var sizePair = g.MeasureString(new string([left, right]), sysFont); + var sizeLeft = g.MeasureString(left.ToString(), sysFont); + var sizeRight = g.MeasureString(right.ToString(), sysFont); + + var kern = sizePair.Width - (sizeLeft.Width + sizeRight.Width); + _kerningCache[key] = kern; + return kern; + } +} diff --git a/SharpEngine.Text/Glyphs/GlyphSegment.cs b/SharpEngine.Text/Glyphs/GlyphSegment.cs new file mode 100644 index 0000000..dea9e77 --- /dev/null +++ b/SharpEngine.Text/Glyphs/GlyphSegment.cs @@ -0,0 +1,21 @@ +using System; +using System.Numerics; + +namespace SharpEngine.Text.Glyphs; + +/// +/// A segment within a glyph contour. +/// Can be a straight line, quadratic bezier, or cubic bezier. +/// Points are stored in order: for Line => [p0,p1], Quadratic => [p0,p1,p2], Cubic => [p0,p1,p2,p3] +/// +public class GlyphSegment +{ + public SegmentType Type { get; } + public Vector2[] Points { get; } + + public GlyphSegment(SegmentType type, params Vector2[] points) + { + Type = type; + Points = points ?? throw new ArgumentNullException(nameof(points)); + } +} diff --git a/SharpEngine.Text/SegmentType.cs b/SharpEngine.Text/SegmentType.cs new file mode 100644 index 0000000..457c64e --- /dev/null +++ b/SharpEngine.Text/SegmentType.cs @@ -0,0 +1,8 @@ +namespace SharpEngine.Text; + +public enum SegmentType +{ + Line, + Quadratic, + Cubic +} diff --git a/SharpEngine.Text/SharpEngine.Core.UI.Text.csproj b/SharpEngine.Text/SharpEngine.Core.UI.Text.csproj new file mode 100644 index 0000000..c0e0754 --- /dev/null +++ b/SharpEngine.Text/SharpEngine.Core.UI.Text.csproj @@ -0,0 +1,32 @@ + + + + $(DotNetTargetFramework) + + + + + + + + + + + + + typography + + + typography + + + + + + + + + + + + diff --git a/SharpEngine.Text/TextRenderer.cs b/SharpEngine.Text/TextRenderer.cs new file mode 100644 index 0000000..18446d5 --- /dev/null +++ b/SharpEngine.Text/TextRenderer.cs @@ -0,0 +1,191 @@ +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Windowing; + +using Microsoft.Extensions.Logging; +using Silk.NET.OpenGL; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using SharpEngine.Telemetry; +using SharpEngine.Core.Entities; + +namespace SharpEngine.Core.Renderers; + +/// +/// A renderer responsible for rendering text in the scene. +/// +public class TextRenderer : RendererBase +{ + private readonly Scene _scene; + private readonly CameraView _camera; + private readonly ILogger _logger; + + private GL _gl = null!; + + // Caching to avoid expensive per-frame scene traversals + private List _cachedTextElements = new(); + private HashSet _initializedElements = new(); + private volatile bool _needsRefresh = false; + private int _frameCounter = 0; + private const int RefreshIntervalFrames = 30; // refresh cache every N frames + + /// + public override RenderFlags RenderFlag => RenderFlags.Text; + + /// + /// Initializes a new instance of . + /// + public TextRenderer(CameraView camera, ISettings settings, Scene scene) + : this(camera, settings, scene, LoggingExtensions.CreateLogger()) { } + + // Handler reference for subscribing/unsubscribing to scene events + private Action? _sceneChangedHandler; + + /// + /// Initializes a new instance of . + /// + public TextRenderer(CameraView camera, ISettings settings, Scene scene, ILogger logger) : base(settings) + { + _scene = scene; + _camera = camera; + _logger = logger; + } + + /// + protected override void OnWindowAttached(Window window) + { + _gl = window.GetGL(); + + // Prime cache immediately so first frames don't pay traversal cost + RefreshTextCache(); + _logger.LogTrace("TextRenderer.OnWindowAttached: cache primed, cachedCount={Count}", _cachedTextElements.Count); + + // Initialize cached elements + foreach (var node in _cachedTextElements) + { + try + { + node.OnInitialized(_gl); + _initializedElements.Add(node); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error initializing text element: {Message}", ex.Message); + } + } + + // Subscribe to scene changes to refresh cache on demand. Only mark that a refresh is needed to avoid doing + // heavy traversals for every scene mutation; the renderer will perform a single refresh per frame when needed. + _sceneChangedHandler = () => _needsRefresh = true; + _scene.SceneChanged += _sceneChangedHandler; + } + + private void RefreshTextCache() + { + // Discover Text elements from the scene graph root so examples that attach text under Root are found. + var newList = _scene.Root.GetObjectsOfType().ToList(); + + // Determine which elements are newly added and which were removed + var newSet = new HashSet(newList); + var added = newList.Where(e => !_initializedElements.Contains(e)).ToList(); + var removed = _initializedElements.Where(e => !newSet.Contains(e)).ToList(); + + // Update internal caches + _cachedTextElements = newList; + + // Initialize newly added elements + foreach (var node in added) + { + try + { + node.OnInitialized(_gl); + _initializedElements.Add(node); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error initializing newly added Text element: {Message}", ex.Message); + } + } + + // Remove references to removed elements + foreach (var node in removed) + _initializedElements.Remove(node); + + try + { + _logger.LogTrace("RefreshTextCache: found {Count} text elements, root children={RootChildren}", _cachedTextElements.Count, _scene.Root.Children.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing text cache: {Message}", ex.Message); + } + } + + /// + public override async Task Render() + { + try + { + // If scene changed since last frame, refresh cache once per frame to coalesce bursty updates. + if (_needsRefresh) + { + _needsRefresh = false; + RefreshTextCache(); + } + + // Use the cached text elements. Cache is maintained in OnWindowAttached and via the Scene.SceneChanged event. + _gl.Enable(EnableCap.DepthTest); + _gl.DepthFunc(DepthFunction.Less); + + // Disable face culling to render both sides of glyph meshes + _gl.Disable(EnableCap.CullFace); + + _frameCounter++; + var swTotal = System.Diagnostics.Stopwatch.StartNew(); + var swRender = System.Diagnostics.Stopwatch.StartNew(); + + // Render each text element sequentially on the GL thread. Avoid per-element Task allocations for performance. + foreach (var elem in _cachedTextElements) + { + await elem.Render(_camera, Window); + } + + swRender.Stop(); + swTotal.Stop(); + + // Log sampling: every RefreshIntervalFrames frames log timings to help profiling + if (_frameCounter % RefreshIntervalFrames == 0) + _logger.LogTrace("Text render: total={TotalMs}ms, draw={DrawMs}ms, elements={Count}", swTotal.Elapsed.TotalMilliseconds, swRender.Elapsed.TotalMilliseconds, _cachedTextElements.Count); + + return; + } + catch (Exception ex) + { + _logger.LogError(ex, "{Message}", ex.Message); + throw; + } + } + + /// + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_sceneChangedHandler != null) + _scene.SceneChanged -= _sceneChangedHandler; + } + + base.Dispose(disposing); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while disposing TextRenderer: {Message}", ex.Message); + } + } +} diff --git a/SharpEngine.Text/TextTextureCache.cs b/SharpEngine.Text/TextTextureCache.cs new file mode 100644 index 0000000..c7099c2 --- /dev/null +++ b/SharpEngine.Text/TextTextureCache.cs @@ -0,0 +1,52 @@ +using System.Collections.Concurrent; +using SharpEngine.Core.Components.Properties.Textures; + +namespace SharpEngine.Text; + +// Simple runtime cache for text rasterized textures keyed by text/font/size. +internal static class TextTextureCache +{ + private static readonly ConcurrentDictionary _cache = new(); + + public static bool TryGet(string key, out Texture? tex, out int w, out int h) + { + if (_cache.TryGetValue(key, out var v)) + { + tex = v.tex; + w = v.w; + h = v.h; + return true; + } + + tex = null; + w = 0; + h = 0; + + return false; + } + + public static Texture GetOrCreate(Silk.NET.OpenGL.GL gl, string key, byte[] rgba, int width, int height, string? path = null) + { + // fast path + if (_cache.TryGetValue(key, out var existing)) + return existing.tex; + + var tex = new Texture(gl, rgba, width, height, path: path ?? $""); + _cache[key] = (tex, width, height); + return tex; + } + + public static void Clear() + { + foreach (var kv in _cache) + { + try + { + kv.Value.tex?.Dispose(); + } + catch { } + } + + _cache.Clear(); + } +} diff --git a/SharpEngine.Text/Translator.cs b/SharpEngine.Text/Translator.cs new file mode 100644 index 0000000..024eba5 --- /dev/null +++ b/SharpEngine.Text/Translator.cs @@ -0,0 +1,101 @@ +extern alias typography; +using SharpEngine.Text.Glyphs; +using System.Collections.Generic; +using System.Numerics; + +namespace SharpEngine.Text; + +// Translator implementing IGlyphTranslator to convert commands into GlyphSegments. +internal sealed class Translator : typography::Typography.OpenFont.IGlyphTranslator +{ + private readonly Glyph _glyph; + private readonly float _scale; + private readonly List _segments = new(); + private float _cx, _cy; + + public Translator(Glyph glyph, float fontSize, typography::Typography.OpenFont.Typeface typeface) + { + _glyph = glyph; + _scale = CalculateScale(typeface, fontSize); + } + + private static float CalculateScale(typography::Typography.OpenFont.Typeface typeface, float fontSize) + { + try + { + float dpi = 96f; + return typeface.CalculateScaleToPixelFromPointSize(fontSize, (int)dpi); + } + catch + { + return typeface.CalculateScaleToPixel(fontSize); + } + } + + public float GetScale() => _scale; + + public void BeginRead(int contourCount) { } + public void EndRead() + { + if (_segments.Count > 0) + { + _glyph.AddContour(_segments); + _segments.Clear(); + } + } + + public void MoveTo(float x0, float y0) + { + Flush(); + + _cx = x0 * _scale; + _cy = -y0 * _scale; + } + + public void LineTo(float x1, float y1) + { + var p0 = new Vector2(_cx, _cy); + var p1 = new Vector2(x1 * _scale, -y1 * _scale); + + _segments.Add(new GlyphSegment(SegmentType.Line, p0, p1)); + + _cx = p1.X; + _cy = p1.Y; + } + + public void Curve3(float x1, float y1, float x2, float y2) + { + var p0 = new Vector2(_cx, _cy); + var p1 = new Vector2(x1 * _scale, -y1 * _scale); + var p2 = new Vector2(x2 * _scale, -y2 * _scale); + + _segments.Add(new GlyphSegment(SegmentType.Quadratic, p0, p1, p2)); + + _cx = p2.X; + _cy = p2.Y; + } + + public void Curve4(float x1, float y1, float x2, float y2, float x3, float y3) + { + var p0 = new Vector2(_cx, _cy); + var p1 = new Vector2(x1 * _scale, -y1 * _scale); + var p2 = new Vector2(x2 * _scale, -y2 * _scale); + var p3 = new Vector2(x3 * _scale, -y3 * _scale); + + _segments.Add(new GlyphSegment(SegmentType.Cubic, p0, p1, p2, p3)); + + _cx = p3.X; + _cy = p3.Y; + } + + public void CloseContour() => Flush(); + + private void Flush() + { + if (_segments.Count > 0) + { + _glyph.AddContour(_segments); + _segments.Clear(); + } + } +} diff --git a/SharpEngine.Text/TypographyGlyphExtractor.cs b/SharpEngine.Text/TypographyGlyphExtractor.cs new file mode 100644 index 0000000..0e0bcbd --- /dev/null +++ b/SharpEngine.Text/TypographyGlyphExtractor.cs @@ -0,0 +1,127 @@ +extern alias typography; +using System; +using System.IO.Abstractions; +using Microsoft.Extensions.Logging; +using SharpEngine.Text.Glyphs; + +namespace SharpEngine.Text; + +/// +/// Bridge to Typography.OpenFont to extract glyph outlines and advance widths. +/// The implementation is a single-file, typed integration (no reflection for advance/outline). +/// +public class TypographyGlyphExtractor +{ + private readonly ILogger? _logger; + private readonly IFileSystem _fileSystem; + + internal TypographyGlyphExtractor(ILogger? logger = null, IFileSystem? fileSystem = null) + { + _logger = logger; + _fileSystem = fileSystem ?? new FileSystem(); + } + + public bool TryExtractGlyph(string fontFilePath, string text, float fontSize, out Glyph glyph, out float advance) + { + glyph = null!; + advance = 0f; + + if (string.IsNullOrEmpty(fontFilePath) || !_fileSystem.File.Exists(fontFilePath) || string.IsNullOrEmpty(text)) + { + _logger?.LogDebug("TryExtractGlyph called with invalid arguments (file exists: {exists}, text empty: {textEmpty}).", _fileSystem.File.Exists(fontFilePath), string.IsNullOrEmpty(text)); + return false; + } + + try + { + if (TryExtractWithTypography(fontFilePath, text, fontSize, out glyph, out advance, _logger)) + { + return true; + } + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "Typography extractor threw an exception."); + } + + _logger?.LogDebug("No glyph extraction succeeded for {file} and text '{text}'.", fontFilePath, text); + glyph = null!; + advance = 0f; + + return false; + } + + // Typed implementation using Typography.OpenFont (compile-time dependency). + private bool TryExtractWithTypography(string fontFilePath, string text, float fontSize, out Glyph glyph, out float advance, ILogger? logger = null) + { + glyph = null!; + advance = 0f; + + try + { + var reader = new typography::Typography.OpenFont.OpenFontReader(); + typography::Typography.OpenFont.Typeface typeface; + using var fs = _fileSystem.File.OpenRead(fontFilePath); + typeface = reader.Read(fs); + + if (typeface == null) + { + logger?.LogDebug("OpenFontReader.Read returned null for {file}.", fontFilePath); + return false; + } + + int codepoint = char.ConvertToUtf32(text, 0); + int glyphIndex = typeface.LookupIndex(codepoint, 0); + + var tfGlyph = typeface.GetGlyphByIndex(glyphIndex); + if (tfGlyph == null) + { + logger?.LogDebug("Typeface.GetGlyphByIndex returned null for index {idx}.", glyphIndex); + return false; + } + + var resultGlyph = new Glyph(text); + + var translator = new Translator(resultGlyph, fontSize, typeface); + float scale = translator.GetScale(); + + var glyphPoints = tfGlyph.GlyphPoints; + var endPoints = tfGlyph.EndPoints; + + if (glyphPoints != null && endPoints != null && glyphPoints.Length > 0 && endPoints.Length > 0) + typography::Typography.OpenFont.IGlyphReaderExtensions.Read(translator, glyphPoints, endPoints, scale); + + advance = GetAdvanceWidth(typeface, glyphIndex, codepoint, scale); + glyph = resultGlyph; + + return true; + } + catch (Exception ex) + { + logger?.LogWarning(ex, "Typed typography extraction failed for {file}.", fontFilePath); + glyph = null!; + advance = 0f; + + return false; + } + } + + private static float GetAdvanceWidth(typography::Typography.OpenFont.Typeface typeface, int glyphIndex, int codepoint, float scale) + { + try + { + return typeface.GetHAdvanceWidthFromGlyphIndex(glyphIndex) * scale; + } + catch + { + try + { + return typeface.GetAdvanceWidth(codepoint) * scale; + } + catch + { + return 0f; + } + } + } +} diff --git a/SharpEngine.slnx b/SharpEngine.slnx index a464882..693c49e 100644 --- a/SharpEngine.slnx +++ b/SharpEngine.slnx @@ -5,6 +5,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -24,6 +59,20 @@ + + + + + + + + + + + + + + @@ -43,16 +92,19 @@ + + + - + - + @@ -62,27 +114,5 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj b/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj index 998faa7..5d3d5cc 100644 --- a/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj +++ b/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj @@ -18,7 +18,6 @@ - \ No newline at end of file diff --git a/Tests/SharpEngine.Core.ArchitectureTests/GameObjectTests.cs b/Tests/SharpEngine.Core.ArchitectureTests/GameObjectTests.cs index eb53eb5..994c52b 100644 --- a/Tests/SharpEngine.Core.ArchitectureTests/GameObjectTests.cs +++ b/Tests/SharpEngine.Core.ArchitectureTests/GameObjectTests.cs @@ -24,5 +24,4 @@ public void GameObjects_Should_Not_Have_Public_Constructors() Assert.Empty(publicConstructors); } } - } diff --git a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj index 397a4c5..d21b654 100644 --- a/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj +++ b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj @@ -12,18 +12,7 @@ - - - - - - - - - - - - + diff --git a/Tests/SharpEngine.Core.UI.Text.Tests/GeometryUtilsTests.cs b/Tests/SharpEngine.Core.UI.Text.Tests/GeometryUtilsTests.cs new file mode 100644 index 0000000..2245c6a --- /dev/null +++ b/Tests/SharpEngine.Core.UI.Text.Tests/GeometryUtilsTests.cs @@ -0,0 +1,48 @@ +using Xunit; +using System.Numerics; +using SharpEngine.Text; + +public class GeometryUtilsTests +{ + /*[Fact] + public void EvaluateQuadratic_MidpointIsCorrect() + { + var p0 = new Vector2(0, 0); + var p1 = new Vector2(0, 2); + var p2 = new Vector2(2, 2); + + var mid = GeometryUtils.EvaluateQuadratic(p0, p1, p2, 0.5f); + + Assert.Equal(0.5f, mid.X, 3); + Assert.Equal(1.5f, mid.Y, 3); + } + + [Fact] + public void EvaluateCubic_Endpoints() + { + var p0 = new Vector2(0, 0); + var p1 = new Vector2(0, 1); + var p2 = new Vector2(1, 2); + var p3 = new Vector2(2, 2); + + var start = GeometryUtils.EvaluateCubic(p0, p1, p2, p3, 0f); + var end = GeometryUtils.EvaluateCubic(p0, p1, p2, p3, 1f); + + Assert.Equal(p0.X, start.X); + Assert.Equal(p0.Y, start.Y); + Assert.Equal(p3.X, end.X); + Assert.Equal(p3.Y, end.Y); + } + + [Fact] + public void IsPointInPolygon_Square() + { + var poly = new System.Collections.Generic.List + { + new Vector2(0,0), new Vector2(2,0), new Vector2(2,2), new Vector2(0,2) + }; + + Assert.True(GeometryUtils.IsPointInPolygon(poly, new Vector2(1,1))); + Assert.False(GeometryUtils.IsPointInPolygon(poly, new Vector2(3,3))); + }*/ +} diff --git a/Tests/SharpEngine.Core.UI.Text.Tests/GlyphTests.cs b/Tests/SharpEngine.Core.UI.Text.Tests/GlyphTests.cs new file mode 100644 index 0000000..dad9168 --- /dev/null +++ b/Tests/SharpEngine.Core.UI.Text.Tests/GlyphTests.cs @@ -0,0 +1,35 @@ +using Xunit; +using SharpEngine.Core.Numerics; +using SharpEngine.Text.Glyphs; +using SharpEngine.Text; + +namespace SharpEngine.Core.Tests; + +public class GlyphTests +{ + [Fact] + public void Triangulate_SimpleSquare_ReturnsTriangles() + { + var g = new Glyph("square"); + var contour = new List + { + // TODO: Resolve package reference conflict with System.Numerics. + // new GlyphSegment(SegmentType.Line, new Vector2(0,0), new Vector2(1,0)), + // new GlyphSegment(SegmentType.Line, new Vector2(1,0), new Vector2(1,1)), + // new GlyphSegment(SegmentType.Line, new Vector2(1,1), new Vector2(0,1)), + // new GlyphSegment(SegmentType.Line, new Vector2(0,1), new Vector2(0,0)), + }; + + g.AddContour(contour); + + var tris = g.Triangulate(); + // Should emit at least one triangle (3 verts) + Assert.True(tris != null); + + // TODO: Resolve package reference conflict with System.Numerics. + // var list = new System.Collections.Generic.List(tris); + // Assert.True(list.Count >= 3); + // // Count should be a multiple of 3 + // Assert.True(list.Count % 3 == 0); + } +} \ No newline at end of file diff --git a/Tests/SharpEngine.Core.UI.Text.Tests/SharpEngine.Core.UI.Text.Tests.csproj b/Tests/SharpEngine.Core.UI.Text.Tests/SharpEngine.Core.UI.Text.Tests.csproj new file mode 100644 index 0000000..06b60eb --- /dev/null +++ b/Tests/SharpEngine.Core.UI.Text.Tests/SharpEngine.Core.UI.Text.Tests.csproj @@ -0,0 +1,24 @@ + + + + $(DotNetTargetFramework) + enable + enable + + + + + + + + + + + + + + + + + + \ No newline at end of file