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 new file mode 100644 index 0000000..5d8e474 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,275 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# 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 + +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 new file mode 100644 index 0000000..b332ad9 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,36 @@ +Copilot/Agent Guidance (canonical: .github/copilot-instructions.md) + +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). + +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. + +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. + +- 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. + +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/.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" 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/.editorconfig b/Engine/.editorconfig deleted file mode 100644 index efb06f1..0000000 --- a/Engine/.editorconfig +++ /dev/null @@ -1,8 +0,0 @@ -[*.cs] - -# IDE0290: Use primary constructor -csharp_style_prefer_primary_constructors = false - -# 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 diff --git a/Engine/Examples/MinecraftClone/Block/Stone.cs b/Engine/Examples/MinecraftClone/Block/Stone.cs deleted file mode 100644 index 13352d3..0000000 --- a/Engine/Examples/MinecraftClone/Block/Stone.cs +++ /dev/null @@ -1,27 +0,0 @@ -using SharpEngine.Core._Resources; -using SharpEngine.Core.Extensions; -using System.Numerics; - -namespace Minecraft.Block; - -internal class Stone : BlockBase -{ - // TODO: #91 Create record for textures files for less parameters - // TODO: #91 Create const files for these file strings - - /// - /// Initializes a new block. - /// - /// The position where the block is created. - /// The name of the object in the scene. - public Stone(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources\\container2.png"), - PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"), - Default.VertexShader, - Default.FragmentShader) { } - - /// - public override BlockType BlockType => BlockType.Stone; - - /// - public override bool IsSolid => true; -} 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/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj b/Engine/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj deleted file mode 100644 index 079bf1a..0000000 --- a/Engine/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - Exe - net8.0 - Tutorial - - - - - - - - - - - - - - - - - - Always - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - \ No newline at end of file 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/Loaders/ObjLoader/ObjLoader.cs b/Engine/ObjLoader/Loaders/ObjLoader/ObjLoader.cs deleted file mode 100644 index 798bc3c..0000000 --- a/Engine/ObjLoader/Loaders/ObjLoader/ObjLoader.cs +++ /dev/null @@ -1,64 +0,0 @@ -using ObjLoader.Loader.Loaders; -using ObjLoader.TypeParsers; -using SharpEngine.Core.Entities.Properties.Meshes; -using Silk.NET.Core.Contexts; -using Silk.NET.OpenGL; -using System.Collections.Generic; -using System.IO; -using Tutorial; - -namespace ObjLoader.Loaders.ObjLoader -{ - public class ObjLoader : LoaderBase - { - private readonly string _path; - private readonly DataStore _dataStore; - private readonly List _typeParsers = []; - private readonly List _unrecognizedLines = []; - - public ObjLoader(string path, DataStore dataStore) - { - _path = path; - _dataStore = dataStore; - } - - public ObjLoader SetupTypeParsers(params ITypeParser[] parsers) - { - foreach (var parser in parsers) - _typeParsers.Add(parser); - - return this; - } - - /// - protected override void ParseLine(string keyword, string data) - { - foreach (var typeParser in _typeParsers) - if (typeParser.CanParse(keyword)) - { - typeParser.Parse(data); - return; - } - - _unrecognizedLines.Add(keyword + " " + data); - } - - public List Load(GL gl) - { - var fileStream = new FileStream(_path, FileMode.Open, FileAccess.Read); - StartLoad(fileStream); - - return - [ - new(gl) - { - Vertices2 = _dataStore.Vertices, - TextureCoordinates2 = _dataStore.Textures, - Normals2 = _dataStore.Normals, - Groups = _dataStore.Groups, - Materials = _dataStore.Materials - } - ]; - } - } -} \ No newline at end of file diff --git a/Engine/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs b/Engine/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs deleted file mode 100644 index 408855c..0000000 --- a/Engine/ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs +++ /dev/null @@ -1,65 +0,0 @@ -using ObjLoader.Loader.TypeParsers; -using ObjLoader.Loaders.MaterialLoader; - -using SharpEngine.Core.Entities.Properties.Meshes; -using Silk.NET.OpenGL; -using System; -using System.Collections.Generic; -using System.IO; -using Tutorial; - -namespace ObjLoader.Loaders.ObjLoader -{ - /// - /// Handles loading 3D models. - /// - public static class ObjLoaderFactory - { - const string FbxExtension = ".fbx"; - const string ObjExtension = ".obj"; - - /// - /// Loads a mesh based on the file extension of the provided path. - /// - /// The OpenGL context where the model should be bound. - /// Specifies the file path of the mesh to be loaded, which determines the loading method based on its extension. - /// The model loaded from the file. - /// Thrown when the file extension of the provided path is not recognized as a supported mesh format. - public static Model_Old Load(GL gl, string path) - => Path.GetExtension(path) switch - { - FbxExtension => LoadFbx("", path), - ObjExtension => LoadObj(gl, path), - _ => throw new NotSupportedException($"The file extension {Path.GetExtension(path)} is not a supported mesh file.") - }; - - // TODO: #3 Load fbx mesh from file - private static Model_Old LoadFbx(string identifier, string meshFilePath) - { - throw new NotImplementedException(); - } - - private static Model_Old LoadObj(GL gl, string path) - { - return new Model_Old(gl, path); - - // TOOD: Use the ObjLoader to load the model instead of the external library. - - /*var dataStore = new DataStore(); - - var faceParser = new FaceParser(dataStore); - var groupParser = new GroupParser(dataStore); - var normalParser = new NormalParser(dataStore); - var textureParser = new TextureParser(dataStore); - var vertexParser = new VertexParser(dataStore); - - var materialLibraryLoader = new MaterialLibraryLoader(path, dataStore); - var materialLibraryParser = new MaterialLibraryParser(materialLibraryLoader); - var useMaterialParser = new UseMaterialParser(dataStore); - - var loader = new ObjLoader(path, dataStore).SetupTypeParsers(faceParser, groupParser, normalParser, textureParser, vertexParser, materialLibraryParser, useMaterialParser); - return loader.Load(gl); - */ - } - } -} \ No newline at end of file diff --git a/Engine/ObjLoader/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/ObjLoader/TypeParsers/VertexParser.cs b/Engine/ObjLoader/TypeParsers/VertexParser.cs deleted file mode 100644 index d6b4fcf..0000000 --- a/Engine/ObjLoader/TypeParsers/VertexParser.cs +++ /dev/null @@ -1,37 +0,0 @@ -using ObjLoader.Loader.Common; -using ObjLoader.TypeParsers; -using SharpEngine.Core.Components.Properties.Meshes.MeshData; -using System; -using Tutorial; - -namespace ObjLoader.Loader.TypeParsers -{ - public class VertexParser : TypeParserBase, ITypeParser - { - private readonly DataStore _dataStore; - - public VertexParser(DataStore dataStore) - { - _dataStore = dataStore; - } - - /// - protected override string Keyword => "v"; - - /// - public override void Parse(string line) - { - string[] parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); - - float x = parts[0].ParseInvariantFloat(); - float y = parts[1].ParseInvariantFloat(); - float z = parts[2].ParseInvariantFloat(); - - var vertex = new Vertex() - { - Position = new System.Numerics.Vector3(x, y, z), - }; - _dataStore.Vertices.Add(vertex); - } - } -} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Components/IComponent.cs b/Engine/SharpEngine.Core.Components/IComponent.cs deleted file mode 100644 index fdeaa85..0000000 --- a/Engine/SharpEngine.Core.Components/IComponent.cs +++ /dev/null @@ -1,13 +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 - -internal interface IComponent -{ -} diff --git a/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IFaceGroup.cs b/Engine/SharpEngine.Core.Components/Obsolete/ObjLoader/DataStore/IFaceGroup.cs 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/Shaders/ShaderExtensions.cs b/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs deleted file mode 100644 index 830a2aa..0000000 --- a/Engine/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs +++ /dev/null @@ -1,261 +0,0 @@ -using SharpEngine.Shared; -using Silk.NET.OpenGL; -using System.Numerics; -using System.Text.RegularExpressions; - -namespace SharpEngine.Core.Shaders; - -/// -/// Represents a shader program in the engine. -/// -public partial class Shader -{ - /// - /// Initializes and compiles vertex and fragment shaders into a shader program. - /// - /// The current instance after initialization. - public Shader Initialize() - { - // Load and compile shader - if (!LoadShader(ShaderType.VertexShader, VertPath, out uint vertexShader)) - { - Debug.Log.Error("Unable to load vertex shader."); - return this; - } - - if (!LoadShader(ShaderType.FragmentShader, FragPath, out uint fragmentShader)) - { - Debug.Log.Error("Unable to load fragment shader."); - return this; - } - - // These two shaders must then be merged into a shader program, which can then be used by OpenGL. - // To do this, create a program... - - Handle = _gl.CreateProgram(); - - // Attach both shaders... - _gl.AttachShader(Handle, vertexShader); - _gl.AttachShader(Handle, fragmentShader); - - // And then link them together. - bool shaderLinked = LinkProgram(Handle); - - // When the shader program is linked, it no longer needs the individual shaders attached to it; the compiled code is copied into the shader program. - // Detach them, and then delete them. - _gl.DetachShader(Handle, vertexShader); - _gl.DetachShader(Handle, fragmentShader); - _gl.DeleteShader(fragmentShader); - _gl.DeleteShader(vertexShader); - - if (!shaderLinked) - { - Debug.Log.Information("Unable to link shader program."); - return this; - } - - // The shader is now ready to go, but first, we're going to cache all the shader uniform locations. - // Querying this from the shader is very slow, so we do it once on initialization and reuse those values - // later. - SetUniformLocations(); - - return this; - } - - private bool LoadShader(ShaderType shaderType, string shaderPath, out uint shader) - { - if (!File.Exists(shaderPath)) - { - Debug.Log.Information("Shader file not found: {Path}", shaderPath); - - shader = 0; - return false; - } - - string shaderSource = File.ReadAllText(shaderPath); - shaderSource = ProcessIncludes(shaderSource, Path.GetDirectoryName(shaderPath)!); - - // GL.CreateShader will create an empty shader (obviously). The ShaderType enum denotes which type of shader will be created. - shader = _gl.CreateShader(shaderType); - _gl.ShaderSource(shader, shaderSource); - - if (!CompileShader(shader)) - { - Debug.Log.Information("Unable to load {Type} shader from '{Path}'.", shaderType, shaderPath); - return false; - } - - return true; - } - - private void SetUniformLocations() - { - // First, we have to get the number of active uniforms in the shader. - _gl.GetProgram(Handle, GLEnum.ActiveUniforms, out var numberOfUniforms); - - Dictionary uniformLocations = []; - - // Loop over all the uniforms, - for (uint i = 0; i < numberOfUniforms; i++) - { - // get the name of this uniform, - var key = _gl.GetActiveUniform(Handle, i, out _, out _); - - // get the location, - var location = _gl.GetUniformLocation(Handle, key); - - // and then add it to the dictionary. - uniformLocations.Add(key, location); - } - - _uniformLocations = uniformLocations; - } - - private static string ProcessIncludes(string shaderCode, string directory) - { - const string includePattern = @"#include\s+""(.+?)"""; - return Regex.Replace(shaderCode, includePattern, match => - { - string includePath = Path.Combine(directory, match.Groups[1].Value); - string includeCode = File.ReadAllText(includePath); - return ProcessIncludes(includeCode, Path.GetDirectoryName(includePath)!); - }, RegexOptions.NonBacktracking); - } - - private bool CompileShader(uint shader) - { - // Try to compile the shader - _gl.CompileShader(shader); - - // Check for compilation errors - _gl.GetShader(shader, GLEnum.CompileStatus, out var statusCode); - if (statusCode != (int)GLEnum.True) - { - // We can use `GL.GetShaderInfoLog(shader)` to get information about the error. - var infoLog = _gl.GetShaderInfoLog(shader); - Debug.Log.Error("Error occurred whilst compiling Shader({Shader}).\n\n{Log}", shader, infoLog); - - return false; - } - - return true; - } - - private bool LinkProgram(uint program) - { - _gl.LinkProgram(program); - _gl.GetProgram(program, GLEnum.LinkStatus, out var statusCode); - - if (statusCode != (int)GLEnum.True) - { - string infoLog = _gl.GetProgramInfoLog(program); - Debug.Log.Error("Error occurred whilst linking Program({Program}): {Info}", program, infoLog); - - return false; - } - - return true; - } - - /// - /// Enables the shader program. - /// - public void Use() - => _gl.UseProgram(Handle); - - /// - /// Checks if the shader attribute exists within the current shader. - /// - /// The name of the attribute that's being looked for. - /// Outputs the location of the attribute in the shader if found; otherwise -1. - /// If the attribute exists, ; otherwise, . - public bool TryGetAttribLocation(string attribName, out int location) - { - location = _gl.GetAttribLocation(Handle, attribName); - if (location == ShaderAttributes.AttributeLocationNotFound) - { - Debug.Log.Warning("Attribute '{Attribute}' not found in shader program.", attribName); - return false; - } - - return true; - } - - // Uniform setters - // Uniforms are variables that can be set by user code, instead of reading them from the VBO. - // You use VBOs for vertex-related data, and uniforms for almost everything else. - - // Setting a uniform is almost always the exact same, so I'll explain it here once, instead of in every method: - // 1. Bind the program you want to set the uniform on - // 2. Get a handle to the location of the uniform with GL.GetUniformLocation. - // 3. Use the appropriate GL.Uniform* function to set the uniform. - - private bool TrySetUniform(string uniformName, T data, Action setter) - { - if (!_uniformLocations.TryGetValue(uniformName, out int location)) - { - Debug.Log.Information("Uniform '{UniformName}' not found in shader '{ShaderName}'.", uniformName, Name); - return false; - } - - // TODO: #95 The _gl.UseProgram should be not be called here. Rather the renderer should call it once before rendering. - _gl.UseProgram(Handle); - setter(location, data); - - return true; - } - - /// - /// Set a uniform int on this shader. - /// - /// The name of the uniform. - /// The data to set. - public bool SetInt(string name, int data) - => TrySetUniform(name, data, _gl.Uniform1); - - /// - /// Set a uniform float on this shader. - /// - /// The name of the uniform. - /// The data to set. - public bool SetFloat(string name, float data) - => TrySetUniform(name, data, _gl.Uniform1); - - /// - /// Set a uniform Matrix4 on this shader - /// - /// The name of the uniform. - /// The data to set. - /// Determines whether or not the matrix should be transposed. Defaults to . - /// - /// - /// The matrix is transposed before being sent to the shader unless is set to . - /// - /// - public bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true) - => TrySetUniform(name, data, (uniform, d) => _gl.UniformMatrix4(uniform, transpose, d.ToSpan())); - - /// - /// Set a uniform Vector2 on this shader. - /// - /// The name of the uniform. - /// The data to set. - public bool SetVector2(string name, Vector2 data) - => TrySetUniform(name, data, _gl.Uniform2); - - /// - /// Set a uniform Vector3 on this shader. - /// - /// The name of the uniform. - /// The data to set. - public bool SetVector3(string name, Vector3 data) - => TrySetUniform(name, data, _gl.Uniform3); - - /// - /// Set a uniform Vector3 on this shader. - /// - /// The name of the uniform. - /// The data to set. - public bool SetVector4(string name, Vector4 data) - => TrySetUniform(name, data, _gl.Uniform4); -} diff --git a/Engine/SharpEngine.Core.Components/Properties/Textures/Texture.cs b/Engine/SharpEngine.Core.Components/Properties/Textures/Texture.cs 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.Components/SharpEngine.Core.Components.csproj b/Engine/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj deleted file mode 100644 index 3ba046b..0000000 --- a/Engine/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - net8.0 - enable - enable - - - - $(GenerateNuGetPackages) - true - - - - - - - - - - - - - - - - - - - - - - diff --git a/Engine/SharpEngine.Core.Numerics/Vector2.cs b/Engine/SharpEngine.Core.Numerics/Vector2.cs deleted file mode 100644 index 0e94630..0000000 --- a/Engine/SharpEngine.Core.Numerics/Vector2.cs +++ /dev/null @@ -1,419 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/Vector2.cs - -using System.Globalization; -using System.Runtime.CompilerServices; -using System.Text; -using Matrix3x2 = System.Numerics.Matrix3x2; -using Matrix4x4 = System.Numerics.Matrix4x4; -using Quaternion = System.Numerics.Quaternion; -using Vector = System.Numerics.Vector; - -namespace SharpEngine.Core.Numerics -{ - /// - /// A structure encapsulating two single precision floating point values and provides hardware accelerated methods. - /// - public partial struct Vector2 : IEquatable, IFormattable - { - #region Public Static Properties - - /// Returns the vector (0,0). - public static Vector2 Zero => new(); - - /// Returns the vector (1,1). - public static Vector2 One => new(1.0f, 1.0f); - - /// Returns the vector (1,0). - public static Vector2 UnitX => new(1.0f, 0.0f); - - /// Returns the vector (0,1). - public static Vector2 UnitY => new(0.0f, 1.0f); - - #endregion Public Static Properties - - #region Public instance methods - - /// - /// Returns the hash code for this instance. - /// - /// The hash code. - public override readonly int GetHashCode() - => HashCodeHelper.CombineHashCodes(X.GetHashCode(), Y.GetHashCode()); - - /// - /// Returns a boolean indicating whether the given Object is equal to this Vector2 instance. - /// - /// The Object to compare against. - /// True if the Object is equal to this Vector2; False otherwise. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override bool Equals(object? obj) - { - if (obj is not Vector2 vec2) - return false; - - return Equals(vec2); - } - - /// - /// Returns a String representing this Vector2 instance. - /// - /// The string representation. - public override readonly string ToString() => ToString("G", CultureInfo.CurrentCulture); - - /// - /// Returns a String representing this Vector2 instance, using the specified format to format individual elements. - /// - /// The format of individual elements. - /// The string representation. - public readonly string ToString(string format) => ToString(format, CultureInfo.CurrentCulture); - - /// - /// Returns a String representing this Vector2 instance, using the specified format to format individual elements - /// and the given IFormatProvider. - /// - /// The format of individual elements. - /// The format provider to use when formatting elements. - /// The string representation. - public readonly string ToString(string? format, IFormatProvider? formatProvider) - { - var sb = new StringBuilder(); - string separator = NumberFormatInfo.GetInstance(formatProvider).NumberGroupSeparator; - sb.Append('<'); - sb.Append(X.ToString(format, formatProvider)); - sb.Append(separator); - sb.Append(' '); - sb.Append(Y.ToString(format, formatProvider)); - sb.Append('>'); - return sb.ToString(); - } - - /// - /// Returns the length of the vector. - /// - /// The vector's length. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly float Length() - { - if (Vector.IsHardwareAccelerated) - { - float ls = Vector2.Dot(this, this); - return (float)MathF.Sqrt(ls); - } - else - { - float ls = (X * X) + (Y * Y); - return MathF.Sqrt(ls); - } - } - - /// - /// Returns the length of the vector squared. This operation is cheaper than Length(). - /// - /// The vector's length squared. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly float LengthSquared() - { - if (Vector.IsHardwareAccelerated) - return Vector2.Dot(this, this); - - else - return (X * X) + (Y * Y); - } - #endregion Public Instance Methods - - #region Public Static Methods - /// - /// Returns the Euclidean distance between the two given points. - /// - /// The first point. - /// The second point. - /// The distance. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Distance(Vector2 value1, Vector2 value2) - { - if (Vector.IsHardwareAccelerated) - { - var difference = value1 - value2; - float ls = Vector2.Dot(difference, difference); - return (float)System.MathF.Sqrt(ls); - } - else - { - float dx = value1.X - value2.X; - float dy = value1.Y - value2.Y; - - float ls = (dx * dx) + (dy * dy); - - return MathF.Sqrt(ls); - } - } - - /// - /// Returns the Euclidean distance squared between the two given points. - /// - /// The first point. - /// The second point. - /// The distance squared. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float DistanceSquared(Vector2 value1, Vector2 value2) - { - if (Vector.IsHardwareAccelerated) - { - var difference = value1 - value2; - return Vector2.Dot(difference, difference); - } - else - { - float dx = value1.X - value2.X; - float dy = value1.Y - value2.Y; - - return (dx * dx) + (dy * dy); - } - } - - /// - /// Returns a vector with the same direction as the given vector, but with a length of 1. - /// - /// The vector to normalize. - /// The normalized vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Normalize(Vector2 value) - { - if (Vector.IsHardwareAccelerated) - { - float length = value.Length(); - return value / length; - } - else - { - float ls = (value.X * value.X) + (value.Y * value.Y); - float invNorm = 1.0f / MathF.Sqrt(ls); - - return new Vector2( - value.X * invNorm, - value.Y * invNorm); - } - } - - /// - /// Returns the reflection of a vector off a surface that has the specified normal. - /// - /// The source vector. - /// The normal of the surface being reflected off. - /// The reflected vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Reflect(Vector2 vector, Vector2 normal) - { - if (Vector.IsHardwareAccelerated) - { - float dot = Vector2.Dot(vector, normal); - return vector - (2 * dot * normal); - } - else - { - float dot = (vector.X * normal.X) + (vector.Y * normal.Y); - - return new Vector2( - vector.X - (2.0f * dot * normal.X), - vector.Y - (2.0f * dot * normal.Y)); - } - } - - /// - /// Restricts a vector between a min and max value. - /// - /// The source vector. - /// The minimum value. - /// The maximum value. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Clamp(Vector2 value1, Vector2 min, Vector2 max) - { - // This compare order is very important!!! - // We must follow HLSL behavior in the case user specified min value is bigger than max value. - float x = value1.X; - x = (x > max.X) ? max.X : x; - x = (x < min.X) ? min.X : x; - - float y = value1.Y; - y = (y > max.Y) ? max.Y : y; - y = (y < min.Y) ? min.Y : y; - - return new Vector2(x, y); - } - - /// - /// Linearly interpolates between two vectors based on the given weighting. - /// - /// The first source vector. - /// The second source vector. - /// Value between 0 and 1 indicating the weight of the second source vector. - /// The interpolated vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Lerp(Vector2 value1, Vector2 value2, float amount) - { - return new Vector2( - value1.X + ((value2.X - value1.X) * amount), - value1.Y + ((value2.Y - value1.Y) * amount)); - } - - /// - /// Transforms a vector by the given matrix. - /// - /// The source vector. - /// The transformation matrix. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Transform(Vector2 position, Matrix3x2 matrix) - { - return new Vector2( - (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M31, - (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M32); - } - - /// - /// Transforms a vector by the given matrix. - /// - /// The source vector. - /// The transformation matrix. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Transform(Vector2 position, Matrix4x4 matrix) - { - return new Vector2( - (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M41, - (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M42); - } - - /// - /// Transforms a vector normal by the given matrix. - /// - /// The source vector. - /// The transformation matrix. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 TransformNormal(Vector2 normal, Matrix3x2 matrix) - { - return new Vector2( - (normal.X * matrix.M11) + (normal.Y * matrix.M21), - (normal.X * matrix.M12) + (normal.Y * matrix.M22)); - } - - /// - /// Transforms a vector normal by the given matrix. - /// - /// The source vector. - /// The transformation matrix. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 TransformNormal(Vector2 normal, Matrix4x4 matrix) - { - return new Vector2( - (normal.X * matrix.M11) + (normal.Y * matrix.M21), - (normal.X * matrix.M12) + (normal.Y * matrix.M22)); - } - - /// - /// Transforms a vector by the given Quaternion rotation value. - /// - /// The source vector to be rotated. - /// The rotation to apply. - /// The transformed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Transform(Vector2 value, Quaternion rotation) - { - float x2 = rotation.X + rotation.X; - float y2 = rotation.Y + rotation.Y; - float z2 = rotation.Z + rotation.Z; - - float wz2 = rotation.W * z2; - float xx2 = rotation.X * x2; - float xy2 = rotation.X * y2; - float yy2 = rotation.Y * y2; - float zz2 = rotation.Z * z2; - - return new Vector2( - (value.X * (1.0f - yy2 - zz2)) + (value.Y * (xy2 - wz2)), - (value.X * (xy2 + wz2)) + (value.Y * (1.0f - xx2 - zz2))); - } - #endregion Public Static Methods - - #region Public operator methods - // all the below methods should be inlined as they are - // implemented over JIT intrinsics - - /// - /// Adds two vectors together. - /// - /// The first source vector. - /// The second source vector. - /// The summed vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Add(Vector2 left, Vector2 right) => left + right; - - /// - /// Subtracts the second vector from the first. - /// - /// The first source vector. - /// The second source vector. - /// The difference vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Subtract(Vector2 left, Vector2 right) => left - right; - - /// - /// Multiplies two vectors together. - /// - /// The first source vector. - /// The second source vector. - /// The product vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Multiply(Vector2 left, Vector2 right) => left * right; - - /// - /// Multiplies a vector by the given scalar. - /// - /// The source vector. - /// The scalar value. - /// The scaled vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Multiply(Vector2 left, float right) => left * right; - - /// - /// Multiplies a vector by the given scalar. - /// - /// The scalar value. - /// The source vector. - /// The scaled vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Multiply(float left, Vector2 right) => left * right; - - /// - /// Divides the first vector by the second. - /// - /// The first source vector. - /// The second source vector. - /// The vector resulting from the division. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Divide(Vector2 left, Vector2 right) => left / right; - - /// - /// Divides the vector by the given scalar. - /// - /// The source vector. - /// The scalar value. - /// The result of the division. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Divide(Vector2 left, float divisor) => left / divisor; - - /// - /// Negates a given vector. - /// - /// The source vector. - /// The negated vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector2 Negate(Vector2 value) => -value; - - #endregion Public operator methods - } -} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs b/Engine/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs deleted file mode 100644 index e747bab..0000000 --- a/Engine/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System.Runtime.CompilerServices; - -namespace SharpEngine.Core.Numerics -{ - // This file contains the definitions for all of the JIT intrinsic methods and properties that are recognized by the current x64 JIT compiler. - // The implementation defined here is used in any circumstance where the JIT fails to recognize these members as intrinsic. - // The JIT recognizes these methods and properties by name and signature: if either is changed, the JIT will no longer recognize the member. - // Some methods declared here are not strictly intrinsic, but delegate to an intrinsic method. For example, only one overload of CopyTo() - // is actually recognized by the JIT, but both are here for simplicity. - - public partial struct Vector3 : IVector - { - /// - public float X { get; set; } - - /// - public float Y { get; set; } - - /// - public float Z { get; set; } - - #region Constructors - - /// - /// Constructs a vector whose elements are all the single specified value. - /// - /// The element to fill the vector with. - [JitIntrinsic] - public Vector3(float value) : this(value, value, value) { } - - /// - /// Constructs a Vector3 from the given Vector2 and a third value. - /// - /// The Vector to extract X and Y components from. - /// The Z component. - public Vector3(Vector2 value, float z) : this(value.X, value.Y, z) { } - - /// - /// Constructs a vector with the given individual elements. - /// - /// The X component. - /// The Y component. - /// The Z component. - [JitIntrinsic] - public Vector3(float x, float y, float z) - { - X = x; - Y = y; - Z = z; - } - - #endregion Constructors - - #region Public Instance Methods - - /// - /// Copies the contents of the vector into the given array. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void CopyTo(float[] array) => CopyTo(array, 0); - - /// - /// Copies the contents of the vector into the given array, starting from index. - /// - /// If array is null. - /// If array is multidimensional. - /// If index is greater than end of the array or index is less than zero. - /// If number of elements in source vector is greater than those available in destination array. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly void CopyTo(float[] array, int index) - { - // Match the JIT's exception type here. For perf, a NullReference is thrown instead of an ArgumentNull. - ArgumentNullException.ThrowIfNull(array); - - if (index < 0 || index >= array.Length) - throw new ArgumentOutOfRangeException(nameof(index)); - - if ((array.Length - index) < 3) - throw new ArgumentException("", nameof(index)); - - array[index] = X; - array[index + 1] = Y; - array[index + 2] = Z; - } - - /// - /// Returns a boolean indicating whether the given Vector3 is equal to this Vector3 instance. - /// - /// The Vector3 to compare this instance to. - /// True if the other Vector3 is approximately equal to this instance; False otherwise. - [JitIntrinsic] - public readonly bool Equals(Vector3 other) - => MathF.Abs(X - other.X) < Float.Tolerance && - MathF.Abs(Y - other.Y) < Float.Tolerance && - MathF.Abs(Z - other.Z) < Float.Tolerance; - - #endregion Public Instance Methods - - #region Public Static Methods - - /// - /// Returns the dot product of two vectors. - /// - /// The first vector. - /// The second vector. - /// The dot product. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static float Dot(Vector3 vector1, Vector3 vector2) - => (vector1.X * vector2.X) + - (vector1.Y * vector2.Y) + - (vector1.Z * vector2.Z); - - /// - /// Returns a vector whose elements are the minimum of each of the pairs of elements in the two source vectors. - /// - /// The first source vector. - /// The second source vector. - /// The minimized vector. - [JitIntrinsic] - public static Vector3 Min(Vector3 value1, Vector3 value2) - => new((value1.X < value2.X) ? value1.X : value2.X, - (value1.Y < value2.Y) ? value1.Y : value2.Y, - (value1.Z < value2.Z) ? value1.Z : value2.Z); - - /// - /// Returns a vector whose elements are the maximum of each of the pairs of elements in the two source vectors. - /// - /// The first source vector. - /// The second source vector. - /// The maximized vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 Max(Vector3 value1, Vector3 value2) - => new((value1.X > value2.X) ? value1.X : value2.X, - (value1.Y > value2.Y) ? value1.Y : value2.Y, - (value1.Z > value2.Z) ? value1.Z : value2.Z); - - /// - /// Returns a vector whose elements are the absolute values of each of the source vector's elements. - /// - /// The source vector. - /// The absolute value vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 Abs(Vector3 value) - => new(MathF.Abs(value.X), - MathF.Abs(value.Y), - MathF.Abs(value.Z)); - - /// - /// Returns a vector whose elements are the square root of each of the source vector's elements. - /// - /// The source vector. - /// The square root vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 SquareRoot(Vector3 value) - => new(MathF.Sqrt(value.X), - MathF.Sqrt(value.Y), - MathF.Sqrt(value.Z)); - - #endregion Public Static Methods - - #region Public Static Operators - - /// - /// Adds two vectors together. - /// - /// The first source vector. - /// The second source vector. - /// The summed vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator +(Vector3 left, Vector3 right) - => new(left.X + right.X, - left.Y + right.Y, - left.Z + right.Z); - - /// - /// Subtracts the second vector from the first. - /// - /// The first source vector. - /// The second source vector. - /// The difference vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator -(Vector3 left, Vector3 right) - => new(left.X - right.X, - left.Y - right.Y, - left.Z - right.Z); - - /// - /// Multiplies two vectors together. - /// - /// The first source vector. - /// The second source vector. - /// The product vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator *(Vector3 left, Vector3 right) - => new(left.X * right.X, - left.Y * right.Y, - left.Z * right.Z); - - /// - /// Multiplies a vector by the given scalar. - /// - /// The source vector. - /// The scalar value. - /// The scaled vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator *(Vector3 left, float right) => left * new Vector3(right); - - /// - /// Multiplies a vector by the given scalar. - /// - /// The scalar value. - /// The source vector. - /// The scaled vector. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator *(float left, Vector3 right) => new Vector3(left) * right; - - /// - /// Divides the first vector by the second. - /// - /// The first source vector. - /// The second source vector. - /// The vector resulting from the division. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator /(Vector3 left, Vector3 right) - => new(left.X / right.X, - left.Y / right.Y, - left.Z / right.Z); - - /// - /// Divides the vector by the given scalar. - /// - /// The source vector. - /// The scalar value. - /// The result of the division. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator /(Vector3 value1, float value2) - { - float invDiv = 1.0f / value2; - - return new Vector3( - value1.X * invDiv, - value1.Y * invDiv, - value1.Z * invDiv); - } - - /// - /// Negates a given vector. - /// - /// The source vector. - /// The negated vector. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector3 operator -(Vector3 value) => Zero - value; - - /// - /// Returns a boolean indicating whether the two given vectors are equal. - /// - /// The first vector to compare. - /// The second vector to compare. - /// True if the vectors are equal; False otherwise. - [JitIntrinsic] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(Vector3 left, Vector3 right) - => Math.Abs(left.X - right.X) < Float.Tolerance && - Math.Abs(left.Y - right.Y) < Float.Tolerance && - Math.Abs(left.Z - right.Z) < Float.Tolerance; - - /// - /// Returns a boolean indicating whether the two given vectors are not equal. - /// - /// The first vector to compare. - /// The second vector to compare. - /// True if the vectors are not equal; False if they are equal. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(Vector3 left, Vector3 right) - => !(left == right); - - /// - /// Converts the SharpEngine representation to a System.Numerics representation. - /// - /// The value to convert - public static explicit operator System.Numerics.Vector3(Vector3 value) - => new(value.X, value.Y, value.Z); - - /// - /// Converts the System.Numerics representation to a SharpEngine representation. - /// - /// The value to convert - public static explicit operator Vector3(System.Numerics.Vector3 value) - => new(value.X, value.Y, value.Z); - - #endregion Public Static Operators - } -} \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Audio/Audio.cs b/Engine/SharpEngine.Core/Audio/Audio.cs deleted file mode 100644 index 7d9d171..0000000 --- a/Engine/SharpEngine.Core/Audio/Audio.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.IO; - -namespace SharpEngine.Core.Audio; - -/// -/// Handles Audio related operations. -/// -public static class Audio -{ - /// - /// Plays an audio file if it is in the WAV format. - /// - /// Specifies the location of the audio file to be played. - /// Thrown when the file format is not supported, such as when it is not a WAV file. - public static void Play(string filePath) - { - switch (Path.GetExtension(filePath)) - { - case ".wav": - new WavPlayer().Play(filePath); - break; - - default: - throw new NotSupportedException("Unsupported file format."); - } - } -} diff --git a/Engine/SharpEngine.Core/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/Layouts/GridLayout.cs b/Engine/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs deleted file mode 100644 index 452cc3e..0000000 --- a/Engine/SharpEngine.Core/Entities/UI/Layouts/GridLayout.cs +++ /dev/null @@ -1,88 +0,0 @@ -using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Numerics; -using SharpEngine.Core.Scenes; -using System; - -namespace SharpEngine.Core.Entities.UI.Layouts; - -/// -/// Represents a grid layout. -/// -/// The type of items that can be stored and retrieved within the grid. -public class GridLayout : LayoutBase where TItem : SceneNode, new() -{ - /// Gets or sets the amount of rows in the grid. - public uint Rows { get; set; } = 2; - - /// Gets or sets the amount of columns in the grid. - public uint Columns { get; set; } = 3; - - /// - /// Retrieves the item at [, ]. - /// - /// The row where the items should be retrieved. - /// The column where the item should be retrieved. - /// The item at [, ]. - public TItem this[uint row, uint column] - { - get - { - uint index = GetIndex(row, column); - return Items[(int)index]; - } - set - { - uint index = GetIndex(row, column); - Items[(int)index] = value; - } - } - - /// - public override void AddItem(TItem item) - { - base.AddItem(item); - - var offset = -100; // TODO: #26 Fix this, this is just a temporary solution. - - if (Items.Count % Columns != 0) - offset = offset + (Items.Count * 30); - - // item.Transform.Position.X = offset; - } - - /// - /// Calculates the index in the flat list based on row and column. - /// - /// The row where the items should be retrieved. - /// The column where the item should be retrieved. - /// The index of the item at [, ]. - /// - private uint GetIndex(uint row, uint column) - { - if (row < 0 || column < 0 || column >= Columns) - throw new ArgumentOutOfRangeException(nameof(row), "Invalid row or column index."); - - var index = row * Columns + column; - if (index >= Items.Count) - throw new ArgumentOutOfRangeException(nameof(row), "Index exceeds the number of items."); - - return index; - } - - /// - public override TItem[][] GetValues() - { - var values = new TItem[Rows][]; - for (uint i = 0; i < Rows; i++) - { - values[i] = new TItem[Columns]; - for (uint j = 0; j < Columns; j++) - { - var item = this[i, j]; - values[i][j] = item; - } - } - - return values; - } -} diff --git a/Engine/SharpEngine.Core/Entities/UI/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/Extensions/PathExtensions.cs b/Engine/SharpEngine.Core/Extensions/PathExtensions.cs deleted file mode 100644 index 0de788f..0000000 --- a/Engine/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/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/Primitives/Plane.cs b/Engine/SharpEngine.Core/Primitives/Plane.cs deleted file mode 100644 index ba046a5..0000000 --- a/Engine/SharpEngine.Core/Primitives/Plane.cs +++ /dev/null @@ -1,41 +0,0 @@ -using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.Windowing; - -namespace SharpEngine.Core.Primitives; - -/// -/// Used to create a primitive plane object. -/// -public static class Plane -{ - /// The plane mesh. - public static Mesh Mesh { get; } = new(Window.GL) - { - 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 - ], - 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 - ] - }; -} 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/Renderers/UIRenderer.cs b/Engine/SharpEngine.Core/Renderers/UIRenderer.cs deleted file mode 100644 index 54ee5da..0000000 --- a/Engine/SharpEngine.Core/Renderers/UIRenderer.cs +++ /dev/null @@ -1,61 +0,0 @@ -using SharpEngine.Core.Entities.UI; -using SharpEngine.Core.Entities.Views; -using SharpEngine.Core.Interfaces; -using SharpEngine.Core.Scenes; -using SharpEngine.Core.Shaders; -using SharpEngine.Core.Windowing; -using SharpEngine.Shared; -using Silk.NET.OpenGL; - -using System; -using System.Threading.Tasks; - -namespace SharpEngine.Core.Renderers; - -/// -/// Represents a renderer dedicated to drawing UI elements to the screen. -/// -public class UIRenderer : RendererBase -{ - private readonly Scene _scene; - private readonly CameraView _camera; - private readonly Window _window; - - /// - public override RenderFlags RenderFlag => RenderFlags.UIRenderer; - - /// - /// Initializes a new instance of . - /// - public UIRenderer(CameraView camera, Window window, ISettings settings, Scene scene) : base(settings) - { - _scene = scene; - _camera = camera; - _window = window; - } - - /// - public override Task Render() - { - // TODO: Make toggling these shaders into a hotkey. - // return Task.CompletedTask; - - try - { - Window.GL.Enable(EnableCap.DepthTest); - Window.GL.DepthFunc(DepthFunction.Less); - - // Disable face culling to render both sides of the quad - Window.GL.Disable(EnableCap.CullFace); - - var uiElementRenderTasks = _scene.IterateAsync(_scene.UIElements, elem => elem.Render(_camera, _window)); - - return Task.WhenAll(uiElementRenderTasks); - } - catch (Exception ex) - { - Debug.Log.Error(ex, "{Message}", ex.Message); - return Task.FromException(ex); - } - } -} diff --git a/Engine/SharpEngine.Core/Scenes/Scene.cs b/Engine/SharpEngine.Core/Scenes/Scene.cs deleted file mode 100644 index e53d524..0000000 --- a/Engine/SharpEngine.Core/Scenes/Scene.cs +++ /dev/null @@ -1,233 +0,0 @@ -using SharpEngine.Core.Entities; -using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Entities.UI; -using SharpEngine.Shared; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; - -namespace SharpEngine.Core.Scenes; - -/// -/// Represents a scene in the game. -/// -public class Scene -{ - /// The file extension by which saved scenes are associated with. - public const string SceneFileExtension = "sharpscene"; - - private string? _fileFullPath; - - /// Gets or sets whether the scene has unsaved changes. - public bool HasUnsavedChanges { get; set; } - - /// Gets or sets the name of the scene file. - public string Name { get; private set; } = "New Scene"; - - /// - /// Initializes a new instance of . - /// - public Scene() { } - - /// - /// Initializes a new instance of . - /// - /// - public Scene(string sceneFile) - { - SetFileFullPath(sceneFile); - } - - /// - /// Gets or sets the root node of the scene. - /// - public SceneNode Root { get; set; } = new EmptyNode("Root"); - - /// Gets or sets the nodes in the scene. - private List Nodes { get; set; } = []; - - /// Gets or sets the UI elements in the scene. - public List UIElements { get; private set; } = []; - - /// Gets or sets the active element in the scene. - /// Editor only. - public SceneNode? ActiveElement { get; set; } - - /// - /// Adds an empty node to the scene root. - /// - /// The name of the new empty node. - public void AddNode(string name) - { - var node = new EmptyNode(name); - Nodes.Add(node); - Root.Children.Add(node); - } - - /// - /// Removes a node from the scene root. - /// - /// The node to be removed. - public virtual void RemoveNode(SceneNode node) - => Nodes.Remove(node); - - /// - /// Gets a node by the given . - /// - /// The name of the node to be retrieved. - /// The found node; if not found. - public SceneNode? GetNode(string name) - { - static SceneNode? FindNode(SceneNode node, string name) - { - if (node.Name == name) - return node; - - foreach (var child in node.Children) - { - var result = FindNode(child, name); - if (result != null) - return result; - } - - return null; - } - - foreach (var node in Nodes) - { - var result = FindNode(node, name); - if (result != null) - return result; - } - - return null; - } - - /// - /// Gets all the objects in the scene of type . - /// - /// The type of the objects to be retrieved. - /// All the game objects in the current scene. - public List GetObjectsOfType(SceneNode? root = null) - { - // TODO: #94 Can we make this async? - var result = new List(); - static void FindsObjects(SceneNode node, List result) - { - if (node is T obj) - result.Add(obj); - - foreach (var child in node.Children) - FindsObjects(child, result); - } - - foreach (var node in root?.Children ?? Root.Children) - FindsObjects(node, result); - - return result; - } - - /// - /// Iterates over the given and performs the given for each element. - /// - /// The type of the element. - /// The scene nodes to iterate over - /// The action to be executed for each element. - public void Iterate(List elements, Action action) where TEntityType : SceneNode - { - foreach (var entity in elements) - { - action(entity); - - var children = entity.Children.OfType().ToList(); - if (children.Count != 0) - Iterate(children, action); - } - } - - /// - /// Iterates over the given and performs the given for each element. - /// - /// The type of the element. - /// The scene nodes to iterate over - /// The action to be executed for each element. - /// A representing an asynchronous operation. - public IEnumerable IterateAsync(IEnumerable elements, Func action) - { - var tasks = new List(); - - foreach (var entity in elements) - { - tasks.Add(action(entity)); - tasks.AddRange(IterateAsync(entity.Children, action)); - } - - return tasks; - } - - /// - /// Iterates over the given and performs the given for each element. - /// - /// The scene nodes to iterate over - /// The action to be executed for each element. - /// A representing an asynchronous operation. - public IEnumerable IterateAsync(List elements, Func action) - => IterateAsync(elements, action); - - /// - /// Loads a scene from the given . - /// - /// The file containing the scene. - /// The 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); - - var loadedScene = JsonSerializer.Deserialize(sceneFile); - - if (loadedScene is not null) - { - loadedScene.SetFileFullPath(sceneFile); - return loadedScene; - } - - return new(); - } - - /// - /// Sets the full path and name of the scene file. - /// - /// The full path of the file to be set and processed for name extraction. - public void SetFileFullPath(string sceneFile) - { - _fileFullPath = sceneFile; - Name = System.IO.Path.GetFileNameWithoutExtension(sceneFile); - } - - /// - /// Determines whether the scene has been saved before and has a save file. - /// - /// if the scene has a save file; otherwise, . - public bool HasSaveFile() - => _fileFullPath is not null; - - /// - /// Saves the scene. - /// - /// A representing an asynchronous operation. - public async Task SaveScene() - => await SaveScene(_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) - { - var json = JsonSerializer.Serialize(this); - await System.IO.File.WriteAllTextAsync(fileName, json); - } -} \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Scenes/SceneNode.cs b/Engine/SharpEngine.Core/Scenes/SceneNode.cs deleted file mode 100644 index 13b3526..0000000 --- a/Engine/SharpEngine.Core/Scenes/SceneNode.cs +++ /dev/null @@ -1,110 +0,0 @@ -using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Entities.Views; -using SharpEngine.Core.Numerics; -using SharpEngine.Core.Windowing; - -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace SharpEngine.Core.Scenes; - -/// -/// Represents a empty node in a scene. -/// -/// Specifies the type used for transformations. -/// Defines vector used by the transform. -public class EmptyNode : SceneNode where TTransform : ITransform, new() where TVector : IVector, new() -{ - /// - /// Initializes a new instance of . - /// - /// - public EmptyNode(string name) : base(name) { } - - /// - /// Gets or sets the transform of the node. - /// - public virtual TTransform Transform { get; set; } = new(); -} - -/// -/// Represents a node in the scene. -/// -public abstract class SceneNode -{ - /// Gets a new empty node. - public static SceneNode Empty => new EmptyNode("Empty Node"); - - /// - /// Gets or sets the name of the node. - /// - public string Name { get; set; } = "New Object"; - - /// - /// Gets or sets the children of the node. - /// - public List Children { get; set; } = []; - - /// - /// Initializes a new empty . - /// - public SceneNode() { } - - /// - /// Initializes a new instance of the with the specified . - /// - /// The name of the new empty node. - public SceneNode(string name) - { - Name = name; - } - - /// - /// Adds an empty child node to this node by the given . - /// - /// The name of the empty node to be added. - /// The created node. - public virtual SceneNode AddChild(string name) - => AddChild(name); - - /// - /// Adds an empty child node to this node by the given . - /// - /// The name of the empty node to be added. - /// The created node. - public virtual SceneNode AddChild(string name) where TTransform : ITransform, new() where TVector : IVector, new() - { - var node = new EmptyNode(name) as SceneNode; - Children.Add(node!); - - return node!; - } - - /// - /// Adds a child node to this node. - /// - /// The nodes to be added. - /// The current node. - public virtual SceneNode AddChild(params SceneNode[] nodes) - { - foreach (var node in nodes) - Children.Add(node); - - return this; - } - - /// - /// Removes a child node from this node. - /// - /// The node to be removed. - public void RemoveChild(SceneNode node) - { - Children.Remove(node); - } - - /// - /// Renders the current object to the screen. - /// - /// A representing an asynchronous operation. - public virtual Task Render(CameraView camera, Window window) => Task.CompletedTask; -} diff --git a/Engine/SharpEngine.Core/Shaders/LampShader.cs b/Engine/SharpEngine.Core/Shaders/LampShader.cs 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/ShaderBase.cs b/Engine/SharpEngine.Core/Shaders/ShaderBase.cs deleted file mode 100644 index e7fd389..0000000 --- a/Engine/SharpEngine.Core/Shaders/ShaderBase.cs +++ /dev/null @@ -1,34 +0,0 @@ -using SharpEngine.Core.Components.Properties; -using SharpEngine.Shared; -using System; - -namespace SharpEngine.Core.Shaders; - -/// -/// Represents the base class for all shaders. -/// -public abstract class ShaderBase -{ - /// 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() - { - if (Shader is null) - { - Debug.Log.Error("Unable to set shader attributes, shader not found."); - return false; - } - - 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/SharpEngine.Core/Windowing/Window.cs b/Engine/SharpEngine.Core/Windowing/Window.cs deleted file mode 100644 index c566254..0000000 --- a/Engine/SharpEngine.Core/Windowing/Window.cs +++ /dev/null @@ -1,347 +0,0 @@ -using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.Entities.Views; -using SharpEngine.Core.Entities.Views.Settings; -using SharpEngine.Core.Enums; -using SharpEngine.Core.Extensions; -using SharpEngine.Core.Renderers; -using SharpEngine.Core.Scenes; -using SharpEngine.Core.Shaders; - -using Silk.NET.Input; -using Silk.NET.Maths; -using Silk.NET.OpenGL; -using Silk.NET.OpenGL.Extensions.ImGui; -using Silk.NET.Windowing; -using MouseButton = Silk.NET.Input.MouseButton; - -using System; -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; - -/// -/// Represents the game window. -/// -public class Window : SilkWindow -{ - private bool _windowInitialized; - private bool _initialized; - - private IEnumerable _renderers = []; - private ImGuiController? _imGuiController; - - /// - /// Gets or sets the view for the window. - /// - public readonly CameraView Camera; - - /// - /// Gets the settings for the current window. - /// - public IViewSettings Settings; - - /// The event executed when mouse events are executed. - public event Action? OnHandleMouse; - - /// The event executed when keyboard events are executed. - public event Action? OnHandleKeyboard; - - /// The event executed when the window is updated. - public event Action? OnUpdate; - - /// The event executed when the mouse wheel is scrolled. - public event Action? HandleMouseWheel; - - /// The event executed when a mouse button is clicked. - public event Action? OnButtonMouseDown; - - /// - /// The scene that is currently being rendered. - /// - protected Scene Scene { get; private set; } - - /// The OpenGL context. - public static GL GL; - - // TODO: #93 Use this method. - /// - /// Gets the current OpenGL context. - /// - /// The OpenGL context for this window. - public static GL GetGL() => GL; - private static void SetGL(GL gl) => GL = gl; - - /// - /// Initializes a new instance of . - /// - /// The camera the window should render from. - /// Contains the game scene. - /// The settings for the window. - public Window(CameraView camera, Scene scene, IViewSettings settings) - { - Scene = scene; - Settings = settings; - Camera = camera; - } - - /// - /// Initializes a new instance of . - /// - /// - /// The new window is initialized without a dedicated camera. - /// - /// Contains the game scene. - /// The settings for the window. - public Window(Scene scene, IViewSettings settings) - { - Scene = scene; - Settings = settings; - Camera = new(Vector3.One, settings); - } - - /// - /// Initializes the ga - /// - public void InitializeWindow() - { - if (_initialized) - return; - - CurrentWindow = CreateWindow(Settings.WindowOptions); - CurrentWindow.Update += deltaTime => OnUpdateFrame(new Frame(deltaTime)); - CurrentWindow.Render += deltaTime => RenderFrame(new Frame(deltaTime)); - CurrentWindow.Resize += OnResize; - CurrentWindow.Load += OnLoad; - CurrentWindow.Closing += OnClosing; - - _windowInitialized = true; - } - - /// - public override void Run(Action onFrame) - { - if (!_windowInitialized) - throw new InvalidOperationException("Window not been initialized for this instance. Try calling 'window.Initialize()' first."); - - try - { - base.Run(onFrame); - Run(); - } - catch (Exception ex) - { - Debug.Log.Error(ex, "Error running window: {Message}", ex.Message); - } - } - - /// - public override void OnLoad() - { - try - { - var context = CurrentWindow.CreateOpenGL(); - SetGL(context); - - Input = CurrentWindow.CreateInput(); - CurrentWindow.MakeCurrent(); - - SetWindowIcon(PathExtensions.GetAssemblyPath("_Resources/icon.png")); - - AssignInputEvents(); - - 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); - } - - foreach (var renderer in _renderers) - renderer.Initialize(); - - _imGuiController = new ImGuiController(GL, CurrentWindow, Input); - - _initialized = true; - } - catch (Exception ex) - { - Debug.Log.Information(ex, "Error loading window: {Message}", ex.Message); - } - - base.OnLoad(); - } - - /// - /// Renders the current view and all specified renderers. - /// - /// Contains information about the previous frame. - protected void RenderFrame(Frame frame) - { - while (!_initialized) - { - // Wait for the window to be initialized. - } - - try - { - PreRender(frame); - - _imGuiController?.Update((float)frame.FrameTime); - - 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(); - - Task.WaitAll([.. renderTasks]); - - AfterRender(frame); - - _imGuiController?.Render(); - } - catch (Exception ex) - { - Debug.Log.Information(ex.Message); - } - } - - /// - /// 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 List _shaders = []; - - private void UseShaders() - { - if (ShaderService.Instance.HasShadersToLoad) - _shaders = ShaderService.Instance.GetAll(); - - _shaders.ForEach(shader => shader.Use()); - } - - /// - protected void OnUpdateFrame(Frame frame) - { - while (!_initialized) - { - // Wait for the window to be initialized. - } - - if (Settings.PrintFrameRate) - Debug.Log.Information($"FPS: {frame.FrameRate}"); - - // TODO: #21 Handle multiple mice? - var mouse = Input?.Mice[0]; - if (mouse is not null) - { - Camera.UpdateMousePosition(mouse.Position); - OnHandleMouse?.Invoke(mouse); - } - - var keyboard = Input?.Keyboards[0]; - if (keyboard is not null) - { - if (keyboard.IsKeyPressed(Key.Escape)) - CurrentWindow.Close(); - - OnHandleKeyboard?.Invoke(keyboard, frame.FrameTime); - } - - if (Input is not null) - OnUpdate?.Invoke(frame.FrameTime, Input); - } - - // TODO: #21 Input system - private void AssignInputEvents() - { - if (Input is null) - { - Debug.Log.Information("Input is null. No input events will be assigned."); - return; - } - - foreach (var keyboard in Input.Keyboards) - keyboard.KeyDown += KeyDown; - - foreach (var mouse in Input.Mice) - { - mouse.Scroll += OnMouseWheel; - mouse.Click += OnMouseClick; - mouse.MouseDown += OnMouseDown; - - if (IsFocused) - mouse.Cursor.CursorMode = CursorMode.Raw; - } - } - - /// - protected void KeyDown(IKeyboard keyboard, Key key, int keyCode) - { - if (key == Key.Escape) - CurrentWindow.Close(); - } - - /// - protected void OnMouseWheel(IMouse mouse, ScrollWheel sw) - { - var direction = sw.Y switch - { - > 0 => MouseWheelScrollDirection.Up, - < 0 => MouseWheelScrollDirection.Down, - _ => throw new NotImplementedException() - }; - - HandleMouseWheel?.Invoke(direction, sw); - Camera.Fov -= sw.Y; - } - - /// - protected void OnResize(Vector2D size) - { - GL.Viewport(size); - - if (size != Vector2D.Zero) - Camera.AspectRatio = (float)size.X / size.Y; - } - - /// - protected override void OnMouseDown(IMouse mouse, MouseButton button) - => OnButtonMouseDown?.Invoke(mouse, button); - - /// - /// Sets the current scene. - /// - /// The contents of the new scene. - protected void SetScene(Scene scene) - { - // TODO: #92 Do we need to clear anything from e.g. the GPU when we change change the scene? - Scene = scene; - } -} diff --git a/Engine/SharpEngine.Core/_Resources/Default.cs b/Engine/SharpEngine.Core/_Resources/Default.cs deleted file mode 100644 index ef3e138..0000000 --- a/Engine/SharpEngine.Core/_Resources/Default.cs +++ /dev/null @@ -1,23 +0,0 @@ -using SharpEngine.Core.Extensions; - -namespace SharpEngine.Core._Resources; - -/// -/// Contains default resources used by the engine. -/// -public static class Default -{ - /// Gets the path to the debug texture. - public static string DebugTexture => PathExtensions.GetAssemblyPath("_Resources\\Textures\\debug.JPG"); - - /// Gets the path to the default vertex shader. - public static string VertexShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\shader.vert"); - - /// Gets the path to the lighting fragment shader. - public static string FragmentShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\lighting.frag"); - - public static string LightShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\shader.frag"); - - public static string UIVertexShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\uiShader.vert"); - public static string UIFragmentShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\uiShader.frag"); -} diff --git a/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.vert b/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.vert deleted file mode 100644 index 7787de5..0000000 --- a/Engine/SharpEngine.Core/_Resources/Shaders/uiShader.vert +++ /dev/null @@ -1,58 +0,0 @@ -#version 330 core - -layout (location = 0) in vec3 aPos; -layout (location = 1) in vec3 aNormal; -layout (location = 2) in vec2 aTexCoords; - -uniform float width; -uniform float height; - -uniform vec2 position; -uniform float rotation; -uniform vec2 screenSize; -uniform mat4 orthoMatrix; - -uniform mat4 uModel; - -out vec3 Normal; -out vec3 FragPos; -out vec2 TexCoords; - -void main() -{ - float reciprScaleOnscreen = 0.005f; - - // Calculate the aspect ratio - float aspectRatio = screenSize.x / screenSize.y; - - // Create the rotation matrix for the z-axis - mat4 rotationMatrix = mat4( - cos(rotation), -sin(rotation), 0.0, 0.0, - sin(rotation), cos(rotation), 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0 - ); - - // Scale the vertex position using width and height - vec3 scaledPos = vec3( - aPos.x * width * reciprScaleOnscreen, - aPos.y * height * reciprScaleOnscreen, - aPos.z - ); - - // Calculate the model-view-projection matrix using the orthographic projection matrix - mat4 mvp = orthoMatrix * rotationMatrix * uModel; - - // Apply the MVP matrix to the scaled vertex position with aspect ratio - gl_Position = mvp * vec4( - (scaledPos.x + (position.x * reciprScaleOnscreen)) * aspectRatio, - scaledPos.y + (position.y * reciprScaleOnscreen), - scaledPos.z, - 1 - ); - - // Pass through other attributes - Normal = mat3(uModel) * aNormal; // Transform the normal - FragPos = vec3(uModel * vec4(aPos, 1.0)); // Transform the fragment position - TexCoords = aTexCoords; -} \ No newline at end of file diff --git a/Engine/SharpEngine.slnx b/Engine/SharpEngine.slnx deleted file mode 100644 index 32cc136..0000000 --- a/Engine/SharpEngine.slnx +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj b/Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj deleted file mode 100644 index 7725988..0000000 --- a/Engine/Tests/ObjLoader.Test/ObjLoader.Tests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net8.0 - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Engine/Tests/SharpEngine.Core.Tests/Class1.cs b/Engine/Tests/SharpEngine.Core.Tests/Class1.cs deleted file mode 100644 index ff34b98..0000000 --- a/Engine/Tests/SharpEngine.Core.Tests/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace SharpEngine.Core.Tests; - -public class Class1 -{ - -} 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/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/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 84% rename from Engine/Examples/MinecraftClone/Inventory.cs rename to Examples/MinecraftClone/Inventory.cs index ed8b7eb..b195560 100644 --- a/Engine/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/Engine/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs similarity index 50% rename from Engine/Examples/MinecraftClone/Minecraft.cs rename to Examples/MinecraftClone/Minecraft.cs index 92494da..3bf7825 100644 --- a/Engine/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -1,25 +1,22 @@ using ImGuiNET; -using Minecraft.Block; -using ObjLoader.Loaders.ObjLoader; + +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.Properties.Meshes; -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.UI.Entities; +using SharpEngine.Core.UI.Entities.Layouts; using SharpEngine.Core.Windowing; -using SharpEngine.Shared; -using Silk.NET.Core.Native; + using Silk.NET.Input; using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Tutorial; namespace Minecraft; @@ -31,30 +28,38 @@ namespace Minecraft; public class Minecraft : Game { private readonly Scene _scene; - - private SceneNode _lightsNode; - private SceneNode _blocksNode; + private readonly ILogger _logger; private Input _input; private readonly Inventory _inventory; private UIElement _uiElem; - /// - /// Gets the main window. - /// - public Window? Window { get; set; } + private readonly Terrain.TerrainGenerator_New _terrain; + private readonly SceneNode _blocksNode; /// /// 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(); + _blocksNode = _scene.Root.AddChild("Blocks"); + _terrain = new Terrain.TerrainGenerator_New(_scene, _blocksNode, + [ + new HeightMapPass(), + new BaseTerrainPass(), + new CavePass(), + // new OrePass(), + // new WaterPass(), + // new SurfaceDecorationPass(), + // new FoliagePass() + ]); } /// @@ -67,38 +72,63 @@ public override void Initialize() _input = new Input(Camera); _inventory.Initialize(); - _lightsNode = _scene.Root.AddChild("lights"); - _blocksNode = _scene.Root.AddChild("blocks"); + // _terrain.InitializeWorld(); - var gridLayout = new GridLayout(); + InitializeUI(); - // TODO: #89 Fix UI renderer - _uiElem = new UIElement("uiElement"); - _scene.UIElements.Add(_uiElem); + // Test text renderer + var _textElem = new TextElement("text element") + { + Text = "Hello, World!", + Height = 200, + Color = new System.Numerics.Vector4(1, 1, 1, 1), + }; - //var uiElem2 = new UIElement("uiElement"); - //uiElem2.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(0.2f, 0.2f); - //uiElem2.Transform.Position = new Vector2(30, 0); + _uiElem.AddChild(_textElem); + } + catch (Exception ex) + { + _logger.LogInformation(ex, "{Message}", ex.Message); + } + } - // gridLayout.AddChild(_uiElem, uiElem2); - // _scene.UIElements.Add(_uiElem); - // _scene.UIElements.Add(uiElem2); + private void InitializeUI() + { + var numElements = 10; + const float elementSpacing = 50; + const float elementSize = 50; - //_scene.UIElements.Add(gridLayout); + 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) + }; - InitializeWorld(); - } - catch (Exception ex) + for (int i = 0; i < numElements; i++) { - Debug.Log.Information(ex.Message, "{Message}", ex.Message); + var uiElem = new UIElement("uiElement " + i) + { + Width = elementSize, + Height = elementSize + }; + + _uiElem = uiElem; + + 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; @@ -128,117 +158,34 @@ 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.GL, @"C:\Users\antti\Documents\Untitled2.obj"); - var go = new GameObject(model); - var go2 = new GameObject(model) - { - Transform = new Transform(new SharpEngine.Core.Numerics.Vector3(0, 0, 30)) - }; - - _scene.Root.AddChild(go); - _scene.Root.AddChild(go2); - } - - private void InitializeLights() - { - _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) - { - 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) - { - //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? /// 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)) { _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); } } @@ -263,42 +210,42 @@ 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 { - Debug.Log.Information("No more {Type}s.", _inventory.SelectedSlot.Items.Type); + _logger.LogInformation("No more {Type}s.", _inventory.SelectedSlot.Items.Type); } } 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. - Debug.Log.Information("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() @@ -313,13 +260,13 @@ 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) { 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); } /// @@ -338,7 +285,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..f821bd8 --- /dev/null +++ b/Examples/MinecraftClone/Minecraft.csproj @@ -0,0 +1,22 @@ + + + + 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..57e1d0e --- /dev/null +++ b/Examples/MinecraftClone/Program.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +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.UI.Renderers; + +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.AddEngine(engine => + { + engine.AddHandler().AddWindow(); + }); + } +} diff --git a/Examples/MinecraftClone/Resources/Slot.png b/Examples/MinecraftClone/Resources/Slot.png new file mode 100644 index 0000000..20fa147 Binary files /dev/null and b/Examples/MinecraftClone/Resources/Slot.png differ 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/MinecraftClone/Block/BlockBase.cs b/Examples/MinecraftClone/Terrain/Block/BlockBase.cs similarity index 92% rename from Engine/Examples/MinecraftClone/Block/BlockBase.cs rename to Examples/MinecraftClone/Terrain/Block/BlockBase.cs index 1c62d38..8ada33a 100644 --- a/Engine/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/Engine/Examples/MinecraftClone/BlockFactory.cs b/Examples/MinecraftClone/Terrain/Block/BlockFactory.cs similarity index 54% rename from Engine/Examples/MinecraftClone/BlockFactory.cs rename to Examples/MinecraftClone/Terrain/Block/BlockFactory.cs index 7991855..44d1deb 100644 --- a/Engine/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/Engine/Examples/MinecraftClone/Block/Dirt.cs b/Examples/MinecraftClone/Terrain/Block/Dirt.cs similarity index 71% rename from Engine/Examples/MinecraftClone/Block/Dirt.cs rename to Examples/MinecraftClone/Terrain/Block/Dirt.cs index 183d5d8..cc936f9 100644 --- a/Engine/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.Defaults; +using SharpEngine.Core.Numerics; +using SharpEngine.IO.Extensions; -namespace Minecraft.Block; +namespace Minecraft.Terrain.Block; /// /// Represents a dirt block. @@ -16,12 +16,9 @@ internal class Dirt : BlockBase /// The name of the block to initialize. public Dirt(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources\\grass.jpg"), PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"), - Default.VertexShader, - Default.FragmentShader) { } + Defaults.VertexShader, + Defaults.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..8cfdf7f --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Grass.cs @@ -0,0 +1,21 @@ +using SharpEngine.Core.Defaults; +using SharpEngine.Core.Numerics; +using SharpEngine.IO.Extensions; + +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"), + Defaults.VertexShader, + Defaults.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..b2d0e2f --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Iron.cs @@ -0,0 +1,17 @@ +using SharpEngine.Core.Defaults; +using SharpEngine.Core.Numerics; +using SharpEngine.IO.Extensions; + +namespace Minecraft.Terrain.Block; + +internal class Iron : BlockBase +{ + public Iron(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Defaults.VertexShader, Defaults.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..35b6fef --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Leaf.cs @@ -0,0 +1,17 @@ +using SharpEngine.Core.Defaults; +using SharpEngine.Core.Numerics; +using SharpEngine.IO.Extensions; + +namespace Minecraft.Terrain.Block; + +internal class Leaf : BlockBase +{ + public Leaf(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Defaults.VertexShader, Defaults.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/Terrain/Block/Stone.cs b/Examples/MinecraftClone/Terrain/Block/Stone.cs new file mode 100644 index 0000000..7b46d2b --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Stone.cs @@ -0,0 +1,28 @@ +using SharpEngine.Core.Defaults; +using SharpEngine.Core.Numerics; +using SharpEngine.IO.Extensions; + +namespace Minecraft.Terrain.Block; + +internal class Stone : BlockBase +{ + // TODO: #91 Create record for textures files for less parameters + // TODO: #91 Create const files for these file strings + + /// + /// Initializes a new block. + /// + /// The position where the block is created. + /// The name of the object in the scene. + public Stone(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Defaults.VertexShader, Defaults.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.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..a35cb23 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/Block/Wood.cs @@ -0,0 +1,17 @@ +using SharpEngine.Core.Defaults; +using SharpEngine.Core.Numerics; +using SharpEngine.IO.Extensions; + +namespace Minecraft.Terrain.Block; + +internal class Wood : BlockBase +{ + public Wood(Vector3 position, string name) + : base(position, name, DiffuseMap(), SpecularMap(), Defaults.VertexShader, Defaults.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..c9d37b5 --- /dev/null +++ b/Examples/MinecraftClone/Terrain/TerrainGenerator.cs @@ -0,0 +1,159 @@ +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; +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.Root.AddChild("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..31ec3ad --- /dev/null +++ b/Examples/MinecraftClone/Terrain/TerrainGenerator_Old.cs @@ -0,0 +1,146 @@ +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; + +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.Root.AddChild("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/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..36a530e 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,10 @@ 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.Initialize(); window.Run(); } } @@ -30,9 +29,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 61% rename from Engine/Examples/MultipleWindows/Program.cs rename to Examples/MultipleWindows/Program.cs index 788b808..bfb5799 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(_cancellationTokenSource); + + 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); window.Initialize(); return window; @@ -116,15 +117,16 @@ private static SharpEngine.Core.Windowing.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.InputManager.OnButtonMouseDown += (mouse, button) => Mouse_Click(mouse, button, (SharpEngine.Core.Numerics.Vector2)mouse.Position); + + if (window.InputManager.Context is not null) + _inputContexts.Add(window.InputManager.Context); + _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, SharpEngine.Core.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 75% rename from Engine/Examples/Tutorial 4.1 - Model Loading/Program.cs rename to Examples/Tutorial 4.1 - Model Loading/Program.cs index 1bffe63..a9b74b2 100644 --- a/Engine/Examples/Tutorial 4.1 - Model Loading/Program.cs +++ b/Examples/Tutorial 4.1 - Model Loading/Program.cs @@ -1,6 +1,5 @@ -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; @@ -12,19 +11,25 @@ using System; using System.Collections.Generic; using System.Numerics; +using SharpEngine.IO.Extensions; +using SharpEngine.Core.Defaults; 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++) { @@ -74,17 +82,21 @@ 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"), Defaults.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) { + if (primaryKeyboard == null) + throw new NullReferenceException("Primary keyboard is not initialized."); + var moveSpeed = 2.5f * (float) deltaTime; //Move forwards @@ -106,6 +118,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 +139,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 +175,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 +210,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/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 new file mode 100644 index 0000000..9f2f669 --- /dev/null +++ b/Examples/Tutorial 4.1 - Model Loading/Tutorial 4.1 - Model Loading.csproj @@ -0,0 +1,30 @@ + + + + Exe + $(DotNetTargetFramework) + Tutorial + + + + + + + + + + + Always + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + \ No newline at end of file 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/README.md b/README.md index 0d657e1..8f9cdfe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@

- + + + + The SharpEngine logo. + SharpEngine

@@ -7,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) + + +[Documentation](https://docs.sharpengine.net/) +
## About @@ -91,17 +100,7 @@ The launcher is an interface for easily accessing your projects and managing the ## Roadmap and plans -The engine is still in it's infant phase where we are just barely hitting all required components working so that we can say the MVP version is done. The current plan is just to use the issues board and execute on that using the "waterfall" method where stuff is just thrown into the engine. - -The long-term plan is to all the crucial functionalities we are still missing like: -- [#3](https://github.com/Av3boy/SharpEngine/issues/3) & [#2](https://github.com/Av3boy/SharpEngine/issues/2) Loading meshes from files -- [#40](https://github.com/Av3boy/SharpEngine/issues/40) UI -- [#17](https://github.com/Av3boy/SharpEngine/issues/17) Audio -- [#15](https://github.com/Av3boy/SharpEngine/issues/15) Text renderer -- [#13](https://github.com/Av3boy/SharpEngine/issues/13) Ray tracing -- [#12](https://github.com/Av3boy/SharpEngine/issues/12) Particle system - -This is just a few among many known issues / missing features from the engine and these will be implemented whenever time permits and we apologize for any inconvenience for now :'D +Currently the issues view in this repo is a shared place for all user reported issues and features planned to be added into the engine. For more information about the future of the engine, see [A Sharp Plan](https://github.com/users/Av3boy/projects/5). 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 diff --git a/SharpEngine.Core.Audio/Audio.cs b/SharpEngine.Core.Audio/Audio.cs new file mode 100644 index 0000000..e3a9cad --- /dev/null +++ b/SharpEngine.Core.Audio/Audio.cs @@ -0,0 +1,52 @@ +using SharpEngine.Core.Audio.Players; + +namespace SharpEngine.Core.Audio; + +/// +/// Handles Audio related operations. +/// +public static class Audio +{ + private static AudioPlayerBase? audioPlayer = null; + + /// + /// Plays an audio file if it is in the WAV format. + /// + /// Specifies the location of the audio file to be played. + /// Thrown when the file format is not supported, such as when it is not a WAV file. + public static void Play(string filePath) + { + switch (Path.GetExtension(filePath)) + { + case ".wav": + 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/Engine/SharpEngine.Core/Audio/AudioBuffer.cs b/SharpEngine.Core.Audio/AudioBuffer.cs similarity index 92% rename from Engine/SharpEngine.Core/Audio/AudioBuffer.cs rename to SharpEngine.Core.Audio/AudioBuffer.cs index 506f426..899651c 100644 --- a/Engine/SharpEngine.Core/Audio/AudioBuffer.cs +++ b/SharpEngine.Core.Audio/AudioBuffer.cs @@ -1,5 +1,4 @@ using Silk.NET.OpenAL; -using System; namespace SharpEngine.Core.Audio; @@ -29,7 +28,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 { @@ -39,5 +38,4 @@ public void LoadData(ReadOnlySpan data, WavData wavData) } } } - } \ No newline at end of file diff --git a/Engine/SharpEngine.Core/Audio/WavData.cs b/SharpEngine.Core.Audio/AudioData.cs similarity index 90% rename from Engine/SharpEngine.Core/Audio/WavData.cs rename to SharpEngine.Core.Audio/AudioData.cs index 80ac937..fa81d42 100644 --- a/Engine/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/Engine/SharpEngine.Core/Audio/AudioDevice.cs b/SharpEngine.Core.Audio/AudioDevice.cs similarity index 79% rename from Engine/SharpEngine.Core/Audio/AudioDevice.cs rename to SharpEngine.Core.Audio/AudioDevice.cs index aa0560d..d5429bc 100644 --- a/Engine/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/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 65% rename from Engine/SharpEngine.Core/Audio/AudioPlayerBase.cs rename to SharpEngine.Core.Audio/AudioPlayerBase.cs index 5778672..b226019 100644 --- a/Engine/SharpEngine.Core/Audio/AudioPlayerBase.cs +++ b/SharpEngine.Core.Audio/AudioPlayerBase.cs @@ -1,7 +1,5 @@ using SharpEngine.Core.Scenes; - -using System; -using System.IO; +using Silk.NET.OpenAL; namespace SharpEngine.Core.Audio; @@ -17,7 +15,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 +28,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 +60,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() + { + // 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 Stop() => AudioSource.Stop(); + public virtual void Resume() => AudioSource.Resume(); } 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 59% rename from Engine/SharpEngine.Core/Audio/AudioSource.cs rename to SharpEngine.Core.Audio/AudioSource.cs index 9e59179..6c11cd4 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; /// @@ -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/Players/Mp3Player.cs b/SharpEngine.Core.Audio/Players/Mp3Player.cs new file mode 100644 index 0000000..5d9aaea --- /dev/null +++ b/SharpEngine.Core.Audio/Players/Mp3Player.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.Logging; +using NLayer; +using Silk.NET.OpenAL; + +namespace SharpEngine.Core.Audio.Players; + +internal class Mp3Player : AudioPlayerBase +{ + private static readonly ILogger Logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(); + + /// + protected override string FileExtension => AudioFileExtensions.Mp3Extension; + + /// + /// Initializes a new instance of + /// + 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/Engine/SharpEngine.Core/Audio/WavPlayer.cs b/SharpEngine.Core.Audio/Players/WavPlayer.cs similarity index 89% rename from Engine/SharpEngine.Core/Audio/WavPlayer.cs rename to SharpEngine.Core.Audio/Players/WavPlayer.cs index a686bea..1d518b8 100644 --- a/Engine/SharpEngine.Core/Audio/WavPlayer.cs +++ b/SharpEngine.Core.Audio/Players/WavPlayer.cs @@ -1,19 +1,19 @@ -using SharpEngine.Shared; +using Microsoft.Extensions.Logging; using Silk.NET.OpenAL; -using System; using System.Buffers.Binary; -using System.IO; -namespace SharpEngine.Core.Audio; +namespace SharpEngine.Core.Audio.Players; /// /// Represents a player for .wav files. /// 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 { @@ -41,7 +41,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 +49,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; } @@ -88,7 +88,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.Audio/SharpEngine.Core.Audio.csproj b/SharpEngine.Core.Audio/SharpEngine.Core.Audio.csproj new file mode 100644 index 0000000..947b969 --- /dev/null +++ b/SharpEngine.Core.Audio/SharpEngine.Core.Audio.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + true + + + + + + + + + + + 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 63% rename from Engine/SharpEngine.Core.Components/Properties/Material.cs rename to SharpEngine.Core.Components/Properties/Material.cs index 07c21be..e22282a 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Material.cs +++ b/SharpEngine.Core.Components/Properties/Material.cs @@ -1,10 +1,32 @@ -using Silk.NET.OpenGL; +using SharpEngine.Core.Shaders; +using Silk.NET.OpenGL; using System.Numerics; using Shader = SharpEngine.Core.Shaders.Shader; using Texture = SharpEngine.Core.Components.Properties.Textures.Texture; namespace SharpEngine.Core.Components.Properties; +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. /// @@ -13,49 +35,39 @@ 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(Texture diffuseMap, Texture? specularMap = null) + 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.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; } @@ -82,22 +94,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. @@ -107,21 +119,21 @@ public void SetUniformValues(Shader shader) { // TODO: Get all shader uniforms and set their values automatically - DiffuseMap.Use(TextureUnit.Texture0); - shader.SetInt("material.diffuse", DIFFUSE_UNIT); - - if (UseSpecularMap) + if (DiffuseMap is not null) { - SpecularMap.Use(TextureUnit.Texture1); - shader.SetInt("material.specular", SPECULAR_UNIT); - shader.SetVector3("material.specular", Specular); - shader.SetFloat("material.shininess", Shininess); + DiffuseMap.Texture?.Use(TextureUnit.Texture0); + shader.SetTextureUnit("material.diffuse", TextureUnitIndex.DIFFUSE_UNIT); } - else + + if (SpecularMap is not null) { - shader.SetInt("material.specular", 0); - shader.SetVector3("material.specular", Vector3.Zero); - shader.SetFloat("material.shininess", 0); + SpecularMap.Texture?.Use(TextureUnit.Texture1); + shader.SetTextureUnit("material.specular", TextureUnitIndex.SPECULAR_UNIT); } + else + shader.SetTextureUnit("material.specular", TextureUnitIndex.DIFFUSE_UNIT); + + shader.SetFloat("material.shininess", Shininess); + } -} +} \ No newline at end of file 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 75% rename from Engine/SharpEngine.Core.Components/Properties/Meshes/Mesh.cs rename to SharpEngine.Core.Components/Properties/Meshes/Mesh.cs index e6f66fc..2d5a8c5 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. @@ -133,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. /// @@ -143,9 +155,25 @@ 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). /// + [MemberNotNull(nameof(EBO), nameof(VBO), nameof(VAO))] public void SetupMesh() { EBO = new BufferObject(GL, Indices, BufferTargetARB.ElementArrayBuffer); @@ -167,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/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..2947ec0 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Meshes/Model.cs @@ -0,0 +1,305 @@ +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; } = []; + + 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) : this(gl, path, Enumerable.Empty()) + { + _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; + + if (meshes.Any()) + Meshes = meshes.Select(ProcessMesh).ToList(); + + Name = meshes.FirstOrDefault()?.Name ?? System.IO.Path.GetFileNameWithoutExtension(path) ?? "UnnamedModel"; + } + + /// + public void Dispose() + { + foreach (var mesh in Meshes) + mesh.Dispose(); + + GC.SuppressFinalize(this); + } + + /// + /// Processes the provided mesh in place, updating its vertex data, materials, textures, and GPU buffers so it is ready for rendering. + /// + /// The mesh to process. Must not be null. + /// + /// 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 is null. + public Mesh ProcessMesh(Mesh mesh) + { + ArgumentNullException.ThrowIfNull(mesh); + + if (mesh.Vertices.Length > 0) + { + 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(); + + 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) + { + 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.DiffuseMap?.Path)) + return null; + + var diffuseTexture = new EngineTexture(_gl, ResolveAssetPath(definition.DiffuseMap.Path), EngineTextureType.Diffuse); + EngineTexture? specularTexture = null; + + if (!string.IsNullOrWhiteSpace(definition.SpecularMap?.Path)) + specularTexture = new EngineTexture(_gl, ResolveAssetPath(definition.SpecularMap.Path), 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, + DiffuseMap = definition.DiffuseMap, + SpecularMap = definition.SpecularMap, + 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?.Texture is not null) + diffuseTexture = new EngineTexture(_gl, material.DiffuseMap.Path, material.DiffuseMap.Texture.Type); + + EngineTexture? specularTexture = null; + 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) + { + Name = material.Name, + AmbientColor = material.AmbientColor, + DiffuseColor = material.DiffuseColor, + SpecularColor = material.SpecularColor, + SpecularCoefficient = material.SpecularCoefficient, + Transparency = material.Transparency, + IlluminationModel = material.IlluminationModel, + DiffuseMap = material.DiffuseMap, + SpecularMap = material.SpecularMap, + 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.SpecularMap?.Texture is not null) + materials.Add(material.SpecularMap.Texture!); + + if (material.DiffuseMap?.Texture is not null) + materials.Add(material.DiffuseMap.Texture!); + + 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); + } + + /// + /// 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(); + + 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/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/Engine/SharpEngine.Core.Components/Properties/Shaders/Shader.cs b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs similarity index 60% rename from Engine/SharpEngine.Core.Components/Properties/Shaders/Shader.cs rename to SharpEngine.Core.Components/Properties/Shaders/Shader.cs index 13aea94..8bee87e 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Shaders/Shader.cs +++ b/SharpEngine.Core.Components/Properties/Shaders/Shader.cs @@ -1,17 +1,14 @@ -using SharpEngine.Shared; +using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; using Silk.NET.OpenGL; -using System.Diagnostics.CodeAnalysis; 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,10 +18,6 @@ public partial class Shader : IDisposable /// Gets or sets the path to the fragment shader file. public string FragPath { get; set; } - private Dictionary _uniformLocations = []; - private bool disposedValue; - private readonly GL _gl; - /// /// Initializes a new instance of . /// @@ -32,14 +25,13 @@ 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) + public Shader(GL gl, string vertPath, string fragPath, string name) : base(gl) { Name = name; - _gl = gl; // There are several different types of shaders, but the only two you need for basic rendering are the vertex and fragment shaders. // The vertex shader is responsible for moving around vertices, and uploading that data to the fragment shader. @@ -48,36 +40,14 @@ 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; - } - - /// - protected virtual void Dispose(bool disposing) - { - if (disposedValue) - return; - - if (disposing) - { - _gl.DeleteProgram(Handle); - Handle = 0; - _uniformLocations.Clear(); - } - - disposedValue = true; - } - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); + Initialize(vertPath, fragPath); } } 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/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs new file mode 100644 index 0000000..eaa5390 --- /dev/null +++ b/SharpEngine.Core.Components/Properties/Shaders/ShaderExtensions.cs @@ -0,0 +1,105 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Components.Properties; +using Silk.NET.OpenGL; +using System.Numerics; +using System.Text.RegularExpressions; + +namespace SharpEngine.Core.Shaders; + +/// +/// Represents a shader program in the engine. +/// +public partial class Shader +{ + // Uniform setters + // Uniforms are variables that can be set by user code, instead of reading them from the VBO. + // You use VBOs for vertex-related data, and uniforms for almost everything else. + + // Setting a uniform is almost always the exact same, so I'll explain it here once, instead of in every method: + // 1. Bind the program you want to set the uniform on + // 2. Get a handle to the location of the uniform with GL.GetUniformLocation. + // 3. Use the appropriate GL.Uniform* function to set the uniform. + + private bool TrySetUniform(string uniformName, T data, Action setter) + { + if (!_uniformLocations.TryGetValue(uniformName, out int location)) + { + _logger.LogInformation("Uniform '{UniformName}' not found in shader '{ShaderName}'.", uniformName, Name); + return false; + } + + // TODO: #95 The GL.UseProgram should be not be called here. Rather the renderer should call it once before rendering. + GL.UseProgram(Handle); + setter(location, data); + + return true; + } + + /// + /// Set a uniform int on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetInt(string name, int data) + => TrySetUniform(name, data, GL.Uniform1); + + public bool SetTextureUnit(string name, TextureUnitIndex textureUnit) + => TrySetUniform(name, (int)textureUnit, GL.Uniform1); + + /// + /// Set a uniform float on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetFloat(string name, float data) + => TrySetUniform(name, data, GL.Uniform1); + + /// + /// Set a uniform Matrix4 on this shader + /// + /// The name of the uniform. + /// The data to set. + /// Determines whether or not the matrix should be transposed. Defaults to . + /// + /// + /// The matrix is transposed before being sent to the shader unless is set to . + /// + /// + public bool SetMatrix4(string name, Matrix4x4 data, bool transpose = true) + => TrySetUniform(name, data, (uniform, d) => GL.UniformMatrix4(uniform, transpose, d.ToSpan())); + + /// + /// Set a uniform Vector2 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetVector2(string name, SharpEngine.Core.Numerics.Vector2 data) + => TrySetUniform(name, (Vector2)data, GL.Uniform2); + + /// + /// Set a uniform Vector2 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetVector2(string name, Vector2 data) + => TrySetUniform(name, data, GL.Uniform2); + + /// + /// Set a uniform Vector3 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetVector3(string name, Vector3 data) + => TrySetUniform(name, data, GL.Uniform3); + + public bool SetVector3(string name, SharpEngine.Core.Numerics.Vector3 data) + => TrySetUniform(name, (Vector3)data, GL.Uniform3); + + /// + /// Set a uniform Vector3 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public bool SetVector4(string name, Vector4 data) + => TrySetUniform(name, data, GL.Uniform4); +} diff --git a/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/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/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/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 78% rename from Engine/SharpEngine.Core.Components/Properties/Transform2D.cs rename to SharpEngine.Core.Components/Properties/Transform2D.cs index 8a44d56..48c9960 100644 --- a/Engine/SharpEngine.Core.Components/Properties/Transform2D.cs +++ b/SharpEngine.Core.Components/Properties/Transform2D.cs @@ -19,16 +19,19 @@ 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 }; /// /// 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 new file mode 100644 index 0000000..77bb1c8 --- /dev/null +++ b/SharpEngine.Core.Components/SharpEngine.Core.Components.csproj @@ -0,0 +1,36 @@ + + + + $(DotNetTargetFramework) + enable + enable + + + + $(GenerateNuGetPackages) + true + + + + + + + + + + + + + + + + + <_Parameter1>SharpEngine.Core.Components.Tests + + + + + + + + diff --git a/SharpEngine.Core.Defaults/Defaults.cs b/SharpEngine.Core.Defaults/Defaults.cs new file mode 100644 index 0000000..09b291a --- /dev/null +++ b/SharpEngine.Core.Defaults/Defaults.cs @@ -0,0 +1,38 @@ +using SharpEngine.IO.Extensions; + +namespace SharpEngine.Core.Defaults; + +/// +/// Contains default resources used by the engine. +/// +public static class Defaults +{ + /// Gets the path to the debug texture. + public static string DebugTexture => PathExtensions.GetAssemblyPath("_Resources\\Textures\\debug.JPG"); + + /// Gets the path to the default vertex shader. + public static string VertexShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\shader.vert"); + + /// Gets the path to the lighting fragment shader. + public static string FragmentShader => PathExtensions.GetAssemblyPath("_Resources\\Shaders\\lighting.frag"); + + /// 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"); + + /// 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/App.cs b/SharpEngine.Core.DependencyInjection/App.cs new file mode 100644 index 0000000..bfd9160 --- /dev/null +++ b/SharpEngine.Core.DependencyInjection/App.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.Windowing; + +using System; +using System.Linq; +using System.Threading; +using SharpEngine.Core.Handlers; +using SharpEngine.Core.Interfaces; + +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 readonly IConfiguration? _configuration; + private readonly IServiceProvider _serviceProvider; + private Engine _engine; + + /// + /// 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>()!; + + var game = _serviceProvider.GetService(); + if (game is null) + throw new InvalidOperationException("A service for the type 'Game' could not be found. One must be registered before running the application with the dependency Injection apporach."); + + _engine = _serviceProvider.GetRequiredService(); + if (_engine is null) + throw new InvalidOperationException("A service for the type 'Engine' could not be found. One must be registered before running the application with the dependency Injection apporach."); + + try + { + logger.LogInformation("App started. Resolving services."); + + if (cancellationToken.IsCancellationRequested) + return; + + logger.LogInformation("Starting engine handlers."); + _engine.Initialize(); + } + 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/EngineRegistrationBuilder.cs b/SharpEngine.Core.DependencyInjection/EngineRegistrationBuilder.cs new file mode 100644 index 0000000..01c020d --- /dev/null +++ b/SharpEngine.Core.DependencyInjection/EngineRegistrationBuilder.cs @@ -0,0 +1,36 @@ +using Microsoft.Extensions.DependencyInjection; +using SharpEngine.Core.Handlers; +using System; +using System.Collections.Generic; + +namespace SharpEngine.Core.DependencyInjection; + +public sealed class EngineRegistrationBuilder +{ + private readonly IServiceCollection _services; + private readonly Dictionary _handlerRegistrations = []; + + public EngineRegistrationBuilder(IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + _services = services; + } + + public HandlerRegistrationBuilder AddHandler() where THandler : class + { + if (_handlerRegistrations.TryGetValue(typeof(THandler), out var existing)) + return (HandlerRegistrationBuilder)existing; + + _services.AddSingleton(); + + var registration = new HandlerRegistrationBuilder(); + _handlerRegistrations.Add(typeof(THandler), registration); + return registration; + } + + internal void Apply(IServiceProvider serviceProvider, Engine engine) + { + foreach (var registration in _handlerRegistrations.Values) + ((IHandlerRegistration)registration).Apply(serviceProvider, engine); + } +} \ No newline at end of file diff --git a/SharpEngine.Core.DependencyInjection/EngineServiceCollectionExtensions.cs b/SharpEngine.Core.DependencyInjection/EngineServiceCollectionExtensions.cs new file mode 100644 index 0000000..2587d8b --- /dev/null +++ b/SharpEngine.Core.DependencyInjection/EngineServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace SharpEngine.Core.DependencyInjection; + +public static class EngineServiceCollectionExtensions +{ + public static IServiceCollection AddEngine(this IServiceCollection services, Action configure) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); + + var builder = new EngineRegistrationBuilder(services); + configure(builder); + + services.AddSingleton(serviceProvider => + { + var engine = new Engine(); + builder.Apply(serviceProvider, engine); + return engine; + }); + + return services; + } +} diff --git a/SharpEngine.Core.DependencyInjection/HandlerRegistrationBuilder.cs b/SharpEngine.Core.DependencyInjection/HandlerRegistrationBuilder.cs new file mode 100644 index 0000000..0d4fabc --- /dev/null +++ b/SharpEngine.Core.DependencyInjection/HandlerRegistrationBuilder.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.DependencyInjection; +using SharpEngine.Core.Handlers; +using System; +using System.Collections.Generic; + +namespace SharpEngine.Core.DependencyInjection; + +public interface IHandlerRegistration +{ + void Apply(IServiceProvider serviceProvider, Engine engine); +} + +public sealed class HandlerRegistrationBuilder : IHandlerRegistration where THandler : class +{ + private readonly List> _configurations = []; + + public HandlerRegistrationBuilder Configure(Action configure) + { + ArgumentNullException.ThrowIfNull(configure); + + _configurations.Add(configure); + return this; + } + + public void Apply(IServiceProvider serviceProvider, Engine engine) + { + var handler = serviceProvider.GetRequiredService(); + + if (handler is not EngineHandler engineHandler) + throw new InvalidOperationException($"The service '{typeof(THandler).Name}' must derive from {nameof(EngineHandler)}."); + + foreach (var configure in _configurations) + configure(serviceProvider, handler, engine); + + engine.ServicesManager.RegisterHandler(engineHandler); + } +} \ No newline at end of file diff --git a/SharpEngine.Core.DependencyInjection/SharpEngine.Core.DependencyInjection.csproj b/SharpEngine.Core.DependencyInjection/SharpEngine.Core.DependencyInjection.csproj new file mode 100644 index 0000000..7933c89 --- /dev/null +++ b/SharpEngine.Core.DependencyInjection/SharpEngine.Core.DependencyInjection.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/SharpEngine.Core.DependencyInjection/WindowServiceCollectionExtensions.cs b/SharpEngine.Core.DependencyInjection/WindowServiceCollectionExtensions.cs new file mode 100644 index 0000000..6de501a --- /dev/null +++ b/SharpEngine.Core.DependencyInjection/WindowServiceCollectionExtensions.cs @@ -0,0 +1,80 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Renderers; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Windowing; +using System; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Handlers; + +namespace SharpEngine.Core.DependencyInjection; + +/// +/// Provides window-related dependency injection registrations. +/// +public static class WindowServiceCollectionExtensions +{ + /// + /// Registers a window factory and an optional configuration callback. + /// + /// + /// When creating multiple windows the last one created will act as the default window. + /// To specify a different default window, try tweaking the parameter. + /// + /// 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 HandlerRegistrationBuilder AddWindow( + this HandlerRegistrationBuilder handler, + Func? factory = null, + Action? configure = null, + string? name = null, + bool isDefaultWindow = true) + { + ArgumentNullException.ThrowIfNull(handler); + + factory ??= CreateWindow; + + configure ??= ConfigureWindow; + + handler.Configure((serviceProvider, windowHandler, engine) => + { + var window = factory(serviceProvider); + configure(serviceProvider, window); + + windowHandler.AddWindow(window, isDefaultWindow); + }); + + return handler; + } + + private static void ConfigureWindow(IServiceProvider serviceProvider, Window window) + { + 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.OnAfterRender += game.OnAfterRender; + + game.Window = window; + } + + private static Window CreateWindow(IServiceProvider serviceProvider) + { + var game = serviceProvider.GetRequiredService(); + var scene = serviceProvider.GetRequiredService(); + var windowLogger = serviceProvider.GetRequiredService>(); + var renderers = serviceProvider.GetServices(); + + var window = new Window(game.Camera, scene, game.Camera.Settings, windowLogger, renderers); + + return window; + } +} 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/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/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..91ad8e7 --- /dev/null +++ b/SharpEngine.Core.Numerics/Noise/PerlinNoiseGenerator.cs @@ -0,0 +1,192 @@ +using MathEx = SharpEngine.Core.Numerics.MathExtensions; +using SysMath = System.Math; + +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 = SysMath.Max(1, value); + } + + /// + public float Persistence + { + get => _persistence; + set => _persistence = SysMath.Max(0f, value); + } + + /// + public float Lacunarity + { + get => _lacunarity; + set => _lacunarity = SysMath.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 SysMath.Clamp(result, 0f, 1f); + } +} \ No newline at end of file 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/SharpEngine.Core.Numerics/Vector2.cs b/SharpEngine.Core.Numerics/Vector2.cs new file mode 100644 index 0000000..89d9d6b --- /dev/null +++ b/SharpEngine.Core.Numerics/Vector2.cs @@ -0,0 +1,418 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/Vector2.cs + +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; +using Matrix3x2 = System.Numerics.Matrix3x2; +using Matrix4x4 = System.Numerics.Matrix4x4; +using Quaternion = System.Numerics.Quaternion; +using Vector = System.Numerics.Vector; + +namespace SharpEngine.Core.Numerics; + +/// +/// A structure encapsulating two single precision floating point values and provides hardware accelerated methods. +/// +public partial struct Vector2 : IEquatable, IFormattable +{ + #region Public Static Properties + + /// Returns the vector (0,0). + public static Vector2 Zero => new(); + + /// Returns the vector (1,1). + public static Vector2 One => new(1.0f, 1.0f); + + /// Returns the vector (1,0). + public static Vector2 UnitX => new(1.0f, 0.0f); + + /// Returns the vector (0,1). + public static Vector2 UnitY => new(0.0f, 1.0f); + + #endregion Public Static Properties + + #region Public instance methods + + /// + /// Returns the hash code for this instance. + /// + /// The hash code. + public override readonly int GetHashCode() + => HashCodeHelper.CombineHashCodes(X.GetHashCode(), Y.GetHashCode()); + + /// + /// Returns a boolean indicating whether the given Object is equal to this Vector2 instance. + /// + /// The Object to compare against. + /// True if the Object is equal to this Vector2; False otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj is not Vector2 vec2) + return false; + + return Equals(vec2); + } + + /// + /// Returns a String representing this Vector2 instance. + /// + /// The string representation. + public override readonly string ToString() => ToString("G", CultureInfo.CurrentCulture); + + /// + /// Returns a String representing this Vector2 instance, using the specified format to format individual elements. + /// + /// The format of individual elements. + /// The string representation. + public readonly string ToString(string format) => ToString(format, CultureInfo.CurrentCulture); + + /// + /// Returns a String representing this Vector2 instance, using the specified format to format individual elements + /// and the given IFormatProvider. + /// + /// The format of individual elements. + /// The format provider to use when formatting elements. + /// The string representation. + public readonly string ToString(string? format, IFormatProvider? formatProvider) + { + var sb = new StringBuilder(); + string separator = NumberFormatInfo.GetInstance(formatProvider).NumberGroupSeparator; + sb.Append('<'); + sb.Append(X.ToString(format, formatProvider)); + sb.Append(separator); + sb.Append(' '); + sb.Append(Y.ToString(format, formatProvider)); + sb.Append('>'); + return sb.ToString(); + } + + /// + /// Returns the length of the vector. + /// + /// The vector's length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float Length() + { + if (Vector.IsHardwareAccelerated) + { + float ls = Vector2.Dot(this, this); + return (float)MathF.Sqrt(ls); + } + else + { + float ls = (X * X) + (Y * Y); + return MathF.Sqrt(ls); + } + } + + /// + /// Returns the length of the vector squared. This operation is cheaper than Length(). + /// + /// The vector's length squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly float LengthSquared() + { + if (Vector.IsHardwareAccelerated) + return Vector2.Dot(this, this); + + else + return (X * X) + (Y * Y); + } + #endregion Public Instance Methods + + #region Public Static Methods + /// + /// Returns the Euclidean distance between the two given points. + /// + /// The first point. + /// The second point. + /// The distance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Distance(Vector2 value1, Vector2 value2) + { + if (Vector.IsHardwareAccelerated) + { + var difference = value1 - value2; + float ls = Vector2.Dot(difference, difference); + return (float)System.MathF.Sqrt(ls); + } + else + { + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + + float ls = (dx * dx) + (dy * dy); + + return MathF.Sqrt(ls); + } + } + + /// + /// Returns the Euclidean distance squared between the two given points. + /// + /// The first point. + /// The second point. + /// The distance squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DistanceSquared(Vector2 value1, Vector2 value2) + { + if (Vector.IsHardwareAccelerated) + { + var difference = value1 - value2; + return Vector2.Dot(difference, difference); + } + else + { + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + + return (dx * dx) + (dy * dy); + } + } + + /// + /// Returns a vector with the same direction as the given vector, but with a length of 1. + /// + /// The vector to normalize. + /// The normalized vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Normalize(Vector2 value) + { + if (Vector.IsHardwareAccelerated) + { + float length = value.Length(); + return value / length; + } + else + { + float ls = (value.X * value.X) + (value.Y * value.Y); + float invNorm = 1.0f / MathF.Sqrt(ls); + + return new Vector2( + value.X * invNorm, + value.Y * invNorm); + } + } + + /// + /// Returns the reflection of a vector off a surface that has the specified normal. + /// + /// The source vector. + /// The normal of the surface being reflected off. + /// The reflected vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Reflect(Vector2 vector, Vector2 normal) + { + if (Vector.IsHardwareAccelerated) + { + float dot = Vector2.Dot(vector, normal); + return vector - (2 * dot * normal); + } + else + { + float dot = (vector.X * normal.X) + (vector.Y * normal.Y); + + return new Vector2( + vector.X - (2.0f * dot * normal.X), + vector.Y - (2.0f * dot * normal.Y)); + } + } + + /// + /// Restricts a vector between a min and max value. + /// + /// The source vector. + /// The minimum value. + /// The maximum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Clamp(Vector2 value1, Vector2 min, Vector2 max) + { + // This compare order is very important!!! + // We must follow HLSL behavior in the case user specified min value is bigger than max value. + float x = value1.X; + x = (x > max.X) ? max.X : x; + x = (x < min.X) ? min.X : x; + + float y = value1.Y; + y = (y > max.Y) ? max.Y : y; + y = (y < min.Y) ? min.Y : y; + + return new Vector2(x, y); + } + + /// + /// Linearly interpolates between two vectors based on the given weighting. + /// + /// The first source vector. + /// The second source vector. + /// Value between 0 and 1 indicating the weight of the second source vector. + /// The interpolated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Lerp(Vector2 value1, Vector2 value2, float amount) + { + return new Vector2( + value1.X + ((value2.X - value1.X) * amount), + value1.Y + ((value2.Y - value1.Y) * amount)); + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 position, Matrix3x2 matrix) + { + return new Vector2( + (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M31, + (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M32); + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 position, Matrix4x4 matrix) + { + return new Vector2( + (position.X * matrix.M11) + (position.Y * matrix.M21) + matrix.M41, + (position.X * matrix.M12) + (position.Y * matrix.M22) + matrix.M42); + } + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 TransformNormal(Vector2 normal, Matrix3x2 matrix) + { + return new Vector2( + (normal.X * matrix.M11) + (normal.Y * matrix.M21), + (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + } + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 TransformNormal(Vector2 normal, Matrix4x4 matrix) + { + return new Vector2( + (normal.X * matrix.M11) + (normal.Y * matrix.M21), + (normal.X * matrix.M12) + (normal.Y * matrix.M22)); + } + + /// + /// Transforms a vector by the given Quaternion rotation value. + /// + /// The source vector to be rotated. + /// The rotation to apply. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 value, Quaternion rotation) + { + float x2 = rotation.X + rotation.X; + float y2 = rotation.Y + rotation.Y; + float z2 = rotation.Z + rotation.Z; + + float wz2 = rotation.W * z2; + float xx2 = rotation.X * x2; + float xy2 = rotation.X * y2; + float yy2 = rotation.Y * y2; + float zz2 = rotation.Z * z2; + + return new Vector2( + (value.X * (1.0f - yy2 - zz2)) + (value.Y * (xy2 - wz2)), + (value.X * (xy2 + wz2)) + (value.Y * (1.0f - xx2 - zz2))); + } + #endregion Public Static Methods + + #region Public operator methods + // all the below methods should be inlined as they are + // implemented over JIT intrinsics + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Add(Vector2 left, Vector2 right) => left + right; + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Subtract(Vector2 left, Vector2 right) => left - right; + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(Vector2 left, Vector2 right) => left * right; + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(Vector2 left, float right) => left * right; + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(float left, Vector2 right) => left * right; + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Divide(Vector2 left, Vector2 right) => left / right; + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Divide(Vector2 left, float divisor) => left / divisor; + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Negate(Vector2 value) => -value; + + #endregion Public operator methods +} \ No newline at end of file diff --git a/Engine/SharpEngine.Core.Numerics/Vector2_Intrinsics.cs b/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/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs b/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs new file mode 100644 index 0000000..a495966 --- /dev/null +++ b/SharpEngine.Core.Numerics/Vector3_Intrinsics.cs @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.CompilerServices; +using SysMath = System.Math; + +namespace SharpEngine.Core.Numerics; + +// This file contains the definitions for all of the JIT intrinsic methods and properties that are recognized by the current x64 JIT compiler. +// The implementation defined here is used in any circumstance where the JIT fails to recognize these members as intrinsic. +// The JIT recognizes these methods and properties by name and signature: if either is changed, the JIT will no longer recognize the member. +// Some methods declared here are not strictly intrinsic, but delegate to an intrinsic method. For example, only one overload of CopyTo() +// is actually recognized by the JIT, but both are here for simplicity. + +public partial struct Vector3 : IVector +{ + /// + public float X { get; set; } + + /// + public float Y { get; set; } + + /// + public float Z { get; set; } + + #region Constructors + + /// + /// Constructs a vector whose elements are all the single specified value. + /// + /// The element to fill the vector with. + [JitIntrinsic] + public Vector3(float value) : this(value, value, value) { } + + /// + /// Constructs a Vector3 from the given Vector2 and a third value. + /// + /// The Vector to extract X and Y components from. + /// The Z component. + public Vector3(Vector2 value, float z) : this(value.X, value.Y, z) { } + + /// + /// Constructs a vector with the given individual elements. + /// + /// The X component. + /// The Y component. + /// The Z component. + [JitIntrinsic] + public Vector3(float x, float y, float z) + { + X = x; + Y = y; + Z = z; + } + + #endregion Constructors + + #region Public Instance Methods + + /// + /// Copies the contents of the vector into the given array. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void CopyTo(float[] array) => CopyTo(array, 0); + + /// + /// Copies the contents of the vector into the given array, starting from index. + /// + /// If array is null. + /// If array is multidimensional. + /// If index is greater than end of the array or index is less than zero. + /// If number of elements in source vector is greater than those available in destination array. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly void CopyTo(float[] array, int index) + { + // Match the JIT's exception type here. For perf, a NullReference is thrown instead of an ArgumentNull. + ArgumentNullException.ThrowIfNull(array); + + if (index < 0 || index >= array.Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + if ((array.Length - index) < 3) + throw new ArgumentException("", nameof(index)); + + array[index] = X; + array[index + 1] = Y; + array[index + 2] = Z; + } + + /// + /// Returns a boolean indicating whether the given Vector3 is equal to this Vector3 instance. + /// + /// The Vector3 to compare this instance to. + /// True if the other Vector3 is approximately equal to this instance; False otherwise. + [JitIntrinsic] + public readonly bool Equals(Vector3 other) + => MathF.Abs(X - other.X) < Float.Tolerance && + MathF.Abs(Y - other.Y) < Float.Tolerance && + MathF.Abs(Z - other.Z) < Float.Tolerance; + + #endregion Public Instance Methods + + #region Public Static Methods + + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Dot(Vector3 vector1, Vector3 vector2) + => (vector1.X * vector2.X) + + (vector1.Y * vector2.Y) + + (vector1.Z * vector2.Z); + + /// + /// Returns a vector whose elements are the minimum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The minimized vector. + [JitIntrinsic] + public static Vector3 Min(Vector3 value1, Vector3 value2) + => new((value1.X < value2.X) ? value1.X : value2.X, + (value1.Y < value2.Y) ? value1.Y : value2.Y, + (value1.Z < value2.Z) ? value1.Z : value2.Z); + + /// + /// Returns a vector whose elements are the maximum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The maximized vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Max(Vector3 value1, Vector3 value2) + => new((value1.X > value2.X) ? value1.X : value2.X, + (value1.Y > value2.Y) ? value1.Y : value2.Y, + (value1.Z > value2.Z) ? value1.Z : value2.Z); + + /// + /// Returns a vector whose elements are the absolute values of each of the source vector's elements. + /// + /// The source vector. + /// The absolute value vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Abs(Vector3 value) + => new(MathF.Abs(value.X), + MathF.Abs(value.Y), + MathF.Abs(value.Z)); + + /// + /// Returns a vector whose elements are the square root of each of the source vector's elements. + /// + /// The source vector. + /// The square root vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 SquareRoot(Vector3 value) + => new(MathF.Sqrt(value.X), + MathF.Sqrt(value.Y), + MathF.Sqrt(value.Z)); + + #endregion Public Static Methods + + #region Public Static Operators + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator +(Vector3 left, Vector3 right) + => new(left.X + right.X, + left.Y + right.Y, + left.Z + right.Z); + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator -(Vector3 left, Vector3 right) + => new(left.X - right.X, + left.Y - right.Y, + left.Z - right.Z); + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(Vector3 left, Vector3 right) + => new(left.X * right.X, + left.Y * right.Y, + left.Z * right.Z); + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(Vector3 left, float right) => left * new Vector3(right); + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(float left, Vector3 right) => new Vector3(left) * right; + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator /(Vector3 left, Vector3 right) + => new(left.X / right.X, + left.Y / right.Y, + left.Z / right.Z); + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator /(Vector3 value1, float value2) + { + float invDiv = 1.0f / value2; + + return new Vector3( + value1.X * invDiv, + value1.Y * invDiv, + value1.Z * invDiv); + } + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator -(Vector3 value) => Zero - value; + + /// + /// Returns a boolean indicating whether the two given vectors are equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are equal; False otherwise. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector3 left, Vector3 right) + => 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.ObjLoader/DataStore.cs b/SharpEngine.Core.ObjLoader/DataStore.cs new file mode 100644 index 0000000..1840908 --- /dev/null +++ b/SharpEngine.Core.ObjLoader/DataStore.cs @@ -0,0 +1,114 @@ +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 +{ + /// + /// 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. + /// + /// + /// 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!; + + /// 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; } = []; + + /// 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. + /// + 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); + + /// + 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/SharpEngine.Core.ObjLoader/IMaterialDataStore.cs b/SharpEngine.Core.ObjLoader/IMaterialDataStore.cs new file mode 100644 index 0000000..9424ed5 --- /dev/null +++ b/SharpEngine.Core.ObjLoader/IMaterialDataStore.cs @@ -0,0 +1,21 @@ +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. + void SetMaterial(string materialName); +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/IObjectNameDataStore.cs b/SharpEngine.Core.ObjLoader/IObjectNameDataStore.cs new file mode 100644 index 0000000..44806a0 --- /dev/null +++ b/SharpEngine.Core.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/SharpEngine.Core.ObjLoader/ISmoothingGroupDataStore.cs b/SharpEngine.Core.ObjLoader/ISmoothingGroupDataStore.cs new file mode 100644 index 0000000..8afc13c --- /dev/null +++ b/SharpEngine.Core.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/SharpEngine.Core.ObjLoader/Loaders/IFileManager.cs b/SharpEngine.Core.ObjLoader/Loaders/IFileManager.cs new file mode 100644 index 0000000..21e33eb --- /dev/null +++ b/SharpEngine.Core.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/SharpEngine.Core.ObjLoader/Loaders/LoaderBase.cs b/SharpEngine.Core.ObjLoader/Loaders/LoaderBase.cs new file mode 100644 index 0000000..206c6d5 --- /dev/null +++ b/SharpEngine.Core.ObjLoader/Loaders/LoaderBase.cs @@ -0,0 +1,83 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; +using System.IO; +using System.IO.Abstractions; + +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 + { + private readonly IFileStreamFactory _fileStreamFactory; + private readonly IFileManager _fileManager; + + private readonly ILogger _logger = LoggingExtensions.CreateLogger(); + + /// + /// 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 . + /// + /// + /// I/O exceptions (for example FileNotFoundException, UnauthorizedAccessException, IOException) are propagated to the caller. + /// + /// The path of the file to open and parse. + /// 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); + + while (!lineStreamReader.EndOfStream) + { + var currentLine = lineStreamReader.ReadLine(); + + if (!string.IsNullOrWhiteSpace(currentLine)) + { + if (!ParseLine(currentLine)) + { + _logger.LogError("Failed to parse line: {Line}", currentLine); + // return false; + } + } + } + + return true; + } + + private bool ParseLine(string currentLine) + { + if (currentLine[0] == '#') + return true; + + var fields = currentLine.Trim().Split(null, 2); + var keyword = fields[0].Trim(); + var data = fields[1].Trim(); + + return 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. + /// if the line was parsed successfully; otherwise, . + protected abstract bool ParseLine(string keyword, string data); + } +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs b/SharpEngine.Core.ObjLoader/Loaders/MaterialLoader/IMaterialLibraryLoaderFacade.cs new file mode 100644 index 0000000..5d4cde3 --- /dev/null +++ b/SharpEngine.Core.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/SharpEngine.Core.ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs b/SharpEngine.Core.ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs new file mode 100644 index 0000000..b45b5a0 --- /dev/null +++ b/SharpEngine.Core.ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoader.cs @@ -0,0 +1,114 @@ +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 +{ + // 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 IMaterialDataStore _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. + /// 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; + + 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 = new TextureDto(m)); + AddParseAction("map_Kd", m => _currentMaterial.DiffuseMap = new TextureDto(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 = new TextureDto(m)); + + AddParseAction("map_bump", m => _currentMaterial.BumpMap = new TextureDto(m)); + AddParseAction("bump", m => _currentMaterial.BumpMap = new TextureDto(m)); + + AddParseAction("disp", m => _currentMaterial.DisplacementMap = new TextureDto(m)); + + AddParseAction("decal", m => _currentMaterial.StencilDecalMap = new TextureDto(m)); + } + + private void AddParseAction(string key, Action action) + => _parseActionDictionary.Add(key.ToLowerInvariant(), action); + + /// + protected override bool ParseLine(string keyword, string data) + { + var parseAction = GetKeywordAction(keyword, out bool found); + + if (!found) + { + _unrecognizedLines.Add(keyword + " " + data); + return false; + } + + parseAction!(data); + return true; + } + + 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.AddMaterial(_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/SharpEngine.Core.ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs b/SharpEngine.Core.ObjLoader/Loaders/MaterialLoader/MaterialLibraryLoaderFacade.cs new file mode 100644 index 0000000..9ce3ee6 --- /dev/null +++ b/SharpEngine.Core.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/SharpEngine.Core.ObjLoader/Loaders/ObjLoader/FileManager.cs b/SharpEngine.Core.ObjLoader/Loaders/ObjLoader/FileManager.cs new file mode 100644 index 0000000..0e44ce3 --- /dev/null +++ b/SharpEngine.Core.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/SharpEngine.Core.ObjLoader/Loaders/ObjLoader/ObjLoader.cs b/SharpEngine.Core.ObjLoader/Loaders/ObjLoader/ObjLoader.cs new file mode 100644 index 0000000..fbb9b0f --- /dev/null +++ b/SharpEngine.Core.ObjLoader/Loaders/ObjLoader/ObjLoader.cs @@ -0,0 +1,101 @@ +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 +{ + /// + /// Loads an OBJ model file and converts its parsed data into runtime Mesh instances. + /// + public class ObjLoader : LoaderBase + { + private readonly string _path; + private readonly DataStore _dataStore; + 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. + /// + /// The path to the OBJ file. + /// The data store to populate during parsing. + /// 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; + } + + /// + /// 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) + _typeParsers.Add(parser); + + return this; + } + + /// + protected override bool ParseLine(string keyword, string data) + { + foreach (var typeParser in _typeParsers) + { + if (typeParser.CanParse(keyword)) + { + typeParser.Parse(data); + 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; + } + + /// + /// 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) + { + 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, + Groups = _dataStore.Groups, + Materials = _dataStore.Materials + } + ]; + } + } +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs b/SharpEngine.Core.ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs new file mode 100644 index 0000000..20b7d20 --- /dev/null +++ b/SharpEngine.Core.ObjLoader/Loaders/ObjLoader/ObjLoaderFactory.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Entities.Properties.Meshes; +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; +using System.Collections.Generic; +using System.IO; +using System.IO.Abstractions; + +namespace SharpEngine.Core.ObjLoader.Loaders.ObjLoader +{ + /// + /// Handles loading 3D models. + /// + 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(); + _fileManager = new FileManager(); + } + + const string FbxExtension = ".fbx"; + const string ObjExtension = ".obj"; + + /// + /// Loads a mesh based on the file extension of the provided path. + /// + /// The OpenGL context where the model should be bound. + /// Specifies the file path of the mesh to be loaded, which determines the loading method based on its extension. + /// The model loaded from the file. + /// The model loaded from the file. + /// Thrown when the file extension of the provided path is not recognized as a supported mesh format. + /// 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) + { + _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) + { + throw new NotImplementedException(); + } + + 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); + } + + private static List LoadObjMeshes(GL gl, string path) + { + var dataStore = new DataStore(); + + var faceParser = new FaceParser(dataStore); + var groupParser = new GroupParser(dataStore); + var normalParser = new NormalParser(dataStore); + var textureParser = new TextureParser(dataStore); + var vertexParser = new VertexParser(dataStore); + var smoothingGroupParser = new SmoothingGroupParser(dataStore); + var objectNameParser = new ObjectNameParser(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, _fileSystem.FileStream, _fileManager) + .SetupTypeParsers(faceParser, groupParser, normalParser, textureParser, vertexParser, materialLibraryParser, useMaterialParser, smoothingGroupParser, objectNameParser); + + return loader.Load(gl); + } + } +} \ No newline at end of file diff --git a/Engine/ObjLoader/SharpEngine.Core.ObjLoader.csproj b/SharpEngine.Core.ObjLoader/SharpEngine.Core.ObjLoader.csproj similarity index 52% rename from Engine/ObjLoader/SharpEngine.Core.ObjLoader.csproj rename to SharpEngine.Core.ObjLoader/SharpEngine.Core.ObjLoader.csproj index 7cc24cf..04ffde4 100644 --- a/Engine/ObjLoader/SharpEngine.Core.ObjLoader.csproj +++ b/SharpEngine.Core.ObjLoader/SharpEngine.Core.ObjLoader.csproj @@ -1,16 +1,17 @@ - + - net8.0 + $(DotNetTargetFramework) - + + - + \ No newline at end of file diff --git a/Engine/ObjLoader/TypeParsers/FaceParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/FaceParser.cs similarity index 55% rename from Engine/ObjLoader/TypeParsers/FaceParser.cs rename to SharpEngine.Core.ObjLoader/TypeParsers/FaceParser.cs index c7328f7..83581d5 100644 --- a/Engine/ObjLoader/TypeParsers/FaceParser.cs +++ b/SharpEngine.Core.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; @@ -19,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); @@ -27,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) { @@ -53,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/SharpEngine.Core.ObjLoader/TypeParsers/GroupParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/GroupParser.cs new file mode 100644 index 0000000..0934906 --- /dev/null +++ b/SharpEngine.Core.ObjLoader/TypeParsers/GroupParser.cs @@ -0,0 +1,32 @@ +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 bool Parse(string line) + { + _dataStore.PushGroup(line); + return true; + } + } +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/TypeParsers/ITypeParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/ITypeParser.cs new file mode 100644 index 0000000..3fd685b --- /dev/null +++ b/SharpEngine.Core.ObjLoader/TypeParsers/ITypeParser.cs @@ -0,0 +1,22 @@ +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. + /// 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. + /// if the line was successfully parsed; otherwise, . + bool Parse(string line); + } +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/TypeParsers/MaterialLibraryParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/MaterialLibraryParser.cs new file mode 100644 index 0000000..7be0889 --- /dev/null +++ b/SharpEngine.Core.ObjLoader/TypeParsers/MaterialLibraryParser.cs @@ -0,0 +1,32 @@ +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 bool Parse(string line) + { + _libraryLoaderFacade.Load(line); + return true; + } + } +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/TypeParsers/NormalParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/NormalParser.cs new file mode 100644 index 0000000..111800b --- /dev/null +++ b/SharpEngine.Core.ObjLoader/TypeParsers/NormalParser.cs @@ -0,0 +1,42 @@ +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 bool 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); + + return true; + } + } +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/TypeParsers/ObjectNameParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/ObjectNameParser.cs new file mode 100644 index 0000000..13d0d9f --- /dev/null +++ b/SharpEngine.Core.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/SharpEngine.Core.ObjLoader/TypeParsers/SmoothingGroupParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/SmoothingGroupParser.cs new file mode 100644 index 0000000..693bcea --- /dev/null +++ b/SharpEngine.Core.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/SharpEngine.Core.ObjLoader/TypeParsers/TextureParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/TextureParser.cs new file mode 100644 index 0000000..df5a0e3 --- /dev/null +++ b/SharpEngine.Core.ObjLoader/TypeParsers/TextureParser.cs @@ -0,0 +1,41 @@ +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 bool 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); + + return true; + } + } +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/TypeParsers/TypeParserBase.cs b/SharpEngine.Core.ObjLoader/TypeParsers/TypeParserBase.cs new file mode 100644 index 0000000..30ac334 --- /dev/null +++ b/SharpEngine.Core.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 bool Parse(string line); + } +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/TypeParsers/UseMaterialParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/UseMaterialParser.cs new file mode 100644 index 0000000..76d6f5d --- /dev/null +++ b/SharpEngine.Core.ObjLoader/TypeParsers/UseMaterialParser.cs @@ -0,0 +1,31 @@ +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 bool Parse(string line) + { + _dataStore.SetMaterial(line); + return true; + } + } +} \ No newline at end of file diff --git a/SharpEngine.Core.ObjLoader/TypeParsers/VertexParser.cs b/SharpEngine.Core.ObjLoader/TypeParsers/VertexParser.cs new file mode 100644 index 0000000..fadb733 --- /dev/null +++ b/SharpEngine.Core.ObjLoader/TypeParsers/VertexParser.cs @@ -0,0 +1,48 @@ +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 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 IVertexDataStore _dataStore; + + /// + /// Initializes a new instance of with the specified data store to populate. + /// + /// The data store to populate. + public VertexParser(IVertexDataStore dataStore) + { + _dataStore = dataStore; + } + + /// + protected override string Keyword => "v"; + + /// + public override bool Parse(string line) + { + string[] parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + float x = parts[0].ParseInvariantFloat(); + float y = parts[1].ParseInvariantFloat(); + float z = parts[2].ParseInvariantFloat(); + + var vertex = new Vertex() + { + Position = new System.Numerics.Vector3(x, y, z), + }; + + _dataStore.AddVertex(vertex); + + return true; + } + } +} \ No newline at end of file diff --git a/Engine/Tests/SharpEngine.Shared.Net8/SharpEngine.Shared.Net8.csproj b/SharpEngine.Core.Physics/SharpEngine.Core.Physics.csproj similarity index 65% rename from Engine/Tests/SharpEngine.Shared.Net8/SharpEngine.Shared.Net8.csproj rename to SharpEngine.Core.Physics/SharpEngine.Core.Physics.csproj index fa71b7a..7cfa364 100644 --- a/Engine/Tests/SharpEngine.Shared.Net8/SharpEngine.Shared.Net8.csproj +++ b/SharpEngine.Core.Physics/SharpEngine.Core.Physics.csproj @@ -6,4 +6,8 @@ enable + + + + diff --git a/SharpEngine.Core.Shared/Debug.cs b/SharpEngine.Core.Shared/Debug.cs new file mode 100644 index 0000000..43c2f7a --- /dev/null +++ b/SharpEngine.Core.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/SharpEngine.Core.UI/Entities/DraggableUIElement.cs b/SharpEngine.Core.UI/Entities/DraggableUIElement.cs new file mode 100644 index 0000000..21e52dc --- /dev/null +++ b/SharpEngine.Core.UI/Entities/DraggableUIElement.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.UI.Entities; + +public class DraggableUIElement : UIElement +{ + public DraggableUIElement(string name) : base(name) + { + } +} diff --git a/Engine/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs b/SharpEngine.Core.UI/Entities/Layouts/FlexLayout.cs similarity index 56% rename from Engine/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs rename to SharpEngine.Core.UI/Entities/Layouts/FlexLayout.cs index b0dded0..f042f62 100644 --- a/Engine/SharpEngine.Core/Entities/UI/Layouts/FlexLayout.cs +++ b/SharpEngine.Core.UI/Entities/Layouts/FlexLayout.cs @@ -1,24 +1,24 @@ using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; +using SharpEngine.Core.UI.Entities; using System; -using System.Numerics; -namespace SharpEngine.Core.Entities.UI.Layouts; +namespace SharpEngine.Core.UI.Entities.Layouts; /// /// Represents a layout that dynamically distributes items in a flex layout. /// -/// The type of items in the collection. +/// The type of items in the collection. /// The amount of dimensions in the content area. -public class FlexLayout : LayoutBase where T : SceneNode, new() where TVector : IVector, new() +public class FlexLayout : LayoutBase where TItem : UIElement, new() where TVector : IVector, new() { /// Gets or sets the gap between each object. public int Gap { get; set; } /// Gets or sets the dimensions of the content area. - public required IVector ContentAreaSize { get; set; } + public required TVector ContentAreaSize { get; set; } /// /// - public override T[][] GetValues() => throw new NotImplementedException(); + public override TItem[][] GetValues() => throw new NotImplementedException(); } diff --git a/SharpEngine.Core.UI/Entities/Layouts/GridLayout.cs b/SharpEngine.Core.UI/Entities/Layouts/GridLayout.cs new file mode 100644 index 0000000..11443a7 --- /dev/null +++ b/SharpEngine.Core.UI/Entities/Layouts/GridLayout.cs @@ -0,0 +1,168 @@ +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Numerics; +using System; +using SharpEngine.Core.UI.Entities; + +namespace SharpEngine.Core.UI.Entities.Layouts; + +/// +/// Represents a grid layout that automatically updates item transforms under the hood. +/// +/// +/// TODO: This component currently starts at the bottom left corner of the grid. +/// A control to change the starting point of the grid should be added in the future. +/// +/// The type of items that can be stored and retrieved within the grid. +public class GridLayout : LayoutBase where TItem : UIElement +{ + private readonly bool _useBounds; + private readonly Vector2 _endPosition; + + public GridLayout() : this("GridLayout") { } + + public GridLayout(Vector2 position) : this("GridLayout") + { + Transform.Position = position; + } + + public GridLayout(Vector2 startPosition, Vector2 endPosition) : this("GridLayout") + { + Transform.Position = startPosition; + _endPosition = endPosition; + _useBounds = true; + } + + public GridLayout(string name) : base(name) { } + + /// Gets or sets the amount of rows in the grid. + /// + /// If is enabled, this value will be overridden and calculated based on the number of items and columns. + /// + public uint Rows { get; set; } = 1; + + /// Gets or sets the amount of columns in the grid. + /// + /// If is enabled, this value will be overridden and calculated based on the number of items and rows. + /// + public uint Columns { get; set; } = 1; + + /// Gets or sets a value indicating whether the number of columns should be automatically calculated. + public bool AutoColumns { get; set; } = false; + + /// Gets or sets a value indicating whether the number of rows should be automatically calculated. + public bool AutoRows { get; set; } = false; + + public float Padding { get; set; } + + /// Gets or sets the spacing between items in the grid. + /// The item size is taken from each UI element's width and height. + public new Vector2 Spacing { get; set; } + + // TODO: This calculation assumes all components are of the same size. This should be changed to support different sizes in the future. + public float TotalWidth => (Columns * (Children.Count > 0 ? ((TItem)Children[0]).Width : 0)) + ((Columns - 1) * Spacing.X); + + /// + /// Retrieves the item at [, ]. + /// + /// The row where the items should be retrieved. + /// The column where the item should be retrieved. + /// The item at [, ]. + public TItem this[uint row, uint column] + { + get + { + uint index = GetIndex(row, column); + return (TItem)Children[(int)index]; + } + set + { + uint index = GetIndex(row, column); + Children[(int)index] = value; + } + } + + /// + public override SceneNode AddChild(SceneNode item) + { + base.AddChild(item); + UpdateItemTransforms(); + + return item; + } + + private void UpdateItemTransforms() + { + if (Columns == 0) + return; + + if (Children.Count == 0) + return; + + var item = (TItem)Children[0]; + var itemWidth = item.Width; + var itemHeight = item.Height; + + var position = Transform.Position; + var baseX = position.X; + var baseY = position.Y; + + if (_useBounds) + { + var startX = MathF.Min(Transform.Position.X, _endPosition.X); + var endX = MathF.Max(Transform.Position.X, _endPosition.X); + var availableWidth = endX - startX; + var horizontalOffset = MathF.Max(0, (availableWidth - TotalWidth) / 2f); + + baseX = startX + horizontalOffset + (itemWidth / 2f); + baseY = Transform.Position.Y + Spacing.Y + (itemHeight / 2f); + } + + for (var index = 0; index < Children.Count; index++) + { + var row = index / (int)Columns; + var column = index % (int)Columns; + item = (TItem)Children[index]; + position = new Vector2( + baseX + (column * (itemWidth + Spacing.X)), + baseY + (row * (itemHeight + Spacing.Y))); + + item.Transform.Position = position; + } + } + + /// + /// Calculates the index in the flat list based on row and column. + /// + /// The row where the items should be retrieved. + /// The column where the item should be retrieved. + /// The index of the item at [, ]. + /// + private uint GetIndex(uint row, uint column) + { + if (row < 0 || column < 0 || column >= Columns) + throw new ArgumentOutOfRangeException(nameof(row), "Invalid row or column index."); + + var index = row * Columns + column; + if (index >= Children.Count) + throw new ArgumentOutOfRangeException(nameof(row), "Index exceeds the number of items."); + + return index; + } + + /// + public override TItem[][] GetValues() + { + var values = new TItem[Rows][]; + for (uint i = 0; i < Rows; i++) + { + values[i] = new TItem[Columns]; + for (uint j = 0; j < Columns; j++) + { + var item = this[i, j]; + values[i][j] = item; + } + } + + return values; + } +} diff --git a/Engine/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs b/SharpEngine.Core.UI/Entities/Layouts/LayoutBase.cs similarity index 50% rename from Engine/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs rename to SharpEngine.Core.UI/Entities/Layouts/LayoutBase.cs index bb65554..435dfdc 100644 --- a/Engine/SharpEngine.Core/Entities/UI/Layouts/LayoutBase.cs +++ b/SharpEngine.Core.UI/Entities/Layouts/LayoutBase.cs @@ -1,16 +1,22 @@ -using SharpEngine.Core.Scenes; +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. /// -/// -public abstract class LayoutBase : SceneNode where TItem : SceneNode, new() +/// The type of the items in the layout. +public abstract class LayoutBase : EmptyNode where TItem : UIElement { - /// Represents the items in the layout. - public List Items { get; set; } = []; + protected LayoutBase() : this($"{typeof(TItem).Name} Layout") { } + protected LayoutBase(string name) : base(name) { } + + /// Gets or sets the distance between items in the grid. + public Vector2 Spacing { get; set; } = new(30, 30); /// /// Adds a new item to the layout. @@ -22,20 +28,11 @@ public override LayoutBase AddChild(params SceneNode[] nodes) base.AddChild(nodes); foreach (var node in nodes) - AddItem((TItem)node); + AddChild(node); return this; } - /// - /// Adds and item to the container. - /// - /// The item to be added. - public virtual void AddItem(TItem item) - { - Items.Add(item); - } - /// /// Gets the items in the container. /// diff --git a/SharpEngine.Core.UI/Entities/UIElement.cs b/SharpEngine.Core.UI/Entities/UIElement.cs new file mode 100644 index 0000000..2b6f8ab --- /dev/null +++ b/SharpEngine.Core.UI/Entities/UIElement.cs @@ -0,0 +1,88 @@ +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 Vector2 = SharpEngine.Core.Numerics.Vector2; + +using Silk.NET.OpenGL; +using System.Numerics; +using SharpEngine.Core.Entities.Interfaces; +using SharpEngine.Core.Shaders.Rendering; +using SharpEngine.Core.Entities; + +namespace SharpEngine.Core.UI.Entities; + +/// +/// Represents a User Interface entity. +/// +public class UIElement : EmptyNode, IRenderable +{ + public List Components { get; } = new List(); + private MeshRenderer _renderer; + private 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; + + [ShaderParameter("orthoMatrix", ShaderParameterType.Mat4)] + public Matrix4x4 OrthoMatrix = Matrix4x4.Identity; + + /// + /// 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) { } + + /// + public override void OnInitialized(GL gl) + { + base.OnInitialized(gl); + + // TODO: #5 Support custom meshes? + var mesh = MeshService.Instance.LoadMesh(nameof(Primitives.Plane), Primitives.Plane.Mesh); + var material = MaterialExtensions.Default(new UIShader(gl)); + + _renderer = new MeshRenderer(mesh, material); + Components.Add(_renderer); + + _paramBinder = new ShaderParameterBinder(); + _paramBinder.Bind(this, material.Shader); + } + + /// + /// Render the UI element. + /// + /// The camera view. + /// The window where the UI element is rendered. + public override Task Render(CameraView camera, Window window) + { + _renderer.Mesh.Bind(); + _renderer.Material.Shader.Use(); + _renderer.Material.DiffuseMap.Texture.Use(TextureUnit.Texture0); + + OrthoMatrix = window.CreateOrthographicOffCenter(); + + _paramBinder.Apply(this, _renderer.Material.Shader); + _renderer.Material.Shader.SetVector2("position", (System.Numerics.Vector2)Transform.Position); + _renderer.Material.Shader.SetFloat("rotation", Math.DegreesToRadians(Transform.Rotation.Angle)); + _renderer.Material.Shader.SetInt("texture1", 0); + _renderer.Material.Shader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); + + _renderer.Mesh.Draw(); + + return Task.CompletedTask; + } +} diff --git a/SharpEngine.Core.UI/Handlers/ParticleEmitter.cs b/SharpEngine.Core.UI/Handlers/ParticleEmitter.cs new file mode 100644 index 0000000..cfb8d3a --- /dev/null +++ b/SharpEngine.Core.UI/Handlers/ParticleEmitter.cs @@ -0,0 +1,106 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Entities; + +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.UI/Renderers/ParticleRenderer.cs b/SharpEngine.Core.UI/Renderers/ParticleRenderer.cs new file mode 100644 index 0000000..23baf3c --- /dev/null +++ b/SharpEngine.Core.UI/Renderers/ParticleRenderer.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Renderers; +using SharpEngine.Core.Scenes; + +namespace SharpEngine.Core.UI.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) + { + } + + /// + /// 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() => Task.CompletedTask; // => throw new NotImplementedException(); +} diff --git a/SharpEngine.Core.UI/Renderers/UIRenderer.cs b/SharpEngine.Core.UI/Renderers/UIRenderer.cs new file mode 100644 index 0000000..2843ea3 --- /dev/null +++ b/SharpEngine.Core.UI/Renderers/UIRenderer.cs @@ -0,0 +1,188 @@ +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 SharpEngine.Telemetry; +using SharpEngine.Core.UI.Entities; +using SharpEngine.Core.Renderers; + +namespace SharpEngine.Core.UI.Renderers; + +/// +/// Represents a renderer dedicated to drawing UI elements to the screen. +/// +public class UIRenderer : 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 _cachedUIElements = 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.UIRenderer; + + /// + /// Initializes a new instance of . + /// + public UIRenderer(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 UIRenderer(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 + RefreshUICache(); + _logger.LogTrace("UIRenderer.OnWindowAttached: cache primed, cachedCount={Count}", _cachedUIElements.Count); + + // Initialize cached elements + foreach (var node in _cachedUIElements) + { + try + { + node.OnInitialized(_gl); + _initializedElements.Add(node); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error initializing UI 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 RefreshUICache() + { + // Discover UI elements from the scene graph root so examples that attach layouts 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 + _cachedUIElements = 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 UI element: {Message}", ex.Message); + } + } + + // Remove references to removed elements + foreach (var node in removed) + _initializedElements.Remove(node); + + try + { + _logger.LogTrace("RefreshUICache: found {Count} UI elements, root children={RootChildren}", _cachedUIElements.Count, _scene.Root.Children.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error refreshing UI 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; + RefreshUICache(); + } + + // Use the cached UI 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 the quad + _gl.Disable(EnableCap.CullFace); + + _frameCounter++; + var swTotal = System.Diagnostics.Stopwatch.StartNew(); + var swRender = System.Diagnostics.Stopwatch.StartNew(); + + // Render each UI element sequentially on the GL thread. Avoid per-element Task allocations for performance. + foreach (var elem in _cachedUIElements) + { + 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("UI render: total={TotalMs}ms, draw={DrawMs}ms, elements={Count}", swTotal.Elapsed.TotalMilliseconds, swRender.Elapsed.TotalMilliseconds, _cachedUIElements.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 UI Renderer: {Message}", ex.Message); + } + } +} diff --git a/SharpEngine.Core.UI/Shaders/UIShader.cs b/SharpEngine.Core.UI/Shaders/UIShader.cs new file mode 100644 index 0000000..4e0c5ec --- /dev/null +++ b/SharpEngine.Core.UI/Shaders/UIShader.cs @@ -0,0 +1,36 @@ +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Windowing; + +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 : Shader +{ + public UIShader(GL gl) : base(gl, Defaults.Defaults.UIVertexShader, Defaults.Defaults.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/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/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/Engine/SharpEngine.Core/Abstractions/IGame.cs b/SharpEngine.Core/Abstractions/IGame.cs similarity index 57% rename from Engine/SharpEngine.Core/Abstractions/IGame.cs rename to SharpEngine.Core/Abstractions/IGame.cs index de0b798..839c474 100644 --- a/Engine/SharpEngine.Core/Abstractions/IGame.cs +++ b/SharpEngine.Core/Abstractions/IGame.cs @@ -1,8 +1,11 @@ -using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Enums; - +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Windowing; using Silk.NET.Input; +using System; namespace SharpEngine.Core.Interfaces; @@ -10,8 +13,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 Vector3(0), new DefaultViewSettings()); + } + + /// + /// Initializes a new instance of the . + /// + /// + /// Sets up a default camera. + /// + public Game(Scene scene) + { + Scene = scene; + Camera = new(new 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,8 +56,21 @@ 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; } + /// + /// Gets the main window. + /// + public Window Window + { + get => field ?? throw new InvalidOperationException("The game window has not been assigned yet."); + set; + } /// /// Executed when a mouse button is pressed. /// @@ -56,4 +105,6 @@ public virtual void Initialize() { } /// Executed each frame. /// public virtual void Update(double deltaTime, IInputContext input) { } + + public virtual void OnAfterRender(Frame frame) { } } 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 61% rename from Engine/SharpEngine.Core/Abstractions/ISettings.cs rename to SharpEngine.Core/Abstractions/ISettings.cs index 23888d2..cc8fba4 100644 --- a/Engine/SharpEngine.Core/Abstractions/ISettings.cs +++ b/SharpEngine.Core/Abstractions/ISettings.cs @@ -1,6 +1,6 @@ -using SharpEngine.Core.Renderers; +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Renderers; using Silk.NET.Input; -using Silk.NET.Windowing; namespace SharpEngine.Core.Interfaces; @@ -27,23 +27,9 @@ public interface ISettings /// Gets or sets the secondary mouse button. public MouseButton SecondaryButton { 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; + /// + /// Gets or sets the log level for the game engine. + /// + public LogLevel LogLevel { get; set; } } diff --git a/SharpEngine.Core/Architecture.cd b/SharpEngine.Core/Architecture.cd new file mode 100644 index 0000000..d813763 --- /dev/null +++ b/SharpEngine.Core/Architecture.cd @@ -0,0 +1,476 @@ + + + + + + AAAAAIAAACAAAQABQAAAAAAAIAAgAAAAAAAAAAAAAAA= + Engine.cs + + + + + + AQAAAAAAAAAAAQAAAEAAAAAAIEAAAAAAAAAAAAAAAAA= + EngineServiceManager.cs + + + + + + AAwAAAAAAoAAhACAAAAIEAIAIAAABAAAIgBAAAEAAEA= + InputManager.cs + + + + + + AAAAAAAAAgAAAAAQACAAAAAAAAAAAAAAAAAAAQgAAAA= + Ray.cs + + + + + + AAAAAAAABAAAAgAAgAAAAAAAAAAAAAAAAAAAAAAAAEI= + RendererManager.cs + + + + + + AAAAAAAAAAAAAAAAIEAAAAAAAAAAAAAAAAAAAAAAAEA= + ShaderManager.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAACAAAAAAAAA= + Windowing\Frame.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Windowing\SilkWindow.cs + + + + + + ACQMoxAETuBd1UAwSADU0EAqHIgMxEIgqhiEBAOACyI= + Windowing\SilkWindow.cs + + + + + + + gAADAAAgACAQAQQAwEAAABBCIAACQAAAIACAIAAAADI= + Windowing\Window.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAA= + Windowing\WindowExtensions.cs + + + + + + AAAJAAAgAAAAAAAAAAgAAAIAAAAAAAAAAAAAAAABAAA= + Windowing\WindowFactory.cs + + + + + + + AAAAAQAAAAAAAAAAIABAAAQAAAAAAAAAAAAAAAAAAAA= + Windowing\WindowRegistration.cs + + + + + + AAAEAAAAAAAAAAAAAAAAAAAAAAAIAAAAQAAAAAAAAAA= + Textures\TextureService.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAQAAIAAAAAAAQAAAAAAAAA= + Shaders\Rendering\ParameterDescriptor.cs + + + + + + ABAAAAAAAEQAAgBAAAAAAAAAAAAAAAAAAAAgAAAAABA= + Shaders\Rendering\ShaderParameter.cs + + + + + + AAgCgAAAABAAAAAAAAAAAABAKAAAAAAAAAAAYAAAAAA= + Shaders\Rendering\ShaderParameterBinder.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Shaders\DefaultShader.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAA= + Shaders\LampShader.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAA= + Shaders\LightingShader.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAQAAIAAAAAAAQAAAAAAAAA= + Shaders\ShaderParameterAttribute.cs + + + + + + AIAAAAAAAAIAAAAIAAAAAAAAAAAAAAAAwgAAAAAAAAg= + Shaders\ShaderService.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAA= + Shaders\UIShader.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA= + Scenes\EmptyNode.cs + + + + + + AAAoAAAAAAIAACAAAAAAAAAAAEQAAAAGAAAAAAAABAA= + Scenes\Scene.cs + + + + + + AAAAAAAAACAAAAAAAAAAAAAJAAAAAAAACAAAAAgAAAA= + Scenes\SceneExtensions.cs + + + + + + AAQAAACAACAAEAACoAIEQAQCACIAAAAACFAAAAAAAAA= + Scenes\SceneNode.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAABAAAAAAAAA= + Renderers\ParticleRenderer.cs + + + + + + AAAAAAAAAAAAAAAAIAAAABCAMAAAABCABDIAACAACAA= + Renderers\Renderer.cs + + + + + + AAAABAAAACAAAQAAIAAAAAAAAAAAAACAgBAACAAAAAA= + Renderers\RendererBase.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAABAAAAAAAAA= + Renderers\TextRenderer.cs + + + + + + AAQAAAAACCAAAAAAYAwAABQAIAACAACAABIAACAAAAA= + Renderers\UIRenderer.cs + + + + + + AAAAAAAgEEAAAAAAAAAAIAEAAgAAAAAAEQgAAAAAAAA= + Primitives\Cube.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAA= + Primitives\Plane.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAA= + Primitives\PrimitiveFactory.cs + + + + + + AAAAAIAAICIIAIABAAgAAAAAIAIAAAACAAAAJAgAAEA= + Handlers\EngineHandler.cs + + + + + + + AABgAAIAAAAAAAgCAAIAAAAAAAAAAAAIAACgAAgCAEA= + Handlers\ParticleEmitter.cs + + + + + + AAAAADAAAAACAAAAAAAEAAAAAAAAAAAAAAIAAEEAAEA= + Handlers\WindowHandler.cs + + + + + + AAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAA= + Entities\Lights\DirectionalLight.cs + + + + + + AEAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAEAAAAA= + Entities\Lights\Light.cs + + + + + + AAAAAAAgAAAAAAAAAACgAAAAAAAAAAgAYBAAAAAAAAA= + Entities\Lights\PointLight.cs + + + + + + AAAQAAAgAAAAAAAAACAAAAAAAAAAAIgAYBAAAAAAAAA= + Entities\Lights\SpotLight.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Entities\Interfaces\IClickable.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAIAAA= + Entities\Interfaces\IClickable.cs + + + + + + + AAAAAIAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAA= + Entities\Interfaces\IComponent.cs + + + + + + AAAAAAAAYAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAA= + Entities\UI\Layouts\FlexLayout.cs + + + + + + AAAAQCAAIAAAEEAYCAAAAABAAEAAAAAAAAIAAAgABgA= + Entities\UI\Layouts\GridLayout.cs + + + + + + AAAAQAAAIAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Entities\UI\Layouts\LayoutBase.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Entities\UI\DraggableUIElement.cs + + + + + + AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQgAAAAAAAA= + Entities\UI\MeshRenderer.cs + + + + + + + QAAAAAAABAAAQIAAAAAAAEAAEAAAAAAACBAAAAAAAAA= + Entities\UI\UIElement.cs + + + + + + + AAAAAAACAAAAAAEAAAAAAAGIICAAAAgAYABAAIAAABA= + Entities\Views\CameraView.cs + + + + + + IAAAEAAAAAAABAAgAgAAAACAACBAAQgAwECAAAAAQAI= + Entities\Views\View.cs + + + + + + QAAAAAAAAAAAAIAAAQAAAAQAECAAAAAAQBgAAgAAAAA= + Entities\GameObject.cs + + + + + + + AAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAA= + Entities\GameObject.cs + + + + + + AIAAAhAAAAAgAAAAAAAAIAACAAAAEAKAAAAAAAAAAAA= + Entities\Views\Settings\DefaultViewSettings.cs + + + + + + + AIAAAhAAAAAgAAAAAAAAIAACAAAAEAKAAAAAAAAAAAA= + Entities\Views\Settings\ViewSettings.cs + + + + + + + AAAIAAAgAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAA= + Windowing\IWindowFactory.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAA= + Entities\Interfaces\IClickable.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Entities\Interfaces\IComponent.cs + + + + + + AIAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Entities\Views\Settings\IViewSettings.cs + + + + + + AAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAIAggEgADA= + Shaders\Rendering\ShaderParameterType.cs + + + + + + AAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAEAAAAAAAAAgA= + Shaders\Rendering\ShaderUpdateFrequency.cs + + + + + + AAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAIAACAFAAAA= + Renderers\RenderFlags.cs + + + + + + AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAA= + Primitives\PrimitiveType.cs + + + + + + AQAAAAAAAAIAAAAIAAAAAAAAAAAAAAAIAAAAAAAAAAA= + Handlers\EngineHandlerState.cs + + + + \ No newline at end of file diff --git a/SharpEngine.Core/Engine.cs b/SharpEngine.Core/Engine.cs new file mode 100644 index 0000000..46e9458 --- /dev/null +++ b/SharpEngine.Core/Engine.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.Handlers; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Windowing; +using SharpEngine.Shared.Dto; +using SharpEngine.Telemetry; + +using System.Threading.Tasks; +using SharpEngine.Core.Extensions; +using System.Threading; +using System; + +namespace SharpEngine.Core; + +/// +/// Manages the engine's services and provides methods to initialize, register handlers, and shut down asynchronously. +/// +public class Engine +{ + /// + /// Gets the manager responsible for handling engine services. + /// + public EngineServiceManager ServicesManager { get; private set; } = new(); + + private bool _initialized = false; + private readonly ILogger _logger; + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + /// + /// Initializes a new instance of . + /// + public Engine() + { + _logger = LoggingExtensions.CreateLogger(typeof(Engine)); + } + + /// + /// Initializes the engine for use. + /// + public void Initialize() + { + _logger.LogDebug("Initializing engine..."); + + if (_initialized) + throw new InvalidOperationException("Engine is already initialized."); + + _initialized = true; + _logger.LogDebug("Engine successfully initialized."); + + ServicesManager.StartHandlers(_cancellationTokenSource); + + while (!_cancellationTokenSource.Token.IsCancellationRequested) + { + + } + } + + /// + /// Stops all engine services and shuts down the engine asynchronously. + /// + /// A that completes when shutdown finishes. + public async Task ShutdownAsync() + { + if (ServicesManager == null) + return; + + _logger.LogDebug("Shutting down engine..."); + + _cancellationTokenSource.Cancel(); + await ServicesManager.StopAllAsync(_cancellationTokenSource.Token); + + _initialized = false; + _logger.LogDebug("Engine successfully shut down."); + } + + /// + /// Checks if the current engine version matches the version specified in the provided project. + /// + /// The project to check the engine version against. + /// + /// if the current engine version matches the project engine version; otherwise, . + /// + public bool CheckEngineVersion(ProjectDto project) + { + var currentAssemblyVersion = typeof(Window).Assembly.GetVersion(); + if (currentAssemblyVersion != project.EngineVersion.Version) + { + _logger.LogWarning("The current engine version ({CurrentVersion}) does not match the project engine version ({ProjectVersion}). This may lead to unexpected behavior.", currentAssemblyVersion, project.EngineVersion); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/SharpEngine.Core/EngineServiceManager.cs b/SharpEngine.Core/EngineServiceManager.cs new file mode 100644 index 0000000..f769c29 --- /dev/null +++ b/SharpEngine.Core/EngineServiceManager.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.Handlers; +using SharpEngine.Telemetry; + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpEngine.Core; + +/// +/// Contains all the engine handlers and manages their lifecycle. +/// +public class EngineServiceManager +{ + public 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 THandler RegisterHandler(THandler handler) where THandler : EngineHandler + { + _logger.LogDebug("Registering handler: '{Handler}'.", handler.GetType().Name); + Handlers.Add(handler); + + _logger.LogDebug("Handler '{Handler}' registered successfully.", handler.GetType().Name); + return handler; + } + + public void StartHandlers(CancellationTokenSource source) + { + foreach (var engineHandler in Handlers) + engineHandler.Start(source); + } + + /// + /// Stops all active handlers asynchronously by calling their StopAsync method. + /// + /// A representing the asynchronous operation. + public async Task StopAllAsync(CancellationToken cancellationToken = default) + { + var stopTasks = Handlers.Select(handler => handler.StopAsync(cancellationToken)); + 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/Components/MeshRenderer.cs b/SharpEngine.Core/Entities/Components/MeshRenderer.cs new file mode 100644 index 0000000..1849168 --- /dev/null +++ b/SharpEngine.Core/Entities/Components/MeshRenderer.cs @@ -0,0 +1,27 @@ +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.UI.Entities; + +public class MeshRenderer : IComponent +{ + 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/Engine/SharpEngine.Core/Entities/GameObject.cs b/SharpEngine.Core/Entities/GameObject.cs similarity index 50% rename from Engine/SharpEngine.Core/Entities/GameObject.cs rename to SharpEngine.Core/Entities/GameObject.cs index 160896a..2a51a6a 100644 --- a/Engine/SharpEngine.Core/Entities/GameObject.cs +++ b/SharpEngine.Core/Entities/GameObject.cs @@ -1,4 +1,6 @@ using SharpEngine.Core.Attributes; +using SharpEngine.Core.Components.Properties.Meshes; +using SharpEngine.Core.Entities.Interfaces; using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; @@ -8,9 +10,11 @@ using SharpEngine.Core.Windowing; using Shader = SharpEngine.Core.Shaders.Shader; -using Silk.NET.OpenGL; -using Tutorial; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using SharpEngine.Core.UI.Entities; namespace SharpEngine.Core.Entities; @@ -19,47 +23,28 @@ namespace SharpEngine.Core.Entities; /// 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"); - } + // TODO: Remove these two. + protected record struct TempShaderDataContainer(string shaderVertPath, string shaderFragPath, string shaderName); + protected readonly TempShaderDataContainer _tempShaderData; - 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"); - } + public List Components { get; } = new List(); + + private MeshRenderer _renderer; /// - /// Initializes a new instance of the with specified textures and shaders. + /// Gets or sets the shader of the game object. /// - /// 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; } + public Shader? Shader { get; set; } /// /// Gets or sets the mesh of the game object. /// - public Model_Old Model { get; set; } + public Model? Model { get; set; } /// /// Gets or sets the transform of the game object. /// - public override Transform Transform + public override Transform Transform { get => _transform; set @@ -68,7 +53,7 @@ public override Transform Transform BoundingBox = BoundingBox.CalculateBoundingBox(_transform); } } - + private Transform _transform = new(); /// @@ -77,8 +62,49 @@ public override Transform Transform [Inspector(DisplayInInspector = false)] public BoundingBox BoundingBox { get; set; } + /// + /// Initializes a new instance of the . + /// + public GameObject() : this(model: null!) { } + + /// + /// Initializes a new instance of the class with a model. + /// + /// The model. + public GameObject(Model model) : this(model, shader: null!) + { + // Shader = ShaderService.Instance.LoadShader(Window.SharedGL, _shaderVertPath, _shaderFragPath, _shaderName); + } + + /// + /// Initializes a new instance of the with specified textures and shaders. + /// + /// The model of the game object. + /// The shader to be used by the game object. + public GameObject(Model model, Shader shader) : base(string.Empty) + { + Model = model; + Shader = shader; + + var material = MaterialExtensions.Default(shader); + _renderer = new MeshRenderer(model, material); + Components.Add(_renderer); + + if (shader is not null) + _tempShaderData = new TempShaderDataContainer(shader.VertPath, shader.FragPath, shader.Name); + else + _tempShaderData = new TempShaderDataContainer(Defaults.Defaults.VertexShader, Defaults.Defaults.FragmentShader, "lighting"); + } + + /// + /// 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. @@ -90,9 +116,12 @@ 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; + if (Shader is null) + Shader = ShaderService.Instance.LoadShader(window, _tempShaderData.shaderVertPath, _tempShaderData.shaderFragPath, _tempShaderData.shaderName); + foreach (var mesh in Model.Meshes) { mesh.Bind(); @@ -106,7 +135,7 @@ public override Task Render(CameraView camera, Window window) foreach (var material in mesh.Materials) material.SetUniformValues(Shader); - Window.GL.DrawArrays(PrimitiveType.Triangles, 0, (uint)mesh.Vertices.Length); + mesh.Draw(); } return Task.CompletedTask; diff --git a/SharpEngine.Core/Entities/Interfaces/IClickable.cs b/SharpEngine.Core/Entities/Interfaces/IClickable.cs new file mode 100644 index 0000000..419d5a8 --- /dev/null +++ b/SharpEngine.Core/Entities/Interfaces/IClickable.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Entities.Interfaces; + +public interface IClickable +{ + void OnClick(PointerClickEvent e); +} + +public class PointerClickEvent +{ +} + +public sealed class ClickableComponent : IComponent +{ + public event Action? Clicked; + + internal void RaiseClick(PointerClickEvent e) + { + Clicked?.Invoke(e); + } +} \ No newline at end of file diff --git a/SharpEngine.Core/Entities/Interfaces/IComponent.cs b/SharpEngine.Core/Entities/Interfaces/IComponent.cs new file mode 100644 index 0000000..72d7c46 --- /dev/null +++ b/SharpEngine.Core/Entities/Interfaces/IComponent.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Entities.Interfaces; + +public interface IComponent +{ +} + +public static class GameObjectExtensions +{ + public static TComponent? GetComponentOrDefault(this GameObject gameObject) where TComponent : IComponent + { + var component = gameObject.Components.OfType().FirstOrDefault(); + return component; + } + + public static T Create(T instance) where T : GameObject + { + instance.OnCreated(); + return instance; + } + + public static T Create(Action? configure = null) where T : GameObject, new() + { + var instance = new T(); + configure?.Invoke(instance); + instance.OnCreated(); + return instance; + } +} diff --git a/Engine/SharpEngine.Core/Entities/Lights/DirectionalLight.cs b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs similarity index 77% rename from Engine/SharpEngine.Core/Entities/Lights/DirectionalLight.cs rename to SharpEngine.Core/Entities/Lights/DirectionalLight.cs index c7c0701..b4516cd 100644 --- a/Engine/SharpEngine.Core/Entities/Lights/DirectionalLight.cs +++ b/SharpEngine.Core/Entities/Lights/DirectionalLight.cs @@ -1,10 +1,8 @@ -using SharpEngine.Core._Resources; -using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Entities.Views; -using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; -using System.Numerics; + using System.Threading.Tasks; namespace SharpEngine.Core.Entities.Lights; @@ -26,13 +24,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, 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/Engine/SharpEngine.Core/Entities/Lights/Light.cs b/SharpEngine.Core/Entities/Lights/Light.cs similarity index 96% rename from Engine/SharpEngine.Core/Entities/Lights/Light.cs rename to SharpEngine.Core/Entities/Lights/Light.cs index dfd2585..d56422d 100644 --- a/Engine/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/Engine/SharpEngine.Core/Entities/Lights/PointLight.cs b/SharpEngine.Core/Entities/Lights/PointLight.cs similarity index 77% rename from Engine/SharpEngine.Core/Entities/Lights/PointLight.cs rename to SharpEngine.Core/Entities/Lights/PointLight.cs index 0522f97..0dec2fc 100644 --- a/Engine/SharpEngine.Core/Entities/Lights/PointLight.cs +++ b/SharpEngine.Core/Entities/Lights/PointLight.cs @@ -1,9 +1,9 @@ -using SharpEngine.Core._Resources; using SharpEngine.Core.Entities.Views; -using SharpEngine.Core.Extensions; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Shaders; using SharpEngine.Core.Windowing; -using System.Numerics; + +using System; using System.Threading.Tasks; namespace SharpEngine.Core.Entities.Lights; @@ -28,7 +28,7 @@ public PointLight(Vector3 position, int index) _index = index; - LampShader = new LampShader(); + LampShader = new LampShader(Window.SharedGL); } @@ -51,9 +51,14 @@ public PointLight(Vector3 position, int index) /// public float Quadratic { get; set; } + /// + /// protected override void SetShaderUniforms(CameraView camera) { - Shader.SetVector3($"pointLights[{_index}].position", (Vector3)Transform.Position); + if (Shader == null) + throw new NullReferenceException(nameof(Shader)); + + 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); @@ -65,8 +70,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, _tempShaderData.shaderVertPath, _tempShaderData.shaderFragPath, _tempShaderData.shaderName); SetShaderUniforms(camera); return Task.CompletedTask; diff --git a/Engine/SharpEngine.Core/Entities/Lights/SpotLight.cs b/SharpEngine.Core/Entities/Lights/SpotLight.cs similarity index 87% rename from Engine/SharpEngine.Core/Entities/Lights/SpotLight.cs rename to SharpEngine.Core/Entities/Lights/SpotLight.cs index 59ad0fe..9c98412 100644 --- a/Engine/SharpEngine.Core/Entities/Lights/SpotLight.cs +++ b/SharpEngine.Core/Entities/Lights/SpotLight.cs @@ -1,8 +1,8 @@ -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 +25,6 @@ public SpotLight() Linear = 0.09f; Quadratic = 0.032f; - Shader = ShaderService.Instance.LoadShader(Default.VertexShader, Default.FragmentShader, "lighting"); } /// @@ -58,8 +57,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,8 +81,9 @@ protected override void SetShaderUniforms(CameraView camera) /// public override Task Render(CameraView camera, Window window) { + Shader = ShaderService.Instance.LoadShader(window, _tempShaderData.shaderVertPath, _tempShaderData.shaderFragPath, _tempShaderData.shaderName); SetShaderUniforms(camera); - + return Task.CompletedTask; } } 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/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/Views/CameraView.cs b/SharpEngine.Core/Entities/Views/CameraView.cs similarity index 89% rename from Engine/SharpEngine.Core/Entities/Views/CameraView.cs rename to SharpEngine.Core/Entities/Views/CameraView.cs index d98a3ff..df06475 100644 --- a/Engine/SharpEngine.Core/Entities/Views/CameraView.cs +++ b/SharpEngine.Core/Entities/Views/CameraView.cs @@ -1,8 +1,9 @@ -using SharpEngine.Core.Components.Properties; 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; @@ -25,6 +26,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; @@ -94,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/Engine/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs b/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs similarity index 88% rename from Engine/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs rename to SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs index cef7852..dbeef01 100644 --- a/Engine/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs +++ b/SharpEngine.Core/Entities/Views/Settings/DefaultViewSettings.cs @@ -1,4 +1,6 @@ -using SharpEngine.Core.Renderers; +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Renderers; + using Silk.NET.Input; using Silk.NET.Maths; using Silk.NET.Windowing; @@ -47,4 +49,7 @@ public DefaultViewSettings() /// public MouseButton SecondaryButton { get; set; } = MouseButton.Right; + + /// + public LogLevel LogLevel { get; set; } = LogLevel.Information; } 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 89% rename from Engine/SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs rename to SharpEngine.Core/Entities/Views/Settings/ViewSettings.cs index 09c65cb..013f31b 100644 --- a/Engine/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; } diff --git a/Engine/SharpEngine.Core/Entities/Views/View.cs b/SharpEngine.Core/Entities/Views/View.cs similarity index 91% rename from Engine/SharpEngine.Core/Entities/Views/View.cs rename to SharpEngine.Core/Entities/Views/View.cs index 02aecba..aac388b 100644 --- a/Engine/SharpEngine.Core/Entities/Views/View.cs +++ b/SharpEngine.Core/Entities/Views/View.cs @@ -1,10 +1,10 @@ -using SharpEngine.Core.Components.Properties; -using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; + using System; -using System.Collections.Generic; -using System.Numerics; +using Matrix4x4 = System.Numerics.Matrix4x4; namespace SharpEngine.Core.Entities.Views; @@ -22,8 +22,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..2501343 --- /dev/null +++ b/SharpEngine.Core/Extensions/AssemblyExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Reflection; + +namespace SharpEngine.Core.Extensions; + +/// +/// Contains extension methods for the . +/// +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. + /// Thrown when the assembly version is null. + 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/SharpEngine.Core/Handlers/EngineHandler.cs b/SharpEngine.Core/Handlers/EngineHandler.cs new file mode 100644 index 0000000..c12dc5b --- /dev/null +++ b/SharpEngine.Core/Handlers/EngineHandler.cs @@ -0,0 +1,162 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Telemetry; +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; + protected static CancellationTokenSource _cancellationTokenSource; + 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 = null) + { + Logger = logger ?? LoggingExtensions.CreateLogger(); + } + + /// + /// 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(CancellationTokenSource cancellationTokenSource) + { + if (State != EngineHandlerState.NotStarted) + throw new InvalidOperationException("Handler already started."); + + _startTime = DateTime.UtcNow; + _cancellationTokenSource = cancellationTokenSource; + _runner = Task.Run(async () => await RunInternalAsync()); + } + + /// + /// 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(CancellationToken cancellationToken = default) + { + if (_cancellationTokenSource == null || _runner == null || State == EngineHandlerState.Stopped) + { + Logger.LogDebug("[EngineHandler] '{Name}' is not running.", GetType().Name); + return; + } + + await _cancellationTokenSource.CancelAsync(); + + try + { + await _runner.WaitAsync(cancellationToken); + } + 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() + { + try + { + State = EngineHandlerState.Running; + await OnInitializedAsync(); + await ExecuteAsync(_cancellationTokenSource.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() + { + _cancellationTokenSource?.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/WindowHandler.cs b/SharpEngine.Core/Handlers/WindowHandler.cs new file mode 100644 index 0000000..3ad5a3f --- /dev/null +++ b/SharpEngine.Core/Handlers/WindowHandler.cs @@ -0,0 +1,140 @@ +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 System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System; + +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. +/// When a window is closing it will be disposed and removed from the managed list. +/// +/// +public class WindowHandler : EngineHandler +{ + private readonly List _windows = []; + private readonly List _inputContexts = []; + private readonly ConcurrentQueue _windowQueue = []; + + /// + /// Gets the main window registered with this handler, if any. + /// + public SilkWindow? MainWindow { get; private set; } + + /// + /// 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) { } + + /// + /// + /// 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 async Task ExecuteAsync(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + for (int i = 0; i < _windows.Count; i++) + UpdateWindow(ref i); + + DequeueWindows(); + } + + foreach (var window in _windows) + window.Close(); + + await StopAsync(); + } + + /// + /// 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 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 void DequeueWindows() + { + if (_cancellationTokenSource.IsCancellationRequested) + return; + + while (_windowQueue.TryDequeue(out var window)) + { + window.Initialize(); + window.Run(); + + _windows.Add(window); + } + } + + // TODO: Instead of enqueing the options, the entire window object should be added to the queue. + /// + /// 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. + public void AddWindow(SilkWindow window, bool isMain = false) + { + ArgumentNullException.ThrowIfNull(window, nameof(window)); + + _windowQueue.Enqueue(window); + + if (isMain) + MainWindow = window; + } +} diff --git a/SharpEngine.Core/InputManager.cs b/SharpEngine.Core/InputManager.cs new file mode 100644 index 0000000..4065269 --- /dev/null +++ b/SharpEngine.Core/InputManager.cs @@ -0,0 +1,185 @@ +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; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpEngine.Core; + +/// +/// Handles input events for the application, including mouse and keyboard input. +/// This class is responsible for managing input contexts and dispatching input events to registered handlers. +/// +public class InputManager : EngineHandler +{ + private readonly ILogger _logger; + + /// Gets or sets the input context for the window. + public IInputContext? Context { get; protected set; } + + /// The event executed when mouse events are executed. + public event Action? OnHandleMouse; + + /// The event executed when keyboard events are executed. + public event Action? OnHandleKeyboard; + + /// The event executed when the window is updated. + public event Action? OnUpdate; + + /// The event executed when the mouse wheel is scrolled. + public event Action? HandleMouseWheel; + + /// The event executed when a mouse button is clicked. + public event Action? OnButtonMouseDown; + + /// + /// Initializes a new instance of the . + /// + /// The logger for the input manager. When , a default logger will be used. + public InputManager(ILogger? logger = null) : base(logger) + { + _logger = logger ?? LoggingExtensions.CreateLogger(); + } + + /// + /// Sets the input for the window and assigns input events. + /// + /// The input to set. + public virtual void Set(IInputContext context) + { + Context = context; + AssignInputEvents(); + } + + /// + /// Updates the input manager, processing mouse and keyboard events, and invoking the update event. + /// + /// The current frame information. + public virtual void Update(Frame frame) + { + UpdateMice(); + UpdateKeyboards(frame); + + if (Context is not null) + OnUpdate?.Invoke(frame.FrameTime, Context); + } + + /// + /// Updates the keyboard input, checking for key presses and invoking the keyboard event handler. + /// + /// The current frame information. + protected virtual void UpdateKeyboards(Frame frame) + { + var keyboard = Context?.Keyboards[0]; + if (keyboard is not null) + { + // if (keyboard.IsKeyPressed(Key.Escape)) + // CurrentWindow.Close(); + + OnHandleKeyboard?.Invoke(keyboard, frame.FrameTime); + } + } + + /// + /// Updates the mouse input, checking for mouse movements and button presses, and invoking the mouse event handler. + /// + public virtual void UpdateMice() + { + // TODO: #21 Handle multiple mice? + var mouse = Context?.Mice[0]; + if (mouse is not null) + { + // Camera.UpdateMousePosition((Vector2)mouse.Position); + OnHandleMouse?.Invoke(mouse); + } + } + + /// + /// Assigns input events to the keyboards and mice in the current input . + /// + /// + /// This method attaches event handlers to the keyboards and mice in the current input . + /// If the input is null, no events will be assigned. + /// + public virtual void AssignInputEvents() + { + if (Context is null) + { + _logger.LogInformation("Input is null. No input events will be assigned."); + return; + } + + foreach (var keyboard in Context.Keyboards) + keyboard.KeyDown += KeyDown; + + foreach (var mouse in Context.Mice) + { + mouse.Scroll += OnMouseWheel; + mouse.Click += OnMouseClick; + mouse.MouseDown += OnMouseDown; + + // if (IsFocused) + // mouse.Cursor.CursorMode = CursorMode.Raw; + } + } + + /// + /// Handles mouse click events, invoked when a mouse button is clicked. + /// + /// 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, System.Numerics.Vector2 vector) { } + + /// + /// Handles key down events, invoked when a key is pressed on the keyboard. + /// + /// + /// + /// + public virtual void KeyDown(IKeyboard keyboard, Key key, int keyCode) + { + // if (key == Key.Escape) + // CurrentWindow.Close(); + } + + /// + /// Handles mouse wheel events, invoked when the mouse wheel is scrolled. + /// + /// The mouse that triggered the event. + /// The scroll wheel event data. + /// + public virtual void OnMouseWheel(IMouse mouse, ScrollWheel sw) + { + var direction = sw.Y switch + { + > 0 => MouseWheelScrollDirection.Up, + < 0 => MouseWheelScrollDirection.Down, + _ => throw new NotImplementedException() + }; + + HandleMouseWheel?.Invoke(direction, sw); + // Camera.Fov -= sw.Y; + } + + /// + /// Handles mouse down events, invoked when a mouse button is pressed down. + /// + /// + /// + public virtual void OnMouseDown(IMouse mouse, MouseButton button) + => OnButtonMouseDown?.Invoke(mouse, button); + + /// + protected override Task ExecuteAsync(CancellationToken token) + { + return Task.CompletedTask; + } +} diff --git a/SharpEngine.Core/Primitives/Cube.cs b/SharpEngine.Core/Primitives/Cube.cs new file mode 100644 index 0000000..6a44032 --- /dev/null +++ b/SharpEngine.Core/Primitives/Cube.cs @@ -0,0 +1,264 @@ +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(Defaults.Defaults.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/SharpEngine.Core/Primitives/Plane.cs b/SharpEngine.Core/Primitives/Plane.cs new file mode 100644 index 0000000..0d4d771 --- /dev/null +++ b/SharpEngine.Core/Primitives/Plane.cs @@ -0,0 +1,25 @@ +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Windowing; + +namespace SharpEngine.Core.Primitives; + +/// +/// Used to create a primitive plane object. +/// +public static class Plane +{ + /// The plane mesh. + /// + /// 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: + [ + // 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, 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, 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/Engine/SharpEngine.Core/Primitives/PrimitiveFactory.cs b/SharpEngine.Core/Primitives/PrimitiveFactory.cs similarity index 70% rename from Engine/SharpEngine.Core/Primitives/PrimitiveFactory.cs rename to SharpEngine.Core/Primitives/PrimitiveFactory.cs index 633580d..a9f0975 100644 --- a/Engine/SharpEngine.Core/Primitives/PrimitiveFactory.cs +++ b/SharpEngine.Core/Primitives/PrimitiveFactory.cs @@ -1,11 +1,8 @@ -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 SharpEngine.Core.Numerics; using System; -using System.Collections.Generic; -using System.Numerics; -using Tutorial; namespace SharpEngine.Core.Primitives; @@ -20,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. @@ -33,19 +30,18 @@ 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() { - Transform = new Transform((Numerics.Vector3)position), + Model = model, + Transform = new Transform(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 97% rename from Engine/SharpEngine.Core/Ray.cs rename to SharpEngine.Core/Ray.cs index 942346b..db84552 100644 --- a/Engine/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; @@ -43,7 +43,7 @@ public bool IsGameObjectInView(Scene scene, out GameObject? intersectingObject, const float maxDistance = 100.0f; // Maximum distance to check for intersections const float stepSize = 0.1f; // Step size for ray marching - var sceneNodes = scene.GetObjectsOfType(); + var sceneNodes = scene.Root.GetObjectsOfType(); if (allowedTypes.Length != 0) sceneNodes = [.. sceneNodes.Where(go => allowedTypes.Any(type => type.IsInstanceOfType(go)))]; diff --git a/SharpEngine.Core/RendererManager.cs b/SharpEngine.Core/RendererManager.cs new file mode 100644 index 0000000..b486899 --- /dev/null +++ b/SharpEngine.Core/RendererManager.cs @@ -0,0 +1,60 @@ +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Handlers; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Renderers; +using SharpEngine.Core.Scenes; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpEngine.Core; + +public class RendererManager : EngineHandler +{ + private readonly RendererBase[] _registeredRenderers; + private ISettings _settings; + + public RendererManager(ISettings settings, IEnumerable? renderers = null) : base(null) + { + _settings = settings; + _registeredRenderers = renderers?.ToArray() ?? []; + } + + public void Run() + { + var activeRenderers = _registeredRenderers.Where(renderer => _settings.RendererFlags.HasFlag(renderer.RenderFlag)) + .OrderBy(renderer => renderer.RenderFlag) + .ToList(); + + foreach (var renderer in activeRenderers) + renderer.Render().GetAwaiter().GetResult(); + } + + public RendererBase[] CreateRenderers(CameraView camera, Scene scene) + { + // 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)!; + })]; + } + + /// + protected override Task ExecuteAsync(CancellationToken token) + { + return Task.CompletedTask; + } +} 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 55% rename from Engine/SharpEngine.Core/Renderers/Renderer.cs rename to SharpEngine.Core/Renderers/Renderer.cs index 91f4417..5c74652 100644 --- a/Engine/SharpEngine.Core/Renderers/Renderer.cs +++ b/SharpEngine.Core/Renderers/Renderer.cs @@ -1,14 +1,16 @@ +using Microsoft.Extensions.Logging; 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 SharpEngine.Telemetry; using Silk.NET.OpenGL; - using System; +using System.Collections.Generic; using System.Numerics; using System.Threading.Tasks; @@ -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,55 +44,76 @@ 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; + } + + /// + /// 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) + { + _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); } /// - public override Task Render() + public override async Task Render() { - // TODO: Make toggling these shaders into a hotkey. - // return Task.CompletedTask; - 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); + _camera.SetShaderUniforms(_lightingShader); + _gl.BindVertexArray(_lightingShader.Vao); - var gameObjectRenderTasks = _scene.IterateAsync(_scene.Root.Children, RenderGameObject); - var renderTask = Task.WhenAll(gameObjectRenderTasks); + // Render lights sequentially to avoid allocating a Task per node. + await TraverseAndAwait(_scene.Root.Children, RenderLight); - Window.GL.BindVertexArray(_lampShader.Vao); + // Render game objects sequentially. Streaming avoids large Task allocations and reduces GC pressure. + await TraverseAndAwait(_scene.Root.Children, RenderGameObject); - return renderTask; + _gl.BindVertexArray(_lampShader.Vao); } catch (Exception ex) { - Debug.Log.Error(ex, "{Message}", ex.Message); - return Task.FromException(ex); + _logger.LogError(ex, "{Message}", ex.Message); + throw; + } + } + + private static async Task TraverseAndAwait(IEnumerable nodes, Func action) + { + foreach (var node in nodes) + { + await action(node).ConfigureAwait(false); + + if (node.Children is { Count: > 0 }) + await TraverseAndAwait(node.Children, action).ConfigureAwait(false); } } 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 +122,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/Scenes/EmptyNode.cs b/SharpEngine.Core/Scenes/EmptyNode.cs new file mode 100644 index 0000000..1ae24b1 --- /dev/null +++ b/SharpEngine.Core/Scenes/EmptyNode.cs @@ -0,0 +1,23 @@ +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Numerics; + +namespace SharpEngine.Core.Scenes; + +/// +/// Represents an empty node in a scene. +/// +/// Specifies the type used for transformations. +/// Defines vector used by the transform. +public class EmptyNode : SceneNode where TTransform : ITransform, new() where TVector : IVector, new() +{ + /// + /// Initializes a new instance of . + /// + /// The name of the node in the scene hierarchy. + public EmptyNode(string name) : base(name) { } + + /// + /// Gets or sets the transform of the node. + /// + public virtual TTransform Transform { get; set; } = new(); +} \ No newline at end of file diff --git a/SharpEngine.Core/Scenes/Scene.cs b/SharpEngine.Core/Scenes/Scene.cs new file mode 100644 index 0000000..2160d66 --- /dev/null +++ b/SharpEngine.Core/Scenes/Scene.cs @@ -0,0 +1,74 @@ +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Telemetry; +using SharpEngine.IO; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Scenes; + +/// +/// Represents a scene in the game. +/// +public class Scene : SaveableFile +{ + private static readonly ILogger Logger = LoggingExtensions.CreateLogger(); + + /// The file extension by which saved scenes are associated with. + public const string SceneFileExtension = "sharpscene"; + + /// + /// Initializes a new instance of . + /// + public Scene() + { + Name = "New Scene"; + // ensure root and its current subtree know which scene they belong to + Root.SetSceneRecursive(this); + } + + /// + /// Initializes a new instance of . + /// + /// + public Scene(string sceneFile) + { + SetFileFullPath(sceneFile); + Root.SetSceneRecursive(this); + } + + /// + /// Gets or sets the root node of the scene. + /// + public SceneNode Root { get; set; } = new EmptyNode("Root"); + + /// Gets or sets the nodes in the scene. + private List Nodes { get; set; } = new List(); + + /// Gets or sets the UI elements in the scene. + public List UIElements { get; set; } = new List(); + + /// Gets or sets the active element in the scene. + /// Editor only. + public SceneNode? ActiveElement { get; set; } + + /// + /// Revision counter incremented when the scene structure changes. + /// + public int Revision { get; private set; } + + /// + /// Event fired when scene structure changes (nodes added/removed). + /// + public event Action? SceneChanged; + + internal void IncrementRevision() + { + Revision++; + SceneChanged?.Invoke(); + } +} \ No newline at end of file diff --git a/SharpEngine.Core/Scenes/SceneExtensions.cs b/SharpEngine.Core/Scenes/SceneExtensions.cs new file mode 100644 index 0000000..48b5507 --- /dev/null +++ b/SharpEngine.Core/Scenes/SceneExtensions.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpEngine.Core.Scenes; + +public static class SceneExtensions +{ + /// + /// Gets all the objects in the scene of type . + /// + /// The type of the objects to be retrieved. + /// All the game objects in the current scene. + public static List GetObjectsOfType(this SceneNode root) + { + // Existing API kept for compatibility. Internally uses the iterator implementation. + return root.GetObjectsOfTypeEnumerable().ToList(); + } + + /// + /// Iterates over the scene tree and yields objects of type . + /// This avoids allocating large temporary lists during traversal when callers can stream results. + /// + public static IEnumerable GetObjectsOfTypeEnumerable(this SceneNode root) + { + // Skip the root itself to preserve original behaviour (which iterated root.Children) + foreach (var node in root.Children) + { + foreach (var child in GetObjectsOfTypeEnumerableImpl(node)) + yield return child; + } + } + + private static IEnumerable GetObjectsOfTypeEnumerableImpl(SceneNode node) + { + if (node is T obj) + yield return obj; + + foreach (var child in node.Children) + { + foreach (var descendant in GetObjectsOfTypeEnumerableImpl(child)) + yield return descendant; + } + } + + /// + /// Iterates over the given and performs the given for each element. + /// + /// The type of the element. + /// The scene nodes to iterate over + /// The action to be executed for each element. + public static void Iterate(this List elements, Action action) where TEntityType : SceneNode + { + foreach (var entity in elements) + { + action.Invoke(entity); + + var children = entity.Children.OfType().ToList(); + if (children.Count != 0) + Iterate(children, action); + } + } + + /// + /// Iterates over the given and performs the given for each element. + /// + /// The type of the element. + /// The scene nodes to iterate over + /// The action to be executed for each element. + /// A representing an asynchronous operation. + public static IEnumerable IterateAsync(this IEnumerable elements, Func action) + { + var tasks = new List(); + + foreach (var entity in elements) + { + tasks.Add(action(entity)); + tasks.AddRange(IterateAsync(entity.Children, action)); + } + + return tasks; + } +} diff --git a/SharpEngine.Core/Scenes/SceneNode.cs b/SharpEngine.Core/Scenes/SceneNode.cs new file mode 100644 index 0000000..0d1829e --- /dev/null +++ b/SharpEngine.Core/Scenes/SceneNode.cs @@ -0,0 +1,201 @@ +using Microsoft.Extensions.Logging; + +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Windowing; +using SharpEngine.Telemetry; + +using Silk.NET.Core; +using Silk.NET.OpenGL; + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace SharpEngine.Core.Scenes; + +/// +/// Represents a node in the scene. +/// +public abstract class SceneNode +{ + /// Gets a new empty node. + public static SceneNode Empty => new EmptyNode("Empty Node"); + + /// + /// Gets or sets the name of the node. + /// + public string Name { get; set; } = "New Object"; + + /// + /// Gets or sets the children of the node. + /// + public List Children { get; set; } = new List(); + + /// + /// The parent node in the scene graph. Null for the root. + /// + public SceneNode? Parent { get; private set; } + + // TODO: This has to be redundant. + // This was a part of a AI vibe code but wasn't able to figure out how to make things work without it. + // Need to investigate if this is really needed or not. + /// + /// The scene this node belongs to. May be null for detached nodes. + /// + public Scene? Scene { get; internal set; } + + private bool _initialized; + + private static readonly ILogger logger = LoggingExtensions.CreateLogger(); + + /// + /// Initializes a new empty . + /// + protected SceneNode() { } + + /// + /// Initializes a new instance of the with the specified . + /// + /// The name of the new empty node. + protected SceneNode(string name) + { + Name = name; + } + + /// + /// Adds an empty child node to this node by the given . + /// + /// The type of the transform. This is usually either a 2D () or 3D transform (). + /// The type of the vector. + /// The name of the new child node. + /// The current node where the child is added to. + public virtual SceneNode AddChild(string name) where TTransform : ITransform, new() where TVector : IVector, new() + { + var node = new EmptyNode(name); + AddChildInternal(node); + + return this; + } + + /// + /// The node to be added. + public virtual SceneNode AddChild(SceneNode node) + { + AddChildInternal(node); + return this; + } + + /// + /// The nodes to be added. + public virtual SceneNode AddChild(params SceneNode[] nodes) + { + foreach (var node in nodes) + { + AddChildInternal(node); + } + + return this; + } + + private SceneNode AddChildInternal(SceneNode node) + { + node.Parent = this; + // propagate scene reference so node knows which scene it belongs to + node.SetSceneRecursive(Scene); + Children.Add(node); + + // Ensure lifecycle hook runs for newly added nodes + try + { + node.OnCreated(); + } + catch (Exception ex) + { + logger.LogError(ex, "{Message}", ex.Message); + } + + // Notify the scene that structure changed + Scene?.IncrementRevision(); + + return node; + } + + /// + /// Removes a child node from this node. + /// + /// The node to be removed. + public void RemoveChild(SceneNode node) + { + if (node is null || !Children.Remove(node)) + return; + + RemoveSubtree(node); + } + + private static void RemoveSubtree(SceneNode node) + { + foreach (var child in node.Children.ToArray()) + RemoveSubtree(child); + + // Detach children + foreach (var child in node.Children) + { + child.Parent = null; + child.SetSceneRecursive(null); + } + + node.Children.Clear(); + node.OnDeleted(); + } + + /// + /// Renders the current object to the screen. + /// + /// A representing an asynchronous operation. + public virtual Task Render(CameraView camera, Window window) + => Task.CompletedTask; + + /// + /// Initializes the component when a scene is loaded. + /// + /// + /// NOTE: This function does not load child nodes.
+ /// It is the responsibility of the scene to call on all child nodes. + ///
+ public virtual void OnInitialized(GL gl) + { + if (_initialized) + return; + + _initialized = true; + } + + /// + /// Called when the node is created and added to the scene. + /// + public virtual void OnCreated() { } + + /// + /// Called every frame to update the node's state. + /// + /// + public virtual void Update(float deltaTime) { } + + /// + /// Called when the node is deleted and removed from the scene. + /// + public virtual void OnDeleted() { } + + /// + /// Set the Scene reference for this node and all descendants. + /// + internal void SetSceneRecursive(Scene? scene) + { + Scene = scene; + foreach (var child in Children) + child.SetSceneRecursive(scene); + } +} diff --git a/SharpEngine.Core/ShaderManager.cs b/SharpEngine.Core/ShaderManager.cs new file mode 100644 index 0000000..9f09bb7 --- /dev/null +++ b/SharpEngine.Core/ShaderManager.cs @@ -0,0 +1,30 @@ +using SharpEngine.Core.Handlers; +using SharpEngine.Core.Shaders; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace SharpEngine.Core; + +public class ShaderManager : EngineHandler +{ + private List _shaders = []; + + public ShaderManager(ILogger logger) : base(logger) + { + } + + public virtual void UseShaders() + { + if (ShaderService.Instance.HasShadersToLoad) + _shaders = ShaderService.Instance.GetAll(); + + _shaders.ForEach(shader => shader.Use()); + } + + protected override Task ExecuteAsync(CancellationToken token) + { + return Task.CompletedTask; + } +} diff --git a/SharpEngine.Core/Shaders/DefaultShader.cs b/SharpEngine.Core/Shaders/DefaultShader.cs new file mode 100644 index 0000000..7509bc0 --- /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, 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 new file mode 100644 index 0000000..ac1d006 --- /dev/null +++ b/SharpEngine.Core/Shaders/LampShader.cs @@ -0,0 +1,33 @@ +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Defaults; + +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +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, Defaults.Defaults.VertexShader, Defaults.Defaults.LightShader, nameof(LampShader)) + { + } + + /// + public override bool SetAttributes(GL gl) + { + if (!base.SetAttributes(gl)) + return false; + + 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); + + return true; + } +} diff --git a/SharpEngine.Core/Shaders/LightingShader.cs b/SharpEngine.Core/Shaders/LightingShader.cs new file mode 100644 index 0000000..e0de59f --- /dev/null +++ b/SharpEngine.Core/Shaders/LightingShader.cs @@ -0,0 +1,42 @@ +using Silk.NET.OpenGL; + +namespace SharpEngine.Core.Shaders; + +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, Defaults.Defaults.VertexShader, Defaults.Defaults.FragmentShader, nameof(LightingShader)) { } + + /// + 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/SharpEngine.Core/Shaders/Rendering/ParameterDescriptor.cs b/SharpEngine.Core/Shaders/Rendering/ParameterDescriptor.cs new file mode 100644 index 0000000..9a8f98a --- /dev/null +++ b/SharpEngine.Core/Shaders/Rendering/ParameterDescriptor.cs @@ -0,0 +1,18 @@ +using System; + +namespace SharpEngine.Core.Shaders.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/Shaders/Rendering/ShaderParameter.cs b/SharpEngine.Core/Shaders/Rendering/ShaderParameter.cs new file mode 100644 index 0000000..da8c4e1 --- /dev/null +++ b/SharpEngine.Core/Shaders/Rendering/ShaderParameter.cs @@ -0,0 +1,37 @@ +using System; + +namespace SharpEngine.Core.Shaders.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/Shaders/Rendering/ShaderParameterBinder.cs b/SharpEngine.Core/Shaders/Rendering/ShaderParameterBinder.cs new file mode 100644 index 0000000..205c7c3 --- /dev/null +++ b/SharpEngine.Core/Shaders/Rendering/ShaderParameterBinder.cs @@ -0,0 +1,189 @@ +using Microsoft.Extensions.Logging; +using SharpEngine.Core.Shaders; +using SharpEngine.Telemetry; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Numerics; +using System.Reflection; + +namespace SharpEngine.Core.Shaders.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 = null) + { + _logger = logger ?? LoggingExtensions.CreateLogger(); + } + + /// + /// 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/Shaders/Rendering/ShaderParameterType.cs b/SharpEngine.Core/Shaders/Rendering/ShaderParameterType.cs new file mode 100644 index 0000000..02f85d9 --- /dev/null +++ b/SharpEngine.Core/Shaders/Rendering/ShaderParameterType.cs @@ -0,0 +1,14 @@ +namespace SharpEngine.Core.Shaders.Rendering +{ + public enum ShaderParameterType + { + Int, + Float, + Vec2, + Vec3, + Vec4, + Mat4, + Texture, + Unknown + } +} diff --git a/SharpEngine.Core/Shaders/Rendering/ShaderUpdateFrequency.cs b/SharpEngine.Core/Shaders/Rendering/ShaderUpdateFrequency.cs new file mode 100644 index 0000000..f54c56e --- /dev/null +++ b/SharpEngine.Core/Shaders/Rendering/ShaderUpdateFrequency.cs @@ -0,0 +1,9 @@ +namespace SharpEngine.Core.Shaders.Rendering +{ + public enum ShaderUpdateFrequency + { + Static, + PerObject, + PerFrame + } +} diff --git a/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs new file mode 100644 index 0000000..1aaebcc --- /dev/null +++ b/SharpEngine.Core/Shaders/ShaderParameterAttribute.cs @@ -0,0 +1,19 @@ +using SharpEngine.Core.Shaders.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 new file mode 100644 index 0000000..5832856 --- /dev/null +++ b/SharpEngine.Core/Shaders/ShaderService.cs @@ -0,0 +1,131 @@ +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(), window.GetShareGroupKey(), 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); + _shaderCache[cacheKey] = shader; + + HasShadersToLoad = true; + + return shader; + } +} diff --git a/Engine/SharpEngine.Core/SharpEngine.Core.csproj b/SharpEngine.Core/SharpEngine.Core.csproj similarity index 56% rename from Engine/SharpEngine.Core/SharpEngine.Core.csproj rename to SharpEngine.Core/SharpEngine.Core.csproj index 8633518..d3ab898 100644 --- a/Engine/SharpEngine.Core/SharpEngine.Core.csproj +++ b/SharpEngine.Core/SharpEngine.Core.csproj @@ -1,6 +1,9 @@  + preview + $(DotNetTargetFramework) + Everything needed to build games using a purely C# game engine. AnyCPU;x64;x86 @@ -10,29 +13,33 @@ True + $(AssemblyName).NET - - - true README.md + - + - - - - - - - - + + + + + + + + + + + + + + - @@ -47,6 +54,14 @@ PreserveNewest + + + + + + + + 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 81% rename from Engine/SharpEngine.Core/Windowing/SilkWindow.cs rename to SharpEngine.Core/Windowing/SilkWindow.cs index 4301fa0..b07e1e0 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,15 @@ 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 + +internal sealed class WindowNotInitializedException : Exception +{ + public WindowNotInitializedException(SilkWindow window) : base($"Window '{window.Title}' not initialized.") { } + public WindowNotInitializedException(string message) : base(message) { } +} + /// /// Represents an abstraction for the interface. /// @@ -31,15 +42,19 @@ public IMonitor? Monitor set => CurrentWindow.Monitor = value; } - /// + /// public bool IsClosing { get => CurrentWindow.IsClosing; set => CurrentWindow.IsClosing = value; } - /// Gets or sets the input context for the window. - public IInputContext? Input { get; protected set; } + /// + /// Gets the settings for the current window. + /// + public IViewSettings Settings { get; protected set; } = ViewSettings.Default; + + public Silk.NET.Maths.Vector4D BackgroundColor { get; set; } = new Silk.NET.Maths.Vector4D(0.2f, 0.3f, 0.3f, 1.0f); /// public virtual Rectangle BorderSize => CurrentWindow.BorderSize; @@ -65,13 +80,39 @@ 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 float Top => Height / 2f; + + public float Bottom => -(Height / 2f); + + public float Left => -(Width / 2f); + + public float Right => Width / 2f; + + // TODO: This doesn't seem to work /// public string Title { - get => CurrentWindow.Title; - set => CurrentWindow.Title = value; + get => !string.IsNullOrWhiteSpace(CurrentWindow?.Title) ? CurrentWindow.Title : "Untitled window"; + set + { + if (CurrentWindow != null) + { + CurrentWindow.Title = value; + Settings.WindowOptions = Settings.WindowOptions with { Title = value }; + } + } } + public string IconPath { get; set; } = "_Resources/icon.png"; + /// public WindowState WindowState { @@ -159,6 +200,7 @@ public bool VSync set => CurrentWindow.ShouldSwapAutomatically = value; } + /// Gets a value indicating whether the window is focused. public bool IsFocused { get; } /// @@ -218,6 +260,7 @@ public bool VSync /// Raised after the scene has been rendered. public event Action? OnAfterRender; + public event Action? OnPreRender; /// An event executed when the window has loaded. public event Action? OnLoaded; @@ -231,9 +274,9 @@ public bool VSync private IWindow? _currentWindow; /// Gets or sets the current window. - public IWindow CurrentWindow + public IWindow? CurrentWindow { - get => _currentWindow!; + get => _currentWindow; // ?? throw new WindowNotInitializedException(this); set => _currentWindow = value; } @@ -344,10 +387,10 @@ public virtual void OnLoad() /// Handles operations needed to be executed before the renderers are executed. /// /// Contains information about the previous frame. - protected virtual void PreRender(Frame frame) { } + protected virtual void PreRender(Frame frame) => OnPreRender?.Invoke(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 new file mode 100644 index 0000000..7c903f6 --- /dev/null +++ b/SharpEngine.Core/Windowing/Window.cs @@ -0,0 +1,338 @@ +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Entities.Views.Settings; +using SharpEngine.Core.Enums; +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; +using Silk.NET.OpenGL; +using Silk.NET.OpenGL.Extensions.ImGui; +using Silk.NET.Windowing; +using MouseButton = Silk.NET.Input.MouseButton; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using SharpEngine.Telemetry; +using Vector3 = SharpEngine.Core.Numerics.Vector3; +using SharpEngine.IO.Extensions; + +namespace SharpEngine.Core.Windowing; + +/// +/// Represents the game window. +/// +public class Window : SilkWindow +{ + private bool _windowInitialized; + private bool _initialized; + private readonly ILogger _logger; + + private IEnumerable _renderers = []; + private ImGuiController? _imGuiController; + + /// Gets the input manager for the window. + public InputManager InputManager { get; private set; } + + /// Gets the shader manager for the window. + public ShaderManager ShaderManager { get; private set; } + + /// Gets the renderer manager for the window. + public RendererManager RendererManager { get; private set; } + + /// + /// Gets or sets the view for the window. + /// + public readonly CameraView Camera; + + /// + /// The scene that is currently being rendered. + /// + protected Scene Scene { get; private set; } + + private GL _gl; + 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."); + + /// + /// Gets the current OpenGL context. + /// + /// The OpenGL context for this window. + public GL GetGL() => _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, 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. + /// 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 = null, IEnumerable? renderers = null) + : this(scene, settings, camera, logger, renderers) { } + + /// + /// Initializes a new instance of . + /// + /// + /// The new window is initialized without a dedicated camera. + /// + /// Contains the game scene. + /// The settings for the window. + /// The camera the window should render from. When , a default camera will be used. + /// The logger for the window. When , a default logger will be used. + /// The renderers for the window. + public Window(Scene scene, IViewSettings settings, CameraView? camera = null, ILogger? logger = null, IEnumerable? renderers = null) + { + Scene = scene; + Settings = settings; + Camera = camera ?? new(Vector3.One, settings); + _logger = logger ?? LoggingExtensions.CreateLogger(); + + // TODO: + // Should the developer need to call for a window initialization? + // Meaning should should we move this project loading part to a separate function? + + InputManager = new InputManager(); + ShaderManager = new ShaderManager(LoggingExtensions.CreateLogger()); + RendererManager = new RendererManager(settings, renderers); + + // NOTE: Window initialization is intentionally not performed automatically here. + // Call InitializeWindow() explicitly when ready to create the underlying native window and load resources. + } + + /// + public override void Initialize() + { + if (_initialized) + return; + + CurrentWindow = CreateWindow(Settings.WindowOptions); + CurrentWindow.Update += deltaTime => OnUpdateFrame(new Frame(deltaTime)); + CurrentWindow.Render += deltaTime => RenderFrame(new Frame(deltaTime)); + CurrentWindow.Resize += OnResize; + CurrentWindow.Load += OnLoad; + CurrentWindow.Closing += OnClosing; + + _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(); + } + + /// + public override void Run(Action onFrame) + { + if (!_windowInitialized) + throw new InvalidOperationException("Window not been initialized for this instance. Try calling 'window.Initialize()' first."); + + try + { + base.Run(onFrame); + Run(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error running window: {Message}", ex.Message); + } + } + + /// + public override void OnLoad() + { + ArgumentNullException.ThrowIfNull(CurrentWindow); + + try + { + SetGL(); + + var context = CurrentWindow.CreateInput(); + InputManager.Set(context); + + CurrentWindow.MakeCurrent(); + + SetWindowIcon(PathExtensions.GetAssemblyPath(IconPath)); + + _gl.ClearColor(BackgroundColor.X, BackgroundColor.Y, BackgroundColor.Z, BackgroundColor.W); + + _renderers = RendererManager.CreateRenderers(Camera, Scene); + + foreach (var renderer in _renderers) + { + renderer.AttachWindow(this); + renderer.Initialize(); + } + + _imGuiController = new ImGuiController(_gl, CurrentWindow, InputManager.Context); + + _initialized = true; + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Error loading window: {Message}", ex.Message); + } + + base.OnLoad(); + } + + private void SetGL() + { + var context = CurrentWindow.CreateOpenGL(); + _gl = 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; + } + + /// + /// Renders the current view and all specified renderers. + /// + /// Contains information about the previous frame. + protected void RenderFrame(Frame frame) + { + if (!_initialized || CurrentWindow == null) + throw new InvalidOperationException("Window has not been initialized. Call Initialize() before rendering."); + + try + { + // TODO: Figure out a better place for this check so we are able to reduce the time a frame takes to execute + // Scene.Root.Children.Iterate(node => node.OnInitialized(_gl)); + + PreRender(frame); + + _imGuiController?.Update((float)frame.FrameTime); + + _gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + + ToggleWireFrame(Settings.UseWireFrame); + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred during pre-rendering."); + } + + try + { + ShaderManager.UseShaders(); + RendererManager.Run(); + } + 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.LogError(ex, "An error occurred while rendering ImGui interface."); + } + } + + /// + /// Toggles the renderer between wireframe and fill mode. + /// + /// Determines whether objects should be rendered in wireframe. + private void ToggleWireFrame(bool useWireFrame) + => _gl.PolygonMode(GLEnum.FrontAndBack, useWireFrame ? PolygonMode.Line : PolygonMode.Fill); + + /// + /// Called when a window frame is rendered. + /// + /// The frame information. + protected void OnUpdateFrame(Frame frame) + { + while (!_initialized) + { + // Wait for the window to be initialized. + } + + if (Settings.PrintFrameRate) + _logger.LogInformation("FPS: {FrameRate}", frame.FrameRate); + + InputManager.Update(frame); + } + + /// + /// Called when the window is resized. + /// + /// The new size of the window. + protected virtual void OnResize(Vector2D size) + { + _gl.Viewport(size); + + if (size != Vector2D.Zero) + Camera.AspectRatio = (float)size.X / size.Y; + } + + /// + /// Sets the current scene. + /// + /// The contents of the new scene. + protected virtual void SetScene(Scene scene) + { + // TODO: #92 Do we need to clear anything from e.g. the GPU when we change change the scene? + Scene = scene; + } + + /// + public Matrix4x4 CreateOrthographicOffCenter() + => Matrix4x4.CreateOrthographicOffCenter(Left, Right, Bottom, Top, -1, 1); +} 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/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/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/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/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/SharpEngine.Core/_Resources/Shaders/uiShader.vert b/SharpEngine.Core/_Resources/Shaders/uiShader.vert new file mode 100644 index 0000000..b778afc --- /dev/null +++ b/SharpEngine.Core/_Resources/Shaders/uiShader.vert @@ -0,0 +1,53 @@ +#version 330 core + +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aNormal; +layout (location = 2) in vec2 aTexCoords; + +uniform float width; +uniform float height; + +uniform vec2 position; +uniform float rotation; +uniform mat4 orthoMatrix; + +uniform mat4 uModel; + +out vec3 Normal; +out vec3 FragPos; +out vec2 TexCoords; + +void main() +{ + // Create the rotation matrix for the z-axis + mat4 rotationMatrix = mat4( + cos(rotation), -sin(rotation), 0.0, 0.0, + sin(rotation), cos(rotation), 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); + + // Scale the vertex position using pixel-space width and height. + vec4 scaledPos = vec4( + aPos.x * width * 0.5, + aPos.y * height * 0.5, + aPos.z, + 1.0 + ); + + // Apply the model scale and rotation before translating in pixel space. + vec4 transformedPos = rotationMatrix * uModel * scaledPos; + + // Project directly using the window-sized orthographic matrix. + gl_Position = orthoMatrix * vec4( + transformedPos.x + position.x, + transformedPos.y + position.y, + transformedPos.z, + 1.0 + ); + + // Pass through other attributes + Normal = mat3(rotationMatrix * uModel) * aNormal; // Transform the normal + FragPos = vec3(transformedPos.x + position.x, transformedPos.y + position.y, transformedPos.z); + TexCoords = aTexCoords; +} \ 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/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.Dto/Class1.cs b/SharpEngine.Dto/Class1.cs new file mode 100644 index 0000000..9a1797e --- /dev/null +++ b/SharpEngine.Dto/Class1.cs @@ -0,0 +1,6 @@ +namespace SharpEngine.Dto; + +public class Class1 +{ + +} diff --git a/Engine/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj b/SharpEngine.Dto/SharpEngine.Core.Dto.csproj similarity index 72% rename from Engine/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj rename to SharpEngine.Dto/SharpEngine.Core.Dto.csproj index fa71b7a..2203718 100644 --- a/Engine/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj +++ b/SharpEngine.Dto/SharpEngine.Core.Dto.csproj @@ -1,7 +1,7 @@  - net8.0 + $(DotNetTargetFramework) enable enable 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 new file mode 100644 index 0000000..693c49e --- /dev/null +++ b/SharpEngine.slnx @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props new file mode 100644 index 0000000..3b85263 --- /dev/null +++ b/Tests/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + 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 74% rename from Engine/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs rename to Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs index 39c13c4..6e8cf13 100644 --- a/Engine/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/MaterialLibraryLoaderTests.cs @@ -1,35 +1,36 @@ using System.Collections.Generic; -using System.IO; -using System.Text; -using NUnit.Framework; -using ObjLoader.Loader.Data; using System.Linq; + using FluentAssertions; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; -using SharpEngine.Core.Entities.Properties; +using Xunit; + +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; +using Moq; +using SharpEngine.Core.ObjLoader.Loader.Loaders; +using System.IO.Abstractions; -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(_materialLibrarySpy, Mock.Of(), Mock.Of()); } - [Test] + [Fact] public void Adds_correct_materials() { LoadMaterial(); @@ -39,7 +40,7 @@ public void Adds_correct_materials() _secondMaterial.Name.Should().BeEquivalentTo("second_material"); } - [Test] + [Fact] public void Sets_correct_ambient_color() { LoadMaterial(); @@ -49,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(); @@ -59,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(); @@ -71,7 +72,7 @@ public void Sets_correct_specular() _firstMaterial.SpecularCoefficient.Should().BeApproximately(32, Epsilon); } - [Test] + [Fact] public void Sets_correct_transparency() { LoadMaterial(); @@ -79,7 +80,7 @@ public void Sets_correct_transparency() _firstMaterial.Transparency.Should().BeApproximately(0.5f, Epsilon); } - [Test] + [Fact] public void Sets_correct_illumination_model() { LoadMaterial(); @@ -87,14 +88,14 @@ public void Sets_correct_illumination_model() _firstMaterial.IlluminationModel.Should().Be(2); } - [Test] + [Fact] 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"); @@ -102,12 +103,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); } @@ -130,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; } @@ -139,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/Engine/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs b/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs similarity index 53% rename from Engine/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs rename to Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs index 8e4190a..9cba209 100644 --- a/Engine/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs +++ b/Tests/ObjLoader.Test/Loaders/ObjLoaderTests.cs @@ -1,67 +1,59 @@ using System.IO; using System.Text; using System.Linq; -using NUnit.Framework; -using ObjLoader.Loader.Loaders; -using ObjLoader.Loader.TypeParsers; + using FluentAssertions; -using ObjLoader.Loaders.MaterialLoader; +using 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 ObjLoader.Test.Loaders +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 ObjLoader.Loaders.ObjLoader.ObjLoader _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); - _materialLibraryLoaderFacade = new MaterialLibraryLoaderFacade(_materialLibraryLoader); - // _materialLibraryParser = new MaterialLibraryParser(_materialLibraryLoaderFacade); + _materialLibraryLoader = new MaterialLibraryLoader(_textureDataStore, fileStreamFactory, fileManager); + _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, fileStreamFactory, fileManager); } - [Test] + [Fact] public void Loads_object_and_material_correctly() { Load(); @@ -71,24 +63,16 @@ 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(); group.Faces.Should().HaveCount(12); - group.Material.Name.Should().BeEquivalentTo("cube_material"); + 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 Loader.Loaders.ObjLoader(_textureDataStore, _faceParser, _groupParser, _normalParser, _textureParser, _vertexParser, materialLibraryParser, _useMaterialParser); - Load(); _loadResult.Vertices.Should().HaveCount(8); @@ -105,15 +89,7 @@ public void Loads_object_correctly_when_material_is_not_found() private void Load() { - var objectStream = CreateMemoryStreamFromString(ObjectFileString); - - // _loadResult = _loader.Load(objectStream); - } - - private Stream CreateMemoryStreamFromString(string str) - { - var data = Encoding.ASCII.GetBytes(str); - return new MemoryStream(data); + _loadResult = _loader.Load(null!).First(); } private const string ObjectFileString = @@ -183,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 new file mode 100644 index 0000000..5d3d5cc --- /dev/null +++ b/Tests/ObjLoader.Test/SharpEngine.Core.ObjLoader.Tests.csproj @@ -0,0 +1,23 @@ + + + + $(DotNetTargetFramework) + + + + + + + + + + + + + CS8618 + + + + + + \ No newline at end of file diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs similarity index 89% rename from Engine/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs index 64ba447..05636a0 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/FaceParserTests.cs @@ -1,27 +1,24 @@ 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; +using Xunit; -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 { - 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/Engine/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs similarity index 71% rename from Engine/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs index 97ea846..1e38c77 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/GroupParserTests.cs @@ -1,26 +1,23 @@ using FluentAssertions; -using NUnit.Framework; -using ObjLoader.Loader.Data; -using ObjLoader.Loader.TypeParsers; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using Xunit; -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] + [Fact] public void CanParse_returns_true_on_normal_line() { const string groupKeyword = "g"; @@ -29,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"; @@ -38,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/Engine/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs similarity index 71% rename from Engine/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs index 91089f1..e1771b6 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/MaterialLibraryParserTests.cs @@ -1,24 +1,23 @@ using FluentAssertions; -using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; -using ObjLoader.Loaders.MaterialLoader; +using Xunit; -namespace ObjLoader.Test.TypeParsers +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.ObjLoader.Loaders.MaterialLoader; + +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] + [Fact] public void CanParse_returns_true_on_mtlib_line() { const string groupKeyword = "mtllib"; @@ -27,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"; @@ -36,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/Engine/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs similarity index 76% rename from Engine/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs index f9e03c7..15bce84 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/NormalParserTests.cs @@ -1,26 +1,24 @@ using FluentAssertions; -using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using Xunit; + +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] + [Fact] public void CanParse_returns_true_on_normal_line() { const string normalLine = "vn"; @@ -29,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"; @@ -38,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/Engine/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs similarity index 77% rename from Engine/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs index 539308b..36402df 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/TextureParserTests.cs @@ -1,26 +1,25 @@ using FluentAssertions; -using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using Xunit; + +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] + [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/Engine/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs similarity index 63% rename from Engine/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs index 863fac8..5b72637 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/UseMaterialParserTests.cs @@ -1,23 +1,25 @@ using FluentAssertions; -using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; +using Xunit; -namespace ObjLoader.Test.TypeParsers +using SharpEngine.Core.ObjLoader.Loader.TypeParsers; +using SharpEngine.Core.Components.Properties; + +using System; + +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] + [Fact] public void CanParse_returns_true_on_usemtl_line() { const string groupKeyword = "usemtl"; @@ -26,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"; @@ -35,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"; @@ -44,10 +46,13 @@ 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; } + public void AddMaterial(Material currentMaterial) + => throw new NotImplementedException(); + public void SetMaterial(string materialName) { MaterialName = materialName; diff --git a/Engine/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs b/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs similarity index 77% rename from Engine/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs rename to Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs index ba27316..dfa5af7 100644 --- a/Engine/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs +++ b/Tests/ObjLoader.Test/TypeParsers/VertexParserTests.cs @@ -1,26 +1,24 @@ using FluentAssertions; -using NUnit.Framework; -using ObjLoader.Loader.TypeParsers; -using SharpEngine.Core.Components.Obsolete.ObjLoader.DataStore; +using Xunit; + +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] + [Fact] public void CanParse_returns_false_on_non_vertex_line() { const string invalidVertex = "vt"; @@ -29,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"; @@ -38,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"; @@ -64,6 +62,5 @@ public void AddVertex(Vertex vertex) ParsedVertex = vertex; } } - } } \ No newline at end of file diff --git a/Tests/SharpEngine.Core.ArchitectureTests/GameObjectTests.cs b/Tests/SharpEngine.Core.ArchitectureTests/GameObjectTests.cs new file mode 100644 index 0000000..994c52b --- /dev/null +++ b/Tests/SharpEngine.Core.ArchitectureTests/GameObjectTests.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using SharpEngine.Core.Entities; +using Xunit; + +namespace SharpEngine.Core.ArchitectureTests; + +public class GameObjectTests +{ + + [Fact] + public void GameObjects_Should_Not_Have_Public_Constructors() + { + var gameObjectTypes = typeof(GameObject).Assembly + .GetTypes() + .Where(t => + !t.IsAbstract && + typeof(GameObject).IsAssignableFrom(t)); + + foreach (var type in gameObjectTypes) + { + var publicConstructors = type.GetConstructors( + BindingFlags.Public | BindingFlags.Instance); + + Assert.Empty(publicConstructors); + } + } +} diff --git a/Tests/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj b/Tests/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj new file mode 100644 index 0000000..ee07ba9 --- /dev/null +++ b/Tests/SharpEngine.Core.ArchitectureTests/SharpEngine.Core.ArchitectureTests.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + 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/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 new file mode 100644 index 0000000..1a08f02 --- /dev/null +++ b/Tests/SharpEngine.Core.Components.Tests/SharpEngine.Core.Components.Tests.csproj @@ -0,0 +1,20 @@ + + + + $(DotNetTargetFramework) + 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/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/Shaders/ShaderParameterBinderTests.cs b/Tests/SharpEngine.Core.Tests/Shaders/ShaderParameterBinderTests.cs new file mode 100644 index 0000000..acae25b --- /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.Shaders; +using SharpEngine.Core.Shaders.Rendering; +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 new file mode 100644 index 0000000..d21b654 --- /dev/null +++ b/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj @@ -0,0 +1,22 @@ + + + + $(DotNetTargetFramework) + enable + enable + + + + + + + + + + + + + + + + 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 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.Core.Tests/SharpEngine.Core.Tests.csproj b/Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj similarity index 71% rename from Engine/Tests/SharpEngine.Core.Tests/SharpEngine.Core.Tests.csproj rename to Tests/SharpEngine.Editor.Tests/SharpEngine.Editor.Tests.csproj index fa71b7a..bbcf245 100644 --- a/Engine/Tests/SharpEngine.Core.Tests/SharpEngine.Core.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 71% rename from Engine/Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj rename to Tests/SharpEngine.Shared.Tests/SharpEngine.Shared.Tests.csproj index fa71b7a..bbcf245 100644 --- a/Engine/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 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 diff --git a/imgui.ini b/imgui.ini new file mode 100644 index 0000000..537aca2 --- /dev/null +++ b/imgui.ini @@ -0,0 +1,10 @@ +[Window][Debug##Default] +Pos=60,60 +Size=400,400 +Collapsed=0 + +[Window][Debug] +Pos=60,60 +Size=240,311 +Collapsed=0 +